android的布局流程,自己实现Android View布局流程

Android View的布局以ViewRootImpl为起点,开启整个View树的布局过程,而布局过程本身分为测量(measure)和布局(layout)两个部分,以View树本身的层次结构递归布局,确定View在界面中的位置。

下面尝试通过最少的代码,自己实现这套机制,注意下面类均为自定义类,未使用Android 源码中的同名类。

MeasureSpec

首先定义MeasureSpec,它是描述父布局对子布局约束的类,在Android源码中它是一个int值,通过位运算获取mode和size,这里我们为了方便起见实现为一个类:

class MeasureSpec(var mode: Int = UNSPECIFIED, var size: Int = 0) {

companion object {

const val UNSPECIFIED = 0

const val EXACTLY = 1

const val AT_MOST = 2

}

}

同样包含三种mode,分别表示父布局对子布局没有限制,父布局对子布局要求为固定值,父布局对子布局有最大值限制。

LayoutParam

LayoutParam在源码中定义在各种ViewGroup的内部,是静态内部类,用于在该ViewGroup布局中的子View中使用,这里我们定义为顶层类,并且只包含宽高两种属性,对应于xml文件中的layout_width和layout_height属性。同样定义MATCH_PARENT与WRAP_CONTENT。

class LayoutParam(var width: Int, var height: Int) {

companion object {

const val MATCH_PARENT = -1

const val WRAP_CONTENT = -2

}

}

下面我们实现View与ViewGroup。

View

(1)处我们定义的View的坐标,和源码中一致,这里表示的是相对于父View的坐标,与上篇View相关文章如何自己实现Android View Touch事件分发流程中不同,那篇的View的坐标是绝对坐标。

(2)处定义了padding,

(3)处表示measure过程的测量宽高,

(4)为布局文件中指定的layoutParam

这些属性,总结下来就是(2)(4)由开发者在布局中指定,(3)通过测量过程由View自己测得,(1)通过布局过程最终确定,也就是我们的目的所在,包括(3)存在的意义也是为了确定(4)中的值。

下面开始编写测量过程,虽然这些代码都是重写的,进行了大量的简化,但整体流程依然和源码是一致的,能够更清晰的理解Android的View树的布局是如何实现的。

(5)处measure直接调用onMeasure开始测量过程,而onMeasure这里简单直接设置了MeasureSpec中父ViewGroup中的限制值作为测量值就结束了自己的测量过程(6),因为onMeasure是需要继承使用的,不同View的测量方式并不相同,所以这里简单处理。

(7)处开始布局过程,首先调用setFrame方法将坐标保存(8),并调用onLayout回调,这里为空实现(9)。

至此View的布局相关方法实现完毕。

open class View {

open var tag = javaClass.simpleName

var left = 0

var right = 0

var top = 0

var bottom = 0//1

var paddingLeft = 0

var paddingRight = 0

var paddingTop = 0

var paddingBottom = 0//2

var measuredWidth = 0

var measuredHeight = 0//3

var layoutParam = LayoutParam(

LayoutParam.WRAP_CONTENT,

LayoutParam.WRAP_CONTENT

)//4

fun measure(widthMeasureSpec: MeasureSpec, heightMeasureSpec: MeasureSpec) {

onMeasure(widthMeasureSpec, heightMeasureSpec)

}//5

open fun onMeasure(widthMeasureSpec: MeasureSpec, heightMeasureSpec: MeasureSpec) {

setMeasuredDimension(widthMeasureSpec.size, heightMeasureSpec.size)//6

}

fun setMeasuredDimension(measuredWidth: Int, measuredHeight: Int) {

this.measuredWidth = measuredWidth

this.measuredHeight = measuredHeight

}

fun layout(l: Int, t: Int, r: Int, b: Int) {

val changed = setFrame(l, t, r, b)//8

onLayout(changed, l, t, r, b)

}//7

private fun setFrame(l: Int, t: Int, r: Int, b: Int): Boolean {

var changed = false

if (l != left || t != top || r != right || b != bottom) {

left = l

top = t

right = r

bottom = b

changed = true

}

println("$tag = L: $l, T: $t, R: $r, B: $b")

return changed

}

open fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {}//9

fun resolveSize(size: Int, measureSpec: MeasureSpec): Int {

return when (measureSpec.mode) {

MeasureSpec.EXACTLY -> measureSpec.size

MeasureSpec.AT_MOST -> minOf(size, measureSpec.size)

else -> size

}

}//10

}

ViewGroup

下面我们实现ViewGroup,只有一个抽象方法,即将View中的onLayout空实现声明为抽象的,即要求子类自行实现布局算法,而ViewGroup本身不允许当做布局使用。

abstract class ViewGroup(vararg val children: View) : View() {

abstract override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int)

}

