简述 自定义View流程

2,343 阅读7分钟

前言

关于自定义View相信稍有经验的Android开发者都很熟悉,几乎每个人都知道通过measurre、layout、draw三个流程,但这三个流程何时执行、又如何自定义?本篇文章会带着这种疑惑讲解View绘制时机、自定义View流程,相信读完你对View的工作机制会有一个全新的认识。

1. View的绘制时机

1.1. 知识储备

  • Window:每个Activity都会创建一个Window用于承载View视图的显示,Window是一个抽象类存在了一个唯一实现类PhoneWindow
  • DecorView:最顶层的View,是一个FrameLayout子类,最终会被加载到Window当中,它内部只有一个垂直方向的LinearLayout分为两部分:
    • TitleBar:屏幕顶部的状态栏
    • ContentView:Activity对应的XML布局,通过setContentView设置到DecorView中。

示意图

1.2. Activity、Window、DecorView之间关系

首先来看一下Activity中setContentView源码:

 public void setContentView(@LayoutRes int layoutResID) {
        //将xml布局传递到Window当中
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

从代码可以看出,ActivitysetContentView实质是将View传递到WindowsetContentView()方法中,WindowsetContenView会在内部调用installDecor()方法创建DecorView,看一下它的部分源码:

 public void setContentView(int layoutResID) { 
        if (mContentParent == null) {
            //初始化DecorView以及其内部的content
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        ...............
        } else {
            //将contentView加载到DecorVoew当中
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        ...............
    }

  private void installDecor() {
        ...............
        if (mDecor == null) {
            //实例化DecorView
            mDecor = generateDecor(-1);
            ...............
            }
        } else {
            mDecor.setWindow(this);
        }
       if (mContentParent == null) {
            //获取Content
            mContentParent = generateLayout(mDecor);
       }  
        ...............
 }

 protected DecorView generateDecor(int featureId) {
        ...............
        return new DecorView(context, featureId, this, getAttributes());
 }

通过generateDecor()new一个DecorView,然后调用generateLayout()获取DecorViewcontent,最终通过inflateActivity视图添加到DecorView中的content中,但此时DecorView还未被添加到Window中。添加操作需要借助ViewRootImpl

ViewRootImpl的作用是用来衔接WindowManagerDecorView,在Activity被创建后会通过WindowManagerDecorView添加到PhoneWindow中并且创建ViewRootImpl实例,随后将DecorViewViewRootImpl进行关联,最终通过执行ViewRootImplperformTraversals()开启整个View树的绘制。

2. 绘制过程

从第一小节可知,View的绘制是从ViewRootImplperformTraversals()方法开始,从最顶层的View(ViewGroup)开始逐层对每个View进行绘制操作,下面来看一下该方法部分源代码:

private void performTraversals() {
     ...............
    //measur过程
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
     ...............
    //layout过程
    performLayout(lp, desiredWindowWidth, desiredWindowHeight);
     ...............
    //draw过程
    performDraw();
}

这方法大概有几百行,机智的作者抽出三句精华呈现给大家~~~

  • measure:为测量宽高过程,如果是ViewGroup还要在onMeasure中对所有子View进行measure操作。
  • layout:用于摆放View在ViewGroup中的位置,如果是ViewGroup要在onLayout方法中对所有子View进行layout操作。
  • draw:往View上绘制图像。

2.1 Measure

performMeasure()源码

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
      if (mView == null) {
          return;
      }
      try {
          mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
      } finally {
          Trace.traceEnd(Trace.TRACE_TAG_VIEW);
      }
}

可以看出从mView(最顶层ViewGroup)开始进行测量操作,然后逐层遍历View并执行measure操作。

MeasureSpac

MeasureView绘制三个过程中的第一步,提到Measure就不得不提MeasureSpac它是一个32位int类型数值,高两位SpacMode代表测量模式,低30位SpacSize代表测量尺寸,是View的内部类,源码如下:

public class MeasureSpec {
        private static final int MODE_SHIFT = 30;  
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;  
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;  
        public static final int EXACTLY = 1 << MODE_SHIFT;  
        public static final int AT_MOST = 2 << MODE_SHIFT;  
  }

内部也包含三种测量模式:

  • UNSPECIFIED :父布局不会对子View做任何限制,例如我们常用的ScrollView就是这种测量模式。
  • EXACTLY :精确数值,比如使用了match_parent或者xxxdp,表示父布局已经决定了子View的大小,通常在这种情况下View的尺寸就是SpacSize
  • AT_MOST :自适应,对应wrap_content子View可以根据内容设置自己的大小,但前提是不能超出父ViewGroup的宽高。

注意点:

在我们自定义View的过程中都会在onMeasure中进行宽高的测量,这个方法会从父布局中接收两个参数widthMeasureSpacheightMeasureSpac,所以子布局的宽高大小需要受限于父布局。

