Android View工作原理:测量、布局、绘制流程 & 自定义View

#.概述
##.PhoneWindow中的DecorView是根布局
     Android中Window是显示和管理View的载体,其实现类是PhoneWindow。
    所有View是按照树的逻辑结构来管理的,父View有多个子View节点,子View节点又可以有多个View节点。
    而这棵View树的根节点是PhoneWindow中的根布局,一个DecorView,DecorView由两部分构成:
            ActionBar(操作条区域,顶部用于显示标题和几个操作按钮的区域)
            contentParent(内容区域)。
    其中actionBar可根据需要来设置,不一定总是有。用户自己自己写的界面最终可通过PhoneWindow.setContentView()方法添加到内容区域。
##.显示流程概述
View的正常显示流程分三大步骤:(这里的View泛指ViewGrop和非ViewGrop的View)
1.测量阶段:计算View自身的长、宽大小,并触发各个子View的测量过程;
2.布局阶段: 计算View在父布局中的位置(即其在父布局中的上下左右坐标),并触发各个子View的布局过程;
3.绘制阶段:将 View绘制在屏幕上,并触发各个子View的绘制过程;
    其中,每个过程都是自顶向下递归进行的:父View处理完自己的本阶段流程,然后会循环遍历调用子View的本阶段流程;子View处理完自己的本阶段路程后,又会继续循环遍历调用它的子View本阶段流程;…….不断重复该流程…….直到View树中每个节点都被处理完。
ViewRoot,实现类为ViewRootImpl,是连接WindowManager和DecorView的纽带,以上三个过程在最初始都是由ViewRoot来发起的。
#.具体流程
三个流程的链条如上图所示,最初由ViewRoot的performTraversals()方法来依次触发顶层View(DecorView)的performMeasure()、performLayout()、performDraw()方法,然后沿着View树结构自顶向下,依次递归传递下去。
##.measure(测量)阶段的传递链条
ViewRoot根据PhoneWindow尺寸和DecorView的LayoutParams信息,计算出顶层作为顶层View的DecorView的宽高MeasureSpec信息,
通过performTraversals(【宽高的MeasureSpec测量信息】)来触发其 measure(【宽高的MeasureSpec测量信息】):
(位于View树顶层的DecorView)
触发顶层View(即DecorView)的 onMeasure(【宽高的MeasureSpec测量信息】) :
                                        ——> 设置自身的测量尺寸宽高:最终是通过setMeasuredDimension(【宽高】)等方法来设置; 
                                        ——>根据自身的宽高MeasureSpec信息 和 子View的LayoutParams,计算出子View的宽高MeasureSpec信息
                                        ——>遍历调用子View的 measure(【宽高的MeasureSpec测量信息】)
(位于View树中间层的ViewGroup)
触发下一层父ViewGroup的 onMeasure(【宽高的MeasureSpec信息】) :
                                        ——> 设置自身的测量尺寸宽高:最终是通过setMeasuredDimension(【宽高】)等方法来设置;
                                        ——>根据自身的宽高MeasureSpec信息 和 子View的LayoutParams,计算出子View的宽高MeasureSpec信息
                                        ——>遍历调用子View的 measure(【宽高的MeasureSpec测量信息】)
…………………………不断递归重复该过程…………………………
(位于View树叶子节点的View)
触发最底层叶子节点View的 onMeasure(【宽高的MeasureSpec信息】) :
                                        ——> 设置自身的测量尺寸宽高:最终是通过setMeasuredDimension(【宽高】)等方法来设置; 
其中要点:
###1.  测量信息的传递
    测量阶段,是自顶向下层层递归进行的,父View调用子view的measure( 【宽高的MeasureSpec信息】 )时,需要将子View的宽高的测量信息作为参数传递过去。这个测量信息,不光包括宽、高对应的数值,还包括对应的测量模式。
    但为了降低空间使用,以宽度测量信息为例。是将宽度的值和测量模式拼装成一个32位的int值来传递的。int值的最高2位对应的是测量模式,低30位对应的是测量值。一共有3种模式,分别为:
    1)UNSPECIFIED:父容器不对子view大小做限制,子View要多大空间,给多大空间。该情况一般用于系统内部,我们自己通过View属性来设置大小时不会遇到。
    2)EXACTLY:父容器已经精准测量出子View的尺寸,子View的大小等于这个尺寸。当子View的LayoutParams大小为match_parent时,一般会是这种模式。
    3)AT_MOST:父容器为子View指定的是最大尺寸,子View的大小不能超过这个尺寸。当子View的LayoutParams大小为wrap_content时,一般会是这种模式。
