Android View 绘制 攻略

119 阅读18分钟

攻略大全

1. 粘贴攻略

1.1 Android坐标体系

Android的坐标系定义为:

  • 屏幕的左上角为坐标原点
  • 向右为x轴增大方向
  • 向下为y轴增大方向

944365-ee0cd39fd788e293.png

1.2 View位置(坐标)描述

View的位置由4个顶点决定的(如下A、B、C、D)

944365-398c610a464cbdc8.png

4个顶点的位置描述分别由4个值决定:

(请记住:View的位置是相对于父控件而言的)

  • Top:子View上边界到父view上边界的距离
  • Left:子View左边界到父view左边界的距离
  • Bottom:子View下边距到父View上边界的距离
  • Right:子View右边界到父view左边界的距离

如下图:

944365-2fb2682c45d05ff9.png

1.3 View位置获取方式

  • View的位置是通过view.getxxx()函数进行获取:
// 获取Top位置
public final int getTop() {
return mTop;
}

// 获取子View左上角距父View左侧的距离
getLeft(); 
// 获取子View右下角距父View顶部的距离
getBottom(); 
// 获取子View右下角距父View左侧的距离
getRight(); 
  • 与MotionEvent中 get()和getRaw()的区别
// get() :触摸点相对于其所在组件坐标系的坐标
event.getX();
event.getY();

// getRaw() :触摸点相对于屏幕默认坐标系的坐标
event.getRawX();
event.getRawY();

具体如下图:

944365-e50a2705cdd632d3.png

1.4 ViewGroup.LayoutParams

  • 简介

    布局参数类

  1. ViewGroup 的子类(RelativeLayout、LinearLayout)有其对应的 ViewGroup.LayoutParams 子类
  2. 如:RelativeLayout的 ViewGroup.LayoutParams子类 = RelativeLayoutParams
  • 作用

    指定视图View 的高度(height) 和 宽度(width)等布局参数。

  • 具体使用

    通过以下参数指定

参数解释
具体指dp/px
fill_parent强制性使子视图的大小扩展至与父视图大小相等(不含 padding )
match_parent与fill_parent相同,用于Android 2.3 & 之后版本
wrap_content自适应大小,强制性地使视图扩展以便显示其全部内容(含 padding )

1.5 MeasureSpec

1.5.1 简介

944365-0cf0a1ffd083cad1.png

1.5.2 组成

测量规格(MeasureSpec) = 测量模式(mode) + 测量大小(size)

944365-7d0f873cee3912bb.png

其中,测量模式(Mode)的类型有3种:UNSPECIFIED、EXACTLY 和 AT_MOST。具体如下:

944365-e631b96ea1906e34.png

1.5.3 具体使用

  • MeasureSpec 是View类中的一个静态内部类
  • MeasureSpec类 用1个变量封装了2个数据(size,mode):通过使用二进制,将测量模式(mode) & 测量大小(size)打包成一个int值来,并提供了打包 & 解包的方法

该措施的目的 = 减少对象内存分配

  • 实际使用

// 1. 获取测量模式(Mode)
int specMode = MeasureSpec.getMode(measureSpec)

// 2. 获取测量大小(Size)
int specSize = MeasureSpec.getSize(measureSpec)

// 3. 通过Mode 和 Size 生成新的SpecMode
int measureSpec = MeasureSpec.makeMeasureSpec(size, mode);

1.6 Window - DecorView - ViewRoot - Activity

1.6.1 Window

image.png

1.6.2 DecorView

  • 定义:顶层View

即 Android 视图树的根节点;同时也是 FrameLayout 的子类

  • 作用:显示 & 加载布局

View层的事件都先经过DecorView,再传递到View

  • 特别说明

    内含1个竖直方向的LinearLayout,分为2部分:上 = 标题栏(titlebar)、下 = 内容栏(content)

944365-4923b6377b032256.png

在Activity中通过 setContentView()所设置的布局文件其实是被加到内容栏之中的,成为其唯一子View = id为content的FrameLayout中.

// 在代码中可通过content得到对应加载的布局
// 1. 得到content
ViewGroup content = (ViewGroup)findViewById(android.R.id.content);

// 2. 得到设置的View
ViewGroup rootView = (ViewGroup) content.getChildAt(0);

1.6.3 ViewRoot

  • 定义

    连接器,对应于ViewRootImpl类

  • 作用

    1. 连接WindowManager 和 DecorView
    2. 完成View的三大流程: measure、layout、draw
  • 特别注意

// 在主线程中,Activity对象被创建后:
// 1. 自动将DecorView添加到Window中 & 创建ViewRootImpll对象
root = new ViewRootImpl(view.getContent(),display);

// 3. 将ViewRootImpll对象与DecorView建立关联
root.setView(view,wparams,panelParentView)

1.6.4 Activity

image.png

1.6.5 总结

944365-b9c41aa994e8ddf4.png

944365-34992eb46bdf93e7.png

1.7 View的Measure过程

测量View的宽 / 高

  1. 在某些情况下,需要多次测量(measure)才能确定View最终的宽/高;
  2. 该情况下,measure过程后得到的宽 / 高可能不准确;
  3. 此处建议:在layout过程中onLayout()去获取最终的宽 / 高

1.8 View的Layout过程

计算视图(View)的位置

即计算View的四个顶点位置:Left、Top、Right 和 Bottom

