Android view绘制流程机制

摘要:
    你是否经常被Android千奇百怪的view所困扰,是否被各种动画所折磨,天下武功分内功和外功,内功讲究的是心法,会驱动外功,各式各样的界面设计就是外功,单学外功很难有质的突破,还需从内功攻克,本文将从view的内功开始,以Android view的根本需求为起点,站在设计者的角度,讲述了Android 系统view部分如何完成整个视图的绘制,结合view、viewgroup的组织结构,阐述整个流程机制,以期掌握Androidview部分的核心设计理念。
一、基本诉求
        学习一个事物,首先要找到它的首要问题,view顾名思义,视图的意思,如何显示视图是它的首要问题,从需求来看,view要满足任意的显示需求,各式各样的界面需求都要满足,如何设计一套方案来满足复杂的需求,变的十分重要,该问题的特点是复杂性,界面千变万化,如何应对复杂性,能更好的管理起来,将是解决方案的重中之重。
       可以借鉴其他领域的复杂问题来处理,比如如何管理一个公司,公司面对的事情各种各样,但公司的金字塔结构或者说树形结构,将各种事情不断的委托处理,分而治之。因此,采用树形结构是能满足问题的处理要求。所以,问题转化为如何通过树形结构,将显示需求一步步传递至子节点,各个子节点各自对上负责。每个子节点负责本节点的显示,然后组合起来就可以组装成各式各样的界面。
       见下面摘自官网的图,可以发现整个结构正是树形结构,viewgroup代表容器,负责布局子view,view代表具体的界面绘制。所以针对千变万化的展示需求,只需构造一颗view树,处理各个结点的布局绘制。绘制主要是确定位置和内容,结合组织结构图,核心就是从viewgroup到view的分解转化,该步骤是降低问题复杂度的关键,将问题一步步分解为下级的问题,一步步简化问题的处理,直到绘制一个图形,绘制背景颜色这种最简单的处理
二、绘制问题分解转化
       结合前面的分析,结合Android源码(主要是viewgroup和view两个类),系统最初提供给view树的是一个以左上角为原点,向右为x轴正方向,向下为y轴正方向的屏幕大小的坐标系;可完成绘制的canvas。要想正确绘制,前提是将view树完成各部分大小和位置的确定,对应于源码中的measure和layout,draw是最终目的。
        结合viewgroup和view中draw部分,输出了下图二者的转化原理,viewgroup中的canvas会完成translate,将坐标原点从父view的左上角,转化到子view的左上角,同时会结合子view的大小,完成cliprect,将绘制范围圈定在子view的范围,如此对子view来说,它的canvas原点在左上角,绘制范围在自身大小,如此子view面对的绘制坐标系和父view面临的情况一致,转化了数学中常说的同一类问题,如此可循环嵌套。值得注意的是,Android中引入了srcoll滑动的概念,会影响该转化,见图中说明,只是在计算坐标系偏移量时需要考虑到滑动的距离,网上经常可见对该部分的说明“Android引入scroll主要是为了滑动内容”。
 

具体到源码中measure、layout、draw部分的作用简述如下:
measure:最终结果是set measured width和height,onmeasure接受到当前view的measurespe,该measurespc是如何确定的呢?参见viewGroup的measureChildWithMargins和getChildMeasureSpec代码,通过parent的spec能够给予的自由(随便、至多,只能),自身大小期望wrap还是match、已经用去的大小,决定自身的spec。   其中最顶层的view由于没有parent,是自己确定的,见viewrootimpl中的getRootMeasureSpec。另外要注意的是,该过程只测量大小,不涉及位置,比如在线性布局文件,width和height两个属性才跟大小有关,经常使用的gravity只跟位置有关
protected void measureChildWithMargins(View child, 
        int parentWidthMeasureSpec, int widthUsed, 
        int parentHeightMeasureSpec, int heightUsed) { 
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); 

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, 
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin 
                    + widthUsed, lp.width); 
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, 
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin 
                    + heightUsed, lp.height); 

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 

