Android View总结

    |     2016年2月7日   |   Android UI界面   |     0 条评论   |    1682

Android 控件大致分两类:ViewGroup 当容器管 View,View 自己负责绘制。整棵界面像 DOM 树:父视图负责测量、定位、绘制。findViewById 之所以贵,是因为它从上到下遍历整棵控件树找实例,重复操作里尽量少用。


一、关于 Android View 控件

现在常用控件大多直接或间接继承自 View。ViewGroup 作为容器管理子 View,父视图负责测量、定位和绘制。


二、UI 界面架构

每个 Activity 含一个 PhoneWindow,它把 DecorView 设成窗口根视图。里面是熟悉的 TitleView 和 ContentView。平时的 setContentView() 设的就是 ContentView。


三、Android 如何绘制 View

Activity 启动时会被要求画出布局。框架处理这个请求的前提是布局合理。绘制从根视图开始,从上到下遍历:ViewGroup 负责让子 View 被绘制,每个 View 通过 draw() 画自己。三步走:

  • Measure
  • Layout
  • Draw

整条流程在 ViewRoot 的 performTraversals() 里展开:

private void performTraversals() {
    ......
    //最外层的根视图的widthMeasureSpec和heightMeasureSpec由来
    //lp.width和lp.height在创建ViewGroup实例时等于MATCH_PARENT
    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
    ......
    mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    ......
    mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
    ......
    mView.draw(canvas);
    ......
}

画之前先知道尺寸和位置,所以先 measure、再 layout。


四、Measure 过程

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    //....
    //回调onMeasure()方法
    onMeasure(widthMeasureSpec, heightMeasureSpec);
    //more
}

计算实际大小,写入 mMeasuredHeight / mMeasuredWidth。MeasureSpec 是 32 位 int:高 2 位是模式,低 30 位是大小。三种模式:

  • EXACTLY:精确值。layout_width/height 为具体数值或 match_parent 时使用
  • AT_MOST:最大值。wrap_content 时不能超过父控件允许的最大尺寸
  • UNSPECIFIED:不指定,View 想多大就多大,一般很少用

measure 不可重写,自定义时重写 onMeasure:

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

最终宽高由 setMeasuredDimension() 设定;不重写则默认走 getDefaultSize。getMeasuredWidth() / getMeasuredHeight() 必须在 onMeasure 之后调用才有效。


五、Layout 过程

知道大小之后还要知道位置才能画。mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());

四个参数是相对父视图的左、上、右、下,用的就是刚测出来的宽高。

public void layout(int l, int t, int r, int b) {
    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;
    boolean changed = setFrame(l, t, r, b);
    if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
        .....
        onLayout(changed, l, t, r, b);
        .....
}

setFrame 写入坐标。坐标变了就重新定位。View 的 onLayout 是空方法,定位由 ViewGroup 完成。layout 结束后 getWidth() / getHeight() 才返回正确值。

getWidth/Height() 和 getMeasuredWidth/Height() 的区别:

  • getWidth():布局设定完成后整个 View 的宽度
  • getMeasuredWidth():对内容测量后,内容占据的宽度
API 何时有效 含义
getMeasuredWidth/Height onMeasure 之后 测量阶段得到的内容尺寸
getWidth/Height layout 之后 布局完成后 View 的实际宽高

六、Draw 过程

public void draw(Canvas canvas) {
        ......
        /*
         * 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)
         */

        // Step 1, draw the background, if needed
        ......
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        // skip step 2 & 5 if possible (common case)
        ......

        // Step 2, save the canvas' layers
        ......
            if (drawTop) {
                canvas.saveLayer(left, top, right, top + length, null, flags);
            }
        ......

        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);

        // Step 4, draw the children
        dispatchDraw(canvas);

        // Step 5, draw the fade effect and restore layers
        ......
        if (drawTop) {
            matrix.setScale(1, fadeHeight * topFadeStrength);
            matrix.postTranslate(left, top);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(left, top, right, top + length, p);
        }
        ......

        // Step 6, draw decorations (scrollbars)
        onDrawScrollBars(canvas);
        ......
    }

重点是第 3 步 onDraw。其余是背景、fade、scrollbar。dispatchDraw 递归画子 View;没有子 View 就不需要。onDraw 要自己实现,每个控件画的内容不同,主要用 Canvas。


七、参考资料

原文出处:http://threezj.com/2015/12/17/Android%20View%E8%AF%A6%E8%A7%A3/

一句话总结:绘制从根向下走 Measure → Layout → Draw;自定义改 onMeasure/onLayout/onDraw,宽高 API 要分清测量值和布局值。

转载请注明来源:Android View总结
本文链接地址:https://ai.zhousir.top/?p=1209
回复 取消