自定义View

自定义View_第1张图片
Andtoid中的颜色

ViewRootImpl的performTraversals()

private void performTraversals() {
        // cache mView since it is used so much below...
        //mView就是DecorView根布局
        final View host = mView;
        //在Step3 成员变量mAdded赋值为true,因此条件不成立
        if (host == null || !mAdded)
            return;
        //是否正在遍历
        mIsInTraversal = true;
        //是否马上绘制View
        mWillDrawSoon = true;
         ...
        //顶层视图DecorView所需要窗口的宽度和高度
        int desiredWindowWidth;
        int desiredWindowHeight;

         ...
        //在构造方法中mFirst已经设置为true,表示是否是第一次绘制DecorView
        if (mFirst) {
            mFullRedrawNeeded = true;
            mLayoutRequested = true;
            //如果窗口的类型是有状态栏的,那么顶层视图DecorView所需要窗口的宽度和高度就是除了状态栏
            if (lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL
                    || lp.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD) {
                // NOTE -- system code, won't try to do compat mode.
                Point size = new Point();
                mDisplay.getRealSize(size);
                desiredWindowWidth = size.x;
                desiredWindowHeight = size.y;
            } else {//否则顶层视图DecorView所需要窗口的宽度和高度就是整个屏幕的宽高
                DisplayMetrics packageMetrics =
                    mView.getContext().getResources().getDisplayMetrics();
                desiredWindowWidth = packageMetrics.widthPixels;
                desiredWindowHeight = packageMetrics.heightPixels;
            }
    }
  ...
//获得view宽高的测量规格,mWidth和mHeight表示窗口的宽高,lp.widthhe和lp.height表示DecorView根布局宽和高
 int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
 int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

  // Ask host how big it wants to be
  //执行测量操作
 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
  ...
  //执行布局操作
 performLayout(lp, desiredWindowWidth, desiredWindowHeight);
  ...
  //执行绘制操作
 performDraw();
}

启动Activity后,视图添加,绘制。是在ViewRootImpl.setView(),这方法中会丢一个Runnable给Choreographer,在下次Vsync信号来的时候会调用performTraversals(),然后会调用relaywindow(),请求WMS关联Java层Surface和native层Surface,然后执行View的绘制流程performMeasure(),performLayout(),performDraw()

Measure

MeasureSpec

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
}
 public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int oWidth  = insets.left + insets.right;
            int oHeight = insets.top  + insets.bottom;
            //根据原有宽高计算获取不同模式下的具体宽高值
            widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
            heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
        }
        ...
        if (forceLayout || needsLayout) {
            // first clears the measured dimension flag
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

            resolveRtlPropertiesIfNeeded();

            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                //在该方法中子控件完成具体的测量
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                ...
            } 
         ...
    }

MeasureSpec是View的内部类,内部封装了View的规格尺寸,以及View的宽高信息。在Measure的流程中,系统会将View的LayoutParams根据父容器是施加的规则转换为MeasureSpec,然后在onMeasure()方法中具体确定控件的宽高信息

MeasureSpec的常量中指定了两种内容,一种为尺寸模式,一种为具体的宽高信息。其中高2位表示尺寸测量模式,低30位表示具体的宽高信息。

①UNSPECIFIED:未指定模式,父容器没有对当前View有任何限制,当前View可以任意取尺寸。一般用于系统内部的测量
②AT_MOST:最大模式,当前尺寸是当前View能取的最大尺寸,对应于在xml文件中指定控件大小为wrap_content属性,子View的最终大小是父View指定的大小值,并且子View的大小不能大于这个值
③EXACTLY :精确模式,当前的尺寸就是当前View应该取的尺寸,对应于在xml文件中指定控件为match_parent属性或者是具体的数值,父容器测量出View所需的具体大小

match_parent--->EXACTLY。怎么理解呢?match_parent就是要利用父View给我们提供的所有剩余空间,而父View剩余空间是确定的,也就是这个测量模式的整数里面存放的尺寸。

wrap_content--->AT_MOST。怎么理解:就是我们想要将大小设置为包裹我们的view内容,那么尺寸大小就是父View给我们作为参考的尺寸,只要不超过这个尺寸就可以啦,具体尺寸就根据我们的需求去设定。

固定尺寸(如100dp)--->EXACTLY。用户自己指定了尺寸大小,我们就不用再去干涉了,当然是以指定的大小为主啦。

对于每一个View,都持有一个MeasureSpec,MeasureSpec保存了该View的尺寸测量模式以及具体的宽高信息,MeasureSpec受自身的LayoutParams和父容器的MeasureSpec共同影响。

自定义View_第2张图片
measure流程

View的Measure

   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
   protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;

            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }
 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;
    }

通过上文对MeasureSpec的分析,在这里我们就能明确,getDefaultSize实质上就是根据测绘模式确定子View的具体大小,而对于自定义View而言,子View的宽高信息不仅由自身决定,如果它被包裹在ViewGroup中就需要具体测量得到其精确值。