public static int getChildMeasureSpec(int spec, int padding, int childDimension) { 
    int specMode = MeasureSpec.getMode(spec); 
    int specSize = MeasureSpec.getSize(spec); 

    int size = Math.max(0, specSize - padding); 

    int resultSize = 0; 
    int resultMode = 0; 

    switch (specMode) { 
    // Parent has imposed an exact size on us 
    case MeasureSpec.EXACTLY: 
        if (childDimension >= 0) { 
            resultSize = childDimension; 
            resultMode = MeasureSpec.EXACTLY; 
        } else if (childDimension == LayoutParams.MATCH_PARENT) { 
            // Child wants to be our size. So be it. 
            resultSize = size; 
            resultMode = MeasureSpec.EXACTLY; 
        } else if (childDimension == LayoutParams.WRAP_CONTENT) { 
            // Child wants to determine its own size. It can't be 
            // bigger than us.
            resultSize = size; 
            resultMode = MeasureSpec.AT_MOST; 
        } 
        break; 

    // Parent has imposed a maximum size on us 
    case MeasureSpec.AT_MOST: 
        if (childDimension >= 0) { 
            // Child wants a specific size... so be it 
            resultSize = childDimension; 
            resultMode = MeasureSpec.EXACTLY; 
        } else if (childDimension == LayoutParams.MATCH_PARENT) { 
            // Child wants to be our size, but our size is not fixed. 
            // Constrain child to not be bigger than us.
            resultSize = size; 
            resultMode = MeasureSpec.AT_MOST; 
        } else if (childDimension == LayoutParams.WRAP_CONTENT) { 
            // Child wants to determine its own size. It can't be 
            // bigger than us.
            resultSize = size; 
            resultMode = MeasureSpec.AT_MOST; 
        } 
        break; 

    // Parent asked to see how big we want to be 
    case MeasureSpec.UNSPECIFIED: 
        if (childDimension >= 0) { 
            // Child wants a specific size... let him have it 
            resultSize = childDimension; 
            resultMode = MeasureSpec.EXACTLY; 
        } else if (childDimension == LayoutParams.MATCH_PARENT) { 
            // Child wants to be our size... find out how big it should 
            // be
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; 
            resultMode = MeasureSpec.UNSPECIFIED; 
        } else if (childDimension == LayoutParams.WRAP_CONTENT) { 
            // Child wants to determine its own size.... find out how 
            // big it should be
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; 
            resultMode = MeasureSpec.UNSPECIFIED; 
        } 
        break; 
    } 
    //noinspection ResourceType 
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode); 

private static int getRootMeasureSpec(int windowSize, int rootDimension) { 
    int measureSpec; 
    switch (rootDimension) { 

    case ViewGroup.LayoutParams.MATCH_PARENT: 
        // Window can't resize. Force root view to be windowSize. 
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY); 
        break;
    case ViewGroup.LayoutParams.WRAP_CONTENT: 
        // Window can resize. Set max size for root view. 
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST); 
        break;
    default: 
        // Window wants to be an exact size. Force root view to be that size. 
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY); 
        break;
    } 
    return measureSpec; 

layout:在测量完大小的基础上,结合各边距、布局方式,设置view在parent中的位置left、top、right、bottom。
draw:由parent到child层级深入时,执行translate、clip操作,将坐标面向范围从viewgroup转化为view,同时考虑滑动偏移scroll,然后判断子view位置是否与对应rect相交,不相交则不显示,相交才显示。源码部分可见viewgroup和view中的draw部分,view里面有两个draw方法,其中一个参数较多的是parent来调用,另一个则被此方法调用
            
三、滑动相关
     为何将滑动单独拿出来说呢,主要是滑动在Android中经常会遇到,像banner、viewpager以及各种动画,其实不管是滑动也好,动画也好,这些均属于view绘制的部分,只不过是动态多次绘制变化的view。如此,变化的view绘制转化为了一系列静态图像的绘制,动态问题转化为静态问题,问题复杂度得以降低,而且前面的分析得以利用上。而不至于将静态view的绘制和动态view的绘制割裂为两个问题看待。
简单分析系统常见view控件的处理如下:
1、listview滑动处理是通过改变child的top、bottom来改变的。
2、view的scroll改变的是内部的内容,是通过translate坐标来改变的,canvas会改变rect top、left位置,但大小不变
3、drwaerlayout 左滑式布局,采用offsetleftandright来进行滑动
4、setTranslationX 是11引入的,主要通过矩阵来变换,并且经过测试验证,不会触发ondraw方法
5、viewpager、scrollview 是通过内容滑动,也就是2来实现的
    结合前面的canvas转化原理图,核心是改变绘制的内容,一共两种方法,要么原点坐标系改变,这样间接的导致绘制的内容改变;一个是改变绘制内容的坐标,原点坐标系不动。两种方法分别对应srcoll和改变child的坐标offsetleftandright。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值