###2.MeasureSpec类:
    这其实是一个辅助类,包含一些方法,可以从32位int值中解析出对应的测量模式 和 测量值,也可以把指定测量模式和测量值拼装成一个32位int值。  
    一个View在进行自己和子View的测量计算时,需要从父view传递过来的32位int值中解析出与自己对应的测量模式 和 测量值,可用到该类的解析方法;当计算出子view对应的测量模式 和 测量值时,可用该类的方法拼装出一个32位int值,传递给子view。
    有时为了描述的简便性,会把这个32位int值称为View的MeasureSpec值。
###3.一个View最终的测量尺寸是在onMeasure()方法中,
最终通过 setMeasuredDimension(【宽高】)等方法来设置的。
###4. 一个View最终的测量尺寸的计算过程
    一个View A的MeasureSpec信息是由其父View计算得到的。用到的是父View的MeasureSpec信息和View A的LayoutParams信息。
    一个View A根据自己的MeasureSpec信息 以及 子View的LayoutParams信息,计算出子View的信息。
    依次类推…….不断递归下去………..
4.0View类中定义MeasureSpec 的默认逻辑
    View类中默认逻辑,自己的MeasureSpec信息由父View计算得到并传递进来,根据自己的MeasureSpec和子View的LayoutParams信息来计算子View的MeasureSpec信息。自己的MeasureSpec、子View的LayoutParams、最终子View的测量模式与尺寸结果,三者对应关系如下表所示:      
(图中parentSize指当前View最终的测量尺寸,childSize指子View的LayoutParams中没用match_parent、wrap_content时设置的具体尺寸值,例如60dp)。
由图中可知:
    自定义的View中,默认情况下设置wrap_content是无效的,其效果等同match_parent。如果希望支持wrap_content属性,可覆盖onMeasure()方法,添加逻辑,根据测量模式信息或LayoutParams信息,来自己设置最终的测量尺寸值。
4.1非ViewGroup的View:
   非ViewGroup的View,默认情况下,最终测量尺寸完全由传入的MeasureSpec信息决定,而父View的计算方式如上面4.0中图表所示。当然,也可以自定义onMeasure()方法来修改默认逻辑。     
4.2ViewGroup:
    ViewGroup的计算自己最终测量尺寸、计算子View测量信息的具体逻辑,要视ViewGroup的具体类型来确定的,例如LinearLayout、RelativeLayout的尺寸计算方式各自不同。所以ViewGroup类本身并未对measure()、onMeasure()方法做实现。而各个ViewGroup的子类,如LinearLayout、RelativeLayout,在onMeasure()方法中根据自身特点重写了自己的计算逻辑。一般子View最终测量模式的生成关系还是如4.0中图标所示,但子View尺寸的计算逻辑就根据ViewGroup的具体特点而各部相同了。
##.layout(布局)阶段的传递链条
(位于View树顶层的DecorView)
顶层View的 layout(【上下左右坐标信息】)被触发:
                            ——>通过setFrame(【上下左右坐标信息】)等方法设置自己的位置信息,只有View类才能调动该方法;
                            ——>触发onLayout(【上下左右坐标信息】),根据自身的位置信息 & 子View的测量尺寸信息,循环遍历,
                                     计算出每一个子View的位置信息,调用其layout(【上下左右坐标信息】);
(位于View树中间层的ViewGroup)
触发下一层父ViewGroup的 layout(【上下左右坐标信息】):
                            ——>通过setFrame(【上下左右坐标信息】)等方法设置自己的位置信息,只有View类才能调动该方法;
                            ——>触发onLayout(【上下左右坐标信息】),根据自身的位置信息 & 子View的测量尺寸信息,循环遍历,
                                     计算出每一个子View的位置信息,调用其layout(【上下左右坐标信息】);       
                             
…………………………不断递归重复该过程…………………………
(位于View树叶子节点的View)
触发最底层叶子节点View的 layout(【上下左右坐标信息】):
                            ——>通过setFrame(【上下左右坐标信息】)等方法设置自己的位置信息,
