【Android】简单理解View的绘制流程

测量Measure

首先,可以明确知道一个Activity的根View是DecorView,而DecorView extends FramLayout extends ViewGroup extends View。根据源码追踪,发现最先调用的是View.measure()

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    ...
    if (forceLayout || needsLayout) {
        ...
        if (cacheIndex < 0 || sIgnoreMeasureCache) {
            // measure ourselves, this should set the measured dimension flag back
            onMeasure(widthMeasureSpec, heightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        } else {
            ...
        }
        ...
    }
    ...
}

首先注意到这个方法是被final修饰的,这意味着开发者无法重写该方法。但是可以看到,实际的测量方法是在onMeasure()中的。

View.onMeasure()

这个时候查看ViewGroup源码发现没有重写onMeasure(),但是在FramLayout中对这个方法进行了重写。想想也是,毕竟不同的控件对测量的方式肯定是不同的。但是在看FramLayout.onMeasure()之前,我们需要先看看默认的测量是怎么样的。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(
            getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

/**
* 如果父view给定了size,那么就用父view的。
* 否则就用最小值(minHeight/minWidth)或者背景的宽高。
*/
public static int getDefaultSize(int size, int measureSpec) {   
    int result = size;// 最小值或者背景的最小值        
    int specMode = MeasureSpec.getMode(measureSpec);       
    int specSize = MeasureSpec.getSize(measureSpec);       
    switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            // 父view没有任何限制
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            // 父view给了准确值或者知道父view给定的最大值
            result = specSize;
            break;
        }
    return result;
}

/**
* 有背景就找出背景和minWidth中最大的值,没有就是用minWidth
* getSuggestedMinimumHeight()一样的逻辑,这里就不贴了
*/
protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

/**
* 这个方法简单的理解为对mMeasuredWidth和mMeasuredHeight这两个变量赋值即可
*/
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
       
    boolean optical = isLayoutModeOptical(this);    
    if (optical != isLayoutModeOptical(mParent)) {    
        Insets insets = getOpticalInsets();        
        int opticalWidth  = insets.left + insets.right;        
        int opticalHeight = insets.top  + insets.bottom;        
        
        measuredWidth  += optical ? opticalWidth  : -opticalWidth;        
        measuredHeight += optical ? opticalHeight : -opticalHeight;        
    }    
    setMeasuredDimensionRaw(measuredWidth, measuredHeight);    
}

逻辑很简单,都在代码中说明了。很明显,这种简单的逻辑对于一些稍微复杂点的控件来说是不够用的,尤其是容器类控件(ViewGroup)。如FramLayout的测量逻辑肯定和LinearLayout不一样。FramLayout应该使用的是子view中最大的宽高,而LinearLayout应该会根据水平或垂直进行子view宽累加或高累加。

FrameLayout.onMeasure()

前面说过,一个Activity的根View是DecorView extends FramLayout,那么下面简单看看FramLayout如何进行测量的。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int count = getChildCount();

    // 是否要测量出当前FrameLayout大小后再告诉Match_Parent的子view,
    // 这里要求当前FrameLayout的父view 不能给到准确的大小。
    // 因为如果能确认大小,那么MatchParent的子view也能确认大小了
    final boolean measureMatchParentChildren =
            MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
            MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
    mMatchParentChildren.clear();

    // 记录最大的宽高
    int maxHeight = 0;
    int maxWidth = 0;
    int childState = 0;

    // 遍历子view
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        // 要求子view不能为gone
        if (mMeasureAllChildren || child.getVisibility() != GONE) {
            // 生成给子view的建议大小和测量模式,并传递给子view,让子view测量自身
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            // 记录当前所测量出的最大值
            maxWidth = Math.max(maxWidth,
                    child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
            maxHeight = Math.max(maxHeight,
                    child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);

            childState = combineMeasuredStates(childState, child.getMeasuredState());

            // 把MATCH_PARENT的子view记录下来
            if (measureMatchParentChildren) {
                if (lp.width == LayoutParams.MATCH_PARENT ||
                        lp.height == LayoutParams.MATCH_PARENT) {
                    mMatchParentChildren.add(child);
                }
            }
        }
    }

    // 累加外边距、内边距等等,确定最后的大小
    ...省略累加的代码

    // 表示自己已经测量完毕了,一定要调用这个方法设置最终的测量结果
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            resolveSizeAndState(maxHeight, heightMeasureSpec,
                    childState << MEASURED_HEIGHT_STATE_SHIFT));

    // 通过前面的第一次遍历,计算完最终的大小后,对需要MATCH_PARENT
    // 的子view传入确定值
    count = mMatchParentChildren.size();
    if (count > 1) {
        for (int i = 0; i < count; i++) {
            // 这里就是告诉MATCH_PARENT的子view,能使用的最大值是多少
        }
    }
}