1.9 View的Draw过程

绘制视图(View)

2. 造火箭攻略

2.1 MeasureSpec

子View的MeasureSpec值根据子View的布局参数(LayoutParams)和父容器的MeasureSpec值计算得来的,具体计算逻辑封装在getChildMeasureSpec()里。如下图:

944365-d059b1afdeae0256.png

即:子view的大小由父view的MeasureSpec值 和 子view的LayoutParams属性 共同决定。

2.2 getChildMeasureSpec()

944365-76261325e6576361.png

944365-6088d2d291bbae09.png

  • 注:区别于顶级View(即DecorView)的测量规格MeasureSpec计算逻辑:取决于 自身布局参数与窗口尺寸。

    944365-560312570515aef4.png

2.3 DecorView

image.png

image.png

2.4 View的Measure过程

944365-6fd614936d045071.png

944365-2ba9801fcad2b48c.png

实际作用的方法:getDefaultSize() = 计算View的宽/高值、setMeasuredDimension() = 存储测量后的View宽 / 高。

2.5 ViewGroup的Measure过程

  • 原理

    1. 遍历 测量所有子View的尺寸

    2. 合并计算所有子View的尺寸,最终得到ViewGroup父视图的测量值

    自上而下、一层层地传递下去,直到完成整个View树的measure()过程

944365-7133935cb1e56190.png

  • 流程

944365-1438a7fbd93d0987.png

ViewGroup的measure过程如下:

944365-c9ea47e8b5e325bf.png

2.6 View的Layout过程

944365-05b688ab79b57ecf.png

944365-756f72f8ccc58d2c.png

2.7 ViewGroup的Layout过程

944365-7ebd03609c758d47.png

944365-6e27d40b50081d60.png

2.8 View的Draw过程

944365-1554a862ed0c3f95.png

2.9 ViewGroup的Draw过程

944365-ff799e17e24e4a3b.png

944365-cf42edcab0a206fa.png

3. 拧螺丝攻略

3.1 MeasureSpec的源码解析

public class MeasureSpec {

    // 进位大小 = 2的30次方
    // int的大小为32位,所以进位30位 = 使用int的32和31位做标志位
    private static final int MODE_SHIFT = 30;

    // 运算遮罩:0x3为16进制,10进制为3,二进制为11
    // 3向左进位30 = 11 00000000000(11后跟30个0)
    // 作用:用1标注需要的值,0标注不要的值。因1与任何数做与运算都得任何数、0与任何数做与运算都得0
    private static final int MODE_MASK = 0x3 << MODE_SHIFT;

    // UNSPECIFIED的模式设置:0向左进位30 = 00后跟30个0,即00 00000000000
    // 通过高2位
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;

    // EXACTLY的模式设置:1向左进位30 = 01后跟30个0 ,即01 00000000000
    public static final int EXACTLY = 1 << MODE_SHIFT;

    // AT_MOST的模式设置:2向左进位30 = 10后跟30个0,即10 00000000000
    public static final int AT_MOST = 2 << MODE_SHIFT;

    /**
     * makeMeasureSpec()方法
     * 作用:根据提供的size和mode得到一个详细的测量结果,即measureSpec
     */
    public static int makeMeasureSpec(int size, int mode) {
        return size + mode;
        // measureSpec = size + mode;此为二进制的加法 而不是十进制
        // 设计目的:使用一个32位的二进制数,其中:32和31位代表测量模式(mode)、后30位代表测量大小(size)
        // 例如size=100(4),mode=AT_MOST,则measureSpec=100+10000...00=10000..00100
    }

    /**
     * getMode()方法
     * 作用:通过measureSpec获得测量模式(mode)
    **/
    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
        // 即:测量模式(mode) = measureSpec & MODE_MASK;
        // MODE_MASK = 运算遮罩 = 11 00000000000(11后跟30个0)
        //原理:保留measureSpec的高2位(即测量模式)、使用0替换后30位
        // 例如10 00..00100 & 11 00..00(11后跟30个0) = 10 00..00(AT_MOST),这样就得到了mode的值
    }

    /**
    * getSize方法
    * 作用:通过measureSpec获得测量大小size
    **/
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
        // size = measureSpec & ~MODE_MASK;
        // 原理类似上面,即 将MODE_MASK取反,也就是变成了00 111111(00后跟30个1),将32,31替换成0也就是去掉mode,保留后30位的size
    }

}

3.2 getChildMeasureSpec()的源码分析

