转载请务必在文章开头注明出处!http://www.jianshu.com/p/a3014f8442b0

一、简介

不论在学习Android还是在做Android开发,我们都离不开View,所以学好View对一个Android开发人员来说尤为重要。Android中的每个控件都会在界面上得到一块矩形的区域,而在Android中,控件大致被分为两类,即ViewGroup 控件和View控件。ViewGroup控件作为父控件可以包含多个View控件,并管理其包含的View控件。下面分条来对View做一个简单的介绍。


二、View的原理介绍

  1. View表示的的屏幕上的某一块矩形的区域,而且所有的View都是矩形的;
  2. 如同简介中介绍,View是不能添加子View的,而ViewGroup是可以添加子View的。ViewGroup之所以能够添加子View,是因为它实现了两个接口:ViewParentViewManager
  3. Activity之所以能加载并且控制View,是因为它包含了一个Window,所有的图形化界面都是由View显示的而Service之所以称之为没有界面的activity是因为它不包含有Window,不能够加载View;
  4. 一个View有且只能有一个父View;
  5. 在Android中Window对象通常由PhoneWindow来实现的,PhoneWindow将一个DecorView设置为整个应用窗口的根View,即DecorView为整个Window界面的最顶层View。也可以说DecorView将要显示的具体内容呈现在了PhoneWindow上;
  6. DecorView是FrameLayout的子类,它继承了FrameLayout,即顶层的FrameLayout的实现类是Decorview,它是在phoneWindow里面创建的;
  7. 顶层的FrameLayout的父view是Handler,Handler的作用除了线程之间的通讯以外,还可以跟WindowManagerService进行通讯;
  8. windowManagerService是后台的一个服务,它控制并且管理者屏幕;
  9. 一个应用可以有很多个window,其由windowManager来管理,而windowManager又由windowManagerService来管理;
  10. 如果想要显示一个view那么他所要经历三个方法:1.测量measure, 2.布局layout, 3.绘制draw

三、View的测量/布局/绘制过程

显示一个View主要进过以下三个步骤:

  • 1、Measure测量一个View的大小
  • 2、Layout摆放一个View的位置
  • 3、Draw画出View的显示内容
    其中measure和layout方法都是final的,无法重写,虽然draw不是final的,但是也不建议重写该方法。
    这三个方法都已经写好了View的逻辑,如果我们想实现自身的逻辑,而又不破坏View的工作流程,可以重写onMeasure、onLayout、onDraw方法。下面来一一介绍这三个方法。
【Android】自定义控件之View原理与使用_第1张图片测量/布局/绘制流程

3.1 View的测量

Android系统在绘制View之前,必须对View进行测量,即告诉系统该画一个多大的View,这个过程在onMeasure()方法中进行。测量过程如下图所示:

【Android】自定义控件之View原理与使用_第2张图片Measure测量流程
3.1.1 MeasureSpec类

Android系统给我们提供了一个设计小而强的工具类———MeasureSpec类
1、MeasureSpe描述了父View对子View大小的期望。里面包含了测量模式和大小。
2、MeasureSpe类把测量模式和大小组合到一个32位的int型的数值中,其中高2位表示模式,低30位表示大小而在计算中使用位运算的原因是为了提高并优化效率。
3、我们可以通过以下方式从MeasureSpec中提取模式和大小,该方法内部是采用位移计算。

int specMode = MeasureSpec.getMode(measureSpec);int specSize = MeasureSpec.getSize(measureSpec);

也可以通过MeasureSpec的静态方法把大小和模式合成,该方法内部只是简单的相加。

MeasureSpec.makeMeasureSpec(specSize,specMode);

3.1.2 测量模式

在对View进行测量时,Android提供了三种测量模式:

  • 1. EXACTLY
    精确值模式,当控件的layout_width属性或layout_height属性指定为具体数值时,例如android:layout_width="100dp",或者指定为match_parent属性时,系统使用的是EXACTLY 模式。
  • 2. AT_MOST
    最大值模式,当控件的layout_width属性或layout_height属性指定为warp_content时,控件大小一般随着控件的子控件或者内容的变化而变化,此时控件的尺寸只要不超过父控件允许的最大尺寸即可。
  • 3.UNSPECIFIED
    这个属性很奇怪,因为它不指定其大小测量的模式,View想多大就多大,通常情况下在绘制自定义View时才会使用。

