View的工作流程:Measure,Layout,Draw

View的工作流程:Measure,Layout,Draw

标签(空格分隔): Android自定义控件

Android开发艺术探索

本文仅作为总结学习用途,感谢开放的互联网,感谢乐于分享的前辈。

在讲View的三大流程之前,先做一下准备工作,熟悉与其相关的类。

MeasureSpec

MeasureSpec类似于测量指导,包括size(低32位)和mode(高2位)两个属性,用来规范子元素的测量标准。
很明显,size表示大小,mode有UNSPECIFIED,EXACTLY,AT_MOST三种模式:

  • UNSPECIFIED:父View不对子View有任何限制
  • EXACTLY:父View给出了精确大小,对应match_parent和具体数值(xxdp)情形。
  • AT_MOST:父View给了一个可用大小,子View不能大于这个值,对应wrap_content。

    1. 对于DecorView,其MeasureSpec由屏幕尺寸和自身的LayoutParams来确定。
    2. 参于普通View,其MeasureSpec由父View的MeasureSpec和自身的LayoutParams来确定。

measureChildWithMargins

ViewGroup.java

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);
}
  1. 第4行获取子View的LayoutParams。
  2. 第6行-11行通过getChildMeasureSpec获取子View的MeasureSpec。
  3. 第13行调用子View的measure进行测量

getChildMeasureSpec

ViewGroup.java

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

    int resultSize = 0;
    int resultMode = 0;

    switch (specMode) {
    // Parent has imposed an exact size on us
    case MeasureSpec.EXACTLY:
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size. So be it.
            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.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent has imposed a maximum size on us
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            // 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.
            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.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent asked to see how big we want to be
    case MeasureSpec.UNSPECIFIED:
        if (childDimension >= 0) {
            // 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
            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
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        }
        break;
    }
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

普通View的MeasureSpec创建规则(图来源于Android开发艺术探索):
1. 子View的LayoutParams是dp/px的情况下,表示子View已经独立自主,确立了自己的标准,所以MeasureSpec模式都是EXACTLY,MeasureSpec大小也是自己设定的dp/px。
2. 子View的LayoutParams是match_parent的情况下,表示子View向父View看齐,所以MeasureSpec模式跟父View保持一致,见表格第二行。
3. 子View的LayoutParams是wrap_content的情况下,表示子View想决定自己的大小,但还是受父View的监管,所以MeasureSpec模式是AT_MOST。见表格第三行。
4. 父View是UNSPECIFIED的情况下有点特殊,一般不需要亲关注,可以参考源码体会。
此处输入图片的描述

measure过程

View和ViewGroup的measure过程有所不同,View只要测量自己就可以了,而ViewGoup除了要测量自己外,还要遍历去调用子元素的measure方法完成测量。

view的measure分析

View$measure方法会去调用View的onMeasure方法。
measure->onMeasure->setMeasuredDimension->getDefaultSize

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

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中主要考虑AT_MOST和EXACTLY两种情况,getDefaultSize返回的大小就是measureSpec中的specSize,对应View的测量值。

setMeasuredDimension和getSuggestedMinimumWidth不是重点,略过。

注意:直接继承View的自定义控件要重写onMeasure方法并设置wrap_content时自身的大小。因为从前面的表格可以看出,当view的layoutParams为wrap_content时,它的specMode是AT_MOST模式。它的大小等于specSize,也就是父View当前剩余的大小。这种效果相当于在布局中使用match_parent完全一致了。

ViewGroup的measure过程

ViewGroup中没有onMeasure方法,而是提供了一个measureChildren方法去遍历所有子View的measure方法。
measureChildren->measureChild->getChildMeasureSpec->child.measure

由于不同的ViewGoup布局特性差异很大,所以ViewGoup把onMeasure方法留给其子类去实现。下面通过LinearLayout的onMeasure方法来分析ViewGroup的measure过程。

LinearLayout的measure过程分析

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (mOrientation == VERTICAL) {
        measureVertical(widthMeasureSpec, heightMeasureSpec);
    } else {
        measureHorizontal(widthMeasureSpec, heightMeasureSpec);
    }
}

/TODO:测量过程比较繁琐,后续再补充

正确获取View的的方法

1.Activity/View$onWindowFocusChanged
当Activity的窗口获得焦点和失去焦点的时候均会调用该方法,示例代码:

@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();
            int height = view.getMeasuredHeight();
        }
    });
}

3.ViewTreeObserver
当View树的状态发生改变或者内部的View的可见性发现改变时,onGlobalLayout方法将被回调。

@Override
protected void onStart() {
    super.onStart();
    ViewTreeObserver observer = view.getViewTreeObserver();
    observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
    });
}

4.view.measure(int widthMeasureSpec, int heightMeasureSpec)
手动对view进行measure,需要根据view的LayoutParams来区分:
4.1 match_parent
这种情况无法准确测量,因为不知道父view的剩余大小。

4.2 具体的数值
假设宽/高是100dp

int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec, heightMeasureSpec);

4.3 wrap_content
MeasureSpec高2位表示SpecMode,低30位表示SpecSize,所以这里使用(1<<30)-1表示View理论上能支持的最大值。

int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30) -1, View.MeasureSpec.AT_MOST);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30) -1, View.MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec, heightMeasureSpec);

layout过程

在完成了View的测量之后,接下来进行的是View的布局。

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 = 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;

        // ...
    }
}

直接捡重点分析
l,t,r,b分别表示view相对于parent左,上,右,下端的距离。
1. setFrame确定View的四个顶点的位置。
2. onLayout确定子View的位置,由于onLayout的实现跟具体布局有关,所以View和ViewGoup都没有实现该方法。

以LinearLayout的onLayout为例,它会遍历子View,会一级一级调用子View的layout方法,完成整个View树的layout过程。
调用逻辑是onLayout->layoutVertical->setChildFrame->child.layout

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

getMeasuredWidth和getWidth的区别:
这两个方法都是用来获取View的宽度,一般情况下二者的值都是相同的。
不同的是getMeasuredWidth的值形成于measure过程,getWidth的值形成于layout过程。

draw过程

draw过程比较简单,步骤如下:
1. 绘制背景background.draw(canvas)
2. 绘制自己(onDraw)
3. 绘制children(dispatchDraw)
4. 绘制装饰(onDrawScrollBars)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值