其中要点:
1.一个View的布局坐标信息如何修改:
    一个View A的坐标信息由父View计算出,通过layout()方法传递过来并进行设置的。所以要修改View A的布局位置,可覆写其自己的layout()方法;也可覆写其父View的onLayout()方法,调用A.layout()方法时修改传递进来的位置信息。
##.draw(布局)阶段的传递链条
(位于View树顶层的DecorView)
顶层View的 draw(Canvas canvas)被触发:
                            ——>绘制背景:backgroud.draw(canvas);
                            ——>绘制自己:onDraw(canvas),其实就是调用Canvas的一系列方法将对应的图形绘制出来。
                            ——>绘制子View:调用dispatchDraw(canvas),在该方法内部会循环遍历调用子View的draw(canvas)方法;
                            ——>绘制装饰:onDrawScrollBars(canvas)。
(位于View树中间层的ViewGroup)
触发下一层父ViewGroup的 draw(Canvas canvas):
                            ——>绘制背景:backgroud.draw(canvas);
                            ——>绘制自己:onDraw(canvas),其实就是调用Canvas的一系列方法将对应的图形绘制出来。
                            ——>绘制子View:调用dispatchDraw(canvas),在该方法内部会循环遍历调用子View的draw(canvas)方法;
                            ——>绘制装饰:onDrawScrollBars(canvas)。 
                                 
…………………………不断递归重复该过程…………………………
(位于View树叶子节点的View)
触发最底层叶子节点View的 draw(Canvas canvas):
                            ——>绘制背景:backgroud.draw(canvas);
                            ——>绘制自己:onDraw(canvas),其实就是调用Canvas的一系列方法将对应的图形绘制出来。
                            ——>绘制装饰:onDrawScrollBars(canvas)。
其中要点:
1.setWillNotDraw(【Boolean】)方法
    当一个View不需要绘制任何内容时,可通过该方法将对应标志位设置为true,以便于系统进行优化。默认情况下,View中该标志位默认为false,而ViewGroup中默认为true。所以通过直接继承ViewGroup类来自定义容器类时,需调用setWillNotDraw(false),某则无法正常显示内容。    
2.绘制的过程
        本质上就是调用Canvas的一系列方法,将对应的图形、文字等内容画出来。measure过程确定的画布的大小,layout过程确定了画布的位置,而draw过程最终在画布上“画出内容”。
        而View的draw(Canvas canvas)、onDraw(Canvas canvas)中的Canvas对象,最初由ViewRootImpl创建,并通过draw(Canvas canvas)、onDraw(Canvas canvas)方法,沿View树自顶向下,一层一层传递给子View。
#.总体额外补充
1. measure()是final类型的方法,无法覆写,要修改对应流程,应该覆写onMeasure()。布局逻辑可以覆写layout()、onLayout(),绘制逻辑可以覆写onDraw()方法。
2.直接继承自View的自定义View,默认情况下,padding属性是无效的。需要修改onDraw()方法,根据padding值来修改内容的绘制位置。
   直接继承自ViewGroup的容器View,默认情况下,padding、margin属性都是无效的,需要根据两个属性的值,修改onMeasure()、onLayout()这两个方法。
3.测量尺寸 与 最终的布局尺寸一般情况下是相等的,特殊情况下不相等。
    例如:有些情况下onLayout()被覆写了,触发了多轮测量过程,前几轮的尺寸与布局尺寸可能不同,当最后一轮的测量尺寸正常应该与布局尺寸相同。
    因此,要想精确获得最后显示的尺寸,应该去获取布局尺寸,而非测量尺寸。相应api:
    测量尺寸:getMeasuredWidth()、getMeasuredHeight()
    布局尺寸:getWidth()、getHeight()
