Android 布局与布局属性(Layouts Attributes)实战指南:从 ViewGroup 到 CoordinatorLayout 的选型与原理
文档教程知识库【免费下载链接】android-tech-frontier【停止维护】一个定期翻译国外Android优质的技术、开源库、软件架构设计、测试等文章的开源项目项目地址https://gitcode.com/gh_mirrors/an/android-tech-frontier点击查看免费下载本文基于 android-tech-frontier 仓库中翻译的 Google 开发者 Ian Lake 的系列文章整理而成聚焦“如何把设计稿变成高性能的 Android 布局”这一核心命题。读完本文你将掌握layout_前缀属性的本质LayoutParams、六大常用布局的适用场景与关键参数以及 CoordinatorLayout Behavior 这套现代 Material Design 界面的核心协作机制并能在实战中做出正确的布局选型。一个老生常谈的问题该用哪种布局“What layout should I be using?”我应该使用哪种布局是 Stack Overflow 上经久不衰的提问。对于 Android 开发者来说将设计师的草图转化为实际可运行的布局界面是一项至关重要的基本功。选错布局可能让界面无法达成预期效果或带来糟糕的性能而选对布局则能简化事件处理、提升可维护性。本文先从布局的本质讲起再逐一拆解常用布局的行为特性与layout_属性最后深入到支撑现代 Material Design 交互的 CoordinatorLayout 与 Behavior 机制。什么是布局打开 developer.android.com 搜索 Layout你会看到大量以Layout结尾的类。它们的共同点是都是ViewGroup的子类——一种支持添加子视图子 View的View。ViewGroup 的核心职责之一就是对其子视图进行布局具体包含两个阶段measure测量确定每个子 View 应该有多大layout布局确定每个子 View 在 ViewGroup 内的位置。注意测量与布局并不是 ViewGroup 的全部工作。它还可以有自定义行为、绘制自己以及子 View。例如 Toolbar 除了支持子视图之外还内置了很多其他功能。因此选择恰当的布局来摆放子视图是界面构建中非常关键的一环错误的布局可能无法完成当前布局或性能不佳而正确的布局可以让事件处理事半功倍。layout_ 前缀属性关于父布局的属性说明和普通 View 一样ViewGroup 也可以使用 XML 属性来影响自身例如 LinearLayout 的android:orientation。但这类属性会影响 ViewGroup 中的每一个子 View为了防止这种递归传递布局系统采用了另一种机制向子 View 添加形如layout_前缀的属性。这些layout_属性是关于父布局的属性说明与子 View 本身无关。经典的例子来自 AppBarLayout 与 Toolbar 的组合android.support.design.widget.AppBarLayout android.support.v7.widget.Toolbar app:layout_scrollFlagsscroll|enterAlways / /android.support.design.widget.AppBarLayout如果你去查看 Toolbar 的源码不会找到任何与layout_scrollFlags相关的信息但你可以也应该在 AppBarLayout 中找到它。这些布局属性实际上存储在LayoutParams本例中是子类AppBarLayout.LayoutParams中。LayoutParams每个子 View 都有一份父布局档案当视图被附属到父 View 上时它就会拥有与父布局相对应的 LayoutParams其中存储着与布局位置相关的信息。默认情况下LayoutParams 只有宽度和高度即每个视图都可以找到的layout_width与layout_height但每个 ViewGroup 都可以声明自己的 LayoutParams 子类从而定义新的布局属性。LinearLayout.LayoutParams引入layout_weightFrameLayout.LayoutParams引入layout_gravityRelativeLayout.LayoutParams引入layout_below、layout_toRightOf等一整套相对定位属性CoordinatorLayout.LayoutParams引入layout_behavior、layout_anchor、layout_anchorGravityAppBarLayout.LayoutParams引入layout_scrollFlagsGridLayout.LayoutParams引入行列位置、跨行跨列与权重。这一机制正是父布局决定子视图摆放规则的底层实现父布局通过自己的 LayoutParams 子类把布局规则以layout_属性的形式下发给每个子 View。注从 XML 加载视图时如果传入了parentnull即调用LayoutInflater.inflate()时传入 null 父布局是一个不好的习惯——因为没有父布局就没有人解析和创建适当的 LayoutParams 对象这意味着所有layout_属性都会被丢弃。这可能不是你想要的。常用 Android 布局逐个解析理解了 LayoutParams 与layout_属性的机制就足以帮你做出布局选型了。下面是对常用布局的快速总结。LinearLayout单方向的线性排列LinearLayout 功能比较单一按照单一的行或列布局具体方向由android:orientation决定horizontal横向或vertical纵向。尽管只有一个方向LinearLayout 有一个绝招layout_weight权重它允许子 View 按比例分配剩余空间以填满整个布局。当你有一部分 View 是自适应的wrap_content而其他 View 需要尽可能多的空间时layout_weight非常有用。LinearLayout android:orientationhorizontal android:layout_widthmatch_parent android:layout_heightwrap_content TextView android:layout_width0dp android:layout_heightwrap_content android:layout_weight1 android:text自适应部分/ TextView android:layout_width0dp android:layout_heightwrap_content android:layout_weight2 android:text占两倍空间的部分/ /LinearLayout注意配合layout_weight使用时建议把对应方向的宽度/高度设为0dp让权重机制完全接管空间分配避免额外的测量开销。FrameLayout子 View 层层叠加与 LinearLayout 大相径庭FrameLayout 中所有的 View 都被压在一起绘制后添加的子 View 会覆盖在先添加的子 View 之上。位置控制的唯一要素就是layout_gravity——它可以将子 View 放置在 FrameLayout 的一侧或中心FrameLayout android:layout_widthmatch_parent android:layout_heightmatch_parent !-- 背景层铺满整个 FrameLayout -- ImageView android:layout_widthmatch_parent android:layout_heightmatch_parent android:srcdrawable/bg/ !-- 悬浮层定位到底部右侧 -- Button android:layout_widthwrap_content android:layout_heightwrap_content android:layout_gravitybottom|end android:text操作按钮/ /FrameLayoutlayout_gravity常用的取值包括top、bottom、left、right、start、end、center、center_horizontal、center_vertical以及用|组合的多方向定位。RelativeLayout子 View 之间的相对依赖RelativeLayout 就没有前两者那么简单了。查看RelativeLayout.LayoutParams你会发现关于子 View 与 RelativeLayout 或其他 View 的位置关系有一大堆属性其数量和 FrameLayout 形成了鲜明对比。例如layout_above/layout_below放置在另一个 View 的上方/下方layout_toLeftOf/layout_toRightOf放置在另一个 View 的左侧/右侧layout_alignParentTop/layout_alignParentBottom/layout_alignParentStart/layout_alignParentEnd相对父容器对齐layout_alignTop/layout_alignBottom/layout_alignBaseline等与另一个 View 的边缘或基线对齐。这赋予了 RelativeLayout 非常强大的优势子 View 之间可以相互依赖。但务必注意布局性能——相对依赖意味着一个 View 的测量可能依赖另一个 View 的结果依赖链越深测量开销越大。对于简单的行列界面优先考虑 LinearLayout只有在需要复杂相对定位时才使用 RelativeLayout。PercentFrameLayout 与 PercentRelativeLayout百分比尺寸作为Percent Support Library的成员PercentFrameLayout 和 PercentRelativeLayout 允许你使用基于百分比的尺寸或 margin。例如用app:layout_widthPercent50%表示布局宽度的一半而无需思考具体的像素值android.support.percent.PercentFrameLayout xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:apphttp://schemas.android.com/apk/res-auto android:layout_widthmatch_parent android:layout_heightmatch_parent ImageView android:layout_widthmatch_parent android:layout_heightmatch_parent app:layout_widthPercent50% app:layout_heightPercent50% app:layout_gravitycenter/ /android.support.percent.PercentFrameLayout它们还有一个令人振奋的功能支持纵横比aspect ratio。你可以指定单个尺寸高度或宽度配合固定的纵横比来确定另一个方向的尺寸。甚至当某个维度是wrap_content或match_parent时纵横比也是起作用的——这为自适应多屏布局提供了非常优雅的解决方案。GridLayout二维网格布局GridLayout 于 2011 年的 Ice Cream SandwichAPI 14被引入如今已成为 Support 库的一部分v7-gridlayout支持 API 7 及以上的设备。它将子 View 放置在任意的行和列中并且与 LinearLayout 一样支持权重weight可以让你的视图层级更浅同时避免使用 RelativeLayout 带来的复杂布局与性能下降。与大多数布局不同GridLayout 不要求每个子 View 都有layout_width和layout_height——列和行会根据**基线baseline**调整布局。GridLayout 的常用属性定义在GridLayout.LayoutParams中包括layout_column/layout_row指定子 View 所在的列/行layout_columnSpan/layout_rowSpan跨列/跨行layout_columnWeight/layout_rowWeight按权重分配列宽/行高。CoordinatorLayoutBehavior 驱动的现代布局CoordinatorLayout 是Android Design Support Library中 FrameLayout 的子类。除了可以使用layout_gravity控制子 View 的位置外它还引入了Behavior行为的概念——这也是 Design Support Library 中大多数炫酷效果FAB 随 Snackbar 上移、AppBarLayout 随滚动收起等的真正来源。给 View 添加 Behavior 有三种方式DefaultBehavior注解在自定义 View 的类声明上标注默认行为app:layout_behavior布局属性在 XML 中指定setBehavior()方法在代码中动态设置对应CoordinatorLayout.LayoutParams#setBehavior()。Behavior 可以在子 View 之前拦截各种事件包括measurement测量、layout布局、嵌套滚动nested scrolling、触摸事件touch events、基于依赖 View 的响应式更改以及 window insets窗口插入。深入理解 Behavior 的机制请参见本仓库的姊妹篇 通过CoordinatorLayout的Behavior拦截一切 与 自定义CoordinatorLayout的行为。纵深剖析CoordinatorLayout 与 Behavior 的协作机制CoordinatorLayout 本身完成的工作并不多——与 Android 标准 UI 框架结合使用时它的作用与 FrameLayout 区别不大。真正让它活起来的是绑定到其子元素上的CoordinatorLayout.Behavior。通过 Behavior你可以拦截点击事件、窗口插入、测量、布局和嵌套滚动。下面结合仓库内的多篇相关译文拆解这套机制。三种绑定 Behavior 的方式1. 通过代码绑定Behavior 实际上存储在子 View 的 LayoutParams 中——这正是 Behavior 必须绑定到 CoordinatorLayout 子元素上的原因只有它们才拥有 CoordinatorLayout.LayoutParams。通过LayoutParams#setBehavior()即可完成绑定FancyBehavior fancyBehavior new FancyBehavior(); CoordinatorLayout.LayoutParams params (CoordinatorLayout.LayoutParams) yourView.getLayoutParams(); params.setBehavior(fancyBehavior);2. 通过 XML 绑定与大多数自定义 LayoutParams 一样也可以用layout_属性设置 BehaviorFrameLayout android:layout_heightwrap_content android:layout_widthmatch_parent app:layout_behavior.FancyBehavior /此时会调用FancyBehavior(Context context, AttributeSet attrs)构造方法因此可以声明任意自定义属性并在代码中读取——如果你想通过 XML 自定义 Behavior 的功能这个方式很合适。与layout_类似Behavior 自身的参数通常使用behavior_前缀命名例如app:behavior_peekHeight。3. 自动绑定如果你创建了需要自定义 Behavior 的自定义 View可以借助注解让该 View 自动绑定默认 Behavior而无需每次使用时显式声明CoordinatorLayout.DefaultBehavior(FancyFrameLayoutBehavior.class) public class FancyFrameLayout extends FrameLayout { }这相当于把layout_behavior默认设置为该 Behavior。Behavior 能拦截什么拦截点击事件通过重写onInterceptTouchEvent()控制 CoordinatorLayout 的触摸分发返回true即可获取所有触摸事件——这是SwipeDismissBehavior的原理在blocksInteractionBelow()中返回true则会阻塞该 View 下方所有交互。拦截窗口插入window insets当fitsSystemWindowstrue时绑定的 Behavior 会调用onApplyWindowInsets()获得比 View 本身更高的优先级。关于 fitsSystemWindows 的机制可参见本仓库译文 为什么我们要用fitsSystemWindows。大多数情况下 Behavior 不应消耗全部窗口插入而应通过ViewCompat.dispatchApplyWindowInsets()继续传递让所有子 View 都有机会处理。拦截测量与布局通过onMeasureChild()和onLayoutChild()回调Behavior 可以先于 CoordinatorLayout 干预子 View 的测量与布局。仓库译文 通过CoordinatorLayout的Behavior拦截一切 中给出了一个完整的MaxWidthBehavior示例它通过读取自定义属性behavior_maxWidth在onMeasureChild()中重写测量规格MeasureSpec.AT_MOST对任意 ViewGroup 施加最大宽度约束。View 间依赖Behavior 的魔力真正来源于 View 之间的依赖关系。通过layout_anchorlayout_anchorGravity可以把两个 View 的位置绑定在一起如将 FAB 锚定到 AppBarLayout或者在layoutDependsOn()中返回true声明依赖。一旦依赖成立依赖 View 发生变化改变大小或位置时Behavior 会收到onDependentViewChanged()回调依赖 View 被移除时收到onDependentViewRemoved()回调。FAB 与 Snackbar 的交互就是典型应用FAB 的默认 Behavior 依赖 CoordinatorLayout 中的 Snackbar通过onDependentViewChanged()将 FAB 移到 Snackbar 上方避免遮挡。嵌套滚动CoordinatorLayout 的每个子元素都能接收到 NestedScrollView 等滚动源的滚动事件且该机制适用于 CoordinatorLayout 中任意深度的子 View。在onStartNestedScroll()中按滚动轴如View.SCROLL_AXIS_VERTICAL声明关注并返回true后将依次收到onNestedPreScroll()可在滚动发生前消耗部分或全部滚动、onNestedScroll()滚动进行中可得知已滚动与未滚动的距离以及onStopNestedScroll()滚动结束回调。自定义 Behavior 实战让 FAB 在 Snackbar 出现时缩放仓库译文 自定义CoordinatorLayout的行为 给出了一个完整实例默认 FAB 在 Snackbar 出现时会向上移动让位而自定义 Behavior 可以让 FAB 在 Snackbar 出现时缩小而非移动。首先在 XML 中使用app:layout_behavior指定自定义 Behaviorandroid.support.design.widget.FloatingActionButton android:idid/fab android:layout_widthwrap_content android:layout_heightwrap_content android:layout_gravitybottom|right|end android:layout_margin8dp android:srcdrawable/ic_add app:layout_behaviorcom.example.app.ShrinkBehavior/然后创建 Behavior 子类需要提供(Context, AttributeSet)构造方法以支持从 XML 实例化public class ShrinkBehavior extends CoordinatorLayout.BehaviorFloatingActionButton { public ShrinkBehavior(Context context, AttributeSet attrs) { super(context, attrs); } Override public boolean layoutDependsOn(CoordinatorLayout parent, FloatingActionButton child, View dependency) { // 声明 FAB 依赖 Snackbar return dependency instanceof Snackbar.SnackbarLayout; } Override public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child, View dependency) { // 根据 Snackbar 出现的高度计算缩放比例 float translationY getFabTranslationYForSnackbar(parent, child); float percentComplete -translationY / dependency.getHeight(); float scaleFactor 1 - percentComplete; child.setScaleX(scaleFactor); child.setScaleY(scaleFactor); return false; } }layoutDependsOn()帮助 CoordinatorLayout 知道 FAB 依赖哪个 View设置依赖后当被依赖的 View 改变大小或位置时就会调用onDependentViewChanged()。这里通过简单的数学运算根据 Snackbar 离屏幕边缘的距离动态缩放 FAB。与 AppBarLayout 配合scrollFlags 与越界滚动CoordinatorLayout 最常见的应用场景是与 AppBarLayout、可滚动内容结合实现 Toolbar 随滚动收起/展开。标准写法如下详见仓库译文 Android-Design-Support-Libraryandroid.support.design.widget.CoordinatorLayout xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:apphttp://schemas.android.com/apk/res-auto android:layout_widthmatch_parent android:layout_heightmatch_parent !-- 可滚动视图必须挂上 appbar_scrolling_view_behavior -- android.support.v7.widget.RecyclerView android:layout_widthmatch_parent android:layout_heightmatch_parent app:layout_behaviorstring/appbar_scrolling_view_behavior / android.support.design.widget.AppBarLayout android:layout_widthmatch_parent android:layout_heightwrap_content android.support.v7.widget.Toolbar android:layout_widthmatch_parent android:layout_height?attr/actionBarSize app:layout_scrollFlagsscroll|enterAlways/ /android.support.design.widget.AppBarLayout /android.support.design.widget.CoordinatorLayoutapp:layout_scrollFlags支持的核心 flag 如下定义在AppBarLayout.LayoutParams中Flag行为scroll所有想滚动出屏幕的 View 都必须设置此 flag未设置该 flag 的 View 将固定在屏幕顶部enterAlways任意向下滚动都会让该 View 变为可见启用快速返回quick return模式enterAlwaysCollapsed当视图设置了minHeight且使用此 flag 时视图只以最小高度进入只有当滚动视图到达顶部时才扩展到完整高度exitUntilCollapsed滚动过程中只有当视图折叠到最小高度minHeight时才退出屏幕注意使用 scroll flag 的视图必须在其他视图之前声明这样才能确保所有视图从顶部撤离剩余元素固定在前面即压在其他元素之上。在此基础上仓库译文 AppBarLayout的越界滚动行为 展示了如何继承默认的AppBarLayout.ScrollingViewBehavior结合onNestedScroll()中dyUnconsumed未被消费的像素值实现滚动到边界时放大内容视图的越界滚动效果并在onStopNestedScroll()中恢复原始尺寸——这印证了 Behavior 继承与组合的扩展能力。更多 layout_behavior 应用BottomSheet 等Behavior 机制还被广泛用于其他 Design 组件。例如在 23.2 版本中加入的 BottomSheet详见仓库译文 Android-Support-Library-23.2 与 如何使用BottomSheet给 CoordinatorLayout 的子 View 挂上app:layout_behaviorandroid.support.design.widget.BottomSheetBehavior即可获得五个状态的触摸回调STATE_COLLAPSED、STATE_DRAGGING、STATE_SETTLING、STATE_EXPANDED、STATE_HIDDEN并可配合app:behavior_peekHeight、app:behavior_hideable等behavior_前缀参数调整行为。布局选型建议Layouts, layouts, layouts即使只使用上面提到的部分布局你也能搭建出很多高性能且易于维护的界面。剩下的事情是针对每一个特定的布局思考是否存在更简单的布局方式或者是否值得使用自定义视图同时考虑扩展性。总结几条实用的选型建议单方向线性排列→ LinearLayout配合layout_weight分配空间单层叠加、需要悬浮定位→ FrameLayout配合layout_gravity复杂相对定位、子 View 相互依赖→ RelativeLayout注意依赖链带来的测量开销需要百分比尺寸或固定纵横比→ PercentFrameLayout / PercentRelativeLayout二维表格型界面、希望层级更浅→ GridLayout支持行列权重与基线对齐需要协调滚动、触摸、insets 等跨 View 交互→ CoordinatorLayout Behavior这是现代 Material Design 应用的事实标准。不论怎么样使用正确的布局和布局属性可以帮助我们更好地构建应用——理解layout_属性背后 LayoutParams 的机制是这一能力的基础。本文所涉及的全部译文与配套主题均可在本仓库对应目录中找到原文例如 Layouts-Attributes-and-you、Android-Design-Support-Library、通过CoordinatorLayout的Behavior拦截一切 等欢迎对照阅读。赞分享文档教程知识库【免费下载链接】android-tech-frontier【停止维护】一个定期翻译国外Android优质的技术、开源库、软件架构设计、测试等文章的开源项目项目地址https://gitcode.com/gh_mirrors/an/android-tech-frontier点击查看免费下载相关推荐Qtile 布局配置指南从 layouts 变量到内置布局实战Qtile 布局配置指南从 layouts 变量到内置布局实战 Qtile 是一个用 Python 编写与配置的平铺tiling窗口管理器。本指南围绕 Q桌面应用操作系统告别Android布局混乱Flexbox Layout中AlignContent与AlignSelf属性的终极实战指南告别Android布局混乱Flexbox Layout中AlignContent与AlignSelf属性的终极实战指南 Flexbox Layout是Andr移动开发UI组件前端Minimal Mistakes Jekyll 主题布局Layouts完全指南从 Default 到 Splash 的配置与实战Minimal Mistakes Jekyll 主题布局Layouts完全指南从 Default 到 Splash 的配置与实战 本篇技术指南以 Mini前端静态站点上一篇Ente 2of3 完全指南基于 Shamir 秘密共享的 2-of-3 恢复卡片系统下一篇Budibase 内嵌 Apache CouchDB Helm Chart 部署与配置全指南集群安装、密钥管理、升级迁移与参数详解创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考