Android自定义View三大核心流程:measure、layout、draw完全解密

一、开篇:为什么你需要深入理解三大流程?

作为Android开发者,你一定遇到过这些令人困惑的场景:

场景1: 你写了一个自定义View,在XML中设置了wrap_content,但显示的效果却和match_parent一样,内容被截断或显示不全。

场景2: 你的自定义ViewGroup在滑动时卡顿,用Profiler分析发现onMeasure被调用了数十次甚至上百次。

场景3: 你照着网上的例子实现了自定义View,但在某些手机上布局错乱,却找不到原因。

这些问题的根源,都是对Android View系统的三大核心流程------measure(测量)、layout(布局)、draw(绘制) 理解不够深入。今天,我将带你彻底解密这三大流程,让你成为自定义View的专家。

错误示例:90%的开发者都踩过这个坑

java 复制代码
public class WrongView extends View {
    public WrongView(Context context) {
        super(context);
    }
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 错误!直接设置固定尺寸,wrap_content将完全失效
        setMeasuredDimension(200, 200);
    }
}

这个看似简单的代码,却让无数开发者掉进坑里。为什么?读完本文,你将彻底明白。

二、宏观视角:从DecorView开始的绘制旅程

在深入细节之前,我们先从整体上理解Android UI系统是如何工作的。

2.1 绘制流程的起点:ViewRootImpl

每个Activity都包含一个Window,每个Window都关联着一个ViewRootImpl。它是连接WindowManager和View树的桥梁。

java 复制代码
// 简化版的绘制流程核心代码
public final class ViewRootImpl {
    public void performTraversals() {
        // 1. 预测量阶段(可能多次)
        if (layoutRequested) {
            windowSizeMayChange = measureHierarchy(...);
        }
        
        // 2. 最终测量
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        
        // 3. 布局阶段
        performLayout(lp, desiredWindowWidth, desiredWindowHeight);
        
        // 4. 绘制阶段
        if (!cancelDraw) {
            performDraw();
        }
    }
}

2.2 三大流程的关系图

text 复制代码
┌─────────────────────────────────────────────────────┐
│            ViewRootImpl.performTraversals()         │
├─────────────┬──────────────┬────────────────────────┤
│ performMeasure │ performLayout │      performDraw     │
│   (测量)     │    (布局)    │        (绘制)         │
└─────────────┴──────────────┴────────────────────────┘
         ↓                ↓                  ↓
    onMeasure()      onLayout()         onDraw()/dispatchDraw()
    (确定尺寸)       (确定位置)          (绘制内容)

关键点理解:

  • 测量决定大小:View需要多大空间
  • 布局决定位置:View放在哪里
  • 绘制决定内容:View长什么样

三、测量(measure)过程深度解析

3.1 MeasureSpec:测量规格的DNA

MeasureSpec是View测量过程中的核心概念,它是一个32位的int值,高2位表示测量模式,低30位表示测量尺寸。

java 复制代码
// MeasureSpec的源码实现
public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;  // 0
    public static final int EXACTLY     = 1 << MODE_SHIFT;  // 1073741824
    public static final int AT_MOST     = 2 << MODE_SHIFT;  // 2147483648
    
    // 打包模式和尺寸为MeasureSpec
    public static int makeMeasureSpec(int size, int mode) {
        return (size & ~MODE_MASK) | (mode & MODE_MASK);
    }
    
    // 从MeasureSpec解包模式
    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }
    
    // 从MeasureSpec解包尺寸
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
}

三种测量模式的实战含义

模式 二进制值 含义 典型场景 开发者需要做什么
UNSPECIFIED 00 父容器对子View无限制,子View想要多大就多大 ScrollView、ListView等可滚动的容器 返回期望的真实尺寸
EXACTLY 01 精确尺寸,View必须是这个大小 match_parent或具体数值(100dp) 直接使用给定的尺寸
AT_MOST 10 最大尺寸限制,View不能超过这个大小 wrap_content 返回不超过限制的期望尺寸

3.2 MeasureSpec的传递:父View如何决定子View的大小

理解MeasureSpec的关键是明白它是如何从父View传递给子View的。看下面这个例子:

java 复制代码
// ViewGroup中的关键方法:如何为子View生成MeasureSpec
protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
    
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    
    // 计算子View的MeasureSpec
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);
    
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);
    
    // 让子View测量自己
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

getChildMeasureSpec方法是理解MeasureSpec传递的核心:

java 复制代码
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);
    
    // 可用尺寸 = 父容器尺寸 - padding
    int size = Math.max(0, specSize - padding);
    
    int resultSize = 0;
    int resultMode = 0;
    
    switch (specMode) {
        // 父容器是EXACTLY模式
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                // 子View指定了具体尺寸
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // 子View是match_parent
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // 子View是wrap_content
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
            
        // 父容器是AT_MOST模式
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
            
        // 父容器是UNSPECIFIED模式
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
    }
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

这个方法的逻辑可以用下面的表格总结:

