Android View 的绘制(2D)

View 是 Android 界面里最基本的组件,一个 View 占据屏幕上一块方形区域,我们平时手机上看到的文字、图片、列表等都是 View,可以说 Android 手机屏幕上能看到的所有东西都是 View。正是这些形形色色的 View,组成了我们手机上千变万化的界面。做为一名 Android 开发人员,我们有必要了解下如此重要的一个界面组件是怎样呈现在手机屏幕上的。首先,我们不妨想想,在现实中,如果我们自己要画一幅画该怎么做

  • 第一,我们要准备笔墨纸砚吧,不过在在手机上,只要这个手机正确安装了 Android 系统,那么系统就已经为我们准备好这些东西啦
  • 第二,我们应该要知道画的这个东西有多大吧?不然下笔的范围、轻重都无法掌握啊
  • 第三,应该把这个东西画在纸上的哪个位置必须要清楚,这样这块位置画什么,那块位置画什么才能有效的组织起来
  • 第四,经过前面几步的准备,最后我们当然要开画了,至于用什么颜色的笔、笔的粗细就看我们自己的风格了

不错,Android 一个 View 的呈现大致也正是需要上面的这些步骤,好了,废话不多说了,让我们开始探索 View 的神秘世界吧!


整体绘制流程图

android_view_draw_flow.jpg


测量

View 要想显示在手机屏幕上,必须放在容器 ViewGroup 类或其子类中,View 的测量是在 View 的 onMeasure 回调方法中进行的,这个方法有两个父容器传给 View 的宽、高的整型参数,这两个参数并不是表面上看起来那么简单,它们是由 父容器约束模式大小 组成的一个 32 位整型值,高 2 位代表模式,低 30 位代表大小,你如果想指定一个 View 的大小,一定要覆写这个方法并最好遵守一些父容器对你这个 View 的约束,至于需要遵守怎样的约束,请看下面 MeasureSpec 类介绍


MeasureSpec

首先,我们先了解下 MeasureSpec 这个类是干嘛的,我们知道在 View 的 onMeasure 回调方法中有两个 32 位的整型的参数 widthMeasureSpec 和 heightMeasureSpec 整型规格参数,如果我们要获取这个 32 位整数的约束模式部分,是不是有点头大?不用担心,MeasureSpec 这个类的作用就是提供了一些方法用来辅助操作这个 32 位整型数的,比如获取模式的 getMode 方法、获取大小的 getSize 方法等。之所以选择一整型而不用一个对象来表示,主要是为了避免对象的分配

MeasureSpec 类为我们提供了 3 种父容器对子 View 的约束模式:

模式 值    约束
UNSPECIFIED 0 << 30 父容器不对当前 View 大小做任何限制,子 View 要多大给多大,一般一些滑动控件会用到,例如 ScrollView 等
EXACTLY 1 << 30 父容器已经测量出了当前子 View 所需的大小,希望子 View 按照这个大小来设置,对应 match_parent 或具体大小值
AT_MOST 2 << 30 View 要多大就多大,但 View 大小不能大于父容器,对应 wrap_content

注意下,上面给出的规则只是常规情况下的一种期望,也就是说这样写你的 View 的宽高绝对不会超出父容器的范围,其实这三种限制模式是限制不了你的 View 要多大的,只是一种建议,比如,父容器给你的高是 300,你的 View 就不能大于或小于这个数了吗?不是的,你可以是任意大小,只要你能通过某种方式能显示出你这个 View 的全部内容就行,比如改变 View 坐标、外边距、内边距等


示例代码

/**
 * 此方法结尾处需调用 setMeasuredDimension 方法使自定义的宽、高生效
 * 不调用会抛出异常
 */
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    setMeasuredDimension(getSuitableSize(suggestedMinimumWidth,widthMeasureSpec),
            getSuitableSize(suggestedMinimumHeight,heightMeasureSpec))
    // 推荐使用 resolveSize 和 resolveSizeAndState 方法来获取 View 的合适大小
    setMeasuredDimension(resolveSize(suggestedMinimumWidth,widthMeasureSpec), 
            resolveSize(suggestedMinimumHeight,heightMeasureSpec))
}

/**
 * @param desiredSize 你期望这个 View 多大
 * @param parenConstraintSpec 父容器约束这个 View 的大小规格
 */
private fun getSuitableSize(desiredSize:Int,parenConstraintSpec:Int):Int{
    val mode = MeasureSpec.getMode(parenConstraintSpec)
    val size = MeasureSpec.getSize(parenConstraintSpec)
    return when (mode){
        MeasureSpec.UNSPECIFIED -> desiredSize
        MeasureSpec.EXACTLY -> size
        MeasureSpec.AT_MOST -> Math.min(desiredSize,size)
        else -> desiredSize
    }
}