/**
 * @param spec 父view的详细测量值(MeasureSpec)
 * @param padding view当前尺寸的的内边距和外边距(padding,margin)
 * @param childDimension 子视图的布局参数(宽/高)
**/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {

    //父view的测量模式
    int specMode = MeasureSpec.getMode(spec);
    //父view的大小
    int specSize = MeasureSpec.getSize(spec);

    //通过父view计算出的子view = 父大小-边距(父要求的大小,但子view不一定用这个值)
    int size = Math.max(0, specSize - padding);

    //子view想要的实际大小和模式(需要计算)
    int resultSize = 0;
    int resultMode = 0;
    
    //通过父view的MeasureSpec和子view的LayoutParams确定子view的大小
    switch (specMode) {
        // 当父view的模式为EXACITY时,父view强加给子view确切的值
        //一般是父view设置为match_parent或者固定值的ViewGroup
        case MeasureSpec.EXACTLY:
            // 当子view的LayoutParams>0,即有确切的值
            if (childDimension >= 0) {
                //子view大小为子自身所赋的值,模式大小为EXACTLY
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;

                // 当子view的LayoutParams为MATCH_PARENT时(-1)
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //子view大小为父view大小,模式为EXACTLY
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;

                // 当子view的LayoutParams为WRAP_CONTENT时(-2)
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //子view决定自己的大小,但最大不能超过父view,模式为AT_MOST
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // 当父view的模式为AT_MOST时,父view强加给子view一个最大的值。(一般是父view设置为wrap_content)
        case MeasureSpec.AT_MOST:
            // 道理同上
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;

            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;

            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;

            }
            break;

        // 当父view的模式为UNSPECIFIED时,父容器不对view有任何限制,要多大给多大
        // 多见于ListView、GridView
        case MeasureSpec.UNSPECIFIED:

            if (childDimension >= 0) {
                // 子view大小为子自身所赋的值
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;

            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // 因为父view为UNSPECIFIED,所以MATCH_PARENT的话子类大小为0
                resultSize = 0;
                resultMode = MeasureSpec.UNSPECIFIED;

            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // 因为父view为UNSPECIFIED,所以WRAP_CONTENT的话子类大小为0
                resultSize = 0;
                resultMode = MeasureSpec.UNSPECIFIED;

            }
            break;
    }

    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);

}

3.3 DecorView

3.3.1 DecorView的创建

/**
  * 具体使用:Activity的setContentView()
  */
  @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

/**
  * 源码分析:Activity的setContentView()
  */
   public void setContentView(int layoutResID) {
        // getWindow() 作用:获得Activity 的成员变量mWindow ->>分析1
        // Window类实例的setContentView() ->>分析2
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
   }

/**
  * 分析1:成员变量mWindow
  */
  // 1. 创建一个Window对象(即 PhoneWindow实例)
  // Window类 = 抽象类,其唯一实现类 = PhoneWindow
  mWindow = new PhoneWindow(this, window);
  
  // 2. 设置回调,向Activity分发点击或状态改变等事件
  mWindow.setWindowControllerCallback(this);
  mWindow.setCallback(this);

  // 3. 为Window实例对象设置WindowManager对象
        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);

    }

/**
  * 分析2:Window类实例的setContentView()
  */
  public void setContentView(int layoutResID) {

        // 1. 若mContentParent为空,创建一个DecroView
        // mContentParent即为内容栏(content)对应的DecorView = FrameLayout子类
        if (mContentParent == null) {
            installDecor(); // ->>分析3
        } else {
            // 若不为空,则删除其中的View
            mContentParent.removeAllViews();
        }

        // 2. 为mContentParent添加子View
        // 即Activity中设置的布局文件
        mLayoutInflater.inflate(layoutResID, mContentParent);

        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            //回调通知,内容改变
            cb.onContentChanged();
        }
    }

/**
  * 分析3:installDecor()
  * 作用:创建一个DecroView
  */
  private void installDecor() {

    if (mDecor == null) {
        // 1. 生成DecorView ->>分析4
        mDecor = generateDecor(); 
        mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
        mDecor.setIsRootNamespace(true);
        if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
            mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
        }
    }
    // 2. 为DecorView设置布局格式 & 返回mContentParent ->>分析5
    if (mContentParent == null) {
        mContentParent = generateLayout(mDecor); 
        ...
        } 
    }
}

/**
  * 分析4:generateDecor()
  * 作用:生成DecorView
  */
  protected DecorView generateDecor() {
        return new DecorView(getContext(), -1);
    }

/**
  * 分析5:generateLayout(mDecor)
  * 作用:为DecorView设置布局格式
  */
  protected ViewGroup generateLayout(DecorView decor) {

        // 1. 从主题文件中获取样式信息
        TypedArray a = getWindowStyle();

        // 2. 根据主题样式,加载窗口布局
        int layoutResource;
        int features = getLocalFeatures();

        // 3. 加载layoutResource
        View in = mLayoutInflater.inflate(layoutResource, null);

        // 4. 往DecorView中添加子View
        // 即文章开头介绍DecorView时提到的布局格式,那只是一个例子,根据主题样式不同,加载不同的布局。
        decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); 
        mContentRoot = (ViewGroup) in;

        // 5. 这里获取的是mContentParent = 即为内容栏(content)对应的DecorView = FrameLayout子类
        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); 
      
        return contentParent;
    }

源码总结

  1. 创建Window抽象类的子类PhoneWindow类的实例对象;
  2. 为PhoneWindow类对象设置WindowManager对象;
  3. 为PhoneWindow类对象创建1个DecroView类对象(根据所选的主题样式增加);
  4. 为DecroView类对象中的content增加Activity中设置的布局文件。

此时,DecorView(即顶层View)已创建和添加Activity中设置的布局文件中,但目前仍未显示出来,即不可见。

3.3.2 DecorView的显示

/**
  * 源码分析:主线程创建时,调用的handleResumeActivity()
  */
  @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