父容器模式 子View的LayoutParams 子View的MeasureSpec
EXACTLY 具体数值(100dp) EXACTLY + 100dp
EXACTLY match_parent EXACTLY + 父容器可用尺寸
EXACTLY wrap_content AT_MOST + 父容器可用尺寸
AT_MOST match_parent AT_MOST + 父容器可用尺寸
AT_MOST wrap_content AT_MOST + 父容器可用尺寸
UNSPECIFIED match_parent UNSPECIFIED + 0
UNSPECIFIED wrap_content UNSPECIFIED + 0

3.3 View的measure过程:从measure()到onMeasure()

让我们看看View的measure()方法到底做了什么:

java 复制代码
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    // 1. 检查是否需要重新测量(性能优化)
    if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
            widthMeasureSpec != mOldWidthMeasureSpec ||
            heightMeasureSpec != mOldHeightMeasureSpec) {
        
        // 2. 清除"已测量"标志
        mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_CHANGED;
        
        // 3. 回调onMeasure,让子类实现测量逻辑
        onMeasure(widthMeasureSpec, heightMeasureSpec);
        
        // 4. 检查子类是否调用了setMeasuredDimension()
        if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_CHANGED) != PFLAG_MEASURED_DIMENSION_CHANGED) {
            throw new IllegalStateException(
                getClass().getName() + " did not call setMeasuredDimension()");
        }
        
        // 5. 设置"需要布局"标志
        mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
    }
    
    // 6. 保存当前的MeasureSpec
    mOldWidthMeasureSpec = widthMeasureSpec;
    mOldHeightMeasureSpec = heightMeasureSpec;
}

关键点:

  1. measure()是final方法,不能重写
  2. 测量有缓存机制,MeasureSpec不变时不会重新测量
  3. 必须在onMeasure中调用setMeasuredDimension(),否则会抛出异常

3.4 正确的onMeasure实现:处理所有测量模式

下面是一个正确处理所有测量模式的完整示例:

java 复制代码
public class CorrectView extends View {
    private Paint mPaint;
    private String mText = "Hello Custom View";
    
    public CorrectView(Context context) {
        super(context);
        init();
    }
    
    private void init() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setTextSize(50);
        mPaint.setColor(Color.BLACK);
    }
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 步骤1:解析MeasureSpec
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        
        // 步骤2:计算内容需要的尺寸(不包含padding)
        int desiredWidth = calculateDesiredWidth();
        int desiredHeight = calculateDesiredHeight();
        
        // 步骤3:考虑padding
        int widthWithPadding = desiredWidth + getPaddingLeft() + getPaddingRight();
        int heightWithPadding = desiredHeight + getPaddingTop() + getPaddingBottom();
        
        // 步骤4:根据测量模式确定最终尺寸
        int finalWidth = resolveSize(widthWithPadding, widthMeasureSpec);
        int finalHeight = resolveSize(heightWithPadding, heightMeasureSpec);
        
        // 步骤5:必须调用!
        setMeasuredDimension(finalWidth, finalHeight);
    }
    
    private int calculateDesiredWidth() {
        // 计算文本宽度
        return (int) mPaint.measureText(mText);
    }
    
    private int calculateDesiredHeight() {
        // 计算文本高度
        Paint.FontMetrics fm = mPaint.getFontMetrics();
        return (int) (fm.bottom - fm.top);
    }
    
    // 通用的尺寸解析方法(Android源码中的实现)
    public static int resolveSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        
        switch (specMode) {
            case MeasureSpec.UNSPECIFIED:
                result = size;
                break;
            case MeasureSpec.AT_MOST:
                result = Math.min(size, specSize);
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
        }
        return result;
    }
    
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 绘制时要考虑padding
        canvas.drawText(mText, 
            getPaddingLeft(), 
            getPaddingTop() - mPaint.ascent(), 
            mPaint);
    }
}

3.5 ViewGroup的测量:测量所有子View

对于ViewGroup,测量过程更加复杂,因为需要测量所有子View:

java 复制代码
public abstract class ViewGroup extends View {
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 1. 测量所有子View
        for (int i = 0; i < getChildCount(); i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                // 测量每个子View
                measureChildWithMargins(child, 
                    widthMeasureSpec, 0, 
                    heightMeasureSpec, 0);
            }
        }
        
        // 2. 根据子View的测量结果确定自己的尺寸
        int width = calculateTotalWidth();
        int height = calculateTotalHeight();
        
        // 3. 考虑自身的padding
        width += getPaddingLeft() + getPaddingRight();
        height += getPaddingTop() + getPaddingBottom();
        
        // 4. 处理测量模式限制
        width = resolveSizeAndState(width, widthMeasureSpec, 0);
        height = resolveSizeAndState(height, heightMeasureSpec, 0);
        
        // 5. 设置最终尺寸
        setMeasuredDimension(width, height);
    }
    
    // 这个方法非常重要!它考虑子View的margin
    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        
        // 为子View创建MeasureSpec(考虑margin)
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);
        
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
}

3.6 实战:实现一个简单的LinearLayout

让我们通过实现一个简化版的LinearLayout来加深理解:

java 复制代码
public class SimpleLinearLayout extends ViewGroup {
    private static final int VERTICAL = 0;
    private static final int HORIZONTAL = 1;
    
    private int orientation = VERTICAL;
    