/**
 * 官方示例
 */
private fun measureView(widthMeasureSpec: Int, heightMeasureSpec: Int){
    // Try for a width based on our minimum
    val minw: Int = paddingLeft + paddingRight + suggestedMinimumWidth
    val w: Int = View.resolveSizeAndState(minw, widthMeasureSpec, 1)
    // Whatever the width ends up being, ask for a height that would let the pie
    // get as big as it can
    val minh: Int = View.MeasureSpec.getSize(w) - desiredSize + paddingBottom + paddingTop
    // 第二个参数传 0 即可满足大多数情况
    val h: Int = View.resolveSizeAndState(minh, heightMeasureSpec, 0)
    setMeasuredDimension(w, h)
}


相关方法

方法名 所在类 备注
measure View
measureChild ViewGroup
measureChildren ViewGroup
getChildMeasureSpec ViewGroup
getMode MeasureSpec
getSize MeasureSpec
makeMeasureSpec MeasureSpec 根据约束模式和大小生成 32 位整型规格参数,区别于 resolveSize 和 resolveSizeAndState 方法
toString MeasureSpec 调试时很有用,可以将模式和大小组合成一个字符串返回
getMeasuredWidthAndState View
getMeasuredState View
combineMeasuredStates View
getMeasuredWidth View 高类似
getSuggestedMinimumWidth View 没有提供 set 方法
getMinimumWidth View 高类似,有提供 set 方法
onSizeChanged View


布局

View 通过调用 layout 方法来确定自己的位置,ViewGroup 及其子类需要覆写 onlayout 方法并在里面确定每个子 View 的摆放位置,至于摆放规则,需要自己指定了,这也是自定义 ViewGroup 比自定义 View 要难的原因之一,当然,系统也为我们提供了很多实现好的 ViewGroup 子类,比如常用的 LinearLayout、RelativeLayout 等,如果我们能直接继承这些子类就能满足需求的话当然能更省事


相关方法

方法名 所在类 备注
onLayout View 回调方法,在方法中调用 layout 方法指定子 View 的大小和位置
注意下这个方法的 changed 参数是用来判断当前 View 目前的位置是否相对于上次位置发生了变化,如果是 ViewGroup 里的子 View 位置发生了变化,ViewGroup 的 changed 参数是不会变的
layout View 指定当前 View 的大小和位置
requestLayout View 重新回调 onLayout 方法对 View 进行布局
bringToFront View 通过改变 View 的 z轴 坐标来达到界面 View 排列顺序的变化


绘制

经过了前面两步的准备工作,终于到最后一步绘制啦,关于绘制阶段,有两个重要的类,一个是 Canvas,一个是 Paint,它们分别决定了当前 View 要画什么和怎么画,比如,前者提供了一个 drawRect 方法用于画方形,后者提供了一个 setStyle 方法规定了你是只需要画要有边的方形呢、还是要实心的方形呢、还是即要有边又要实心的方法


相关方法

方法名 所在类 备注
onDraw View 绘制具体图形的回调方法,注意不要在此方法中执行太耗时的操作,对象的创建也不要放在此方法中,因为绘制期间,此方法回调会很频繁
invalidate View 重绘,onDraw 方法会被回调,此方法必须在主线程中调用
postInvalidate View onDraw 方法会被回调,此方法即可在主线程也可在子线程中调用
setColor Paint 设置画笔颜色
setTypeface Paint 绘制文字时,设置字体,黑体、斜体等
setStyle Paint 设置绘制方式,画边、填充等
setShader Paint 图形是否需要渐变
drawPoint Canvas
drawLine Canvas
drawText Canvas
drawRect Canvas
drawRoundRect Canvas
drawOval Canvas
drawArc Canvas
drawPath Canvas 根据路径画图形,一般用于绘制复杂的图形
drawBitmap Canvas
drawCircle Canvas


其它相关方法

方法名 所在类 备注
onFinishInflate View
onVisibilityChanged View
onAttachedToWindow View
onDetachedFromWindow View
setTouchDelegate View 扩展 View 的点击区域,用法见这里


总结

了解 View 的绘制机制对自定义我们自己的 View 尤为重要,当然这只是自定义 View 需要掌握的重要知识之一,希望大家都能掌握,还有就是这篇文章是基于本人目前的能力所写,难免会有错误与不足之处,我也会持续修正和补充,希望大家批评指正

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

推荐阅读更多精彩内容