4.View的测量阶段、布局阶段都需要一定的执行时间,如果在布局阶段尚未完成,就去获取布局尺寸,有可能获取的值为0。
例如:界面中View B的位置参数依赖于View A的高度,那么在窗口刚显示View A还未完成布局就去获取A的高度,获取的值就有可能为0。
    可通过一些手段,延迟获取尺寸的时间,在确保View已经布局完成后,触发回调接口,在回调接口中获取尺寸并执行相应业务逻辑,常用方法:
    方法1:使用以下方法将一个Runnable放到主线程消息队列尾部,则当执行到该Runnable时,view已经加载完了。如下例所示:
         view.post(new Runnable() {
            @Override
            public void run() {
                 int width = view.getWidth();
            }
        });
    方法2:获取指定View的ViewTreeObserver观察者对象,通过addOnGlobalLayoutListener()为其添加监听器,每当该View的View树发生变化时,就会执行相应回调。但View树有可能多次变化,触发多次回调,因此要及时移除监听器。如下例所示:
         ViewTreeObserver viewTreeObserver = view.getViewTreeObserver();
        viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                //首次回调后,移除监听器,避免反复多次回调。
                view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
                int width = view.getWidth();
            }
        });
#.自定义View相关
##.常用的方式
1.从已有的特定View、ViewGroup组件继承
    例如继承自TextView、LinearLayout等,在其基础之上修改个别特性,实现自己想要的效果。这种方法相对比较省事一些。
2.直接从View类或ViewGroup类继承,
    所有特性都由自己来实现。如果想要的效果不太适合从已有View组件基础上扩充,那只能完全由自己来实现了。
    需要注意的是:
    2.1直接继承自View的自定义View,默认情况下,padding属性是无效的。需要修改onDraw()方法,根据padding值来修改内容的绘制位置。
    2.2直接继承自ViewGroup的容器View,默认情况下,padding、margin属性都是无效的,需要根据两个属性的值,修改onMeasure()、onLayout()这两个方法。
    2.3注意setWillNotDraw(【Boolean】)方法: 当一个View不需要绘制任何内容时,可通过该方法将对应标志位设置为true,以便于系统进行优化。默认情况下,View中该标志位默认为false,而ViewGroup中默认为true。所以通过直接继承ViewGroup类来自定义容器类时,需调用setWillNotDraw(false),某则无法正常显示内容。   
##.其它注意要点 
1.除非特别需要,不需要新建Handler来发送消息,可通过View.post(Runnable)方法来替代。
2.若View中有新起的线程或动画,应在在退出时结束掉,否则有可能导致内存泄漏。 可在 onDetachedFromWindow时执行结束操作。两个相关回调方法的触发时机:
    onDetachedFromWindow:当View所在的Activity退出,或者View被从View树中remove()移除时;
    onAttachedToWindow:当View所在的Activity启动,或者View被add()添加到View树中时。
3.可通过 View的invalidate()方法,触发其重新绘制界面,最后会调用到draw()和onDraw()方法。当不在主线程中,要触发View的重新绘制,可调用其postInvalidate()方法。
4.如果View有滑动嵌套情况,需要注意处理好滑动冲突。
##.为指定的View自定义属性
1.在values/attrs.xml文件中,定义如下结构,示例中指定的View类名是CircleAvatarView:
<declare-styleable name="CircleAvatarView"> <!--指定目标View-->
    <attr name="bgWidth" format="dimension"/>
    <attr name="drawable" format="reference|color"/> <!--指定属性名称、值的类型-->
</declare-styleable>
2.xml布局文件中的属性使用示例:
<per.permanenthought.mystudy.canvas_study.CircleAvatarView
    android:layout_width="100dp"
    android:layout_height="100dp"
    android:layout_gravity="center_horizontal"
    app:bgWidth="10dp"
    app:drawable="@drawable/ic_sample_1"/>
3.在自定义的View类CircleAvatarView中,获取对应的属性值,以便后继逻辑处理中使用:
public CircleAvatarView(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
    TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CircleAvatarView);
    mBgWidth = array.getDimension(R.styleable.CircleAvatarView_bgWidth, 4);
    mDrawable = array.getDrawable(R.styleable.CircleAvatarView_drawable);
}

public CircleAvatarView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CircleAvatarView);
    mBgWidth = array.getDimension(R.styleable.CircleAvatarView_bgWidth, 10);//获取到的长度单位是px
    mDrawable = array.getDrawable(R.styleable.CircleAvatarView_drawable);
}
参考: 《Android艺术探索》
   
(声明:部分图片获取自网络,这里只是用于学习分享,侵删!)

猜你喜欢

转载自blog.csdn.net/u013914309/article/details/124595312