整个流程是这样的:

  • 遍历子view,获取子view的LayoutParam,然后再根据自身的父view传来的测量建议,生成对子view的测量建议,并传递给子view,让子view进行自身测量。如果遇到有MATCH_PARENT,则先记录,等测量完其他的子view后再告诉这些子view大小。
  • 累加自身的内边距、外边距、前景Drawable等,确定自身的大小,完成测量。
  • 告诉MATCH_PAREN的子view最终的大小是多少。

这里面,需要关注的是如何生成给子view的测量建议。首先是调用到measureChildWithMargins()

ViewGroup.measureChildWithMargins()

protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

    // 生成给子view的建议
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);

    // 传递测量流程
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

方法很短,完成两件事:

  1. 调用childWidthMeasureSpec()生成给子view的测量建议
  2. 将测量建议传递给子view,让其完成自身的测量。

下面看看,整个测量流程中,最关键的一个方法,如何生成给子view的测量建议

ViewGroup.getChildMeasureSpec()

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    // 解析出自身的测量模式和值
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);

    int size = Math.max(0, specSize - padding);

    // 最终传递给子view的测量模式和值
    int resultSize = 0;
    int resultMode = 0;

    switch (specMode) {
    // Parent has imposed an exact size on us
        // 1. 本身的大小是精确值,如100dp
    case MeasureSpec.EXACTLY:
        if (childDimension >= 0) {
            // 1.1 子view有确定值,那么不管父view多大,子view就是设置的大小
            //     虽然有大小,但是不一定能显示出来,因为可能超过父view的大小了
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size. So be it.
            // 1.2 子view想要充满父view,由于父view的大小确定了,那么子view大小也确定了
            //     因此此时子view的测量模式为EXACTLY,精确值
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can't be
            // bigger than us.
            // 1.3 子view想要根据自身的内容来确定大小,但是由于父view大小已确定
            //     子view无论内容多大,最大也就父view的大小,所以先设置子view为
            //     父view大小,但是测量模式允许在大小的范围内进行变动,AT_MOST
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent has imposed a maximum size on us
        // 2. 本身的大小未知,但是知道最大值,即size为最大值
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            // 2.1 子view有确定值,那么不管父view多大,子view就是设置的大小
            //     虽然有大小,但是不一定能显示出来,因为可能超过父view的大小了
            // Child wants a specific size... so be it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size, but our size is not fixed.
            // Constrain child to not be bigger than us.
            // 2.2 子view想充满父viwe,但是由于目前父view也不确定到底多大
            //     子view自然也无法确定,但是父view知道自己最大是多少,
            //     因此子view的最大值也能确定
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can't be
            // bigger than us.
            // 2.3 子view想根据自己的内容去决定大小,但是子view无论多大,都
            //     不能超过父view的最大值
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent asked to see how big we want to be
        // 3. 本身想多大就多大
    case MeasureSpec.UNSPECIFIED:
        if (childDimension >= 0) {
            // 3.1 子view有确定值,那么不管父view多大,子view就是设置的大小
            //     虽然有大小,但是不一定能显示出来,因为可能超过父view的大小了
            // Child wants a specific size... let him have it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size... find out how big it should
            // be
            // 3.2 父view无法确定自己多少,那就只能继续按照父view的结果继续传递下去
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size.... find out how
            // big it should be
            // 3.3 父view无法确定自己多少,那就只能继续按照父view的结果继续传递下去
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        }
        break;
    }
    //noinspection ResourceType
    // 将测量模式和测量值拼接起来
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

现在,来总结一下,几个测量模式所表示的意思:

  • MeasureSpec.EXACTLY:精确值模式,此时的size是父view建议的子view大小,是确定的数值,如:100dp。当作为子view收到这个测量模式建议时,表明父view的大小是已经确定了,此时子view的LayoutParams是数值或MATCH_PARENT
  • MeasureSpec.AT_MOST:最大值模式,此时的size是父view允许子view的最大值。当作为子view收到这个测量模式建议时,可分为两种情况:1. 父view已经确定了大小,但是此时子view的LayoutParamsWRAP_CONTENT;2. 父view大小未确定,但是知道自己的最大值。此时子view可能是WRAP_CONETNTMATCH_PARENT
  • MeasureSpec.UNSPECIFIED:任意值模式,随便子view想要多大都可以。

注意:以上方法生成的测量模式以及测量值,都仅仅是父view根据子view的LayoutParams以及父view自身接收到的测量模式和测量值生成的建议。作为子view,你可以不按照建议进行测量,当然后果就要自己承担了。

总结

其实view的测量并没有特别复杂,关键是我们要明白,在onMeasure()中收到的测量模式以及size表示什么意思。然后我们只要按照父view的建议,再结合自身的实际情况,计算出作为子view的大小即可。

布局Layout

布局相对于测量而言,相对比较简单。首先依然从View.layout()入手,看看默认情况。

public void layout(int l, int t, int r, int b) {
    // 判断一下是否需要再测量一次
    if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
        onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
        mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }

    // 之前的位置
    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;

    // 判断是否有过变动
    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

    // 位置有过变动或者强制要求布局的时候才进行布局
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b); // 调用到布局方法
     	...省略部分代码
    }
}

