View的工作流程

概述  

     View的绘制流程是从ViewRoot的performTraversals开始的,经过measure、layout和draw三个过程才能最终将一个View绘制出来。


MeasureSpec
     
      View的measure过程在绘制的三个流程中是最复杂的。这里先看measure中关键的一个类——MeasureSpec。
      MeasureSpec即是测量规格或测量说明书。MeasureSpec是一个32位int值,高2位代表SpecMode(测量模式),低30位代表SpecSize(测量模式下的规格大小)。之所以把二者打包在一起是为了避免过多的对象内存分配,可以通过makeMeasureSpec方法打包,通过getMode/getSize方法解包。

public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                  @MeasureSpecMode int mode) {
    if (sUseBrokenMakeMeasureSpec) {
        return size + mode;
    } else {
        return (size & ~MODE_MASK) | (mode & MODE_MASK);
    }
}

public static int getMode(int measureSpec) {
    //noinspection ResourceType
    return (measureSpec & MODE_MASK);
}

public static int getSize(int measureSpec) {
    return (measureSpec & ~MODE_MASK);
}

     在MeasureSpec中,SpecMode有三种可能取值:
          ①、UNSPECIFIED:父容器不对View有任何限制,要多大给多大,这种情况一般比较少见,常用于系统内部。
          ②、EXACTLY:父容器已经检测View所需要的精确大小,这个时候View的最终大小就是SpecSize。对应于LayoutParams中的match_parent和具体的数值这两种模式。
          ③、AT_MOST:父容器指定了一个可用的大小SpecSize,View的大小不能大于这个值。对应于LayoutParams中的wrap_content。
     
  
如何确定MeasureSpec:
     DecorView作为顶级View,其MeasureSpec由窗口的尺寸和其自身的LayoutParams来共同决定,遵守如下规则:
          ①、LayoutParams.MATCH_PARENT:即精确模式,大小就是窗口的大小。
          ②、LayoutParams.WRAP_CONTENT:即最大模式,大小不定,但是不能超过窗口的大小。
          ③、固定大小:也是精确模式,大小为LayoutParams中指定的大小。

     对于普通View,measure过程由ViewGroup传递而来,在ViewGroup中由measureChildWithMargins方法:

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

    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);
}
     在ViewGroup的上面的方法中,在对子View进行测量之前,会通过getChildMeasureSpec方法先计算它的MeasureSpec。在getChildMeasureSpec方法中,结合父容器的MeasureSpec和子View的LayoutParams来计算得到子View的MeasureSpec。创建规则表格如下:




measure过程

     

      View的measure过程:
          measure过程是在View的measure方法中进行的,measure方法有final修饰不可重写,在measure方法中会调用onMeasure方法。
          
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
          在默认的onMeasure方法中,setMeasuredDimension是设置View的宽高。传入的参数中调用了getDefaultSize方法,来获取默认的宽高。下面看看getDefaultSize方法:

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:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}
          在getDefaultSize方法中,接收了size和measureSpec。当specMode是AT_MOST或EXACTLY时,测量结果就是传入的measureSpec中的specSize。当specMode是UNSPECIFIED时,测量结果就是传入的size。传入的size在调用处由getSuggestedMinimumWidth()/getSuggestedMinimumHeight()方法来获取。下面以getSuggestedMinimumWidth方法为例:

protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
          在上面的方法中,先判断该View是否设置了background,如果没有,就返回minWidth属性的值(可为0);如果有设置background,就返回background的width和minWidth中的较大值。

          从上面View的默认onMeasure方法可以看出,如果我们没有重写onMeasure方法,那么使用wrap_content(即是specMode为AT_MOST)时的效果和使用match_parent的效果是一样的,测量的结果都为parentSize,即是父容器当前剩余的空间大小。

      ViewGroup的measure过程
          ViewGroup除了完成对自己的测量,还会遍历去调用所有子元素的measure方法。要注意的是,ViewGroup是抽象类,没有重写onMeasure方法,而是提供了一个叫measureChildren的方法。之所以没有onMeasure方法,是因为各个ViewGroup的特性差异很大,测量细节各不相同。


measure过程总结

     当一个View完成measure过程后,可以通过getMeasuredWidth/getMeasureHeight来获取它的宽/高。某些极端情况下,系统可能需要多次measure才能确定最终的测量宽/高。一个比较好的习惯是在onLayout方法中去获取View的测量宽/高。
      View的measure过程和Activity的生命周期方法是不同步的,因此无法保证Activity执行onCreate/onStart/onResume时View已经完成测量,因此此时获取的宽/高有可能为0。可以通过以下方式来解决问题:
     ①、在Activity/View的onWindowFocusChanged方法中获取:在这个方法调用时,View已经初始完毕了。但要注意的是,onWindowFocusChanged方法会在Activity窗口得到/失去焦点时被调用,因此可能被频繁调用。典型代码如下:

@Override
public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);
    if(hasFocus){
        width = view.getMeasuredWidth();
        height = view.getMeasuredHeight();
    }
}

     ②、view.post(runnable):将一个Runnable对象投递到UI线程的消息队列尾部,当Looper调用到此runnable时,View也已经初始好了。
     ③、ViewreeObeserver:通过获取view的ViewTreeObserver来设置各种监听。
     ④、手动对View进行measure。


layout过程
     在measure过程结束后,就是layout过程。ViewRoot的performTraversals方法在measure结束后会继续执行,并调用View的layout方法,在layout方法中,通过setFrame方法来设定View的四个顶点的位置。四个顶点一旦确定,那么View在父容器中的位置也就确定了。

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);
        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);
            }
        }
    }

    mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
    mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}

     接着会调用onLayout方法。View的onLayout方法是空的, 因为这个方法是用于父容器确定子元素的位置。而ViewGroup中的onLayout方法是个抽象方法,ViewGroup的所有子类都必须重写这个方法。
    
      在onLayout结束后,可以使用getWidth/getHeight来获取视图的宽/高。getWidth于getMeasuredWidth的区别有两点:①、getMeasuredWidth中的值是通过onMeasure方法中的setMeasuredDimension方法来设置的,因此在measure结束后就能获取。而getWidth是通过视图右边的坐标减去左边的左边计算出来的,要在layout方法结束后才能获取。


draw过程

     View的绘制过程遵循如下几步(参考下面代码):
     ①、绘制背景:background.draw(canvas),绘制通过 android:background 设置的背景图片,先得到一个Drawable对象,再根据layout确定位置,然后调用Drawable对象的draw方法完成绘制(参考drawBackground方法代码)。
     ②、绘制自己:onDraw,这个方法是个空方法,需要由子类去重写。
     ③、绘制children:dispatchDraw,View中的dispatchDraw是空方法,ViewGroup中有具体实现。
     ④、绘制装饰:onDrawScrollBars,任何视图都会有滚动条,只是一般情况下没有让它显示出来。
     
     部分代码如下:

public void draw(Canvas canvas) {

... ...

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

if (!dirtyOpaque) {
    drawBackground(canvas);
}

// skip step 2 & 5 if possible (common case)
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
    dispatchDraw(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);

    // we're done...
    return;
}

... ...

}























  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值