    public SimpleLinearLayout(Context context) {
        super(context);
    }
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (orientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }
    
    private void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        
        // 可用宽度 = 父容器宽度 - padding
        int availableWidth = widthSize - getPaddingLeft() - getPaddingRight();
        
        int totalHeight = getPaddingTop() + getPaddingBottom();
        int maxChildWidth = 0;
        
        // 第一遍:测量所有子View
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            if (child.getVisibility() == GONE) {
                continue;
            }
            
            // 测量子View
            measureChildWithMargins(child, 
                widthMeasureSpec, 0,
                heightMeasureSpec, totalHeight);
            
            MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
            
            // 更新最大子View宽度
            maxChildWidth = Math.max(maxChildWidth,
                child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
            
            // 累加高度(包括margin)
            totalHeight += child.getMeasuredHeight() + 
                          lp.topMargin + lp.bottomMargin;
        }
        
        // 确定自己的宽度
        int width;
        if (widthMode == MeasureSpec.EXACTLY) {
            width = widthSize;
        } else {
            width = maxChildWidth + getPaddingLeft() + getPaddingRight();
            if (widthMode == MeasureSpec.AT_MOST) {
                width = Math.min(width, widthSize);
            }
        }
        
        // 确定自己的高度
        int height;
        if (heightMode == MeasureSpec.EXACTLY) {
            height = heightSize;
        } else {
            height = totalHeight;
            if (heightMode == MeasureSpec.AT_MOST) {
                height = Math.min(height, heightSize);
            }
        }
        
        setMeasuredDimension(width, height);
    }
    
    @Override
    protected LayoutParams generateLayoutParams(LayoutParams p) {
        return new MarginLayoutParams(p);
    }
}

3.7 测量优化:避免不必要的测量

测量是性能敏感的操作,特别是在列表或滚动视图中。以下是一些优化技巧:

java 复制代码
public class OptimizedView extends View {
    private int mCachedWidthMeasureSpec = Integer.MIN_VALUE;
    private int mCachedHeightMeasureSpec = Integer.MIN_VALUE;
    private int mCachedMeasuredWidth;
    private int mCachedMeasuredHeight;
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 如果MeasureSpec没有变化,使用缓存结果
        if (widthMeasureSpec == mCachedWidthMeasureSpec &&
            heightMeasureSpec == mCachedHeightMeasureSpec) {
            setMeasuredDimension(mCachedMeasuredWidth, mCachedMeasuredHeight);
            return;
        }
        
        // 重新计算尺寸
        int width = calculateWidth(widthMeasureSpec);
        int height = calculateHeight(heightMeasureSpec);
        
        // 更新缓存
        mCachedWidthMeasureSpec = widthMeasureSpec;
        mCachedHeightMeasureSpec = heightMeasureSpec;
        mCachedMeasuredWidth = width;
        mCachedMeasuredHeight = height;
        
        setMeasuredDimension(width, height);
    }
    
    @Override
    public void requestLayout() {
        // 清除缓存,强制重新测量
        mCachedWidthMeasureSpec = Integer.MIN_VALUE;
        mCachedHeightMeasureSpec = Integer.MIN_VALUE;
        super.requestLayout();
    }
}

四、布局(layout)过程深度解析

4.1 layout()方法:确定View的位置

测量完成后,接下来就是布局。layout()方法确定View在其父容器中的位置。

java 复制代码
public void layout(int l, int t, int r, int b) {
    // 记录旧的边界
    int oldL = mLeft;
    int oldT = mTop;
    int oldR = mRight;
    int oldB = mBottom;
    
    // 判断布局是否改变
    boolean changed = setFrame(l, t, r, b);
    
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        // 调用onLayout
        onLayout(changed, l, t, r, b);
        
        // 清除需要布局的标志
        mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
        
        // 通知布局变化监听器
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnLayoutChangeListeners != null) {
            ArrayList<OnLayoutChangeListener> listenersCopy =
                    (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
            int numListeners = listenersCopy.size();
            for (int i = 0; i < numListeners; ++i) {
                listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
            }
        }
    }
}

关键点:

  1. setFrame()方法实际设置View的四个边界值
  2. onLayout()在布局改变或需要布局时被调用
  3. 可以注册OnLayoutChangeListener监听布局变化

4.2 View的onLayout:单个View不需要布局子View

对于单个View(非ViewGroup),onLayout()是空实现,因为它没有子View需要布局:

java 复制代码
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    // 单个View不需要布局子View
}

4.3 ViewGroup的onLayout:布局所有子View

对于ViewGroup,必须实现onLayout()来布局所有子View:

java 复制代码
public class SimpleLayout extends ViewGroup {
    
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        // 获取padding
        int paddingLeft = getPaddingLeft();
        int paddingTop = getPaddingTop();
        
        int currentLeft = paddingLeft;
        int currentTop = paddingTop;
        
        // 布局每个子View
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            
            if (child.getVisibility() == GONE) {
                continue;
            }
            
            // 获取子View的测量尺寸
            int childWidth = child.getMeasuredWidth();
            int childHeight = child.getMeasuredHeight();
            
