欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

自己實現(xiàn)Android View布局流程

 更新時間:2021年03月26日 10:22:23   作者:CAZ  
這篇文章主要介紹了自己實現(xiàn)Android View布局流程,幫助大家更好的理解和學(xué)習(xí)使用Android,感興趣的朋友可以了解下

相關(guān)閱讀:嘗試自己實現(xiàn)Android View Touch事件分發(fā)流程

Android View的布局以ViewRootImpl為起點,開啟整個View樹的布局過程,而布局過程本身分為測量(measure)和布局(layout)兩個部分,以View樹本身的層次結(jié)構(gòu)遞歸布局,確定View在界面中的位置。

下面嘗試通過最少的代碼,自己實現(xiàn)這套機制,注意下面類均為自定義類,未使用Android 源碼中的同名類。

MeasureSpec

首先定義MeasureSpec,它是描述父布局對子布局約束的類,在Android源碼中它是一個int值,通過位運算獲取mode和size,這里我們?yōu)榱朔奖闫鹨妼崿F(xiàn)為一個類:

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的內(nèi)部,是靜態(tài)內(nèi)部類,用于在該ViewGroup布局中的子View中使用,這里我們定義為頂層類,并且只包含寬高兩種屬性,對應(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
 }
}

下面我們實現(xiàn)View與ViewGroup。

View

(1)處我們定義的View的坐標(biāo),和源碼中一致,這里表示的是相對于父View的坐標(biāo),與上篇View相關(guān)文章嘗試自己寫Android View Touch事件分發(fā)中不同,那篇的View的坐標(biāo)是絕對坐標(biāo)。

(2)處定義了padding,(3)處表示measure過程的測量寬高,(4)為布局文件中指定的layoutParam

這些屬性,總結(jié)下來就是(2)(4)由開發(fā)者在布局中指定,(3)通過測量過程由View自己測得,(1)通過布局過程最終確定,也就是我們的目的所在,包括(3)存在的意義也是為了確定(4)中的值。

下面開始編寫測量過程,雖然這些代碼都是重寫的,進行了大量的簡化,但整體流程依然和源碼是一致的,能夠更清晰的理解Android的View樹的布局是如何實現(xiàn)的。

(5)處measure直接調(diào)用onMeasure開始測量過程,而onMeasure這里簡單直接設(shè)置了MeasureSpec中父ViewGroup中的限制值作為測量值就結(jié)束了自己的測量過程(6),因為onMeasure是需要繼承使用的,不同View的測量方式并不相同,所以這里簡單處理。

(7)處開始布局過程,首先調(diào)用setFrame方法將坐標(biāo)保存(8),并調(diào)用onLayout回調(diào),這里為空實現(xiàn)(9)。

至此View的布局相關(guān)方法實現(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

下面我們實現(xiàn)ViewGroup,只有一個抽象方法,即將View中的onLayout空實現(xiàn)聲明為抽象的,即要求子類自行實現(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)
}

如此,整個Android的View層次結(jié)構(gòu)的骨架已經(jīng)搭建完成了,在源碼中,對于View的布局方面,主要也就干了這么點事情。其他各種各樣的View與ViewGroup均是通過繼承,實現(xiàn)各自的測量算法(即子View實現(xiàn)onMeasure),和布局算法(即子ViewGroup實現(xiàn)onMeasure與onLayout)。

下面我們依托這個框架各實現(xiàn)一個View與ViewGroup。

Text

下面我們實現(xiàn)一個TextView,這里因為我們只是為了說明View測量的原理,因此只支持兩個屬性text與textSize。

只需實現(xiàn)onMeasure即可,將左右padding相加,并加上字符串長度與字號的乘積作為寬(1),將上下padding相加,并加上字號作為高,當(dāng)然這里我們只是簡單這樣計算示意,實際計算TextView長寬肯定不能這樣來算。

如此算得的長寬就是Text自身理想的長寬,但是,還需要施加上父布局的限制才行,即MeasureSpec,這里即調(diào)用resolveSize,將限制與理想值傳入即可(2)。

resolveSize定義在View節(jié)的(10)處,里面處理邏輯即,當(dāng)限制為固定值時,測量值取限制值,當(dāng)限制上限時,測量值為限制值與理想值取小,當(dāng)限制為不限時,取理想值。

