《Android开发艺术探索》笔记总结——第四章:View的工作原理

这一章中主要介绍了View的相关知识,包括View的基本概念,View的测量流程、布局流程和绘制流程,最终根据这些来实现自定义View。

View的加载流程

在Activity启动完毕以后,Activity对象创建完毕后,会将DecorView添加到Window中,同时创建ViewRootImpl对象(ViewRootImpl对象是 ViewRoot 的实现),系统将ViewRootImpl 对象和 DecorView 关联起来。而 View 的加载流程是从ViewRoot的PerformTraversals方法开始的,经过measure、layout 和 draw 三个过程将View绘制出来。其中measure用来测量View的宽和高,layout用来确定View在父容器中的放置位置,而draw则负责将View绘制到屏幕上。

在这里插入图片描述
上面提到的DecorView是页面的顶级View,一般它的内部包含一个LinearLayout,分为上下两部分,上面是标题栏,下面是内容栏,内容栏就是我们通过setContentView所设置的布局文件。上面说到的Window是Android视图中最外层的管理者,它的具体实现是PhoneWindow,但是它和开发者接触的机会不多,它实际是View的直接管理者。
在这里插入图片描述
上图介绍了从ViewRoot的 performTraversals 方法开始的整个绘制流程,performTraversals 会依次调用performMeasure、performLayout和performDraw三个方法,这三个方法分别完成顶级View的measure、layout和draw三大流程,然后在他们又分别调用它们对应的onMeasure、onLayout和onDraw方法,在onMeasure方法中会对所有子元素的measure方法进行调用,然后子元素会重复父容器的整个measure过程,其他的两个方法同样是这样完成了子View的绘制。唯一不同的是performDraw的传递过程是在draw方法中通过dispachDraw来实现的。

measure过程决定了View的宽和高,Measure完成以后,可以通过getMeasureWidth和getMeasureHeight来获取View测量或的宽和高,他们基本上就等同于View最中的宽和高,因为在有些极端情况下,系统可能需要多次measure才能确定最终的测量宽和高,在这个时候,在onMeasure方法中获取到的测量宽高就可能不准确了,因为前几次的测量宽高有可能和最终的宽高不一致,这个时候就要去onLayout获取最终View的测量宽高或者最终宽高,但是从最终来说测量宽高和最终宽高相同,因为底层实现来看实际宽高是从测量匡高获得的。

layout过程决定了View的四个顶点的坐标和实际的View的宽和高,完成以后可以通过getTop、getBottom、getLeft和getRight来拿到View的四个顶点的位置(这一点在上一章的View的事件体系中已经有具体说明),并且可以通过getWidth和getHeight方法来拿到View最终的宽和高,在这里可是可以使用 getMeasureWidth 和 getMeasureHeight 获取测量的宽和高的,在这里获取的测量宽高就是实际的宽高。

MeasureSpec

在View的onMeasure方法中需要使用到MeasureSpec来获取View的测量模式和测量大小,测量模式什么呢?就是我们在xml文件中设置的宽和高,我们设置宽和高不一定总是精确的dp值,有时是 match_parent 或者 wrap_content ,这就有了不同的测量模式,需要我们在测量View的时候获取到用户设置的测量模式,而测量大小即在某种测量模式下的规格大小,即用户在xml中设置完View的宽高以后,View真正的宽高大小。

MeasureSpec代表一个32位int值,高2位代表测量模式SpecMode,低30位代表SpecSize,因为测量模式只有三种,所以2个bit位即能存储三种情况。

SpecMode分为如下三类:
UNSPECIFIED:父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,所以我们实际上用不到,可以忽略此种测量模式。

EXACTLY:父容器已经检测出View所需要的精确大小,View是精确测量模式,VIew的大小就是SpecSize的值。它对应的是xml中使用的match_parent和具体的dp数据这两种模式。

AT_MOST:父容器指定了一个可用大小,它对应的xml中使用的warp_content,此时View的大小要看其View的具体实现和父容器的 “脸色”。

