AndroidAndroid技术汇总Android自定义View

android 自定义View之ViewRoot到三大方法

2018-10-22  本文已影响32人  土肥圆的诺诺

写博客就好像吃甜品,明知道发胖还忍不住,胖并快乐着。
我写东西基本就是在各种总结别人的文章和自己的理解,所以可能不是很专业。
在事件分发篇,我们扯到了window和DecorView ,这次我们引入一个ViewRoot。
其实这块还是很有意思的,后面我们讲handle这块还能和这里有关。

ViewRoot

ViewRoot 或者说对应的 ViewRootImlp 类,它其实是连接 WindowManger 和 DecorView 的桥梁。View 的三大流程都是通过 ViewRoot 来完成的。
Activity创建完毕后,会讲DecorView添加到window,同时创建了ViewRootImlp,将它和DecorView绑定。



View的绘制是从ViewRootImlp的performTraversals开始的,经过measure.layout.draw三方法最终将一个View绘制出来。measure测量View宽高。layout确定View位置,draw负责绘制。


从上图我们能看到,performTraversals调用的时候,调用了performMeasure.performLayout.performDraw 三方法,然后调用最顶级View的Measure,Layout,Draw,顶级View的Measure调用onMersure对子view进行测量,子view对自己子view进行一样的操作。Layout,Draw也是一样,遍历进行处理,不过performDraw的传递是在draw方法种用了dispatchDeaw完成的,不过没啥区别。
这里用我们自己的话总结,其实就是ViewRootImlp自己的测量,布局,绘制的方法掉用顶级view的测量,布局,绘制的方法,顶级view又对自己下面子view进行了递归的测量,布局和绘制。ViewRootImlp三方法跑完,view就有了自己的宽高和位置,才能绘制完毕显示到页面上。
到现在为止 我们大体上了解了,Activity有一个window,收到事件以后,window持有DecorView,DecorView在被添加到window到时候,会产生一个ViewRootImlp,ViewRootImlp在调用performTraversals(遍历表演)的时候会调用顶级view的测量,布局和绘制,然后顶级view会挨个调用自己子布局的测量布局和绘制,这样一个页面就展现出来了。

DecorView

DecorView 继承自 FrameLayout,是一个 ViewGroup。在整个 ViewTree 中, DecorView 是整个 ViewTree 的顶层 View。View 的所有事件,都先经过 DecorView,然后再传递给 View。
DecorView 在一般情况下,内部会包含一个竖直的 LInearLayout,里面有上下两部分,上面是标题栏,下面是内容,内容布局有一个默认的 id: content。
我们在 Activity 中 通过 setContentView() 方法设置的布局,其实就是添加到了内容部分里。如果我们想获取到内容布局的话,可以通过如下方法获取:
ViewGroup content = (ViewGroup) findViewById(android.R.id.content)
想获取到我们设置的 View 的话,可以通过如下方式获取: View childAt = content.getChildAt(0);
顶级View调用三大方法其实也是调用了View自带的onMeasure,onLayout和onDraw。

MeasureSpec

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

其实我一一直好奇MeasureSpec到底是个啥?
MeasureSpec其实是一个32位数int值, 高二位指的是SpeceMode,低30位指的是SpeceSize。
SpeceMode就是测量模式,这玩意就是我们自定义view的时候,想知道是很准却的多少dp?还是我们写的match_parent之类的。android 将SpeceMode和SpeceSize和在了一起。
android 里面SpeceMode有三种模式

MeasureSpec和LayoutParamas有什么关系呢

对于顶级的DecorView,它的MeasureSpec是由窗口尺寸和自身的LayoutParamas决定大小。
普通View是由父容器的MeasureSpec和自身的LayoutParamas决定。
这块其实有点晕,可以这么理解,MeasureSpec决定了控件的大小,DecorView是由窗口决定的和自己大小决定,别人限制不了,但是普通View哪怕写了match也得看父容器给不给你这么大。
DecorView



普通View


当普通View是精确模式的时候,不管父的MeasureSpec是啥,View就是那么大。
当普通View是match模式时候,父容器是精确模式,那么view大小就是剩下的大小。
如果父容器也是match模式,那么view也是最大模式,但是最大不能超过父容器剩余空间。
当view是warp模式的时候,不管父容器是什么模式,view总是最大模式,但是不能超过父容器剩下的空间。
UNSPECIFIED 这个模式一般用于在Measure过程中一般不需要理睬。
总结一波就是精确模式,我多大就是多大。 match模式,最大不能超过父容器剩余空间,模式和父容器一样。warp模式,最大化,不能超过父容器。