如此,整个Android的View层次结构的骨架已经搭建完成了,在源码中,对于View的布局方面,主要也就干了这么点事情。其他各种各样的View与ViewGroup均是通过继承,实现各自的测量算法(即子View实现onMeasure),和布局算法(即子ViewGroup实现onMeasure与onLayout)。

下面我们依托这个框架各实现一个View与ViewGroup。

Text

下面我们实现一个TextView,这里因为我们只是为了说明View测量的原理,因此只支持两个属性text与textSize。

只需实现onMeasure即可,将左右padding相加,并加上字符串长度与字号的乘积作为宽(1),将上下padding相加,并加上字号作为高,当然这里我们只是简单这样计算示意,实际计算TextView长宽肯定不能这样来算。

如此算得的长宽就是Text自身理想的长宽,但是,还需要施加上父布局的限制才行,即MeasureSpec,这里即调用resolveSize,将限制与理想值传入即可(2)。

resolveSize定义在View节的(10)处,里面处理逻辑即,当限制为固定值时,测量值取限制值,当限制上限时,测量值为限制值与理想值取小,当限制为不限时,取理想值。

如此,整个TextView的测量过程完毕。对于布局过程,由于,layout方法内已经设置了自身的坐标,onLayout保持空实现即可,并不需要重写。

class Text(private val text: String, private val textSize: Int = 10) : View() {

override var tag: String = "Text($text)"

override fun onMeasure(widthMeasureSpec: MeasureSpec, heightMeasureSpec: MeasureSpec) {

val width = paddingLeft + paddingRight + text.length * textSize//1

val height = paddingTop + paddingBottom + textSize

setMeasuredDimension(

resolveSize(width, widthMeasureSpec),//2

resolveSize(height, heightMeasureSpec)

)

}

}

Column

下面定义一个类似于orientation为vertical的LinearLayout来说明ViewGroup的布局过程。

对于源码中的LinearLayout,子布局中使用的layout_开头的布局属性,对应的是LinearLayout内部类中的LayoutParams,而这里我们直接使用上面已经定义的LayoutParams,相当于LinearLayout中有部分功能并未实现,比如layout_margin,layout_weight,layout_gravity,这里我们简单处理。

在onMeasure中,要做两件事,第一件事是向父类View一样测量自己的长宽,即需要调用setMeasuredDimension;第二件事是对于每个子View,开始它们的测量,其实,第二件事本身就是第一件的前提,因为子View的测量没有结束的话,自己的长宽根本就无法确定。

(1)处在循环中调用子View的measure开启它们的测量过程,但需要传递给它们限制,即childWidthMeasureSpec和childHeightMeasureSpec,这里通过getChildMeasureSpec方法确定长与宽的限制(2),该方法在源码中是定义在ViewGroup中的。

(3)处该方法接收3个参数,spec为Column自身的受到的父View的限制,padding为测量到该View时,Column已经用完的大小(因为Column是要将View一个挨着一个排布的,肯定需要这个值),childDimension是开发者在布局文件中指定的layout_width或layout_height值。

因此spec有UNSPECIFIED,EXACTLY,AT_MOST三种类型,childDimension有MATCH_PARENT,WRAP_CONTENT和精确值3种类型,这些交织的情况都需要分别考虑。在源码中,将spec放在外层,childDimension放在内层,这里我们将childDimension放在放在外层(4),spec放在内层,实现更为简洁。

(5)当childDimension为MATCH_PARENT,只要忠实将限制mode传递下去即可,大小使用(6)处计算的剩余大小。

(6)当childDimension为WRAP_CONTENT,需限制mode设为AT_MOST,同样使用(6)处计算的剩余大小,但是需要考虑spec.mode为UNSPECIFIED的情况,需要将这种不限制给传递下去(7)。

(8)最后对应于childDimension为开发者指定精确值的情况,只要如实传递开发者指定值即可,不必考虑父布局限制。

如此就得到了(1)处传给各自View的限制,开始子View的测量,当前遍历到的子View测量完成后,需要获取测得的子View高度来更新已使用的高度值(9),因为Column是单行纵向排布的,usedWidth就不需要更新。但需要更新width值,作为Column本身的期望宽度。

(10)当遍历完成后,和上节Text一样,将resolveSize返回值传入setMeasuredDimension即可,如此就完成了Column的测量过程。