            // 布局子View
            child.layout(currentLeft, 
                        currentTop, 
                        currentLeft + childWidth, 
                        currentTop + childHeight);
            
            // 更新下一个View的位置
            currentLeft += childWidth;
        }
    }
}

4.4 支持margin的完整布局实现

实际开发中,我们需要考虑子View的margin:

java 复制代码
public class MarginSupportLayout extends ViewGroup {
    
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int currentLeft = getPaddingLeft();
        int currentTop = getPaddingTop();
        
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            
            if (child.getVisibility() == GONE) {
                continue;
            }
            
            MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
            
            // 考虑margin计算子View的位置
            int childLeft = currentLeft + lp.leftMargin;
            int childTop = currentTop + lp.topMargin;
            int childRight = childLeft + child.getMeasuredWidth();
            int childBottom = childTop + child.getMeasuredHeight();
            
            // 布局子View
            child.layout(childLeft, childTop, childRight, childBottom);
            
            // 更新位置,考虑rightMargin
            currentLeft = childRight + lp.rightMargin;
        }
    }
    
    @Override
    protected LayoutParams generateLayoutParams(LayoutParams p) {
        return new MarginLayoutParams(p);
    }
    
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }
}

4.5 测量与布局的协作:优化性能

聪明的开发者会在测量阶段就计算好布局信息,这样在布局阶段就可以直接使用:

java 复制代码
public class SmartLayout extends ViewGroup {
    // 在测量阶段就计算好子View的位置
    private int[] childPositions;
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int childCount = getChildCount();
        childPositions = new int[childCount * 4]; // 每个子View存储left, top, right, bottom
        
        int totalWidth = getPaddingLeft() + getPaddingRight();
        int maxHeight = 0;
        
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() == GONE) {
                continue;
            }
            
            // 测量子View
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            
            // 计算并存储位置
            childPositions[i*4] = totalWidth; // left
            childPositions[i*4+1] = getPaddingTop(); // top
            childPositions[i*4+2] = totalWidth + child.getMeasuredWidth(); // right
            childPositions[i*4+3] = getPaddingTop() + child.getMeasuredHeight(); // bottom
            
            totalWidth += child.getMeasuredWidth();
            maxHeight = Math.max(maxHeight, child.getMeasuredHeight());
        }
        
        // 考虑padding设置自身尺寸
        int height = maxHeight + getPaddingTop() + getPaddingBottom();
        
        setMeasuredDimension(
            resolveSize(totalWidth, widthMeasureSpec),
            resolveSize(height, heightMeasureSpec)
        );
    }
    
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        // 直接使用测量阶段计算好的位置
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                child.layout(
                    childPositions[i*4],
                    childPositions[i*4+1],
                    childPositions[i*4+2],
                    childPositions[i*4+3]
                );
            }
        }
    }
}

五、绘制(draw)过程深度解析

5.1 draw()方法的六个步骤

View的draw()方法实际上执行了六个步骤:

java 复制代码
public void draw(Canvas canvas) {
    // Step 1: 绘制背景
    drawBackground(canvas);
    
    // Step 2: 保存图层(如果需要特殊效果)
    int saveCount = 0;
    if (!dirtyOpaque) {
        saveCount = canvas.saveLayer(...);
    }
    
    // Step 3: 绘制View自身内容
    onDraw(canvas);
    
    // Step 4: 绘制子View
    dispatchDraw(canvas);
    
    // Step 5: 绘制装饰(如滚动条、前景)
    onDrawForeground(canvas);
    
    // Step 6: 恢复图层
    if (!dirtyOpaque) {
        canvas.restoreToCount(saveCount);
    }
}

5.2 onDraw():绘制View的内容

对于自定义View,最常重写的就是onDraw()方法:

java 复制代码
public class CustomCircleView extends View {
    private Paint mPaint;
    private int mCircleColor = Color.RED;
    private int mStrokeColor = Color.BLUE;
    private int mStrokeWidth = 10;
    
    public CustomCircleView(Context context) {
        super(context);
        init();
    }
    
    private void init() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    }
    
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        
        int centerX = getWidth() / 2;
        int centerY = getHeight() / 2;
        int radius = Math.min(centerX, centerY) - mStrokeWidth;
        
        // 1. 绘制背景(如果需要)
        canvas.drawColor(Color.WHITE);
        
        // 2. 绘制圆形填充
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(mCircleColor);
        canvas.drawCircle(centerX, centerY, radius, mPaint);
        
        // 3. 绘制圆形边框
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(mStrokeWidth);
        mPaint.setColor(mStrokeColor);
        canvas.drawCircle(centerX, centerY, radius, mPaint);
        
        // 4. 绘制文字
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setTextSize(40);
        mPaint.setTextAlign(Paint.Align.CENTER);
        canvas.drawText("Hello", centerX, centerY, mPaint);
    }
}

5.3 dispatchDraw():绘制子View

对于ViewGroup,还需要绘制子View,这是通过dispatchDraw()实现的:

java 复制代码
public abstract class ViewGroup extends View {
    @Override
    protected void dispatchDraw(Canvas canvas) {
        // 按子View的绘制顺序进行绘制
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                // 绘制子View
                drawChild(canvas, child, drawingTime);
            }
        }
    }
}

