《Android开发艺术探索》第四章重点笔记

                                                                         第4章   View的工作原理

##1、初识ViewRoot和DecorView       
          ViewRoot的实现是 ViewRootImpl 类,是连接WindowManager和DecorView的纽带,View的三大流程( mearsure、layout、draw) 均是通过ViewRoot来完成。当Activity对象被创建完毕后,会将DecorView添加到Window中,同时创建 ViewRootImpl 对象,并将ViewRootImpl 对象和DecorView建立连接,源码如下:

    root = new ViewRootImpl(view.getContext(),display);
    root.setView(view,wparams, panelParentView);

View的绘制流程是从ViewRoot的performTraversals开始的


performTraversals会依次调用 performMeasure 、 performLayout 和performDraw 三个方法,这三个方法分别完成顶级View的measure、layout和draw这三大流程。其中 performMeasure 中会调用 measure 方法,在 measure 方法中又会调用 onMeasure 方法,在 onMeasure 方法中则会对所有子元素进行measure过程,这样就完成了一次measure过程;子元素会重复父容器的measure过程,如此反复完成了整个View数的遍历。另外两个过程同理。

    Measure完成后, 可以通过getMeasuredWidth 、getMeasureHeight 方法来获取View测量后的宽/高。特殊情况下,测量的宽高不等于最终的宽高,详见后面。
    Layout过程决定了View的四个顶点的坐标和实际View的宽高,完成后可通过 getTop 、 getBotton 、 getLeft 和 getRight 拿到View的四个定点坐标。