View默认的onMeasure()方法只支持EXACTLY模式,所以如果在自定义控件的时候不重写onMeasure()方法的话,就只能使用EXACTLY模式,且控件只可以响应你指定的具体宽高值或者是match_parent属性。如果要让自定义的View支持wrap_content属性,那么就必须重写onMeasure()方法来指定wrap_content时的大小。
而通过上面介绍的MeasureSpec这个类,我们就可以获取View的测量模式和View想要绘制的大小。


3.1.3 MeasureSpec判定规则

在自定义View的时候要通过判断测量的模式,给出不同的测量值,下面的一张图表罗列了 MeasureSpec判定规则。

【Android】自定义控件之View原理与使用_第3张图片MeasureSpec判定规则
3.1.4 实例演示

step1:自定义一个类继承FrameLayout重写构造方法:

public class CustomView extends FrameLayout {    //构造方法省略...}

step2:重写onMeasure()方法:

@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    super.onMeasure(widthMeasureSpec, heightMeasureSpec);}

通过查看super.onMeasure()方法,发现系统最终会调用setMeasureDimension(int measuredWidth, int measuredHeight)方法将测量后的宽高设置进去,从而完成测量工作。所以接下来要做的就是将最终测量后的宽高值作为参数设置给setMeasureDimension()方法,即重写的onMeasure()方法代码如下:

@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    setMeasureDimension(          measureWidth(widthMeasureSpec),           measureHeigth(heightMeasureSpec));}

因为在上面我们调用了自定义的measureWidth()方法和measureHeight()方法对宽高进行了重新定义,接下来我们就来自定义测量值。


step3:自定义测量值:
这里以measureWidth()方法为例,来进行自定义测量值操作。
首先,从MeasureSpec对象中获取到测量模式和测量大小值:

int specMode = MeasureSpec.getMode(widthMeasureSpec);int specSize = MeasureSpec.getSize(widthMeasureSpec);

其次,通过判断测量模式,给出不同的测量值:
①当specMode为EXACTLY时,直接使用指定的specSize即可;
②当specMode为其他两种模式时,需要给它一个默认的大小。
注:如果指定的是wrap_content属性,即AT_MOST模式,则需要取出我们指定的大小与specSize中最小的一个来作为最后的测量值。参考代码如下:

private int measureWidth(int measureSpec) {        int width = 0;    /**     * 1、从MeasureSpec对象中提出出具体的测量模式和大小     */    int specMode = MeasureSpec.getMode(widthMeasureSpec);    int specSize = MeasureSpec.getSize(widthMeasureSpec);    /**     * 2、通过判断测量模式,给出不同的测量值     */    if (specMode == MeasureSpec.EXACTLY) {   // match_parent , accurate        width = specSize;    } else {        width = 200;    //给一个默认的大小        if (specMode == MeasureSpec.AT_MOST) {  // wrap_content            width = Math.min(width,specSize); //注意取两者之间小的值        }    }    return width;}

对于measureHeight()方法基本上与上面的measureWidth()方法一致,此处省略。
通过以上三个步骤即可搞定View的测量,接下来简单介绍一下布局。


3.1.5 拓展

如果想在activity中的onCreat()方法中获取控件测量以后的宽跟高,那么可以用下面的方法:

final TextView tv = (TextView) findViewById(R.id.tv);tv.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {    @Override    public void onGlobalLayout() {        int measuredWidth = tv.getMeasuredWidth();        int measuredHeight = tv.getMeasuredHeight();    }});

3.2 View的布局

首先我们来看一下layout布局流程图:

【Android】自定义控件之View原理与使用_第4张图片View布局

接下来一一介绍上面流程图中的三个参数:

  • layout
    Layout方法中接受四个参数,是由父View提供,指定了子View在父View中的左、上、右、下的位置。父View在指定子View的位置时通常会根据子View在measure中测量的大小来决定。
    子View的位置通常还受有其他属性左右,例如父View的orientation,gravity,自身的margin等等,特别是RelativeLayout,影响布局的因素非常多。
    layout方法虽然可以被复写,但是不建议去复写,我们可以直接调用layout方法去确定自身的位置, 而且可以去复写onLayout方法去确定子view的位置

  • setFrame
    setFrame方法是一个隐藏方法,所以作为应用层程序员来说,无法重写该方法。该方法体内部通过比对本次的l、t、r、b四个值与上次是否相同来判断自身的位置和大小是否发生了改变。
    如果发生了改变,将会调用invalidate请求重绘。
    记录本次的l、t、r、b,用于下次比对。
    如果大小发生了变化,onSizeChanged方法,该方法在大多数View中都是空实现,我们可以重写该方法用于监听View大小发生变化的事件,在可以滚动的视图中重载了该方法,用于重新根据大小计算出需要滚动的值,以便显示之前显示的区域。

  • onLayout
    onLayout是ViewGroup用来决定子View摆放位置的,各种布局的差异都在该方法中得到了体现。
    onLayout比layout多一个参数,changed,该参数是在setFrame通过比对上次的位置得出是否发生了变化,通常该参数没有被使用的意义,因为父View位置和大小不变,并不能代表子View的位置和大小没有发生改变。
@Overrideprotected void onLayout(boolean changed, int left, int top, int right, int bottom) {       // super.onLayout(changed, left, top, right, bottom);       //重写~ ~ ~(略)}

3.3 View的绘制

draw方法绘制要遵循一定的顺序:
1.画背景
2,5.画边缘
3.画自身: ondraw方法
4.画子View: dispatchDraw方法
6.画滚动条

首先我们来看一下draw绘制流程:

【Android】自定义控件之View原理与使用_第5张图片draw绘制流程

以下是对上面三个方法的说明:

  • draw
    draw是由ViewRoot的performTraversals方法发起,它将调用DecorView的draw方法,并把成员变量canvas传给给draw方法。而在后面draw遍历中,传递的都是同一个canvas。所以android的绘制是同一个window中的所有View都绘制在同一个画布上。等绘制完成,将会通知WMS把canvas上的内容绘制到屏幕上。自定义View时一般不重写该方法。

  • onDraw
    View用来绘制自身的实现方法,如果我们想要自定义View,通常需要重载该方法。
    TextView中在该方法中绘制文字、光标和CompoundDrawable
    ImageView中相对简单,只是绘制了图片
    因为我们的目的就是自定义View,所以当我们测量好了一个View之后,我们就可以间的重写onDraw()这个方法,并在Canvas对象上来绘制所需要的图形。在onDraw()中就有一个参数,该参数就是Canvas canvas对象,使用这个对象即可进行绘图操作;而如果在其他地方,通常需要使用代码创建一个Canvas对象:
    Canvas canvas = new Canvas(bitmap);

之所以要传入一个bitmap,是因为传进来的bitmap与通过这个bitmap创建的Canvas画布是紧紧联系在一起的,这个过程称之为装载画布。
在View类的onDraw()方法中,我们通过下面的代码,让canvas与bitmap发生直接的联系:
canvas.drawBitmap(bitmap, 0, 0, null);
然后将bitmap装载到另外一个Canvas对象中:
Canvas mCanvas = new Canvas(bitmap);
通过mCanvas将绘制效果作用在了bitmap上,再通过invalidate()刷新的时候,我们就会发现通过onDraw()方法画出来的bitmap已经发生了改变。


  • dispatchDraw
    先根据自身的padding剪裁画布,所有的子View都将在画布剪裁后的区域绘制。
    遍历所有子View,调用子View的computeScroll对子View的滚动值进行计算。
    根据滚动值和子View在父View中的坐标进行画布原点坐标的移动,根据子在父View中的坐标计算出子View的视图大小,然后对画布进行剪裁。
    dispatchDraw的逻辑其实比较复杂,但是幸运的是对子View流程都采用该方式,而ViewGroup已经处理好了,我们不必要重载该方法对子View进行绘制事件的派遣分发。
    重写时,千万千万不要注释了super.方法

好了,关于View的原理与使用就介绍到这里,关于View的更深层的理解有机会再进行探讨。参考资料:徐宜生《Android群英传》

更多相关文章

  1. 布局中文件中【控件间距参数详解以及单位选择】
  2. Android—TextView的XML属性和方法
  3. android 各种控件颜色值的设置(使用Drawable,Color)
  4. Android 基本控件及表单三大控件,事件处理
  5. Android来电铃声默认设置的实现方法与怎么设置语音来电的默认铃
  6. Android L新控件RecyclerView简介
  7. NDK开发历程(一):android native code的调试方法
  8. Android ListView拖动时背景变黑的解决方法

随机推荐

  1. android开发常用小知识点整理
  2. Android(安卓)keytool 生成证书MD5指纹
  3. android 自定义的AlertDialog强化版
  4. android widget 之CheckBox
  5. Android(安卓)编译手册
  6. android 应用中加入支付功能(支付宝集成)
  7. onRetainNonConfigurationInstance和getL
  8. Android(安卓)Sdk Manager更新
  9. Android入门教学视频免费下载
  10. Android(安卓)Adapter