5.4 绘制顺序的重要性

绘制顺序决定了哪些内容在上层,哪些在下层:

java 复制代码
@Override
protected void onDraw(Canvas canvas) {
    // 错误的绘制顺序示例
    mPaint.setColor(Color.RED);
    canvas.drawText("Hello", 100, 100, mPaint);  // 文字先绘制
    
    mPaint.setColor(Color.BLUE);
    canvas.drawRect(50, 50, 150, 150, mPaint);  // 矩形后绘制,会覆盖文字
    
    // 正确的绘制顺序应该是:
    // 1. 先绘制底层内容
    canvas.drawRect(50, 50, 150, 150, mPaint);
    
    // 2. 再绘制上层内容
    canvas.drawText("Hello", 100, 100, mPaint);
}

5.5 避免过度绘制的技巧

过度绘制是Android UI性能的常见杀手。以下是一些优化技巧:

技巧1:使用clipRect限制绘制区域

java 复制代码
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    
    // 保存画布状态
    canvas.save();
    
    // 只绘制可见区域,避免绘制不可见的部分
    canvas.clipRect(mVisibleRect);
    
    // 执行复杂的绘制操作
    drawComplexContent(canvas);
    
    // 恢复画布状态
    canvas.restore();
}

技巧2:快速拒绝不可见区域

java 复制代码
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    
    // 快速拒绝:如果View完全不可见
    if (getWidth() == 0 || getHeight() == 0) {
        return;
    }
    
    // 快速拒绝:如果脏区域与内容区域不相交
    Rect dirtyRect = new Rect();
    canvas.getClipBounds(dirtyRect);
    
    if (!Rect.intersects(dirtyRect, mContentBounds)) {
        return;
    }
    
    // 只绘制需要更新的部分
    drawPartialContent(canvas, dirtyRect);
}

技巧3:使用Canvas的快速绘制方法

java 复制代码
@Override
protected void onDraw(Canvas canvas) {
    // 使用这些方法比单独绘制多个形状更快
    canvas.drawRect(mRectArray, mPaint);
    canvas.drawLines(mLineArray, mPaint);
    canvas.drawPoints(mPointArray, mPaint);
    
    // 批量绘制文本
    canvas.drawText(mText, mStart, mEnd, x, y, mPaint);
}

六、综合实战:实现一个Tag流式布局

现在,让我们把学到的知识综合起来,实现一个完整的、生产可用的TagFlowLayout。

6.1 需求分析

  • 标签自动换行
  • 可设置水平和垂直间距
  • 支持最大行数限制
  • 支持标签点击效果
  • 良好的性能表现

6.2 完整实现

java 复制代码
/**
 * 流式标签布局
 * 支持自动换行、间距设置、最大行数限制
 */
public class TagFlowLayout extends ViewGroup {
    private static final int DEFAULT_HORIZONTAL_SPACING = dp2px(8);
    private static final int DEFAULT_VERTICAL_SPACING = dp2px(8);
    private static final int DEFAULT_MAX_LINES = Integer.MAX_VALUE;
    
    private int horizontalSpacing = DEFAULT_HORIZONTAL_SPACING;
    private int verticalSpacing = DEFAULT_VERTICAL_SPACING;
    private int maxLines = DEFAULT_MAX_LINES;
    
    // 存储每行的View和行高
    private final List<List<View>> lines = new ArrayList<>();
    private final List<Integer> lineHeights = new ArrayList<>();
    private final List<Integer> lineWidths = new ArrayList<>();
    
    public TagFlowLayout(Context context) {
        this(context, null);
    }
    
