View 是 Android 界面里最基本的组件,一个 View 占据屏幕上一块方形区域,我们平时手机上看到的文字、图片、列表等都是 View,可以说 Android 手机屏幕上能看到的所有东西都是 View。正是这些形形色色的 View,组成了我们手机上千变万化的界面。做为一名 Android 开发人员,我们有必要了解下如此重要的一个界面组件是怎样呈现在手机屏幕上的。首先,我们不妨想想,在现实中,如果我们自己要画一幅画该怎么做
- 第一,我们要准备笔墨纸砚吧,不过在在手机上,只要这个手机正确安装了 Android 系统,那么系统就已经为我们准备好这些东西啦
- 第二,我们应该要知道画的这个东西有多大吧?不然下笔的范围、轻重都无法掌握啊
- 第三,应该把这个东西画在纸上的哪个位置必须要清楚,这样这块位置画什么,那块位置画什么才能有效的组织起来
- 第四,经过前面几步的准备,最后我们当然要开画了,至于用什么颜色的笔、笔的粗细就看我们自己的风格了
不错,Android 一个 View 的呈现大致也正是需要上面的这些步骤,好了,废话不多说了,让我们开始探索 View 的神秘世界吧!
整体绘制流程图
测量
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 需要掌握的重要知识之一,希望大家都能掌握,还有就是这篇文章是基于本人目前的能力所写,难免会有错误与不足之处,我也会持续修正和补充,希望大家批评指正