获取方式:

int mode = MeasureSpec.getMode(measurespea);//测量模式
int size = MeasureSpec.getSize(measurespea);//测量大小

MeasureSpec是由什么决定的呢?
对于一般的View,即我们自己创建的View,MeasureSpec是由父容器的MeasureSpec和View自身的LayoutParams包括View的margin和padding来共同决定的,然后MeasureSpec确定后,在onMeasure方法中就可以确定View的测量宽和高。
但是对于顶级View——DecorView来说,它的MeasureSpec由窗口的尺寸和自身的LayoutParams来共同决定的。

measure过程

measure过程分为两种,一种是View的measure过程,一种是ViewGroup的measure过程,在这里需要说明的重申的一点是,调用流程是measure --> onMeasure,然后不断重复知道遍历完毕。

1)View的measure过程

view的measure方法是final类型的,所以当我们自定义View的时候measure方法是不能重写的,但是我们知道measure方法会去调用onMeasure方法,所以重写onMeasure方法即可。

在源码中,系统也是分了三种测量模式来获取View的测量大小,大小都是SpecSize。UNSPECIFIED测量模式是系统内部的测量模式,略过;EXACTLY测量模式,返回的大小即是用户设定的精确值dp,或者是match_parent,即父布局留下的所有空间;AT_MOST则留给具体的View,比如TextView、ImageView去自己做特殊的处理。

给我们的启示:当我们直接集成View去自定义控件的时候,需要重写onMeasure方法并且要设置控件为wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent,因为在AT_MOST模式下,宽高等于SpecSize,这种情况下它的大小就是父容器当前剩余空间的大小,效果和布局中使用match_parent完全一致。

2)ViewGroup的measure过程

对于ViewGroup除了要完成自己的measure过程以外,还会遍历调用所有子元素的measure方法,各个子元素再递归的去执行这个过程,ViewGroup没有重写View的onMeasure,提供了一个measureChildren方法,用来测量所有的子元素,然后子元素再调用自己的measure方法去测量。

ViewGroup没有测量自己的具体过程,是为了让子类去实现,比如LinearLayout、RelativeLayout,因为不一样的子类布局特性不同,所以测量的方法也会不同。

如上提到的,当measure完成以后,就可通过getMeasureWidth/Height来获取View的宽和高。

3)在View外部怎么获取View的宽和高

上面说的获取宽和高都是在View的内部来获取,那么当想在Activity获取某个View的宽和高该怎么获取呢?在Activity中的各个回调方法中是无法正确得到View的宽和高信息的,因为View的measure过程和Activity的生命周期方法不是同步执行的,如果View还没测试完毕,获得宽和高就是0.

方法1:使用Activity或者View的onWindowFocusChanged方法

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

方法二:view.post(runnable)

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

方法三:ViewTreeObserver

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

layout过程

layout的过程是ViewGroup确定子元素的过程,真正的流程是ViewGroup通过调用自身的onLayout方法遍历所有子元素,并调用子元素的layout方法,layout方法中onLayout方法又会被调用。layout方法确定本身的位置,onLayout方法则确定子元素的位置。

layout方法的大致流程是,先通过setFrame方法确定View四个顶点的给位置,顶点确定那么view在父容器中的位置就确定了,接着会调用onLayout方法,目的是确定子元素在父容器中的位置。onLayout的具体实现和onMeasure一样都交给了子类去实现,因为onLayout具体实现和具体的布局相关,LinearLayout和RelativeLayout是不同的。

draw过程

draw过程的绘制过程主要是如下几步:
1:绘制背景(background.draw(canvas))
2:绘制自己(onDraw)
3:绘制children(dispatchDraw)
4:绘制装饰(onDrawScrollBars)

通过上面的过程可以看出来,绘制过程不是通过draw方法来传递,是通过dispatchDraw来传递到子元素的,dispatchDraw会遍历调用所有子元素的draw方法

