自己實(shí)現(xiàn)Android View布局流程
相關(guān)閱讀:嘗試自己實(shí)現(xiàn)Android View Touch事件分發(fā)流程
Android View的布局以ViewRootImpl為起點(diǎn),開(kāi)啟整個(gè)View樹(shù)的布局過(guò)程,而布局過(guò)程本身分為測(cè)量(measure)和布局(layout)兩個(gè)部分,以View樹(shù)本身的層次結(jié)構(gòu)遞歸布局,確定View在界面中的位置。
下面嘗試通過(guò)最少的代碼,自己實(shí)現(xiàn)這套機(jī)制,注意下面類(lèi)均為自定義類(lèi),未使用Android 源碼中的同名類(lèi)。
MeasureSpec首先定義MeasureSpec,它是描述父布局對(duì)子布局約束的類(lèi),在Android源碼中它是一個(gè)int值,通過(guò)位運(yùn)算獲取mode和size,這里我們?yōu)榱朔奖闫鹨?jiàn)實(shí)現(xiàn)為一個(gè)類(lèi):
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,分別表示父布局對(duì)子布局沒(méi)有限制,父布局對(duì)子布局要求為固定值,父布局對(duì)子布局有最大值限制。
LayoutParamLayoutParam在源碼中定義在各種ViewGroup的內(nèi)部,是靜態(tài)內(nèi)部類(lèi),用于在該ViewGroup布局中的子View中使用,這里我們定義為頂層類(lèi),并且只包含寬高兩種屬性,對(duì)應(yīng)于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 }}
下面我們實(shí)現(xiàn)View與ViewGroup。
View(1)處我們定義的View的坐標(biāo),和源碼中一致,這里表示的是相對(duì)于父View的坐標(biāo),與上篇View相關(guān)文章嘗試自己寫(xiě)Android View Touch事件分發(fā)中不同,那篇的View的坐標(biāo)是絕對(duì)坐標(biāo)。
(2)處定義了padding,(3)處表示measure過(guò)程的測(cè)量寬高,(4)為布局文件中指定的layoutParam
這些屬性,總結(jié)下來(lái)就是(2)(4)由開(kāi)發(fā)者在布局中指定,(3)通過(guò)測(cè)量過(guò)程由View自己測(cè)得,(1)通過(guò)布局過(guò)程最終確定,也就是我們的目的所在,包括(3)存在的意義也是為了確定(4)中的值。
下面開(kāi)始編寫(xiě)測(cè)量過(guò)程,雖然這些代碼都是重寫(xiě)的,進(jìn)行了大量的簡(jiǎn)化,但整體流程依然和源碼是一致的,能夠更清晰的理解Android的View樹(shù)的布局是如何實(shí)現(xiàn)的。
(5)處measure直接調(diào)用onMeasure開(kāi)始測(cè)量過(guò)程,而onMeasure這里簡(jiǎn)單直接設(shè)置了MeasureSpec中父ViewGroup中的限制值作為測(cè)量值就結(jié)束了自己的測(cè)量過(guò)程(6),因?yàn)閛nMeasure是需要繼承使用的,不同View的測(cè)量方式并不相同,所以這里簡(jiǎn)單處理。
(7)處開(kāi)始布局過(guò)程,首先調(diào)用setFrame方法將坐標(biāo)保存(8),并調(diào)用onLayout回調(diào),這里為空實(shí)現(xiàn)(9)。
至此View的布局相關(guān)方法實(shí)現(xiàn)完畢。
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
下面我們實(shí)現(xiàn)ViewGroup,只有一個(gè)抽象方法,即將View中的onLayout空實(shí)現(xiàn)聲明為抽象的,即要求子類(lèi)自行實(shí)現(xiàn)布局算法,而ViewGroup本身不允許當(dāng)做布局使用。
abstract class ViewGroup(vararg val children: View) : View() { abstract override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int)}
如此,整個(gè)Android的View層次結(jié)構(gòu)的骨架已經(jīng)搭建完成了,在源碼中,對(duì)于View的布局方面,主要也就干了這么點(diǎn)事情。其他各種各樣的View與ViewGroup均是通過(guò)繼承,實(shí)現(xiàn)各自的測(cè)量算法(即子View實(shí)現(xiàn)onMeasure),和布局算法(即子ViewGroup實(shí)現(xiàn)onMeasure與onLayout)。
下面我們依托這個(gè)框架各實(shí)現(xiàn)一個(gè)View與ViewGroup。
Text下面我們實(shí)現(xiàn)一個(gè)TextView,這里因?yàn)槲覀冎皇菫榱苏f(shuō)明View測(cè)量的原理,因此只支持兩個(gè)屬性text與textSize。
只需實(shí)現(xiàn)onMeasure即可,將左右padding相加,并加上字符串長(zhǎng)度與字號(hào)的乘積作為寬(1),將上下padding相加,并加上字號(hào)作為高,當(dāng)然這里我們只是簡(jiǎn)單這樣計(jì)算示意,實(shí)際計(jì)算TextView長(zhǎng)寬肯定不能這樣來(lái)算。
如此算得的長(zhǎng)寬就是Text自身理想的長(zhǎng)寬,但是,還需要施加上父布局的限制才行,即MeasureSpec,這里即調(diào)用resolveSize,將限制與理想值傳入即可(2)。
resolveSize定義在View節(jié)的(10)處,里面處理邏輯即,當(dāng)限制為固定值時(shí),測(cè)量值取限制值,當(dāng)限制上限時(shí),測(cè)量值為限制值與理想值取小,當(dāng)限制為不限時(shí),取理想值。
如此,整個(gè)TextView的測(cè)量過(guò)程完畢。對(duì)于布局過(guò)程,由于,layout方法內(nèi)已經(jīng)設(shè)置了自身的坐標(biāo),onLayout保持空實(shí)現(xiàn)即可,并不需要重寫(xiě)。
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
下面定義一個(gè)類(lèi)似于orientation為vertical的LinearLayout來(lái)說(shuō)明ViewGroup的布局過(guò)程。
對(duì)于源碼中的LinearLayout,子布局中使用的layout_開(kāi)頭的布局屬性,對(duì)應(yīng)的是LinearLayout內(nèi)部類(lèi)中的LayoutParams,而這里我們直接使用上面已經(jīng)定義的LayoutParams,相當(dāng)于LinearLayout中有部分功能并未實(shí)現(xiàn),比如layout_margin,layout_weight,layout_gravity,這里我們簡(jiǎn)單處理。
在onMeasure中,要做兩件事,第一件事是向父類(lèi)View一樣測(cè)量自己的長(zhǎng)寬,即需要調(diào)用setMeasuredDimension;第二件事是對(duì)于每個(gè)子View,開(kāi)始它們的測(cè)量,其實(shí),第二件事本身就是第一件的前提,因?yàn)樽覸iew的測(cè)量沒(méi)有結(jié)束的話,自己的長(zhǎng)寬根本就無(wú)法確定。
(1)處在循環(huán)中調(diào)用子View的measure開(kāi)啟它們的測(cè)量過(guò)程,但需要傳遞給它們限制,即childWidthMeasureSpec和childHeightMeasureSpec,這里通過(guò)getChildMeasureSpec方法確定長(zhǎng)與寬的限制(2),該方法在源碼中是定義在ViewGroup中的。
(3)處該方法接收3個(gè)參數(shù),spec為Column自身的受到的父View的限制,padding為測(cè)量到該View時(shí),Column已經(jīng)用完的大小(因?yàn)镃olumn是要將View一個(gè)挨著一個(gè)排布的,肯定需要這個(gè)值),childDimension是開(kāi)發(fā)者在布局文件中指定的layout_width或layout_height值。
因此spec有UNSPECIFIED,EXACTLY,AT_MOST三種類(lèi)型,childDimension有MATCH_PARENT,WRAP_CONTENT和精確值3種類(lèi)型,這些交織的情況都需要分別考慮。在源碼中,將spec放在外層,childDimension放在內(nèi)層,這里我們將childDimension放在放在外層(4),spec放在內(nèi)層,實(shí)現(xiàn)更為簡(jiǎn)潔。
(5)當(dāng)childDimension為MATCH_PARENT,只要忠實(shí)將限制mode傳遞下去即可,大小使用(6)處計(jì)算的剩余大小。
(6)當(dāng)childDimension為WRAP_CONTENT,需限制mode設(shè)為AT_MOST,同樣使用(6)處計(jì)算的剩余大小,但是需要考慮spec.mode為UNSPECIFIED的情況,需要將這種不限制給傳遞下去(7)。
(8)最后對(duì)應(yīng)于childDimension為開(kāi)發(fā)者指定精確值的情況,只要如實(shí)傳遞開(kāi)發(fā)者指定值即可,不必考慮父布局限制。
如此就得到了(1)處傳給各自View的限制,開(kāi)始子View的測(cè)量,當(dāng)前遍歷到的子View測(cè)量完成后,需要獲取測(cè)得的子View高度來(lái)更新已使用的高度值(9),因?yàn)镃olumn是單行縱向排布的,usedWidth就不需要更新。但需要更新width值,作為Column本身的期望寬度。
(10)當(dāng)遍歷完成后,和上節(jié)Text一樣,將resolveSize返回值傳入setMeasuredDimension即可,如此就完成了Column的測(cè)量過(guò)程。
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 } }}
而對(duì)于onLayout方法,因?yàn)橐呀?jīng)知道各子View的測(cè)量寬高,只需要在此遍歷各子View,逐個(gè)設(shè)置坐標(biāo)即可,Column本身的坐標(biāo)設(shè)置已經(jīng)在View中l(wèi)ayout方法中實(shí)現(xiàn)。
如此整個(gè)類(lèi)Android的布局重寫(xiě)完畢。
使用下面驗(yàn)證我們代碼:
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)處定義一個(gè)布局page,就像在Android中寫(xiě)的布局文件那樣,只不過(guò)這里更像是Flutter中聲明式UI的書(shū)寫(xiě)方式。
在源碼中布局流程可以簡(jiǎn)單的認(rèn)為在ViewRootImpl中發(fā)起,內(nèi)部有performMeasure,performLayout從DecorView開(kāi)啟整個(gè)布局流程,這里在(2)處的Column就類(lèi)似于DecorView,下面兩行就類(lèi)似于ViewRootImpl中perform開(kāi)頭的方法發(fā)起的布局流程(這里因?yàn)闊o(wú)關(guān),我們不考慮draw部分)。
運(yùn)行查看打印,與預(yù)想一致。
Column = L: 0, T: 0, R: 1080, B: 1920Column = L: 0, T: 0, R: 110, B: 70Text(Marshmallow) = L: 10, T: 0, R: 120, B: 10Text(Nougat) = L: 10, T: 10, R: 70, B: 20Text(Oreo) = L: 10, T: 20, R: 50, B: 50Text(Pie) = L: 10, T: 50, R: 40, B: 60總結(jié) 整個(gè)View和ViewGroup關(guān)于布局(包含measure,layout)的框架代碼是十分簡(jiǎn)單的,具體的布局算法需要各子類(lèi)自行實(shí)現(xiàn)。 ViewGroup關(guān)于子View的遍歷,因?yàn)樾枰貙?xiě),均發(fā)生在on開(kāi)頭的方法內(nèi)。而父View的測(cè)量寬高的確定本身需要子View的測(cè)量寬高,因此,setMeasuredDimension的調(diào)用在onMeasure中的遍歷之后;而父View坐標(biāo)的確定就不需要另外關(guān)注子View了,因此和View一樣在layout方法中設(shè)置,發(fā)生在onLayout對(duì)子View的遍歷之前。 measure過(guò)程即限制的傳遞過(guò)程以及View的期望大小(代碼中的width,height)匹配限制得到測(cè)量大小(measuredWidth,measuredHeight)的過(guò)程。 整個(gè)布局流程的根本目的在于確定View中的4個(gè)坐標(biāo)值,而這個(gè)值是在layout方法中設(shè)置的,因此對(duì)layout方法的調(diào)用決定了布局流程的結(jié)果,measure可以說(shuō)是對(duì)這個(gè)流程的輔助。
以上就是自己實(shí)現(xiàn)Android View布局流程的詳細(xì)內(nèi)容,更多關(guān)于實(shí)現(xiàn)Android View布局流程的資料請(qǐng)關(guān)注好吧啦網(wǎng)其它相關(guān)文章!
相關(guān)文章:
1. python中scrapy處理項(xiàng)目數(shù)據(jù)的實(shí)例分析2. 教你在 IntelliJ IDEA 中使用 VIM插件的詳細(xì)教程3. IntelliJ IDEA導(dǎo)入jar包的方法4. js抽獎(jiǎng)轉(zhuǎn)盤(pán)實(shí)現(xiàn)方法分析5. Python requests庫(kù)參數(shù)提交的注意事項(xiàng)總結(jié)6. vue-electron中修改表格內(nèi)容并修改樣式7. iOS實(shí)現(xiàn)點(diǎn)贊動(dòng)畫(huà)特效8. 通過(guò)Python pyecharts輸出保存圖片代碼實(shí)例9. SpringBoot參數(shù)校驗(yàn)與國(guó)際化使用教程10. PHP橋接模式Bridge Pattern的優(yōu)點(diǎn)與實(shí)現(xiàn)過(guò)程