逻辑都在注释之中了,在View.onLayout(),默认实现是空的,而在ViewGrouponLayout()是被abstract修饰的。意味着,如果是自定义ViewGroup,必须要实现这个方法。

另外需要说明的是,layout()传入的4个参数l、t、r、b分别表示子view相对于父view左边距离、顶部的距离、右边的距离、底部的距离。大概如图:

在这里插入图片描述

下面,来看看最简单的FrameLayout是如何布局的,借此来理解一下这4个参数的意义以及用处。

FrameLayout.onLayout()

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}

调用了layoutChildren(),需要注意的是,最后一个参数在这里传入了false

FrameLayout.layoutChildren()

这个方法,我将进行分步的讲解,这样比较好理解。

  1. 计算出可以提供给子view的布局使用的边界:
	final int parentLeft = getPaddingLeftWithForeground();
    final int parentRight = right - left - getPaddingRightWithForeground();
    final int parentTop = getPaddingTopWithForeground();
    final int parentBottom = bottom - top - getPaddingBottomWithForeground();

直接看代码可能比较难理解,可以配合这张图来看。

在这里插入图片描述

  1. 计算水平方向的边界:
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                case Gravity.CENTER_HORIZONTAL:
                    // 2.1 水平居中
                    // 这里之所以要在外面加多一层parentLeft,是因为要计算后的
                    // 坐标是相对坐标
                    childLeft = parentLeft + (parentRight - parentLeft - width) / 2 + lp.leftMargin - lp.rightMargin;
                    break;
                case Gravity.RIGHT:
                    if (!forceLeftGravity) {
                        // 2.2 从右到左布局
                        childLeft = parentRight - width - lp.rightMargin;
                        break;
                    }
                case Gravity.LEFT:
                    // 从左到右布局
                default:
                    childLeft = parentLeft + lp.leftMargin;
            }

这里的话,就只分解最具代表的水平居中。这里默认子view没有设置leftMarginrightMargin