/**
  * 源码分析:Activity的setContentView()
  */
 final void handleResumeActivity(IBinder token,
    boolean clearHide, boolean isForward, boolean reallyResume) {

    ActivityClientRecord r = performResumeActivity(token, clearHide);

    if (r != null) {
        final Activity a = r.activity;
          if (r.window == null && !a.mFinished && willBeVisible) {
        // 1. 获取Window实例中的Decor对象
        r.window = r.activity.getWindow();
        View decor = r.window.getDecorView();

        // 2. DecorView对用户不可见
        decor.setVisibility(View.INVISIBLE);
        ViewManager wm = a.getWindowManager();
        WindowManager.LayoutParams l = r.window.getAttributes();
        a.mDecor = decor;
      
        l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;

        // 3. DecorView被添加进WindowManager了
        // 此时,还是不可见
        if (a.mVisibleFromClient) {
            a.mWindowAdded = true;
            wm.addView(decor, l);
        }

        // 4. 此处设置DecorView对用户可见
        if (!r.activity.mFinished && willBeVisible
            && r.activity.mDecor != null && !r.hideForNow) {
             if (r.activity.mVisibleFromClient) {
                    r.activity.makeVisible();
                    // —>>分析1
                }
            }
    }
/**
  * 分析1:Activity.makeVisible()
  */
  void makeVisible() {
   if (!mWindowAdded) {
            ViewManager wm = getWindowManager();
            // 1. 将DecorView添加到WindowManager ->>分析2
            wm.addView(mDecor, getWindow().getAttributes());
            mWindowAdded = true;
        }
        // 2. DecorView可见
        mDecor.setVisibility(View.VISIBLE);
    }

/**
  * 分析2:wm.addView
  * 作用:WindowManager = 1个接口,由WindowManagerImpl类实现
  */
  public final class WindowManagerImpl implements WindowManager {    
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

    @Override
    public void addView(View view, ViewGroup.LayoutParams params) {
        mGlobal.addView(view, params, mDisplay, mParentWindow); ->>分析3
    }
}

/**
  * 分析3:WindowManagerGlobal 的addView()
  */
  public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow) {

     final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;

     ...

     synchronized (mLock) {

     // 1. 实例化一个ViewRootImpl对象
     ViewRootImpl root;
     root = new ViewRootImpl(view.getContext(), display);
     view.setLayoutParams(wparams);

     mViews.add(view);
     mRoots.add(root);
     mParams.add(wparams);
     }

     // 2. WindowManager将DecorView实例对象交给ViewRootImpl 绘制View
     root.setView(view, wparams, panelParentView);
     // ->> 分析4
       }
    }
 }

/**
  * 分析4:ViewRootImpl.setView()
  */
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
                requestLayout(); // ->>分析5
    }

/**
  * 分析5:ViewRootImpl.requestLayout()
  */
    @Override
    public void requestLayout() {

        if (!mHandlingLayoutInLayoutRequest) {
            // 1. 检查是否在主线程
            checkThread();
            mLayoutRequested = true;//mLayoutRequested 是否measure和layout布局。
            // 2. ->>分析6
            scheduleTraversals();
        }
    }

/**
  * 分析6:ViewRootImpl.scheduleTraversals()
  */
    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();

            // 通过mHandler.post()发送一个runnable,在run()方法中去处理绘制流程
            // 与ActivityThread的Handler消息传递机制相似
            // ->>分析7
            mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        }
    }

/**
  * 分析7:Runnable类的子类对象mTraversalRunnable
  * 作用:在run()方法中去处理绘制流程
  */
    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal(); // ->>分析8
        }
    }
    final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

/**
  * 分析8:doTraversal()
  */
    void doTraversal() {
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            performTraversals(); 
            // 最终会调用performTraversals(),从而开始View绘制的3大流程:Measure、Layout、Draw
    }

// 注:
// a. ViewRootImpl中W类是Binder的Native端,用于接收WmS处理操作
// b. 因W类的接收方法是在线程池中的,故可通过Handler将事件处理切换到主线程中

源码总结

  1. 将DecorView对象添加到WindowManager中;
  2. 创建ViewRootImpl对象;
  3. WindowManager将DecorView对象交给ViewRootImpl对象;
  4. ViewRootImpl对象通过Handler向主线程发送了一条触发遍历操作的消息:performTraversals();该方法用于执行View的绘制流程(measure、layout、draw)。

