【Android】自定义控件之View原理与使用

转载请务必在文章开头注明出处!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. windowManagerService是后台的一个服务,它控制并且管理者屏幕;
  8. 一个应用可以有很多个window,其由windowManager来管理,而windowManager又由windowManagerService来管理;
  9. 如果想要显示一个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方法。下面来一一介绍这三个方法。
测量/布局/绘制流程

3.1 View的测量

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

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判定规则。

MeasureSpec判定规则

3.1.4 实例演示

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

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

step2:重写onMeasure()方法:

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

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

@Override
protected 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布局流程图:

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的位置和大小没有发生改变。
@Override
protected 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绘制流程:

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群英传》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容