View的工作原理

View的工作原理

ViewRoot和DecorView

ViewRoot对应于ViewRootImpl,连接WindowManager和DecorView的纽带。
View的绘制流程从ViewRoot的performTraversals方法开始,经过以下三个过程:

  1. measure
  2. layout
  3. draw

理解MeasureSpec

MeasureSpec是一个会影响到View测量过程的参数。在测量View的宽高的过程中,系统会将View的LayoutParams根据父View的规则转换成对应的MeasureSpec,在进行宽高测量。

MeasureSpec

MeasureSpec是一个32位的int值,高两位代表SpecMode,低30位代表SpecSize。即模式+尺寸。Android里将这两个参数打包成了一个int值来避免过都的内存分配,可以通过get方法解包得到mode和size的分别值。源码里主要是一些“位操作”,类似:

        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has not imposed any constraint
         * on the child. It can be whatever size it wants.
         */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has determined an exact size
         * for the child. The child is going to be given those bounds regardless
         * of how big it wants to be.
         */
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        /**
         * Measure specification mode: The child can be as large as it wants up
         * to the specified size.
         */
        public static final int AT_MOST     = 2 << MODE_SHIFT;

如源码里给出的,SpecMode有三类:

  • UNSPECIFIED:父容器不对View有任何限制,一般系统内部使用。
  • EXACTLY:父容器已经检测出View的精确大小,由SpecSize指定。
  • AT_MOST:父容器指定一个可用大小SpecSize,子View的大小不能超过这个值。

子View Mode会被父View的specMode所影响,在getChildMeasureSpec方法中,给出了这种影响的具体过程,其流程图如下:

子View会根据父View的Spec不同模式,得到不同的结果。

从流程图和表格可以总结出:

  1. View的MesureSpec由父View的MesureSpec和自身的LayoutParams共同决定;
  2. 若View指定了大小,则不管父View的MeasureSpec如何,其Spec将总是ECACTLY,而大小为其指定的大小;
  3. 子View的LayoutParams为Wrap_content时,无论父类为何种模式,子View总是AT_MOST。因此,对于自定义控件来说,当指定view为wrap_content时,需要指定自身的大小,否则子View会在AT_MOST的模式下,最大程度的利用父View的空间。
  4. getMeasureSpec方法返回的是一个打包后的MesureSpec,子View的Mode将由其前2位确定,而后30位事实上代表了父View的可用大小,子View将参考这一值,但并不是最终子View的大小(事实上,View的最终大小是在layout阶段被确定的,但是一般情况下,View的测量大小和最终大小相等)。

View的工作流程

View的工作流程主要有:measure、layout、draw,即测量,布局和绘制。

View的measure过程

对于View来说,measure过程就是测量自身尺寸的过程;对于ViewGroup来说,measure过程除了测量自身尺寸外,还要递归的去测量所有children的尺寸。

View的measure过程比较简单:

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

基本上,所有的onMeasure都要干一件事:计算好自己的宽高,然后调用setMeasuredDimension方法保存。对于自定义的View,我们要自己计算width和height数值。这里就不贴getDefaultSize的代码了,也比较简单,就是根据SpecMode的值,来判断应该使用什么样的size。

ViewGroup的measure过程

ViewGroup的measure过程除了绘制自身外,还要绘制其children。ViewGroup本身是个抽象类,并没有去实现View的onMeasure方法,其通过一个measureChildren的方法对所有的Children进行测量。

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }

其中调用了measureChild对每个child进行测量:


测量过程本质上和View是一致的,外部传入了需要测量的child视图和父View的MeasureSpec,在调用View中getChildMeasureSpec方法创建MeasureSpec,而测量结果传递到View的measure方法中进行测量。接下去就是一个递归遍历的过程。

由于ViewGroup本身是抽象类,没有实现onMeasure方法,因此需要其具体的实现类,来完成这个方法。典型如LinearLayout、RelativeLayout等。事实上,每个ViewGroup的onMeasure方法考虑的东西很多,Android里LinearLayout源码还比较长,值得一看,可以了解下具体的测量过程。


View 的Measure过程和Activity的生命周期方法并不同步,往往在onCreate方法中去获取View的尺寸,得到的值并不是最终View的尺寸大小,为了在Activity启动时获取一个View的尺寸,有四种方法。

(1) Activity/View#onWindowsFocusChanged

当Activity窗口获得焦点时会被调用,并且这个方法表示View已经初始化完毕。

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

(2) view.post(Runnable)

    @Override
    protected void onStart() {
        super.onStart();
        view.post(new Runnable() {
            @Override
            public void run() {
                int width = view.getMeasuredWidth();
            }
        });
    }

(3) ViewTreeObserver

    @Override
    protected void onStart() {
        super.onStart();
        ViewTreeObserver observer = view.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @SuppressLint("NewApi")
            @Override
            public void onGlobalLayout() {
                view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                int widt = view.getMeasuredHeight();
            }
        });
    }

layout过程

layout是在Measure结束后的步骤,将用来确定子View的位置。对于ViewGroup来说,layout方法确定本身的位置,然后调用onlayout方法确定所有子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;
    }

layout方法会先使用setFrame来设定View本身的四个顶点位置,在调用onLayout方法去测量子View的位置,而onlayout是一个抽象方法,对于一个view而言,将不会有什么作用,对于一个ViewGroup而言,将会去确定其中所有子view的位置;同样的,在子view内,也会再调用layout方法确定自身和onlayout方法确定子子view,因此通过一层一层的传递,完成整个view树的layout过程。