ViewRootImpl对象中接收的各种变化(如来自WMS的窗口属性变化、来自控件树的尺寸变化、重绘请求等都引发performTraversals()的调用及完成相关处理,并最终显示到可见的Activity中。整个流程如图下所示。

image.png

从上面的结论可以看出:

  • 一次次performTraversals()的调用驱动着控件树有条不紊的工作;
  • 一旦此方法无法正常执行,整个控件树都将处于僵死状态;
  • 因此performTraversals()可以说是ViewRootImpl类对象的核心逻辑。而performTraversals()的后续逻辑,则是View绘制的三大流程:测量流程(measure)、布局流程(layout)、绘制流程(draw)。

3.4 View的Measure过程

/**
 * 源码分析:measure()
 * 定义:Measure过程的入口;属于View.java类 & final类型,即子类不能重写此方法
 * 作用:基本测量逻辑的判断
**/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {

    // 参数说明:View的宽 / 高测量规格
    ...

    int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? 
    -1 :mMeasureCache.indexOfKey(key);

    if (cacheIndex < 0 || sIgnoreMeasureCache) {
        // 计算视图大小 ->>分析1
        onMeasure(widthMeasureSpec, heightMeasureSpec);
    } else {
        ...
    }


    /**
     * 分析1:onMeasure()
     * 作用:a. 根据View宽/高的测量规格计算View的宽/高值:getDefaultSize()
     * b. 存储测量后的View宽 / 高:setMeasuredDimension()
     */
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         // 参数说明:View的宽 / 高测量规格
         
        // setMeasuredDimension() :获得View宽/高的测量值 ->>分析2
        // 传入的参数通过getDefaultSize()获得 ->>分析3 
       setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
        getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

    /**
     * 分析2:setMeasuredDimension()
     * 作用:存储测量后的View宽 / 高
     * 注:该方法即为我们重写onMeasure()所要实现的最终目的
     */
    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        //参数说明:测量后子View的宽 / 高值
        
        // 将测量后子View的宽 / 高值进行传递
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;
        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }

    /**
     * 分析3:getDefaultSize()
     * 作用:根据View宽/高的测量规格计算View的宽/高值
    **/
    public static int getDefaultSize(int size, int measureSpec) {
         // 参数说明:
        // size:提供的默认大小
        // measureSpec:宽/高的测量规格(含模式 & 测量大小)
        
        // 设置默认大小
        int result = size;

        // 获取宽/高测量规格的模式 & 测量大小
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {

            // 模式为UNSPECIFIED时,使用提供的默认大小 = 参数Size
            case MeasureSpec.UNSPECIFIED:
                result = size;
            break;

            // 模式为AT_MOST,EXACTLY时,使用View测量后的宽/高值 = measureSpec中的Size
            case MeasureSpec.AT_MOST:
            case MeasureSpec.EXACTLY:
                result = specSize;
            break;
        }

    // 返回View的宽/高值
    return result;
    }
}
  • 上面提到,当模式是UNSPECIFIED时,使用的是提供的默认大小(即第一个参数size);那么,提供的默认大小具体是多少呢?
  • 答:在onMeasure()方法中,getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec)中传入的默认大小是getSuggestedMinimumWidth()。

接下来我们继续看getSuggestedMinimumWidth()的源码分析

由于getSuggestedMinimumHeight()类似,所以此处仅分析getSuggestedMinimumWidth()

  • 源码分析如下:
protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth,mBackground.getMinimumWidth());
}

从代码可以看出:

  • 若 View 未设置背景,那么View的宽度 = mMinWidth
  1. mMinWidth = android:minWidth属性所指定的值;
  2. 若android:minWidth没指定,则默认为0。
  • 若 View设置了背景,View的宽度为mMinWidth和mBackground.getMinimumWidth()中的最大值

那么,mBackground.getMinimumWidth()的大小具体指多少?继续看getMinimumWidth()的源码分析:

public int getMinimumWidth() {
    final int intrinsicWidth = getIntrinsicWidth();
    //返回背景图Drawable的原始宽度
    return intrinsicWidth > 0 ? intrinsicWidth :0 ;
}

// 由源码可知:mBackground.getMinimumWidth()的大小 = 背景图Drawable的原始宽度
// 若无原始宽度,则为0
// 注:BitmapDrawable有原始宽度,而ShapeDrawable没有

总结:getDefaultSize()计算View的宽/高值的逻辑:

944365-bf6b3dc2261012dc.png

至此,单一View的宽/高值已经测量完成,即对于单一View的measure过程已经完成。

3.5 ViewGroup的的Measure过程

/**
 * 源码分析:measure()
 * 作用:基本测量逻辑的判断;调用onMeasure()
 * 注:与单一View measure过程中讲的measure()一致
**/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    ...
    int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
    mMeasureCache.indexOfKey(key);

    if (cacheIndex < 0 || sIgnoreMeasureCache) {
        // 调用onMeasure()计算视图大小
        onMeasure(widthMeasureSpec, heightMeasureSpec);
        mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    } else {
        ...
    }
}

/**
* 分析1:onMeasure()
* 作用:遍历子View & 测量
**/

为什么ViewGroup的measure过程不像单一View的measure过程那样对onMeasure()做统一的实现?(如下代码)

/**
 * 分析:子View的onMeasure()
 * 作用:a. 根据View宽/高的测量规格计算View的宽/高值:getDefaultSize()
 *      b. 存储测量后的View宽 / 高:setMeasuredDimension()
 * 注:与单一View measure过程中讲的onMeasure()一致
**/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 参数说明:View的宽 / 高测量规格
    
    // setMeasuredDimension() :获得View宽/高的测量值
    // 传入的参数通过getDefaultSize()获得 
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
    getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    
}
  • 答:因为不同的ViewGroup子类(LinearLayout、RelativeLayout / 自定义ViewGroup子类等)具备不同的布局特性,这导致他们子View的测量方法各有不同

而onMeasure()的作用 = 测量View的宽/高值

因此,ViewGroup无法对onMeasure()作统一实现。这个也是单一View的measure过程与ViewGroup过程最大的不同。

  1. 即 单一View measure过程的onMeasure()具有统一实现,而ViewGroup则没有
  2. 注:其实,在单一View measure过程中,getDefaultSize()只是简单的测量了宽高值,在实际使用时有时需更精细的测量。所以有时候也需重写onMeasure()

在自定义ViewGroup中,关键在于:根据需求复写onMeasure()从而实现你的子View测量逻辑。复写onMeasure()的套路如下:

/**
 * 根据自身的测量逻辑复写onMeasure(),分为3步
 * 1. 遍历所有子View & 测量:measureChildren()
 * 2. 合并所有子View的尺寸大小,最终得到ViewGroup父视图的测量值(自身实现)
 * 3. 存储测量后View宽/高的值:调用setMeasuredDimension()
**/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    // 定义存放测量后的View宽/高的变量
    int widthMeasure ;
    int heightMeasure ;

    // 1. 遍历所有子View & 测量(measureChildren())
    // ->> 分析1
    measureChildren(widthMeasureSpec, heightMeasureSpec);

    // 2. 合并所有子View的尺寸大小,最终得到ViewGroup父视图的测量值
    void measureMerge{
        ... // 自身实现
    }

    // 3. 存储测量后View宽/高的值:调用setMeasuredDimension()
    // 类似单一View的过程,此处不作过多描述
    setMeasuredDimension(widthMeasure, heightMeasure);
}

// 从上可看出:
// 复写onMeasure()有三步,其中2步直接调用系统方法
// 需自身实现的功能实际仅为步骤2:合并所有子View的尺寸大小

/**
 * 分析1:measureChildren()
 * 作用:遍历子View & 调用measureChild()进行下一步测量
**/
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    // 参数说明:父视图的测量规格(MeasureSpec)
    
    final int size = mChildrenCount;
    final View[] children = mChildren;
    // 遍历所有子view
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        // 调用measureChild()进行下一步的测量 ->>分析1
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }

}

/**
 * 分析2:measureChild()
 * 作用:a. 计算单个子View的MeasureSpec
 *      b. 测量每个子View最后的宽 / 高:调用子View的measure()
 **/
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {

// 1. 获取子视图的布局参数
final LayoutParams lp = child.getLayoutParams();

// 2. 根据父视图的MeasureSpec & 布局参数LayoutParams,计算单个子View的MeasureSpec
// 获取 ChildView 的 widthMeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
// 获取 ChildView 的 heightMeasureSpec
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);

// 3. 将计算好的子View的MeasureSpec值传入measure(),进行最后的测量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

至此,ViewGroup的measure过程分析完毕。

3.6 View的Layout过程

/**
 * 源码分析:layout()
 * 作用:确定View本身的位置,即设置View本身的四个顶点位置
 **/
public void layout(int l, int t, int r, int b) {

    // 当前视图的四个顶点
    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;

    // 1. 确定View的位置:setFrame() / setOpticalFrame()
    // 即初始化四个顶点的值、判断当前View大小和位置是否发生了变化 & 返回
    // ->>分析1、分析2
    boolean changed = isLayoutModeOptical(mParent) ?
    setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

    // 2. 若视图的大小 & 位置发生变化
    // 会重新确定该View所有的子View在父容器的位置:onLayout()
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
            
            // 对于单一View的laytou过程:由于单一View是没有子View的,故onLayout()是一个空实现->>分析3
            // 对于ViewGroup的laytou过程:由于确定位置与具体布局有关,所以onLayout()在ViewGroup为1个抽象方法,需重写实现(后面会详细说)
            ...
        }
}

/**
 * 分析1:setFrame()
 * 作用:根据传入的4个位置值,设置View本身的四个顶点位置
 * 即:最终确定View本身的位置
**/
protected boolean setFrame(int left, int top, int right, int bottom) {

    ...
    // 通过以下赋值语句记录下了视图的位置信息,即确定View的四个顶点
    // 从而确定了视图的位置
    mLeft = left;
    mTop = top;
    mRight = right;
    mBottom = bottom;
    mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
}

/**
 * 分析2:setOpticalFrame()
 * 作用:根据传入的4个位置值,设置View本身的四个顶点位置
 * 即:最终确定View本身的位置
**/
private boolean setOpticalFrame(int left, int top, int right, int bottom) {

    Insets parentInsets = mParent instanceof View ?
    ((View) mParent).getOpticalInsets() : Insets.NONE;

    Insets childInsets = getOpticalInsets();

    // 内部实际上是调用setFrame()
    return setFrame(
        left + parentInsets.left - childInsets.left,
        top + parentInsets.top - childInsets.top,
        right + parentInsets.left + childInsets.right,
        bottom + parentInsets.top + childInsets.bottom);
}

/**
 * 分析3:onLayout()
 * 注:对于单一View的laytou过程
 * a. 由于单一View是没有子View的,故onLayout()是一个空实现
 * b. 由于在layout()中已经对自身View进行了位置计算,所以单一View的layout过程在layout()后就已完成了
**/
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    // 参数说明
    // changed 当前View的大小和位置改变了
    // left 左部位置
    // top 顶部位置
    // right 右部位置
    // bottom 底部位置
}

3.7 ViewGroup的Layout过程

/**
 * 源码分析:layout()
 * 作用:确定View本身的位置,即设置View本身的四个顶点位置
 * 注:与单一View的layout()源码一致
 **/
