15、自适应布局:ConstraintLayout与百分比布局在大屏上的应用
大屏适配,说白了就是让同一个界面在手机、平板、电视上都能看得舒服。我刚开始做TV端开发时,踩过一个坑:用LinearLayout写了个菜单,手机上看挺好,一上电视,左边空出一大片,右边按钮挤成一团。嗯,那时候我就意识到——布局不能写死。
这一章,咱们就聊聊两个核心武器:ConstraintLayout 和 百分比布局。它们不是二选一的关系,而是互补的。我个人习惯是:ConstraintLayout搭骨架,百分比布局调细节。
15.1 为什么大屏上不能用固定尺寸?
你想想看,电视屏幕从32寸到85寸都有,分辨率从720p到4K甚至8K。如果你写死一个按钮宽200dp,在手机上可能占半个屏幕,在电视上就变成一个小不点。用户得拿着遥控器凑近了点,体验极差。
我在项目中遇到过最典型的场景:一个视频列表页,手机上是2列,平板上是4列,电视上我们希望是6列。如果用固定宽度写,每种屏幕都得单独调。后来我用ConstraintLayout的链+百分比,一套布局搞定。
核心原则:永远不要用dp写死宽高,除非是图标或分割线这种固定元素。大屏上所有容器尺寸都应该是相对的。
15.2 ConstraintLayout:大屏布局的骨架
ConstraintLayout最厉害的地方,是它允许你用「约束」来描述控件之间的相对位置。说白了,就是告诉系统:「这个按钮的左边,贴着那个图片的右边,间距16dp」。系统会自动计算位置,不管屏幕多大。
15.2.1 相对定位:告别嵌套
以前用LinearLayout做复杂布局,经常要嵌套三四层。嵌套多了,性能就下来了。ConstraintLayout可以做到扁平化——一个根布局,所有子控件通过约束相连。
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="电影名称"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginTop="32dp"/>
<ImageView
android:id="@+id/poster"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_margin="16dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
注意看,layout_width="0dp" 表示宽度由约束决定。这里标题的左右都约束到父容器,所以它会自动撑满屏幕宽度。图片的上下左右都有约束,它会自动填满剩余空间。嗯,这就是自适应。
我的习惯:在大屏布局中,尽量把宽高都设成0dp,让约束来决定尺寸。只在需要固定比例时用wrap_content。
15.2.2 链(Chain):均匀分布的利器
电视上经常需要一排按钮均匀分布,比如「首页、分类、搜索、我的」四个Tab。用Chain可以轻松实现。
<Button
android:id="@+id/btn_home"
android:layout_width="0dp"
android:layout_height="48dp"
android:text="首页"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/btn_category"
app:layout_constraintTop_toTopOf="parent"/>
<Button
android:id="@+id/btn_category"
android:layout_width="0dp"
android:layout_height="48dp"
android:text="分类"
app:layout_constraintStart_toEndOf="@id/btn_home"
app:layout_constraintEnd_toStartOf="@id/btn_search"
app:layout_constraintTop_toTopOf="parent"/>
<Button
android:id="@+id/btn_search"
android:layout_width="0dp"
android:layout_height="48dp"
android:text="搜索"
app:layout_constraintStart_toEndOf="@id/btn_category"
app:layout_constraintEnd_toStartOf="@id/btn_mine"
app:layout_constraintTop_toTopOf="parent"/>
<Button
android:id="@+id/btn_mine"
android:layout_width="0dp"
android:layout_height="48dp"
android:text="我的"
app:layout_constraintStart_toEndOf="@id/btn_search"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
然后在代码或XML中设置链式约束:
app:layout_constraintHorizontal_chainStyle="spread"
这样四个按钮就会自动等宽、等间距地铺满屏幕。电视上看起来非常整齐。
15.2.3 Guideline:百分比定位的替代方案
ConstraintLayout自带Guideline,可以按百分比定位。比如我想让一个控件位于屏幕30%的位置:
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_left"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.3"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="登录"
app:layout_constraintStart_toStartOf="@id/guideline_left"
app:layout_constraintTop_toTopOf="parent"/>
这个Guideline就像一根看不见的线,位置是屏幕宽度的30%。按钮的左边对齐这根线。不管屏幕多宽,按钮始终在30%的位置。我在电视遥控器菜单里经常用这个技巧。
我曾经踩过的坑:Guideline的百分比是相对于父容器的。如果你在嵌套布局里用Guideline,它参考的是直接父容器,不是整个屏幕。记得检查父容器是不是match_parent。
15.3 百分比布局:精细控制每一寸
ConstraintLayout虽然强大,但有些场景还是不够灵活。比如我想让一个卡片占屏幕宽度的45%,另一个占55%,中间留10%的间距。用ConstraintLayout的链可以实现,但代码写起来有点绕。这时候百分比布局就派上用场了。
15.3.1 PercentFrameLayout 和 PercentRelativeLayout
这两个是support库提供的,现在已经迁移到androidx。用法很简单:
<androidx.percentlayout.widget.PercentFrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="0dp"
android:layout_height="0dp"
android:text="左半屏"
app:layout_widthPercent="45%"
app:layout_heightPercent="50%"
app:layout_marginStartPercent="2.5%"/>
<Button
android:layout_width="0dp"
android:layout_height="0dp"
android:text="右半屏"
app:layout_widthPercent="45%"
app:layout_heightPercent="50%"
app:layout_marginStartPercent="52.5%"/>
</androidx.percentlayout.widget.PercentFrameLayout>
你看,layout_widthPercent="45%" 直接告诉系统:这个按钮占父容器宽度的45%。两个按钮各45%,中间自动留出10%的间距。非常直观。
15.3.2 百分比margin:间距也自适应
大屏上不仅控件尺寸要变,间距也要变。电视上16dp的间距看起来太小,48dp才合适。用百分比margin可以做到:
app:layout_marginStartPercent="3%"
app:layout_marginTopPercent="2%"
这样间距会随着屏幕尺寸等比缩放。我在做电视端「推荐位」布局时,就是靠百分比margin让卡片之间的间距在不同尺寸下都保持视觉舒适。
实战建议:百分比布局适合用在「固定比例」的场景,比如二分屏、三分屏、九宫格。ConstraintLayout适合用在「相对位置」复杂的场景,比如图文混排、表单。两者结合使用效果最好。
15.4 组合实战:一个电视端视频详情页
咱们来看一个真实案例。电视端视频详情页,布局要求:
- 左侧是视频封面,占屏幕宽度的35%
- 右侧是标题、简介、按钮,占屏幕宽度的60%
- 中间留5%的间距
- 底部有一排操作按钮,均匀分布
我的做法是:外层用ConstraintLayout,左侧封面用Guideline定位到35%位置,右侧内容用链约束到Guideline右侧。底部按钮用Chain实现均匀分布。
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="24dp">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/gl_left"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.35"/>
<!-- 左侧封面 -->
<ImageView
android:id="@+id/cover"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/gl_left"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:scaleType="centerCrop"/>
<!-- 右侧内容 -->
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toEndOf="@id/gl_left"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginStart="16dp"
android:textSize="28sp"/>
<!-- 底部按钮链 -->
<Button
android:id="@+id/btn_play"
android:layout_width="0dp"
android:layout_height="48dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/btn_fav"
app:layout_constraintBottom_toBottomOf="parent"/>
<Button
android:id="@+id/btn_fav"
android:layout_width="0dp"
android:layout_height="48dp"
app:layout_constraintStart_toEndOf="@id/btn_play"
app:layout_constraintEnd_toStartOf="@id/btn_share"
app:layout_constraintBottom_toBottomOf="parent"/>
<Button
android:id="@+id/btn_share"
android:layout_width="0dp"
android:layout_height="48dp"
app:layout_constraintStart_toEndOf="@id/btn_fav"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
这个布局在手机上、平板上、电视上都能自适应。左侧封面始终占35%,右侧内容占60%,底部三个按钮均匀分布。你想想看,如果不用这些技术,得写多少个dimens文件?
我的经验:在大屏开发中,dimens文件只用来定义字体大小和最小触摸目标(48dp),其他所有尺寸都用百分比或约束。这样一套布局走天下。
15.5 避坑指南
最后分享几个我踩过的坑,希望能帮你少走弯路。
- ConstraintLayout版本问题:早期版本对百分比支持不完善,建议使用2.0以上版本。我曾在1.1版本上折腾了半天,升级后就好了。
- 百分比布局的父容器:PercentFrameLayout的百分比是相对于父容器的宽高。如果父容器不是match_parent,百分比计算会出错。记得检查。
- 链的权重:ConstraintLayout的链也支持权重(类似LinearLayout的weight),用
app:layout_constraintHorizontal_weight设置。但注意,权重只在链模式下生效。 - 性能问题:不要在一个ConstraintLayout里放太多控件(超过50个)。虽然它比嵌套LinearLayout好,但也不是万能的。复杂页面建议分组。
嗯,这一章的内容就到这里。自适应布局的核心就是「相对」二字——相对位置、相对尺寸、相对间距。掌握了ConstraintLayout和百分比布局,大屏适配就不再是难题。
公众号:蓝海资料掘金营,微信deep3321