##2、理解MeasureSpec
         MeasureSpec决定了一个View的尺寸规格。但是父容器会影响View的MeasureSpec的创建过程。系统将View的 LayoutParams 根据父容器所施加的规则转换成对应的MeasureSpec,然后根据这个MeasureSpec来测量出View的宽高。 MeasureSpec代表一个32位int值,高2位代表SpecMode( 测量模式) ,低30位代表SpecSize( 在某个测量模式下的规格大小)

   SpecMode有三种:
    UNSPECIFIED :父容器不对View进行任何限制,要多大给多大,一般用于系统内部
    EXACTLY:父容器检测到View所需要的精确大小,这时候View的最终大小就是SpecSize所指定的值,对应LayoutParams中的 match_parent 和具体数值这两种模式
    AT_MOST :对应View的默认大小,不同View实现不同,View的大小不能大于父容器的SpecSize,对应 LayoutParams 中的 wrap_content


 ##3、View的工作流程
      (1) measure过程

                View的measure过程

              直接继承View的自定义控件需要重写 onMeasure 方法并设置 wrap_content ( 即specMode是 AT_MOST 模式) 时的自身大小,否则在布局中使用 wrap_content 相当于使用 match_parent 。对于非 wrap_content 的情形,我们沿用系统的测量值即可。
                    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
                        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
                        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
                        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
                        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
                        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
                          // 在 MeasureSpec.AT_MOST 模式下,给定一个默认值mWidth,mHeight。默认宽高灵活指定
                          //参考TextView、ImageView的处理方式
                          //其他情况下沿用系统测量规则即可
                        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
                            setMeasuredDimension(mWith, mHeight);
                        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
                            setMeasuredDimension(mWith, heightSpecSize);
                        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
                            setMeasuredDimension(widthSpecSize, mHeight);
                        }
                    }


            ViewGroup的measure过程

                ViewGroup是一个抽象类,没有重写View的 onMeasure 方法,但是它提供了一个 measureChildren 方法。这是因为不同的ViewGroup子类有不同的布局特性,导致他们的测量细节各不相同,比如 LinearLayout 和 RelativeLayout ,因此ViewGroup没办法同一实现 onMeasure方法。

                 measureChildren方法的流程:
                            1、   取出子View的 LayoutParams
                            2、    通过 getChildMeasureSpec 方法来创建子元素的 MeasureSpec
                            3、    将 MeasureSpec 直接传递给View的measure方法来进行测量
                            
                 通过LinearLayout的onMeasure方法里来分析ViewGroup的measure过程:
                          1、  LinearLayout在布局中如果使用match_parent或者具体数值,测量过程就和View一致,即高度为specSize
                          2、  LinearLayout在布局中如果使用wrap_content,那么它的高度就是所有子元素所占用的高度总和,但不超过它的父容器的剩余空间
                          3、  LinearLayout的的最终高度同时也把竖直方向的padding考虑在内

                        
            View的measure过程是三大流程中最复杂的一个,measure完成以后,通过 getMeasuredWidth/Height 方法就可以正确获取到View的测量后宽/高。在某些情况下,系统可能需要多次measure才能确定最终的测量宽/高,所以在onMeasure中拿到的宽/高很可能不是准确的。如果我们想要在Activity启动的时候就获取一个View的宽高,怎么操作呢?因为View的measure过程和Activity的生命周期并不是同步执行,无法保证在Activity的 onCreate、onStart、onResume 时某个View就已经测量完毕。所以有以下四种方式来获取View的宽高:

   1、 Activity/View#onWindowFocusChanged
    onWindowFocusChanged这个方法的含义是:VieW已经初始化完毕了,宽高已经准备好了,需要注意:它会被调用多次,当Activity的窗口得到焦点和失去焦点均会被调用。
   2、 view.post(runnable)
    通过post将一个runnable投递到消息队列的尾部,当Looper调用此runnable的时候,View也初始化好了。
   3、 ViewTreeObserver
    使用 ViewTreeObserver 的众多回调可以完成这个功能,比如OnGlobalLayoutListener 这个接口,当View树的状态发送改变或View树内部的View的可见性发生改变时,onGlobalLayout 方法会被回调,这是获取View宽高的好时机。需要注意的是,伴随着View树状态的改变, onGlobalLayout 会被回调多次。
   4、  view.measure(int widthMeasureSpec,int heightMeasureSpec)
          手动对view进行measure。需要根据View的layoutParams分情况处理:
                   1、 match_parent:
                    无法measure出具体的宽高,因为不知道父容器的剩余空间,无法测量出View的大小
                   2、 具体的数值( dp/px):
                          int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
                          int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
                          view.measure(widthMeasureSpec,heightMeasureSpec);     
                  3、  wrap_content:
                          int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
                          // View的尺寸使用30位二进制表示,最大值30个1,在AT_MOST模式下,我们用View理论上能支持的最大值去构造MeasureSpec是合理的
                          int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
                          view.measure(widthMeasureSpec,heightMeasureSpec);

            
            (2)、layout过程      
            Layout的作用是ViewGroup用来确定子View的位置,当ViewGroup的位置被确定后,它会在onLayout中遍历所有的子View并调用其layout方法,在 layout 方法中, onLayout 方法又会被调用。

      View的 layout 方法确定本身的位置,源码流程如下:
        1、  setFrame 确定View的四个顶点位置,即确定了View在父容器中的位置
        2、 调用 onLayout 方法,确定所有子View的位置,和onMeasure一样,onLayout的实现和布局有关,因此View和ViewGroup均没有真正实现 onLayout 方法。

       View的测量宽高和最终宽高的区别:
               View的测量宽高和最终宽高相等,只不过测量宽高形成于measure过程,最终宽高形成于layout过程。但重写view的layout方法可以使他们不相等。

            (3)、draw过程
                  View的绘制过程遵循如下几步:
                       1、 绘制背景 drawBackground(canvas)
                       2、 绘制自己 onDraw
                       3、绘制children dispatchDraw 遍历所有子View的 draw 方法
                       4、绘制装饰 onDrawScrollBars
                    
         ViewGroup会默认启用 setWillNotDraw 为ture,导致系统不会去执行 onDraw ,所以自定义ViewGroup需要通过onDraw来绘制内容时,必须显式的关闭 WILL_NOT_DRAW 这个优化标记位,即调用 setWillNotDraw(false);

##4、自定义View
     (1)自定义View的分类                     
 1、继承View 重写onDraw方法
通过 onDraw 方法来实现一些不规则的效果,这种效果不方便通过布局的组合方式来达到。这种方式需要自己支持 wrap_content 并且padding也要去进行处理。