public void layout(int l, int t, int r, int b) {

    // 当前视图的四个顶点
    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;

    // 1. 确定View的位置:setFrame() / setOpticalFrame()
    // 即初始化四个顶点的值、判断当前View大小和位置是否发生了变化 & 返回
    // ->>分析1、分析2
    boolean changed = isLayoutModeOptical(mParent) ?
    setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

    // 2. 若视图的大小 & 位置发生变化
    // 会重新确定该View所有的子View在父容器的位置:onLayout()
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b);

        // 对于单一View的laytou过程:由于单一View是没有子View的,故onLayout()是一个空实现(上面已分析完毕)
        // 对于ViewGroup的laytou过程:由于确定位置与具体布局有关,所以onLayout()在ViewGroup为1个抽象方法,需重写实现 ->>分析3
        ...
    }
}


/**
 * 分析1:setFrame()
 * 作用:确定View本身的位置,即设置View本身的四个顶点位置
 */
protected boolean setFrame(int left, int top, int right, int bottom) {
    ...
    // 通过以下赋值语句记录下了视图的位置信息,即确定View的四个顶点

    // 从而确定了视图的位置
    mLeft = left;
    mTop = top;
    mRight = right;
    mBottom = bottom;
    mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
}


/**
 * 分析2:setOpticalFrame()
 * 作用:确定View本身的位置,即设置View本身的四个顶点位置
 */
private boolean setOpticalFrame(int left, int top, int right, int bottom) {

    Insets parentInsets = mParent instanceof View ?
    ((View) mParent).getOpticalInsets() : Insets.NONE;

    Insets childInsets = getOpticalInsets();

    // 内部实际上是调用setFrame()
    return setFrame(
        left + parentInsets.left - childInsets.left,
        top + parentInsets.top - childInsets.top,
        right + parentInsets.left + childInsets.right,
        bottom + parentInsets.top + childInsets.bottom);
}


/**
 * 分析3:onLayout()
 * 作用:计算该ViewGroup包含所有的子View在父容器的位置()
 * 注:
 * a. 定义为抽象方法,需重写,因:子View的确定位置与具体布局有关,所以onLayout()在ViewGroup没有实现
 * b. 在自定义ViewGroup时必须复写onLayout()!!!!!
 * c. 复写原理:遍历子View 、计算当前子View的四个位置值 & 确定自身子View的位置(调用子View layout())
 */
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

     // 参数说明
    // changed 当前View的大小和位置改变了
    // left 左部位置
    // top 顶部位置
    // right 右部位置
    // bottom 底部位置

        // 1. 遍历子View:循环所有子View
        for (int i=0; i<getChildCount(); i++) {
            View child = getChildAt(i);
            // 2. 计算当前子View的四个位置值
            // 2.1 位置的计算逻辑
            ...// 需自己实现,也是自定义View的关键
            // 2.2 对计算后的位置值进行赋值
            int mLeft = Left
            int mTop = Top
            int mRight = Right
            int mBottom = Bottom

            // 3. 根据上述4个位置的计算值,设置子View的4个顶点:调用子view的layout() & 传递计算过的参数
            // 即确定了子View在父容器的位置
            child.layout(mLeft, mTop, mRight, mBottom);
            // 该过程类似于单一View的layout过程中的layout()和onLayout(),此处不作过多描述
        }

}

3.8 View的Draw过程

/**
 * 源码分析:draw()
 * 作用:根据给定的 Canvas 自动渲染 View(包括其所有子 View)。
 * 注:
 * a. 在调用该方法之前必须要完成 layout 过程
 * b. 所有的视图最终都是调用 View 的 draw ()绘制视图( ViewGroup 没有复写此方法)
 * c. 在自定义View时,不应该复写该方法,而是复写 onDraw(Canvas) 方法进行绘制
 * d. 若自定义的视图确实要复写该方法,那么需先调用 super.draw(canvas)完成系统的绘制,然后再进行自定义的绘制
 */
public void draw(Canvas canvas) {

    /*
     * 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
     *      4. Draw children
     *      5. If necessary, draw the fading edges and restore layers
     *      6. Draw decorations (scrollbars for instance)
     */

    ...// 仅贴出关键代码
    int saveCount;

    // 步骤1: 绘制本身View背景
    if (!dirtyOpaque) {
        drawBackground(canvas);
    }

    // 若有必要,则保存图层(还有一个复原图层)
    // 优化技巧:当不需绘制 Layer 时,“保存图层“和“复原图层“这两步会跳过
    // 因此在绘制时,节省 layer 可以提高绘制效率

    final int viewFlags = mViewFlags;

    if (!verticalEdges && !horizontalEdges) {

        // 步骤3:绘制本身View内容
        if (!dirtyOpaque)
            onDraw(canvas);
        // View 中:默认为空实现,需复写
        // ViewGroup中:需复写



        // 步骤4:绘制子View
        // 由于单一View无子View,故View 中:默认为空实现
        // ViewGroup中:系统已经复写好对其子视图进行绘制我们不需要复写
        dispatchDraw(canvas);

        // Step 6, draw decorations (foreground, scrollbars)
        // 步骤6,绘制前景
        onDrawForeground(canvas);

        // Step 7, draw the default focus highlight
        // 步骤7,绘制默认焦点高亮
        drawDefaultFocusHighlight(canvas);

        return;
    
    }
    ...
}

/**
 * 步骤1:drawBackground(canvas)
 * 作用:绘制View本身的背景
 */
