自己实现Android View布局流程

相关阅读:尝试自己实现Android View Touch事件分发流程

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

总结

  1. 整个View和ViewGroup关于布局(包含measure,layout)的框架代码是十分简单的,具体的布局算法需要各子类自行实现。
  2. ViewGroup关于子View的遍历,因为需要重写,均发生在on开头的方法内。而父View的测量宽高的确定本身需要子View的测量宽高,因此,setMeasuredDimension的调用在onMeasure中的遍历之后;而父View坐标的确定就不需要另外关注子View了,因此和View一样在layout方法中设置,发生在onLayout对子View的遍历之前。
  3. measure过程即限制的传递过程以及View的期望大小(代码中的width,height)匹配限制得到测量大小(measuredWidth,measuredHeight)的过程。
  4. 整个布局流程的根本目的在于确定View中的4个坐标值,而这个值是在layout方法中设置的,因此对layout方法的调用决定了布局流程的结果,measure可以说是对这个流程的辅助。

以上就是自己实现Android View布局流程的详细内容,更多关于实现Android View布局流程的资料请关注我们其它相关文章!

(0)

相关推荐

  • Android RecyclerView多类型布局卡片解决方案

    背景 随着公司业务越来越复杂,在同一个列表中需要展示各种类型的数据. 总体结构 ItemViewAdapter: 每种类型的卡片分别都是不同的ItemViewAdapter ItemViewAdapterFactory: 使用ItemViewAdapterFactory根据不同数据对应不同的ItemViewAdapter MultiRecyclerViewAdapter: MultiRecyclerViewAdapter就是RecylerView.Adapter,并是个ItemViewAdapt

  • Android自定View流式布局根据文字数量换行

    本文实例为大家分享了Android根据文字数量换行的具体代码,供大家参考,具体内容如下 //主页 定义数据框 package com.example.customwaterfallviewgroup; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.EditText; import java.util

  • Android RecyclerView实现多种item布局的方法

    在项目中列表是基本都会用到的,然而在显示列表时,我们需要的数据可能需要不止一种item显示,对于复杂的数据就需要多种item,以不同的样式显示出来,这样效果是很棒的,我们先看一下效果 我们可以看到,这个RecyclerView中有多种item显示出来,那么具体怎么实现呢,其实在RecyclerView中,我们可以重写方法getItemViewType(),这个方法会传进一个参数position表示当前是第几个Item,然后我们可以通过position拿到当前的Item对象,然后判断这个item对

  • Android控件CardView实现卡片布局

    CardView介绍 CardView是Android 5.0系统引入的控件,相当于FragmentLayout布局控件然后添加圆角及阴影的效果:CardView被包装为一种布局,并且经常在ListView和RecyclerView的Item布局中,作为一种容器使用.CardView应该被使用在显示层次性的内容时:在显示列表或网格时更应该被选择,因为这些边缘可以使得用户更容易去区分这些内容. 使用 先看效果 首先在build.gradle文件添加依赖库 dependencies { compil

  • Android 自定义View实现任意布局的RadioGroup效果

    前言 RadioGroup是继承LinearLayout,只支持横向或者竖向两种布局.所以在某些情况,比如多行多列布局,RadioGroup就并不适用 . 本篇文章通过继承RelativeLayout实现自定义RadioGroup,实现RadioButton的任意布局.效果图如下: 代码(RelativeRadioGroup) /** * Author : BlackHao * Time : 2018/10/26 10:46 * Description : 自定义 RadioGroup */ p

  • Android进阶教程之ViewGroup自定义布局

    前言 在我们的实际应用中, 经常需要用到自定义控件,比如自定义圆形头像,自定义计步器等等.但有时我们不仅需要自定义控件,举个例子,FloatingActionButton 大家都很常用,所以大家也很经常会有一种需求,点击某个 FloatingActionButton 弹出更多 FloatingActionButton ,这个需求的一般思路是写 n 个 button 然后再一个个的去设置动画效果.但这实在是太麻烦了,所以网上有个 FloatingActionButtonMenu 这个开源库,这就是

  • Android RecyclerView网格布局示例解析

    一个简单的网格布局 activity_main.xml <?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/ap

  • Android网格布局GridView实现漂亮的多选效果

    上一篇文章中主要讲了GridView的简单应用,以网格的形式展示了一些图片,对于图片也有点击监听操作.但是,如果我们在浏览图片的时候需要一些选中操作.甚至是多选操作的时候.这样的功能我们又该如何实现呢? 可以使用ActionBar +GridView的形式实现!在谈及具体实现之前,首先我们先了解一下什么是 ActionBar: Action Bar是活动中的一种控件,用以代替传统的品目顶端的标题栏,它提供了多便利性.有关其详细内容会在以后研究,现在主要考虑上述需求的实现. 先上效果图 首先是关于

  • android如何获取view在布局中的高度与宽度详解

    前言 可能很多情况下,我们都会有在activity中获取view 的尺寸大小(宽度和高度)的需求.面对这种情况,很多同学立马反应:这么简单的问题,还用你说?你是不是傻..然后立马写下getWidth().getHeight()等方法,洋洋得意的就走了.然而事实就是这样的吗?实践证明,我们这样是获取不到View的宽度和高度大小的. 当我们在 onCreate() 方法中获取某个 View 组件的宽度和高度,直接调用 getWidth().getHeight().getMeasuredWidth()

  • Android自定义ViewGroup实现流式布局

    本文实例为大家分享了Android自定义ViewGroup实现流式布局的具体代码,供大家参考,具体内容如下 1.概述 本篇给大家带来一个实例,FlowLayout,什么是FlowLayout,我们常在App 的搜索界面看到热门搜索词,就是FlowLayout,我们要实现的就是图中的效果,就是根据容器的宽,往容器里面添加元素,如果剩余的控件不足时候,自行添加到下一行,FlowLayout也叫流式布局,在开发中还是挺常用的. 2.对所有的子View进行测量 onMeasure方法的调用次数是不确定的

  • Android列表RecyclerView排列布局

    本文实例为大家分享了Android列表RecyclerView排列布局的具体代码,供大家参考,具体内容如下 效果图: 1.要添加相关的依赖 implementation 'androidx.recyclerview:recyclerview:1.1.0' 2.然后布局文件中准备容器 这个标签是显示目标容器对象的,其他需求可自定义 <androidx.recyclerview.widget.RecyclerView android:id="@+id/rv_list" android

随机推荐