onMeasure

该方法是一个 finall 方法,所以不能被重写。

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

从上述代码上可以看到,关于我们关心的 AT_MOST 和 EXACTLY 测量模式,其实 getDefaultSize() 方法返回的就是 MeasureSpec 的 specSize。
到这里也就理解了,为什么当我们在布局中写 wrap_content,如果不重写 onMeasure() 方法,则默认大小是父控件的可用大小了。 当我们在布局中写 wrap_content 时,那么测量模式就是: AT_MOST,在该模式下,它的宽高等于 specSize。而 specSize 由 ViewGroup 传递过来时就是 parentSize,也就是父控件的可用大小。 当我们在布局中写 match_parent 时,那么不用多说,宽高当然也是 parentSize。这时候,我们只需对 AT_MOST 测量模式进行处理。
其实我们再自定义一个view的时候,其实如果父布局没有固定宽高时候,我们希望我们的控件wrap_content模式可以有个固定宽高,不然我们的view永远是最大模式。
如果父控件传递给的MeasureSpec的mode是MeasureSpec.UNSPECIFIED,就说明,父控件对自己没有任何限制,那么尺寸就选择自己需要的尺寸size
如果父控件传递给的MeasureSpec的mode是MeasureSpec.EXACTLY,就说明父控件有明确的要求,希望自己能用measureSpec中的尺寸,这时就推荐使用MeasureSpec.getSize(measureSpec)
如果父控件传递给的MeasureSpec的mode是MeasureSpec.AT_MOST,就说明父控件希望自己不要超出MeasureSpec.getSize(measureSpec),如果超出了,就选择MeasureSpec.getSize(measureSpec),否则用自己想要的尺寸就行了。

public class TestView extends View {
    private static final String TAG = "TestView";

    public TestView(Context context) {
        super(context);
    }

    public TestView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = 300;//默认值300
        int height=300;
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        switch (widthMode) {
            case MeasureSpec.AT_MOST://当得不到精确值的时候就300
                width = 300;
                break;
            case MeasureSpec.EXACTLY://精确模式采用系统返回的数值
                width = MeasureSpec.getSize(widthMeasureSpec);
                break;
            case MeasureSpec.UNSPECIFIED:
                break;


        }
        switch (heightMode) {
            case MeasureSpec.AT_MOST:
                height = 300;
                break;
            case MeasureSpec.EXACTLY:
                height = MeasureSpec.getSize(heightMeasureSpec);
                break;
            case MeasureSpec.UNSPECIFIED:
                break;


        }

        //设置
        setMeasuredDimension(width, height);
    }


}

setMeasuredDimension(width, height);
都为wrap_content的时候,父布局和我们的View一样大



父布局固定的时候,我们按照自己默认大小。


父布局固定的时候,我们match



父布局固定,我们固定的时候



父布局和我们都match的时候

在onMeasure方法里我们主要去处理自定义View的宽高。

ViewGropu的measure

对于ViewGropu来说,完成除了自己的measure过程外,会遍历子元素的measure过程,ViewGropu是一个抽象类,没重写View的onMeasure方法,但是它提供了一个meausreChildren的方法。


android艺术探索

取出子元素的LayoutParams,通过getChildeMeasureSpec创建子元素的MeasureSpce,传递给View去测量。
View的具体宽高在页面的onCreat,onMeasure,onStart方法都是不能准确获取的,因为View的measure不能保证和生命周期同步。
想测量准确,第一个方法onWindowFocusChanged,页面获得焦点失去焦点的时候都会被调用

  @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        int width = text.getWidth();
    }

第二个方法是View.post,发送一个runnable到消息队列尾部。

  text.post(new Runnable() {
            @Override
            public void run() {
                int width = text.getWidth();
            }
        });

第三个是ViewTreeObserver,但是可能会被调用多次。

 ViewTreeObserver viewTreeObserver = text.getViewTreeObserver();
        viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                int width = text.getWidth();
            }
        });

第四种是View.measure,比较麻烦不建议使用。

onLayout

layout可以说是三大方法里面最简单的一个了,layout会确定View自己的位置,onLayout会确定子元素的位置。



onLayout

上面代码我们可以看出来,测量完毕我们获得上下左右四个定点位置,如果不是march,那么就设置进去上下左右四个点位置,然后掉onLayout方法,让父布局知道自己位置。

draw

View的draw可以说是最简单,也是最复杂的一个,简单是因为遵循如下几步

上一篇 下一篇

猜你喜欢

热点阅读