    public TagFlowLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    
    public TagFlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs, defStyleAttr);
    }
    
    private void init(Context context, AttributeSet attrs, int defStyleAttr) {
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TagFlowLayout, 
                defStyleAttr, 0);
        
        try {
            horizontalSpacing = ta.getDimensionPixelSize(
                R.styleable.TagFlowLayout_horizontalSpacing, DEFAULT_HORIZONTAL_SPACING);
            verticalSpacing = ta.getDimensionPixelSize(
                R.styleable.TagFlowLayout_verticalSpacing, DEFAULT_VERTICAL_SPACING);
            maxLines = ta.getInt(R.styleable.TagFlowLayout_maxLines, DEFAULT_MAX_LINES);
        } finally {
            ta.recycle();
        }
    }
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 清空之前的数据
        lines.clear();
        lineHeights.clear();
        lineWidths.clear();
        
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        
        // 计算可用宽度
        int availableWidth = widthSize - getPaddingLeft() - getPaddingRight();
        if (availableWidth <= 0) {
            availableWidth = widthSize;
        }
        
        // 当前行的数据
        List<View> currentLine = new ArrayList<>();
        int currentLineWidth = 0;
        int currentLineHeight = 0;
        int totalHeight = 0;
        int lineCount = 0;
        
        // 遍历所有子View
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            
            if (child.getVisibility() == GONE) {
                continue;
            }
            
            // 测量子View
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            
            MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
            
            int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
            
            // 判断是否需要换行
            boolean needNewLine = !currentLine.isEmpty() && 
                currentLineWidth + childWidth + horizontalSpacing > availableWidth;
            
            if (needNewLine) {
                // 保存当前行
                saveCurrentLine(currentLine, currentLineWidth, currentLineHeight);
                lineCount++;
                
                // 检查是否超过最大行数
                if (lineCount >= maxLines) {
                    break;
                }
                
                // 开始新的一行
                currentLine.clear();
                currentLineWidth = 0;
                currentLineHeight = 0;
            }
            
            // 添加子View到当前行
            currentLine.add(child);
            if (currentLine.isEmpty()) {
                currentLineWidth = childWidth;
            } else {
                currentLineWidth += childWidth + horizontalSpacing;
            }
            currentLineHeight = Math.max(currentLineHeight, childHeight);
        }
        
        // 保存最后一行
        if (!currentLine.isEmpty() && lineCount < maxLines) {
            saveCurrentLine(currentLine, currentLineWidth, currentLineHeight);
            lineCount++;
        }
        
        // 计算总高度
        totalHeight = calculateTotalHeight();
        
        // 确定最终尺寸
        int measuredWidth = resolveSize(widthSize, widthMeasureSpec);
        int measuredHeight = resolveSize(totalHeight, heightMeasureSpec);
        
        setMeasuredDimension(measuredWidth, measuredHeight);
    }
    
    private void saveCurrentLine(List<View> line, int width, int height) {
        lines.add(new ArrayList<>(line));
        lineWidths.add(width);
        lineHeights.add(height);
    }
    
    private int calculateTotalHeight() {
        if (lineHeights.isEmpty()) {
            return getPaddingTop() + getPaddingBottom();
        }
        
        int totalHeight = getPaddingTop() + getPaddingBottom();
        
        for (int i = 0; i < lineHeights.size(); i++) {
            totalHeight += lineHeights.get(i);
            if (i > 0) {
                totalHeight += verticalSpacing;
            }
        }
        
        return totalHeight;
    }
    
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int leftStart = getPaddingLeft();
        int topStart = getPaddingTop();
        
        // 遍历每一行
        for (int i = 0; i < lines.size(); i++) {
            List<View> line = lines.get(i);
            int lineHeight = lineHeights.get(i);
            int lineWidth = lineWidths.get(i);
            
            // 计算行内对齐(居左、居中、居右)
            int currentLeft = calculateLineStartLeft(leftStart, lineWidth);
            
            // 布局当前行的每个View
            for (View child : line) {
                MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
                
                int childLeft = currentLeft + lp.leftMargin;
                int childTop = topStart + lp.topMargin;
                int childRight = childLeft + child.getMeasuredWidth();
                int childBottom = childTop + child.getMeasuredHeight();
                
                child.layout(childLeft, childTop, childRight, childBottom);
                
                currentLeft = childRight + lp.rightMargin + horizontalSpacing;
            }
            
            // 更新下一行的起始top
            topStart += lineHeight + verticalSpacing;
        }
    }
    
    private int calculateLineStartLeft(int leftStart, int lineWidth) {
        // 可以根据需要实现不同的对齐方式
        // 这里实现居左对齐
        return leftStart;
        
        // 居中对齐的实现:
        // return leftStart + (getWidth() - getPaddingLeft() - getPaddingRight() - lineWidth) / 2;
        
        // 居右对齐的实现:
        // return getWidth() - getPaddingRight() - lineWidth;
    }
    
    @Override
    protected LayoutParams generateLayoutParams(LayoutParams p) {
        return new MarginLayoutParams(p);
    }
    
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }
    
    // 添加标签的方法
    public void addTag(String text) {
        TextView tagView = new TextView(getContext());
        tagView.setText(text);
        tagView.setBackgroundResource(R.drawable.tag_background);
        tagView.setPadding(dp2px(8), dp2px(4), dp2px(8), dp2px(4));
        tagView.setOnClickListener(v -> {
            // 处理标签点击
            onTagClick(text);
        });
        
        addView(tagView);
    }
    
    private void onTagClick(String tag) {
        // 标签点击回调
        if (listener != null) {
            listener.onTagClick(tag);
        }
    }
    
    public interface OnTagClickListener {
        void onTagClick(String tag);
    }
    
    private OnTagClickListener listener;
    
    public void setOnTagClickListener(OnTagClickListener listener) {
        this.listener = listener;
    }
    
    private static int dp2px(float dp) {
        return (int) TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_DIP, dp, 
            Resources.getSystem().getDisplayMetrics());
    }
}

6.3 属性定义(attrs.xml)

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="TagFlowLayout">
        <attr name="horizontalSpacing" format="dimension" />
        <attr name="verticalSpacing" format="dimension" />
        <attr name="maxLines" format="integer" />
    </declare-styleable>
</resources>

6.4 使用示例

xml 复制代码
<com.example.view.TagFlowLayout
    android:id="@+id/tag_flow_layout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="16dp"
    app:horizontalSpacing="8dp"
    app:verticalSpacing="8dp"
    app:maxLines="3" />