private void drawBackground(Canvas canvas) {

    // 获取背景 drawable
    final Drawable background = mBackground;

    if (background == null) {
        return;
    }

    // 根据在 layout 过程中获取的 View 的位置参数,来设置背景的边界
    setBackgroundBounds();

    .....

    // 获取 mScrollX 和 mScrollY值
    final int scrollX = mScrollX;
    final int scrollY = mScrollY;

    if ((scrollX | scrollY) == 0) {
        background.draw(canvas);
    } else {
        // 若 mScrollX 和 mScrollY 有值,则对 canvas 的坐标进行偏移
        canvas.translate(scrollX, scrollY);

        // 调用 Drawable 的 draw 方法绘制背景
        background.draw(canvas);
        canvas.translate(-scrollX, -scrollY);
    }

}


/**
 * 步骤3:onDraw(canvas)
 * 作用:绘制View本身的内容
 * 注:
 * a. 由于 View 的内容各不相同,所以该方法是一个空实现
 * b. 在自定义绘制过程中,需由子类去实现复写该方法,从而绘制自身的内容
 * c. 谨记:自定义View中 必须 且 只需复写onDraw()
 */
protected void onDraw(Canvas canvas) {
    // 复写从而实现绘制逻辑
    ... 
}


/**
 * 步骤4: dispatchDraw(canvas)
 * 作用:绘制子View
 * 注:由于单一View中无子View,故为空实现
 */
protected void dispatchDraw(Canvas canvas) {
    // 空实现
}


/**
 * 步骤6: onDrawForeground(canvas)
 * 作用:绘制装饰,如 滚动指示器、滚动条、和前景等
 */
public void onDrawForeground(Canvas canvas) {

    onDrawScrollIndicators(canvas);
    onDrawScrollBars(canvas);

    final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;

    if (foreground != null) {

        if (mForegroundInfo.mBoundsChanged) {
            mForegroundInfo.mBoundsChanged = false;
            final Rect selfBounds = mForegroundInfo.mSelfBounds;
            final Rect overlayBounds = mForegroundInfo.mOverlayBounds;

            if (mForegroundInfo.mInsidePadding) {
                selfBounds.set(0, 0, getWidth(), getHeight());
            } else {
                selfBounds.set(getPaddingLeft(), getPaddingTop(),
                getWidth() - getPaddingRight(), getHeight() - getPaddingBottom());
            }

            final int ld = getLayoutDirection();
            Gravity.apply(mForegroundInfo.mGravity, foreground.getIntrinsicWidth(),
            foreground.getIntrinsicHeight(), selfBounds, overlayBounds, ld);
            foreground.setBounds(overlayBounds);
        }
        foreground.draw(canvas);
    }
}

至此,View的draw过程已分析完毕。

3.9 ViewGroup的Draw过程


/**
 * 源码分析:draw()
 * 与单一View的draw()流程类似
 * 作用:根据给定的 Canvas 自动渲染 View(包括其所有子 View)
 * 注:
 * a. 在调用该方法之前必须要完成 layout 过程
 * b. 所有的视图最终都是调用 View 的 draw ()绘制视图( ViewGroup 没有复写此方法)
 * c. 在自定义View时,不应该复写该方法,而是复写 onDraw(Canvas) 方法进行绘制
 * d. 若自定义的视图确实要复写该方法,那么需先调用 super.draw(canvas)完成系统的绘制,然后再进行自定义的绘制
 */
public void draw(Canvas canvas) {
    // 与单一View的draw过程类似
}

由于drawBackground()、onDraw()、onDrawForeground()与单一View的draw过程类似,此处不作过多描述。
/**
 * 源码分析:dispatchDraw()
 * 作用:遍历子View & 绘制子View
 * 注:
 * a. ViewGroup中:由于系统为我们实现了该方法,故不需重写该方法
 * b. View中默认为空实现(因为没有子View可以去绘制)
 */
protected void dispatchDraw(Canvas canvas) {

    ......

    // 1. 遍历子View
    final int childrenCount = mChildrenCount;
    ......

    for (int i = 0; i < childrenCount; i++) {
        ......
        if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
        transientChild.getAnimation() != null) {
            // 2. 绘制子View视图 ->>分析1
            more |= drawChild(canvas, transientChild, drawingTime);
        }
         ....
    }
}

/**
 * 分析1:drawChild()
 * 作用:绘制子View
 */
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    // 最终还是调用了子 View 的 draw ()进行子View的绘制
    return child.draw(canvas, this, drawingTime);

至此,ViewGroup的draw过程已分析完毕。

3.10 View.setWillNotDraw()

/**
 * 源码分析:setWillNotDraw()
 * 定义:View 中的特殊方法
 * 作用:设置 WILL_NOT_DRAW 标记位;
 * 注:
 * a. 该标记位的作用是:当一个View不需要绘制内容时,系统进行相应优化
 * b. 默认情况下:View 不启用该标记位(设置为false);ViewGroup 默认启用(设置为true)
 */
public void setWillNotDraw(boolean willNotDraw) {
    setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}

// 应用场景
// a. setWillNotDraw参数设置为true:当自定义View继承自 ViewGroup 且本身并不具备任何绘制时,设置为 true 后,系统会进行相应的优化。
// b. setWillNotDraw参数设置为false:当自定义View继承自 ViewGroup 且需要绘制内容时,那么设置为 false,来关闭 WILL_NOT_DRAW 这个标记位。

4. 复制攻略

4.1 《Adnroid开发艺术探索》

4.2 总结UI原理和高级的UI优化方式