class Column(vararg children: View) : ViewGroup(*children) {

override fun onMeasure(widthMeasureSpec: MeasureSpec, heightMeasureSpec: MeasureSpec) {

var usedHeight = paddingTop + paddingBottom

val usedWidth = paddingLeft + paddingRight

var width = 0

children.forEach { child ->

val childWidthMeasureSpec =

getChildMeasureSpec(widthMeasureSpec, usedWidth, child.layoutParam.width)

val childHeightMeasureSpec =

getChildMeasureSpec(heightMeasureSpec, usedHeight, child.layoutParam.height)

child.measure(childWidthMeasureSpec, childHeightMeasureSpec)//1

usedHeight += child.measuredHeight//9

width = maxOf(width, child.measuredWidth)

}

setMeasuredDimension(

resolveSize(width, widthMeasureSpec),

resolveSize(usedHeight, heightMeasureSpec)

)//10

}

private fun getChildMeasureSpec(

spec: MeasureSpec,

padding: Int,

childDimension: Int

): MeasureSpec {//3

val childWidthSpec = MeasureSpec()

val size = spec.size - padding//6

when (childDimension) {//4

LayoutParam.MATCH_PARENT -> {

childWidthSpec.mode = spec.mode

childWidthSpec.size = size

}//5

LayoutParam.WRAP_CONTENT -> {

if (spec.mode == MeasureSpec.AT_MOST || spec.mode == MeasureSpec.EXACTLY) {

childWidthSpec.mode = MeasureSpec.AT_MOST

childWidthSpec.size = size

} else if (spec.mode == MeasureSpec.UNSPECIFIED) {

childWidthSpec.mode = MeasureSpec.UNSPECIFIED

childWidthSpec.size = 0//7

}

}

else -> {

childWidthSpec.mode = MeasureSpec.EXACTLY

childWidthSpec.size = childDimension//8

}

}

return childWidthSpec

}//2

override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {

var childTop = paddingTop

children.forEach { child ->

child.layout(

paddingLeft,

childTop,

paddingLeft + child.measuredWidth,

childTop + child.measuredHeight

)

childTop += child.measuredHeight

}

}

}

而对于onLayout方法,因为已经知道各子View的测量宽高,只需要在此遍历各子View,逐个设置坐标即可,Column本身的坐标设置已经在View中layout方法中实现。

如此整个类Android的布局重写完毕。

使用

下面验证我们代码:

fun main() {

val page = Column(

Text("Marshmallow").apply {

layoutParam = LayoutParam(

LayoutParam.WRAP_CONTENT,

LayoutParam.WRAP_CONTENT

)

},

Text("Nougat").apply {

layoutParam = LayoutParam(

LayoutParam.WRAP_CONTENT,

LayoutParam.WRAP_CONTENT

)

},

Text("Oreo").apply {

layoutParam = LayoutParam(

LayoutParam.WRAP_CONTENT,

LayoutParam.WRAP_CONTENT

)

paddingTop = 10

paddingBottom = 10

},

Text("Pie").apply {

layoutParam = LayoutParam(

LayoutParam.WRAP_CONTENT,

LayoutParam.WRAP_CONTENT

)

}

).apply {

layoutParam = LayoutParam(

LayoutParam.WRAP_CONTENT,

LayoutParam.WRAP_CONTENT

)

paddingLeft = 10

paddingRight = 10

paddingBottom = 10

}//1

val root = Column(page)//2

root.measure(MeasureSpec(MeasureSpec.AT_MOST, 1080), MeasureSpec(MeasureSpec.AT_MOST, 1920))

root.layout(0, 0, 1080, 1920)//3

}

(1)处定义一个布局page,就像在Android中写的布局文件那样,只不过这里更像是Flutter中声明式UI的书写方式。

在源码中布局流程可以简单的认为在ViewRootImpl中发起,内部有performMeasure,performLayout从DecorView开启整个布局流程,这里在(2)处的Column就类似于DecorView,下面两行就类似于ViewRootImpl中perform开头的方法发起的布局流程(这里因为无关,我们不考虑draw部分)。

运行查看打印,与预想一致。

Column = L: 0, T: 0, R: 1080, B: 1920

Column = L: 0, T: 0, R: 110, B: 70

Text(Marshmallow) = L: 10, T: 0, R: 120, B: 10

Text(Nougat) = L: 10, T: 10, R: 70, B: 20

Text(Oreo) = L: 10, T: 20, R: 50, B: 50

Text(Pie) = L: 10, T: 50, R: 40, B: 60

总结

整个View和ViewGroup关于布局(包含measure,layout)的框架代码是十分简单的,具体的布局算法需要各子类自行实现。

ViewGroup关于子View的遍历,因为需要重写,均发生在on开头的方法内。而父View的测量宽高的确定本身需要子View的测量宽高,因此,setMeasuredDimension的调用在onMeasure中的遍历之后;而父View坐标的确定就不需要另外关注子View了,因此和View一样在layout方法中设置,发生在onLayout对子View的遍历之前。

measure过程即限制的传递过程以及View的期望大小(代码中的width,height)匹配限制得到测量大小(measuredWidth,measuredHeight)的过程。

整个布局流程的根本目的在于确定View中的4个坐标值,而这个值是在layout方法中设置的,因此对layout方法的调用决定了布局流程的结果,measure可以说是对这个流程的辅助。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值