java 复制代码
// 在Activity或Fragment中使用
TagFlowLayout tagLayout = findViewById(R.id.tag_flow_layout);
tagLayout.setOnTagClickListener(tag -> {
    Toast.makeText(this, "点击了: " + tag, Toast.LENGTH_SHORT).show();
});

// 添加标签
String[] tags = {"Android", "Java", "Kotlin", "Flutter", "React Native", 
                 "iOS", "Python", "机器学习", "人工智能"};
for (String tag : tags) {
    tagLayout.addTag(tag);
}

七、性能优化最佳实践

7.1 测量优化策略

java 复制代码
public class PerformanceView extends View {
    // 缓存测量结果
    private int cachedWidth;
    private int cachedHeight;
    private int cachedWidthMeasureSpec = Integer.MIN_VALUE;
    private int cachedHeightMeasureSpec = Integer.MIN_VALUE;
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 使用缓存避免重复测量
        if (widthMeasureSpec == cachedWidthMeasureSpec && 
            heightMeasureSpec == cachedHeightMeasureSpec) {
            setMeasuredDimension(cachedWidth, cachedHeight);
            return;
        }
        
        // 实际测量逻辑
        int width = measureWidth(widthMeasureSpec);
        int height = measureHeight(heightMeasureSpec);
        
        // 更新缓存
        cachedWidth = width;
        cachedHeight = height;
        cachedWidthMeasureSpec = widthMeasureSpec;
        cachedHeightMeasureSpec = heightMeasureSpec;
        
        setMeasuredDimension(width, height);
    }
    
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        // 尺寸变化时清除缓存
        cachedWidthMeasureSpec = Integer.MIN_VALUE;
        cachedHeightMeasureSpec = Integer.MIN_VALUE;
    }
}

7.2 绘制优化策略

java 复制代码
public class OptimizedDrawView extends View {
    private Bitmap cacheBitmap;
    private Canvas cacheCanvas;
    private boolean cacheDirty = true;
    
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (w > 0 && h > 0) {
            // 创建缓存Bitmap
            cacheBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
            cacheCanvas = new Canvas(cacheBitmap);
            cacheDirty = true;
        }
    }
    
    @Override
    protected void onDraw(Canvas canvas) {
        if (cacheDirty) {
            // 只在需要时重新绘制到缓存
            drawToCache(cacheCanvas);
            cacheDirty = false;
        }
        
        // 绘制缓存Bitmap
        canvas.drawBitmap(cacheBitmap, 0, 0, null);
    }
    
    private void drawToCache(Canvas canvas) {
        // 复杂的绘制操作
        long start = System.currentTimeMillis();
        
        // ... 绘制逻辑
        
        long duration = System.currentTimeMillis() - start;
        Log.d("OptimizedDrawView", "缓存绘制耗时: " + duration + "ms");
    }
    
    public void invalidateCache() {
        cacheDirty = true;
        invalidate();
    }
}

7.3 使用硬件加速

java 复制代码
public class HardwareAcceleratedView extends View {
    public HardwareAcceleratedView(Context context) {
        super(context);
        // 启用硬件加速
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            setLayerType(LAYER_TYPE_HARDWARE, null);
        }
    }
    
    @Override
    protected void onDraw(Canvas canvas) {
        // 硬件加速支持的绘制操作(性能更好)
        canvas.drawRect(0, 0, 100, 100, paint);
        canvas.drawCircle(150, 50, 50, paint);
        
        // 硬件加速不支持的绘制操作(性能较差)
        // canvas.drawPath(complexPath, paint); // 复杂Path
        // canvas.clipPath(path); // 非矩形裁剪
    }
    
    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        // 释放资源
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            setLayerType(LAYER_TYPE_NONE, null);
        }
    }
}

八、调试技巧与工具

8.1 添加调试信息

java 复制代码
public class DebugView extends View {
    private static final boolean DEBUG = BuildConfig.DEBUG;
    private int measureCount = 0;
    private int layoutCount = 0;
    private int drawCount = 0;
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        
        if (DEBUG) {
            measureCount++;
            Log.d("DebugView", String.format(
                "onMeasure #%d: mode=%s, size=%d",
                measureCount,
                modeToString(MeasureSpec.getMode(widthMeasureSpec)),
                MeasureSpec.getSize(widthMeasureSpec)));
        }
    }
    
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        
        if (DEBUG) {
            layoutCount++;
            Log.d("DebugView", String.format(
                "onLayout #%d: [%d, %d, %d, %d] changed=%b",
                layoutCount, l, t, r, b, changed));
        }
    }
    
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        
        if (DEBUG) {
            drawCount++;
            Log.d("DebugView", "onDraw #" + drawCount);
            
            // 绘制调试信息
            Paint paint = new Paint();
            paint.setColor(Color.RED);
            paint.setTextSize(24);
            canvas.drawText("Measure: " + measureCount, 10, 30, paint);
            canvas.drawText("Layout: " + layoutCount, 10, 60, paint);
            canvas.drawText("Draw: " + drawCount, 10, 90, paint);
        }
    }
    
    private String modeToString(int mode) {
        switch (mode) {
            case MeasureSpec.UNSPECIFIED: return "UNSPECIFIED";
            case MeasureSpec.EXACTLY: return "EXACTLY";
            case MeasureSpec.AT_MOST: return "AT_MOST";
            default: return "UNKNOWN";
        }
    }
}