现在看图说话,我想计算childLeft,需要怎么计算呢?

在这里插入图片描述

根据图可以列出算式:childLeft = (w1-childWidth)/2 + parentLeft

其中,w1 = parentRight-parentLeft

两个算式进行合并得到:childLeft = (parentRight-parentLeft-childWidth)/2 + parentLeft

  1. 计算垂直方向的边界:
switch (verticalGravity) {
                    case Gravity.TOP:
                        // 3.1 从上到下布局
                        childTop = parentTop + lp.topMargin;
                        break;
                    case Gravity.CENTER_VERTICAL:
                        // 3.2 垂直居中
                        childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                        lp.topMargin - lp.bottomMargin;
                        break;
                    case Gravity.BOTTOM:
                        // 3.3 从下到上布局
                        childTop = parentBottom - height - lp.bottomMargin;
                        break;
                    default:
                        // 3.4 默认情况,从上到下布局
                        childTop = parentTop + lp.topMargin;
                }

这里同样选择最具代表性的,垂直居中进行分解。同样,这里默认子View没有设置topMarginbottomMargin

依旧看图说话,计算出childTop。

在这里插入图片描述

根据图可列出算式:childTop = (h1-childHeight) / 2 + parentTop

其中,h1 = parentBottom - parentTop。

两个算式合并得到:childTop = (parentBottom - parentTop - childHeight)/2 + parentTop

至此,子view的四个边距都计算出来了,你可能会问:“childBottom和childRight不是还没计算吗?”。的确,FrameLayout并没有计算,那是因为没必要啊,因为childRight=childLeft+childWidth;childBottom=childTop+childHeight。然后就可以调用child.layout()将计算结果传递给子view了。

总结

通过分析FrameLayout的布局方法,可以看到,布局这个动作其实是需要进行大量的计算的。尤其是LinearLayoutRelativeLayout这种布局属性更多,布局更加复杂的控件。因此,在日常的开发中,要尽可能的避免嵌套过多的布局,这样可以降低View绘制的时候,在布局这里计算的时间,提高应用的流畅度。

绘制Draw

Draw是自定义view的时候最常见的方法,但是确实整个流程中最简单的方法,因为只需要画图就好了,不需要像MeasureLayout需要联系父view和子view去进行。下面,先看看在View.draw()方法中,官方为我们做了哪些事情。

View.draw()