自定义View

熟悉了上面的View的绘制流程,自定义View就是在他们的三个方法中做相应的处理,这里再总结下上面绘制流程的三个方法的作用:
对于View来说,一般会重写三个方法:onMeasure、onDraw
重写onMeasure是因为measure方法是final的,在measure方法中会调用onMeasure方法,所以我们只能重写onMeasure方法。重写这个方法是为了测量View

重写layout,一般自定义View不会重写layout,因为layout方法是在摆放自己在ViewGroup的位置,对于View来说不需要考虑这个问题,

重写onDraw,为什么不重写draw方法,因为这是系统建议的,在draw方法的系统注释中,系统建议重写onDraw来做自己的View绘制工作,因为重写draw方法需要处理的细节更多,比如draw的背景都需要自己处理,而这些一般是我们不需要关心的,所以重写onDraw即可。

对于ViewGroup来说,一般会重写三个方法:onMeasure、onLayout、onDraw
唯一不同的就是onLayout,其他的两个同上。
重写onLayout,作用是确定子View在ViewGroup中的位置,根据自己需要的View排布方式来处理。

总结:不管是自定义View还是ViewGroup基本都不用重写layout方法,因为在使用布局的时候我们一般最外层都会是一个ViewGroup,所以只需要处理好onLayout方法即可。

自定义View的分类:

1)继承View重写onDraw方法
2)集成ViewGroup派生特殊的Layout
3)继承特定的View(比如TextView)
4)继承特定的ViewGroup(比如LinearLayout)

1)继承View重写onDraw方法

对于这种情况,主要是为了实现一些不规则的效果,这种效果不是通过布局的组合方式达到的,只能通过自己绘制来实现,这种情况就要重新onDraw来去自己绘制想要的效果。

这种方式需要自己在onMeasure方法中处理wrap_content,和padding

2)集成ViewGroup派生特殊的Layout

这种情况主要是用来实现自定义的布局,除了系统提供的几种ViewGroup布局之外,需要实现自己想要的布局方式,当我们要的效果是看起来需要几种View组合到一起的效果的时候,我们就需要采用这种方式来实现。

这种方式需要在onMeasure中测量子元素的宽高,在onLayout中处理子元素的布局

3)继承特定的View(比如TextView)

这是比较常见的方式,站在巨人的肩膀上,当我们想要的View效果是在系统提供的View基础之上的效果的时候就可以采用这种方式,我们可以针对自己需要改动的地方来复写对应的方法来绘制。

这种方式的好处是,不需要自己处理wrap_content和padding

4)继承特定的ViewGroup(比如LinearLayout)

这种方式同上,我们可以借助系统原有的ViewGroup来实现自己的布局。

这种方式的好处是,不需要自己处理ViewGroup的测量和布局这两个过程。

自定义View注意事项:
1:让View支持wrap_content

不管是继承VIew还是ViewGroup,都需要在onMeasure中处理宽或者高设置成wrap_content的情况,如果不处理,当使用者设置wrap_content的时候就和match_parent一样了。

2:如有必要,让View支持padding,让ViewGroup支持margin

继承View的控件,需要在draw方法中处理padding,否则padding属性无法起作用,如果继承ViewGroup,需要在onMeasure和onLayout中考虑padding和子元素的margin属性。

3:不要在View中使用Handler

因为View内部本身就提供了post系列方法,完全可以替代Handler

4:VIew中如果有线程或者动画,要及时停止

可以使用onDetachedFromWindow方法来回收资源,因为当包含此View的Activity退出或者当前View被remove时,View的onDetachedFromWindow方法会被调用。

5:View带有嵌套情况,需要处理好滑动冲突。

最后是一些自定义View的示例,除了上面的方法,自定义View还有很多高级技巧,需要在实践中取总结,不过在实际开发中自定义View使用不是特别频繁,了解以上知识即可,具体的效果实现,可以具体问题具体解决,Google很强大。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值