如此,整個TextView的測量過程完畢。對于布局過程,由于,layout方法內(nèi)已經(jīng)設(shè)置了自身的坐標(biāo),onLayout保持空實現(xiàn)即可,并不需要重寫。

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_開頭的布局屬性,對應(yīng)的是LinearLayout內(nèi)部類中的LayoutParams,而這里我們直接使用上面已經(jīng)定義的LayoutParams,相當(dāng)于LinearLayout中有部分功能并未實現(xiàn),比如layout_margin,layout_weight,layout_gravity,這里我們簡單處理。

在onMeasure中,要做兩件事,第一件事是向父類View一樣測量自己的長寬,即需要調(diào)用setMeasuredDimension;第二件事是對于每個子View,開始它們的測量,其實,第二件事本身就是第一件的前提,因為子View的測量沒有結(jié)束的話,自己的長寬根本就無法確定。

(1)處在循環(huán)中調(diào)用子View的measure開啟它們的測量過程,但需要傳遞給它們限制,即childWidthMeasureSpec和childHeightMeasureSpec,這里通過getChildMeasureSpec方法確定長與寬的限制(2),該方法在源碼中是定義在ViewGroup中的。

(3)處該方法接收3個參數(shù),spec為Column自身的受到的父View的限制,padding為測量到該View時,Column已經(jīng)用完的大?。ㄒ驗镃olumn是要將View一個挨著一個排布的,肯定需要這個值),childDimension是開發(fā)者在布局文件中指定的layout_width或layout_height值。

因此spec有UNSPECIFIED,EXACTLY,AT_MOST三種類型,childDimension有MATCH_PARENT,WRAP_CONTENT和精確值3種類型,這些交織的情況都需要分別考慮。在源碼中,將spec放在外層,childDimension放在內(nèi)層,這里我們將childDimension放在放在外層(4),spec放在內(nèi)層,實現(xiàn)更為簡潔。

(5)當(dāng)childDimension為MATCH_PARENT,只要忠實將限制mode傳遞下去即可,大小使用(6)處計算的剩余大小。

(6)當(dāng)childDimension為WRAP_CONTENT,需限制mode設(shè)為AT_MOST,同樣使用(6)處計算的剩余大小,但是需要考慮spec.mode為UNSPECIFIED的情況,需要將這種不限制給傳遞下去(7)。

(8)最后對應(yīng)于childDimension為開發(fā)者指定精確值的情況,只要如實傳遞開發(fā)者指定值即可,不必考慮父布局限制。

如此就得到了(1)處傳給各自View的限制,開始子View的測量,當(dāng)前遍歷到的子View測量完成后,需要獲取測得的子View高度來更新已使用的高度值(9),因為Column是單行縱向排布的,usedWidth就不需要更新。但需要更新width值,作為Column本身的期望寬度。

(10)當(dāng)遍歷完成后,和上節(jié)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方法,因為已經(jīng)知道各子View的測量寬高,只需要在此遍歷各子View,逐個設(shè)置坐標(biāo)即可,Column本身的坐標(biāo)設(shè)置已經(jīng)在View中l(wèi)ayout方法中實現(xiàn)。

如此整個類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的書寫方式。

在源碼中布局流程可以簡單的認(rèn)為在ViewRootImpl中發(fā)起,內(nèi)部有performMeasure,performLayout從DecorView開啟整個布局流程,這里在(2)處的Column就類似于DecorView,下面兩行就類似于ViewRootImpl中perform開頭的方法發(fā)起的布局流程(這里因為無關(guān),我們不考慮draw部分)。

運行查看打印,與預(yù)想一致。

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

總結(jié)

  1. 整個View和ViewGroup關(guān)于布局(包含measure,layout)的框架代碼是十分簡單的,具體的布局算法需要各子類自行實現(xiàn)。
  2. ViewGroup關(guān)于子View的遍歷,因為需要重寫,均發(fā)生在on開頭的方法內(nèi)。而父View的測量寬高的確定本身需要子View的測量寬高,因此,setMeasuredDimension的調(diào)用在onMeasure中的遍歷之后;而父View坐標(biāo)的確定就不需要另外關(guān)注子View了,因此和View一樣在layout方法中設(shè)置,發(fā)生在onLayout對子View的遍歷之前。
  3. measure過程即限制的傳遞過程以及View的期望大?。ùa中的width,height)匹配限制得到測量大小(measuredWidth,measuredHeight)的過程。
  4. 整個布局流程的根本目的在于確定View中的4個坐標(biāo)值,而這個值是在layout方法中設(shè)置的,因此對layout方法的調(diào)用決定了布局流程的結(jié)果,measure可以說是對這個流程的輔助。

以上就是自己實現(xiàn)Android View布局流程的詳細內(nèi)容,更多關(guān)于實現(xiàn)Android View布局流程的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評論