2、继承ViewGroup派生特殊的layout
实现自定义的布局方式,需要合适地处理ViewGroup的测量、布局这两个过程,并同时处理子View的测量和布局过程。

3、继承特定的View子类( 如TextView、Button)
扩展某种已有的控件的功能,比较简单,不需要自己去管理 wrap_content 和padding。

4、继承特定的ViewGroup子类( 如LinearLayout)
比较常见,实现几种view组合一起的效果。与方法二的差别是方法二更接近底层实现。

    (2)自定义View的须知

1、直接继承View或ViewGroup的控件, 需要在onmeasure中对wrap_content做特殊处理。指定wrap_content下的默认宽/高。

2、直接继承View的控件,如果不在draw方法中处理padding,那么padding属性就无法起作用。直接继承ViewGroup的控件也需要在onMeasure和onLayout中考虑padding和子元素margin的影响,不然padding和子元素的margin无效。

3、尽量不要用在View中使用Handler,因为没必要。View内部提供了post系列的方法,完全可以替代Handler的作用。

4、View中有线程和动画,需要在View的onDetachedFromWindow中停止。当View不可见时,也需要停止线程和动画,否则可能造成内存泄漏。

5、View带有滑动嵌套情形时,需要处理好滑动冲突


   (3)自定义View实例

  1、继承View重写onDraw方法:采用这种方式需要自己支持wrap_content,并且padding也需要自己处理。

CircleView   遵循以下步骤:
    1、在values目录下创建自定义属性的XML,如attrs.xml。      

<?xml version="1.0" encoding="utf-8"?>
                 <resources>
                     <declare-styleable name="CircleView">
                         <attr name="circle_color" format="color" />
                     </declare-styleable>
                 </resources>


      2、在View的构造方法中解析自定义属性的值并做相应处理,这里我们解析circle_color。 然后在onDraw中绘制圆。 

public class CicleView extends View {
    private  int mColor = Color.RED;
   Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    public CicleView(Context context) {
        super(context);
        init();
    }
    public CicleView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    public CicleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
        typedArray.getColor(R.styleable.CircleView_circle_color,Color.RED);
        typedArray.recycle();
        init();
    }
    private void init() {
        mPaint.setColor(mColor);
    }
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width = getWidth();
        int height = getHeight();
        int radius = Math.min(width,height) / 2;
        canvas.drawCircle( width/2,height/2,radius,mPaint);
    }
}


3、在Activity布局文件中使用自定义属性

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                 xmlns:app="http://schemas.android.com/apk/res-auto"
                 android:layout_width="match_parent"
                 android:layout_height="match_parent"
                 android:background="#ffffff"
                 android:orientation="vertical" >
                 <com.ryg.chapter_4.ui.CircleView
                     android:id="@+id/circleView1"
                     android:layout_width="wrap_content"
                     android:layout_height="100dp"
                     android:layout_margin="20dp"
                     android:background="#000000"
                     android:padding="20dp"
                     app:circle_color="@color/light_green" />
                 </LinearLayout>

实现的效果为:

虽然设置了 android:padding="20dp",但是并没有效果。使用android:layout_width="wrap_content" 或者设置为match_parent都不行,前面章节提到过,对于直接继承自View的控件,如果不对wrap_content做处理,那么就相当于使用match_parent;
在这里只需要指定一个wrap_content模式的默认宽高即可,比如选择200dp作为默认的宽高。

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(200,200);
        }else if(widthSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(200,heightSpecSize);
        }else if(heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSpecSize,200);
        }
    }

实现效果为

发现 android:padding="20dp" 没有达到效果,对此我们要对onDraw()做一下修改

 protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();
        int width = getWidth() -paddingLeft-paddingRight;
        int height = getHeight() -paddingBottom-paddingTop;
        int radius = Math.min(width,height) / 2;
        canvas.drawCircle(paddingLeft + width/2,paddingTop + height/2,radius,mPaint);
    }

就是在绘制的时候考虑下view四周的空白即可,其中圆心和半径都会考虑到view四周的padding,就达到了要求。

 

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值