public void draw(Canvas canvas) {
    final int privateFlags = mPrivateFlags;
    // 是否不透明,即是否有背景
    final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
            (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
    mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

    // Step 1, draw the background, if needed
    int saveCount;

    if (!dirtyOpaque) { // 第一步,绘制背景
        drawBackground(canvas);
    }

    // skip step 2 & 5 if possible (common case)
    // 看看是否有设置Fading Edge属性,可以理解为边缘羽化的效果
    final int viewFlags = mViewFlags;
    boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
    boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
    if (!verticalEdges && !horizontalEdges) {
        // Step 3, draw the content 第三步,绘制内容
        if (!dirtyOpaque) onDraw(canvas);

        // Step 4, draw the children 第四步,绘制子view
        dispatchDraw(canvas);

        // 自动填充功能,需要绘制一下高亮色
        drawAutofilledHighlight(canvas);

        // Overlay is part of the content and draws beneath Foreground
        // 绘制悬浮层
        if (mOverlay != null && !mOverlay.isEmpty()) {
            mOverlay.getOverlayView().dispatchDraw(canvas);
        }

        // Step 6, draw decorations (foreground, scrollbars)
        // 第六步,绘制装饰,如前景、滚动条
        onDrawForeground(canvas);

        // Step 7, draw the default focus highlight
        // 第七步,绘制默认获取到焦点时的高亮色
        drawDefaultFocusHighlight(canvas);

        if (debugDraw()) {
            debugDrawFocus(canvas);
        }

        // we're done...
        return;
    }
    ...省略部分绘制Edge的代码
}

在源码中,google的工程师就使用注释告诉了我们,draw的流程是怎么样子的:

  1. 如果有背景,绘制背景,调用drawBackground()
  2. 绘制自身内容,调用onDraw()
  3. 绘制子view,调用dispatchDraw()
  4. 绘制自动填充功能的高亮色,如果有的话,调用drawAutofilledHighlight()
  5. 绘制悬浮层,调用mOverlay.getOverlayView().dispatchDraw()
  6. 绘制装饰物,如前景、滚动条等,调用onDrawForeground()
  7. 绘制当获取到焦点时的高亮效果,调用drawDefaultFocusHighlight()

整个流程中,我们需要关心的只有绘制自身时的onDraw()和绘制子view时的dispatchDraw()onDraw()不用说,肯定是空的,毕竟每个view的样子都不一样,需要开发者自己去绘制。那么这里,我们就着重看看dispatchDraw()是如何工作的。

ViewGroup.dispatchDraw()

跳转到View.dispatchDraw(),你会发现是空方法。这个和布局Layout时一样,作为view是没有子view的,所以也没必要去分发,因此这里我们直接去看ViewGroup.dispatchDraw()

@Override
protected void dispatchDraw(Canvas canvas) {
    // 这里只贴一些比较重要的代码
    ...省略部分代码
    
    // 执行子view进场动画,给viewgroup设置AnimationController
    // 可以使得viewgroup下的所有子view的进场动画一致
    // 感兴趣的可以去看看LayoutAnimationController这个类
    if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
        ...省略部分代码
    }

    // 裁剪掉非可见区域,调用setClipChildren()
    int clipSaveCount = 0;
    final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
    if (clipToPadding) {
        clipSaveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
        canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,
                mScrollX + mRight - mLeft - mPaddingRight,
                mScrollY + mBottom - mTop - mPaddingBottom);
    }

    ...省略部分代码
    for (int i = 0; i < childrenCount; i++) {
        // 不知道是什么,翻译为瞬态视图,但是其add方法已经被标记hide了,所以这里就不分析了
        while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
            ...省略部分无关代码
        }

        // 绘制子view
        final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
        final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
            // 这里要求子view可见或者有设置动画
            // 这里调用的drawChild(),最终其实调用到了child.draw()
            more |= drawChild(canvas, child, drawingTime);
        }
    }
    
    // 这里同样是和瞬态view相关的,所以也不分析了
    while (transientIndex >= 0) {
        ...省略部分代码
    }
    ...省略部分代码

    // Draw any disappearing views that have animations
    // 绘制有动画但是不可见的子view
    // 调用removeView的时候,会将有动画的view加入到mDisappearingChildren中
    if (mDisappearingChildren != null) {
        final ArrayList<View> disappearingChildren = mDisappearingChildren;
        final int disappearingCount = disappearingChildren.size() - 1;
        // Go backwards -- we may delete as animations finish
        // 采用倒序,因为可能在动画结束后移除view
        for (int i = disappearingCount; i >= 0; i--) {
            final View child = disappearingChildren.get(i);
            more |= drawChild(canvas, child, drawingTime);
        }
    }
    ...省略部分无关代码
}

整个逻辑已经在代码中进行了注释,通过阅读这里的源码,其实可以总结出两个优化的UI布局的技巧。

总结

通过阅读源码,我们可以知道,越先添加的View将越先绘制,而先绘制的View将会被后绘制的View遮挡。其次,在ViewGroup.dispatchDraw()这个方法中,我们可以得出两个优化UI布局的小技巧:

  1. 通过调用setChildClicp(true)来裁剪掉非可见区域,减少绘制内容。
  2. 对于不可见的view,我们可以移除它的动画,因为如果它有动画的情况下,虽然不可见,但是依然会进行绘制。

总结

通过这次的源码阅读,我们可以知道,在优化UI布局的时候,基本着手点都在布局Layout和绘制Draw这两个方向。具体可以概述为:

  1. 控件数量尽量少。
  2. .控件之间的布局尽量简单。
  3. 只绘制对用户可见的区域。
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值