ViewGroup的一个实现类是LinearLayout,在LinearLayout中,会重写onlayout方法,来完成自身和子View的布局位置确定:

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mOrientation == VERTICAL) {
            layoutVertical(l, t, r, b);
        } else {
            layoutHorizontal(l, t, r, b);
        }
    }

LinearLayout布局可以选择横向排列或者纵向排列内部的子View,两者实现逻辑类似,看看layoutVertical(l,t,r,b)的一些代码:

void layoutVertical(int left, int top, int right, int bottom) {
  ......
  for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
    if (hasDividerBeforeChildAt(i)) {
                    childTop += mDividerHeight;
                }
......
                childTop += lp.topMargin;
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

                i += getChildrenSkipCount(child, i);
  }
}

在layoutVertical中,通过Gravity的属性,来判断child的left、right、top等参数如何计算,这里省略贴代码了。在对一个子view计算好四个坐标后,通过setChildFrame函数记录。注意到在setChildFrame后,childTop会加上这个chil自身的高度,这就意味着下一个child的视图位置一定会在当前child下面,实现垂直排列的效果。而在setChildFrame中,实际上也是调用了view的layout方法:

    private void setChildFrame(View child, int left, int top, int width, int height) {        
        child.layout(left, top, left + width, top + height);
    }

draw过程

在Measure和layout之后,意味着每一个view在屏幕上的最终大小和位置都被确定了,这时候就通过draw过程将其绘制到屏幕上,其步骤:

  1. 绘制背景background.draw(canvas)
  2. 绘制自己(onDraw)
  3. 绘制Children(dispatchDraw)
  4. 绘制装饰(onDrawScrollBars)
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
*      1. Draw the background
*      2. If necessary, save the canvas' layers to prepare for fading
*      3. Draw view's content
*      4. Draw children
*      5. If necessary, draw the fading edges and restore layers
*      6. Draw decorations (scrollbars for instance)
*/

View有一个特殊的方法setWillNotDraw,它表示如果一个View不需要绘制本身,可以把这个标志位设为true,以便于系统对其进行优化。显然,一个普通的view一般不会去设置这个标志位,但是在某些ViewGroup中,可能本身并不需要经行绘制,那么可以通过这个方法设置从而优化性能。

    /**
     * If this view doesn't do any drawing on its own, set this flag to
     * allow further optimizations. By default, this flag is not set on
     * View, but could be set on some View subclasses such as ViewGroup.
     *
     * Typically, if you override {@link #onDraw(android.graphics.Canvas)}
     * you should clear this flag.
     *
     * @param willNotDraw whether or not this View draw on its own
     */
    public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }

自定义View

按照Android开发艺术探索里的分类,有如下四种情况:

  1. 继承View重写onDraw 方法
  2. 继承ViewGroup派生
  3. 继承已有的实体View,如TextView
  4. 继承已有的实体ViewGroup,如LinearLayout

总结一下就是:自定义View,毫无疑问都需要直接或者间接继承于View,然后根据具体需要实现的功能,来决定是利用现有的View来扩展,还是从底层开始重写。继承的层次越少,自定义空间就越大,同时难度也越高。因此,自定义View时,需要我们找到一种cost最小的方法去实现我们需要的功能。

自定义View时,一些注意事项:

  • View需要去支持wrap_content $$ wrap_content对应的MeasureSpec是AT_MOST,如果View不对wrap_content进行处理,会最大限度的利用父view的空间
  • View需要去处理padding 和margin $$ 从之前的三大过程来看,padding和margin是参与到了view的绘制计算中的,如果不处理,则这些属性会无效
  • View中尽量不使用Handler $$ 因为View本身提供了post方法来发送消息
  • View中如果有线程或者动画,需要及时停止 $$ 一般在onDetachedFromWindow中处理,否则可能造成内存泄露
  • View如果带有滑动嵌套,需要处理滑动冲突 $$ 有外部拦截发和内部拦截法

重写onDraw方法

public class CircleView extends View {    
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        final int paddingLeft = getPaddingLeft();
        final int paddingRight = getPaddingRight();
        final int paddingTop = getPaddingTop();
        final int paddingBottom = getPaddingBottom();

        int width = getWidth()-paddingLeft-paddingRight;
        int height = getHeight()-paddingTop-paddingBottom;

        int radus = Math.min(width,height)/2;
        canvas.drawCircle(paddingLeft+width/2,paddingTop+height/2,radus,mPaint);
    }
}

通过自定义了一个CircleView,重写其onDraw方法,来实现画圆。可以看到,onDraw方法中,我对padding属性经行了处理,使得自定义View才能够对xml文件里的padding属性经行支持;另外,在onMeasure方法里,也对wrap_content的默认属性进行了设置。为了使一个自定义View支持我们需要的自定义属性,需要在values目录下创建一个自定义属性是xml文件:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CircleView">
        <attr name="circle_color" format="color"/>
    </declare-styleable>
</resources>

之后,在CircleView的构造函数里对属性进行解析:

    public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
        mColor = array.getColor(R.styleable.CircleView_circle_color, Color.RED);
        array.recycle();
        init();
    }

最后,在xml布局文件中,正常使用即可。需要注意的是,要对命名空间进行声明,类似:

xmlns:app="http://schemas.android.com/apk/res-auto"
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容