View的Measure过程中遇到的问题以及解决方案

View 的measure过程和Activity的生命周期方法不是同步执行的,因此无法保证Activity执行了onCreate、onStart、onResume时某个View已经测量完毕了。如果View还没有测量完毕,那么获得的宽和高都是0。下面是3种解决该问题的方法:
①Activity/View的onWindowsChanged()方法
onWindowFocusChanged()方法表示 View 已经初始化完毕了,宽高已经准备好了,这个时候去获取是没问题的。这个方法会被调用多次,当Activity继续执行或者暂停执行的时候,这个方法都会被调用,代码如下:

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

②View.post(runnable)方法
通过post将一个 Runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候View也已经初始化好了

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

③ViewTreeObsever
使用 ViewTreeObserver 的众多回调方法可以完成这个功能,比如使用onGlobalLayoutListener 接口,当 View树的状态发生改变或者View树内部的View的可见性发生改变时,onGlobalLayout 方法将被回调。伴随着View树的变化,这个方法也会被多次调用。

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

Layout

自定义View_第3张图片
layout过程

layout 的作用是ViewGroup来确定子元素的位置,当 ViewGroup 的位置被确定后,在layout中会调用onLayout ,在onLayout中会遍历所有的子元素并调用子元素的 layout 方法。
在代码中设置View的成员变量 mLeft,mTop,mRight,mBottom 的值,这几个值是在屏幕上构成矩形区域的四个坐标点,就是该View显示的位置,不过这里的具体位置都是相对与父视图的位置而言,而 onLayout 方法则会确定所有子元素位置,ViewGroup在onLayout函数中通过调用其children的layout函数来设置子视图相对与父视图中的位置,具体位置由函数 layout 的参数决定。下面我们先看View的layout 方法(只展示关键性代码)如下:

/*  
 *@param l view 左边缘相对于父布局左边缘距离 
 *@param t view 上边缘相对于父布局上边缘位置 
 *@param r view 右边缘相对于父布局左边缘距离 
 *@param b view 下边缘相对于父布局上边缘距离 
 */  
    public void layout(int l, int t, int r, int b) {
        ...
        //记录 view 原始位置  
        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;
        //调用 setFrame 方法 设置新的 mLeft、mTop、mBottom、mRight 值,  
        //设置 View 本身四个顶点位置  
        //并返回 changed 用于判断 view 布局是否改变  
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
        //第二步,如果 view 位置改变那么调用 onLayout 方法设置子 view 位置  
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        //调用 onLayout  
            onLayout(changed, l, t, r, b);
            ...
        }
        ...
    }

Draw

自定义View_第4张图片
Draw流程

在View的draw()方法的注释中,说明了绘制流程中具体每一步的作用,源码中对于draw()方法的注释如下,我们在这里重点分析注释中除第2、第5步外的其他步骤。

  /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background(绘制背景)
         *      2. If necessary, save the canvas' layers to prepare for fading(如果需要的话,保存画布背景以展示渐变效果)
         *      3. Draw view's content(绘制View的内容)
         *      4. Draw children(绘制子View)
         *      5. If necessary, draw the fading edges and restore layers(如果需要的话,绘制渐变边缘并恢复画布图层。)
         *      6. Draw decorations (scrollbars for instance)(绘制装饰(例如滚动条scrollbar))
         */

视图重绘
1、requestLayout重新绘制视图
子View调用requestLayout方法,会标记当前View及父容器,同时逐层向上提交,直到ViewRootImpl处理该事件,ViewRootImpl会调用三大流程,从measure开始,对于每一个含有标记位的view及其子View都会进行测量、布局、绘制。

2、invalidate在UI线程中重新绘制视图
当子View调用了invalidate方法后,会为该View添加一个标记位,同时不断向父容器请求刷新,父容器通过计算得出自身需要重绘的区域,直到传递到ViewRootImpl中,最终触发performTraversals方法,进行开始View树重绘流程(只绘制需要重绘的视图)。

3、postInvalidate在非UI线程中重新绘制视图
这个方法与invalidate方法的作用是一样的,都是使View树重绘,但两者的使用条件不同,postInvalidate是在非UI线程中调用,invalidate则是在UI线程中调用。

一般来说,如果View确定自身不再适合当前区域,比如说它的LayoutParams发生了改变,需要父布局对其进行重新测量、摆放、绘制这三个流程,往往使用requestLayout。而invalidate则是刷新当前View,使当前View进行重绘,不会进行测量、布局流程,因此如果View只需要重绘而不需要测量,布局的时候,使用invalidate方法往往比requestLayout方法更高效。

参考
自定义View心法——View工作流程
自定义View,有这一篇就够了
Android自定义View全解
教你步步为营掌握自定义View
Android自定义控件,你们是如何系统学习的?
安卓自定义View教程目录

你可能感兴趣的:(自定义View)