8.2 使用Android Studio的调试工具

  1. Layout Inspector:查看View的层次结构和属性
text 复制代码
Tools → Layout Inspector
  1. GPU渲染模式分析:检查绘制性能
text 复制代码
 开发者选项 → GPU渲染模式分析 → 在屏幕上显示为条形图
  1. Profile GPU Rendering:更详细的渲染分析
text 复制代码
Run → Profile 'app' → CPU/GPU

8.3 过度绘制检测

在开发者选项中开启"调试GPU过度绘制",颜色含义:

  • 无颜色:没有过度绘制(理想状态)
  • 蓝色:1次过度绘制(可以接受)
  • 绿色:2次过度绘制(勉强接受)
  • 粉色:3次过度绘制(需要优化)
  • 红色:4次或更多过度绘制(必须优化)

九、总结与要点回顾

9.1 三大流程的核心要点总结

流程 关键方法 主要职责 注意事项
测量 onMeasure() 确定View的尺寸 必须调用setMeasuredDimension()
布局 onLayout() 确定View的位置 必须调用子View的layout()
绘制 onDraw() 绘制View的内容 避免过度绘制,优化性能

9.2 常见问题解决方案

问题1:wrap_content不生效

原因:直接设置了固定尺寸,没有根据内容计算尺寸 解决:在onMeasure中计算内容尺寸,根据测量模式调整

问题2:测量次数过多

原因:没有正确使用测量缓存 解决:缓存测量结果,MeasureSpec不变时直接使用缓存

问题3:布局错乱

原因:没有正确处理padding和margin 解决:在测量和布局时都要考虑padding和margin

问题4:绘制性能差

原因:过度绘制或复杂的绘制操作 解决:使用clipRect限制绘制区域,启用硬件加速

9.3 性能优化检查清单

  • 是否正确处理了wrap_content?
  • 是否考虑了padding和margin?
  • 是否使用了测量缓存?
  • 是否避免了不必要的测量?
  • 是否使用clipRect限制绘制区域?
  • 是否启用了硬件加速?
  • 是否避免了过度绘制?
  • 是否在onDraw中创建了新对象?

十、扩展学习

10.1 推荐阅读

  1. Android源码

    • View.java - 查看measure、layout、draw的实现
    • ViewGroup.java - 学习ViewGroup的测量和布局逻辑
    • FrameLayout.java、LinearLayout.java - 学习系统布局的实现
  2. 官方文档

  3. 开源项目

10.2 下一步学习方向

  1. 事件分发机制

    • 理解onInterceptTouchEvent和onTouchEvent
    • 学习多点触控和手势识别
  2. 属性动画

    • 掌握ValueAnimator和ObjectAnimator
    • 学习自定义插值器和估值器
  3. 高级绘制技术

    • 学习Path的高级用法
    • 掌握Shader和Xfermode
    • 了解SurfaceView和TextureView

10.3 思考题

  1. 问题:如果一个View同时设置了layout_width="wrap_content"和minWidth属性,测量过程如何处理?
  2. 问题:如何实现一个支持权重(weight)的自定义LinearLayout?
  3. 问题:在RecyclerView中使用自定义View时,测量有哪些特别需要注意的地方?
  4. 挑战:实现一个自定义ViewGroup,要求子View可以重叠,且后添加的View显示在上层。

实践建议:

  1. 从简单的自定义View开始,逐步增加复杂度
  2. 使用View的post方法延迟操作,避免性能问题
  3. 为自定义View编写单元测试
  4. 在不同尺寸和密度的设备上测试你的View

记住:理解原理比记住API更重要。当你真正理解了Android View系统的工作原理,你就能创造出既美观又高性能的自定义View。


如果你在实现过程中遇到问题,或者有更好的实现方案,欢迎在评论区交流讨论。让我们一起进步,成为Android UI开发专家!

相关推荐
小麦在野1 小时前
独立 App 开发系列:用 GitHub Pages 托管隐私政策和用户协议
android·github
silianpan1 小时前
Office 文档预览 UTS 插件
android·微信小程序·harmonyos
明君6892 小时前
一套专注 Android 逆向分析的 Skills
android·android逆向·agents·skills
stone20352 小时前
造一个"录制回放 + LLM 自主操作手机"的 Android 自动化测试平台(一)整体架构与录制引擎
android
事圆则缓2 小时前
Android 声明式 API 的翻译过程
android
mmsx2 小时前
MapLibre 实战 13|让比例尺显示 100m 而不是 347.2m:屏幕距离换算与两个易错点
android·前端·app
邪修king2 小时前
Re:Linux 系统篇(三十一):库的制作与原理Chapter2:静态链接与程序加载 —— 从磁盘 ELF 到运行中进程的完整旅程
android·linux·运维·开发语言
邪修king4 小时前
Re:Linux 系统篇(二十九):动静态库Chapter2:动态库深度辨析 —— 核心本质、制作流程、双阶段查找模型与排错指南
android·java·linux·开发语言