点击查看Android 如何绘制视图官网
一、简介
Android 框架会在 Activity 获得焦点时请求 Activity 绘制其布局。Android 框架会处理绘制流程,但该 Activity 必须提供其布局层次结构的根节点。
Android 框架会绘制布局的根节点,并测量和绘制布局树。它会通过遍历布局树并渲染与无效区域相交的每个 View 进行绘制。每个 ViewGroup 负责请求绘制其每个子级(使用 draw() 方法),而每个 View 负责绘制其本身。由于布局树已经过系统预先遍历,因此该框架会在子级之前(即后方)绘制父级,而其同级会按照它们在布局树中出现的顺序进行绘制。
注意:该框架不会绘制有效区域之外的 View 对象,也不会负责为您绘制 View 背景。您可以通过调用 invalidate() 来强制绘制 View。
Android 框架绘制布局包含两个遍历流程:一个测量遍历和一个布局遍历。该框架会在 measure(int, int) 中执行测量遍历,并执行 View 树的自上而下遍历。在递归过程中,每个 View 都会将维度规范下推到布局树。测量遍历结束时,每个 View 都会存储其测量值。该框架会在 layout(int, int, int, int) 中执行第二次遍历,也是自上而下遍历。在此次遍历中,每个父级负责使用测量遍历中计算的尺寸来定位其所有的子级。
以下各部分更详细地介绍了布局流程的两个遍历。
二、启动测量遍历
当返回 View 对象的 measure() 方法时,请设置其 getMeasuredWidth() 和 getMeasuredHeight() 值,以及 View 对象的所有后代的值。View 对象的测量宽度值和测量高度值必须遵守 View 对象的父级所施加的限制。这有助于保证在测量遍历结束时,所有父级都会接受其子级的所有测量值。
父级 View 可以对其子级多次调用 measure()。例如,父级可以使用未指定的维度测量子级一次,以确定它们的首选尺寸。如果子级的不受限尺寸的总和过大或过小,则父级可以使用限制子级尺寸的值再次调用 measure()。
测量遍历使用两个类来传达维度。View 对象使用 ViewGroup.LayoutParams 来传达其首选尺寸和位置。基本 ViewGroup.LayoutParams 类描述了 View 的首选宽度和高度。针对每个维度,它可以指定以下某一项:
- 一个确切的尺寸。
- MATCH_PARENT,此参数意味着 View 的首选尺寸是其父级的尺寸(负填充)。
- WRAP_CONTENT,此参数意味着 View 的首选尺寸恰好足以容纳其内容(正填充)。
有适用于 ViewGroup 的不同子类的 ViewGroup.LayoutParams 子类。例如,RelativeLayout 有自己的 ViewGroup.LayoutParams 子类,其中包括可使子级 View 对象水平居中和垂直居中的功能。
MeasureSpec 对象用于在树中将要求从父级下推到子级。MeasureSpec 可以是下述三种模式之一:
- UNSPECIFIED:父级使用此模式来确定子级 View 的目标维度。例如,LinearLayout 可对高度设置为 UNSPECIFIED 且宽度设置为 EXACTLY 240 的子级调用 measure(),以确定给定宽度为 240 像素的子级 View 所需的高度。
- EXACTLY:父级使用此模式来强制子级使用某个确切尺寸。子级必须使用此尺寸,并保证其所有的后代都能放入此尺寸。
- AT MOST:父级使用此模式来强制规定子级的最大尺寸。子级必须保证它及其所有的后代都能放入此尺寸。
三、启动布局遍历
如需启动布局,请调用 requestLayout()。当 View 认为自己无法再放入当前范围时,通常会调用此方法。
四、实现自定义测量和布局逻辑
如果您要实现自定义测量或布局逻辑,请替换实现该逻辑的方法:onMeasure(int, int) 和 onLayout(boolean, int, int, int, int)。 这些方法分别由 measure(int, int) 和 layout(int, int, int, int) 调用。请勿尝试替换 measure(int, int) 或 layout(int, int) 方法 - 这两种方法都是 final,因此无法被替换。
以下示例展示了如何在 WindowManager 示例应用的“SplitLayout”类中执行此操作。如果 SplitLayout 具有两个或更多子视图,并且显示屏具有折叠边,则它会将两个子视图放置在折叠边的任一侧。以下示例展示了用于替换测量和布局的用例,但对于生产环境,请使用 SlidingPaneLayout(如果您希望出现此行为)。
/**
* An example of split-layout for two views, separated by a display
* feature that goes across the window. When both start and end views are
* added, it checks whether there are display features that separate the area
* in two—such as a fold or hinge—and places them side-by-side or
* top-bottom.
*/
class SplitLayout : FrameLayout {
private var windowLayoutInfo: WindowLayoutInfo? = null
private var startViewId = 0
private var endViewId = 0
private var lastWidthMeasureSpec: Int = 0
private var lastHeightMeasureSpec: Int = 0
...
fun updateWindowLayout(windowLayoutInfo: WindowLayoutInfo) {
this.windowLayoutInfo = windowLayoutInfo
requestLayout()
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
val startView = findStartView()
val endView = findEndView()
val splitPositions = splitViewPositions(startView, endView)
if (startView != null && endView != null && splitPositions != null) {
val startPosition = splitPositions[0]
val startWidthSpec = MeasureSpec.makeMeasureSpec(startPosition.width(), EXACTLY)
val startHeightSpec = MeasureSpec.makeMeasureSpec(startPosition.height(), EXACTLY)
startView.measure(startWidthSpec, startHeightSpec)
startView.layout(
startPosition.left, startPosition.top, startPosition.right,
startPosition.bottom
)
val endPosition = splitPositions[1]
val endWidthSpec = MeasureSpec.makeMeasureSpec(endPosition.width(), EXACTLY)
val endHeightSpec = MeasureSpec.makeMeasureSpec(endPosition.height(), EXACTLY)
endView.measure(endWidthSpec, endHeightSpec)
endView.layout(
endPosition.left, endPosition.top, endPosition.right,
endPosition.bottom
)
} else {
super.onLayout(changed, left, top, right, bottom)
}
}
/**
* Gets the position of the split for this view.
* @return A rect that defines of split, or {@code null} if there is no split.
*/
private fun splitViewPositions(startView: View?, endView: View?): Array
五、SlidingPaneLayout
5.1 创建双窗格布局
SlidingPaneLayout 组件支持在较大的设备和可折叠设备上并排显示两个窗格,同时自动进行调整,以便在手机等较小的设备上一次仅显示一个窗格。
5.2 添加依赖
如需使用 SlidingPaneLayout,请在应用的 build.gradle 文件中添加以下依赖项:
dependencies {
implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0"
}
5.4 XML 布局配置
SlidingPaneLayout 提供一种水平的双窗格布局,可在界面的顶层使用。在这种布局中,第一个窗格用作内容列表或浏览器,从属于另一个窗格中用于显示内容的主要详细信息视图。
六、SlidingPaneLayout 示例
点击查查GitHub 上的 SlidingPaneLayout 示例