在自定义View宽高测量的过程中,我们需要获取MeasurSpac中的宽高和测量模式,自定义ViewGroup也必须给子View传递MeasurSpac,Android也给我们提供了计算MeasurSpac 和通过MeasurSpac 获取相应值的方式,都位于MeasurSpac中,具体代码如下:

public static class MeasureSpec {
     public static int makeMeasureSpec( int size, int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
     }

     public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
     }

     public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK)
     }
}

ViewGroupView对尺寸和模式进行了一次封装和拆解,其目的是为了减少对象的创建,避免造成不必要的内存浪费。

LayoutParams

在刚接触Android的时候经常有一个疑问,为什么View设置自己的宽高,还要创建一个xxx.LayoutParams?前面也提到了,子View的宽高是要受限于父布局的,所以不能通过setWidth或者setHeight直接设置宽高的,另外 LayoutParams的作用不仅如此,比如一个View的父布局是RelativeLayout,可以通过设置RelativeLayout.LayoutParamsabovebelow等属性来调整在父布局中的位置。

自定义View宽高测量演示

创建一个类继承View,重写其onMeasure()方法

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       //默认宽  
       int defaultWidth = 0;    
       //默认高
       int defaultHeight = 0;    
       setMeasuredDimension(
            getDefaultSize(defaultWidth, widthMeasureSpec),  
            getDefaultSize(defaultHeight, heightMeasureSpec));  
}

一般的自定义View中,如果对宽高没有特殊需求可直接通过getDefaultSize()方法获取,该方法位于View中源码如下:

   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;
    }

从代码分析可知,获取modesize后会分别对三种测量模式进行判断,UNSPECIFIED使用默认尺寸,而AT_MOSTEXACTLY使用父布局给出的测量尺寸。尺寸计算完毕后通过setMeasuredDimension(width,height)设置最终宽高。

2.2 Layout

performLayout()部分源码:

 private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
        .........
        final View host = mView;
        if (host == null) {
            return;
        }
        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
        .........
}

跟measure类似,同样是从mView(最顶层ViewGroup)开始进行layout操作,随后逐层遍历。layout(l,t,r,b)四个参数分别对应左上右下的位置,从而确定View在ViewGroup中的位置。下面来看一下layout()部分源码:

public void layout(int l, int t, int r, int b) {
    .......
    //通过setOpticalFrame()和setFrame()老确定四个点的位置
    boolean changed = isLayoutModeOptical(mParent) ? 
    setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    .......
    //调用onLayout(),ViewGroup须重写此方法
    onLayout(changed, l, t, r, b);
    .......
}

结合源码可知layout()会将四个位置参数传递给setOpticalFrame()或者setFrame(),而setOpticalFrame()内部会调用setFrame(),所以最终通过setFrame()确定ViewViewGroup中的位置。位置确定完毕会调用onLayout(l,t,r,b)对子View进行摆放。

onLayout()

ViewViewGroup在执行完setFrame()后都会调用onLayout()方法,但上面也有提到该方法的作用是对子View进行位置摆放,所以单一View是不需要重写此方法。而ViewGroup会根据自己的特性任意对子View进行摆放。

2.3 Draw

相信很多学习自定义View的同学都是奔着有朝一日自己也实现那些眼花缭乱的效果,起码我自己就是。我们在手机上看到的那些五彩缤纷的图片,动画都是在这个方法内绘制而成。

相比于measure和layout阶段,draw阶段中View和ViewGroup变得没那么紧密了,View的绘制过程中不需要考虑ViewGroup,而ViewGroup也只需触发子View的绘制方法即可。

performDraw()执行后同样会从根布局开始逐层对每个View进行draw操作,在View中绘制操作时通过draw()进行,来看一下其主要源码:

public void draw(Canvas canvas) {
     ........
    // 绘制背景
    drawBackground(canvas);
    // 绘制内容
    onDraw(canvas);
    // 绘制子View
    dispatchDraw(canvas);
    // 绘制装饰,如scrollBar
    onDrawForeground(canvas)
    ........
}

draw()方法中主要包含四部分内容,其中我们开发者只需要关心onDraw(canvas)即可,即自身的内容绘制。

绘制内容简述

关于绘制内容这部分可利用到的知识点很多,多到可以写一本书出来,所以仅靠本文全部详细描述显然是不现实的。下面我罗列一部分常用内容供大家参考:

  • Canvas:画布,不管是文字,图形,图片都要通过画布绘制而成
  • Paint:画笔,可设置颜色,粗细,大小,阴影等等等等,一般配合画布使用
  • Path:路径,用于形成一些不规则图形。
  • Matrix:矩阵,可实现对画布的几何变换。

总结

文章从四个方面总结了View的绘制流程:绘制时机,宽高测量,位置摆放,图像绘制,因为侧重于流程所以只是把这四部分的精华给拎出来分享给大家,起到一个抛砖引玉的作用,想要透彻理解启动流程、玩转自定义View还需要对各部分知识系统的学习。