Wear OS 上的叠加层采用与其他 Android 设备相同的布局技术,但在设计时需要遵循特定于手表的约束。
注意:请不要将具体功能和界面从移动应用移植到 Wear OS 上,不要指望这样能带来良好的用户体验。
如果您设计的应用用于方形手表,那么在圆形手表上,屏幕角落附近的内容可能会被剪裁掉。如果您使用的是可滚动的垂直列表,这就没什么问题,因为用户可以滚动屏幕,居中显示内容。但是,对于单屏,这样可能会导致糟糕的用户体验。
例如,图 1 展示了以下布局在方形屏幕和圆形屏幕的显示效果:

图 1. 展示专为方形屏幕设计的布局在圆形屏幕上可能无法正常显示的示例。
如果您为布局使用以下设置,文本在圆形屏幕的设备上会无法正确显示:
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/text" android:layout_width="0dp" android:layout_height="0dp" android:text="@string/very_long_hello_world" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
可以通过两种方法解决此问题:
- 对方形设备和圆形设备都使用 Wear OS 界面库中的布局。
- BoxInsetLayout:此布局根据设备屏幕的形状应用不同的窗口边衬区。如果您要对这两种屏幕形状使用相似的布局而又不希望圆形屏幕边缘附近的视图被剪裁掉,则可以使用此方法。
- 曲线布局:当您要显示和操作针对圆形屏幕而优化的垂直项目列表时,可使用此布局。
- 按照提供备用资源指南中所述,为方形设备和圆形设备提供备用布局资源。在运行时,Wear OS 会检测设备屏幕的形状并加载正确的布局。
如需详细了解如何设计叠加层,请参阅 Wear OS 设计指南。
使用 BoxInsetLayout

图 2.圆形屏幕上的窗口边衬区。
您可以使用 Wear OS 界面库中的 BoxInsetLayout
类定义同时适用于方形和圆形屏幕的布局。此类会根据屏幕形状应用所需的窗口边衬区,并可让您轻松地在屏幕中心或边缘附近对齐视图。
图 2 中的灰色方形区域显示了在应用所需的窗口边衬区后,BoxInsetLayout
可在圆形屏幕上自动放置其子视图的区域。要使子视图显示在此区域内,可通过以下值指定 layout_boxedEdges
属性:
top
、bottom
、left
和right
的组合。例如,"left|top"
值可将子视图的左侧边缘和顶部边缘定位在图 2 中的灰色方形内。"all"
值可确定图 2 灰色方形中所有子视图内容的位置。这是包含ConstraintLayout
的最常用方法。
在方形屏幕上,窗口边衬区为零,并且会忽略 layout_boxedEdges
属性。

图 3. 同时适用于方形屏幕和圆形屏幕的布局定义。
图 3 中显示的布局使用了 <BoxInsetLayout>
元素,同时适用于方形屏幕和圆形屏幕:
<androidx.wear.widget.BoxInsetLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_height="match_parent" android:layout_width="match_parent" android:padding="15dp"> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" android:padding="5dp" app:layout_boxedEdges="all"> <TextView android:layout_height="wrap_content" android:layout_width="match_parent" android:text="@string/sometext" android:textAlignment="center" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <ImageButton android:background="@android:color/transparent" android:layout_height="50dp" android:layout_width="50dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" android:src="@drawable/ok" /> <ImageButton android:background="@android:color/transparent" android:layout_height="50dp" android:layout_width="50dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" android:src="@drawable/cancel" /> </androidx.constraintlayout.widget.ConstraintLayout> </androidx.wear.widget.BoxInsetLayout>
请注意该布局中以粗体标记的部分:
-
android:padding="15dp"
此行为
<BoxInsetLayout>
元素指定内边距。 -
android:padding="5dp"
此行为内部
ConstraintLayout
元素指定内边距。 -
app:layout_boxedEdges="all"
此行可确保
ConstraintLayout
元素及其子项位于圆形屏幕上的窗口边衬区所定义的区域内。此行对方形屏幕没有影响。
使用曲线布局
您可以通过 Wear OS 界面库中的
WearableRecyclerView
类选择使用针对圆形屏幕进行了优化的曲线布局。如需为应用中的可滚动列表启用曲线布局,请参阅在 Wear OS 上创建列表。
对方形屏幕和圆形屏幕使用不同的布局
Wear OS 设备可能具有方形屏幕或圆形屏幕。您的应用需要能够支持任一设备配置。为此,您应提供备用资源。对于布局、尺寸或其他资源类型,将资源限定符设置为 round
或 notround
。
例如,考虑按如下方式组织布局:
-
layout/
目录包含同时适用于圆形和方形手表的布局。 -
layout-round/
和layout-notround/
目录包含特定屏幕形状的专用布局。
您还可以使用 res/values
、res/values-round
和 res/values-notround
资源目录。以这种方式组织资源,您可以共享一个布局,仅需根据设备类型更改特定属性即可。
改变值
使用 values/dimens.xml
和 values-round/dimens.xml
可轻松为圆形手表和方形手表构建布局。通过指定不同的内边距设置,您可以使用单个 layout.xml
文件和两个 dimens.xml
文件创建以下布局:
您应尝试使用不同的值,看看哪个值效果最佳。
使用 XML 填补下巴
某些手表的圆形屏幕上有边衬区(也称为“下巴”)。如果不填补下巴,部分设计可能会被它遮挡住。
例如,您可能有以下设计:

图 6. 简单的心形设计。
此 activity_main.xml
代码段定义了其布局。
<FrameLayout ... <androidx.wear.widget.RoundedDrawable android:id="@+id/androidbtn" android:src="@drawable/ic_android" .../> <ImageButton android:id="@+id/lovebtn" android:src="@drawable/ic_favourite" android:paddingTop="5dp" android:paddingBottom="5dp" android:layout_gravity="bottom" .../> </FrameLayout>
如果不采取任何措施,部分设计会被下巴挡住。

图 7. 未加以修正。
您可以使用 fitsSystemWindows
属性设置内边距来避开下巴。以下 activity_main.xml
代码段展示了 fitsSystemWindows
的用法:
<ImageButton android:id="@+id/lovebtn" android:src="@drawable/ic_favourite" android:paddingTop="5dp" android:paddingBottom="5dp" android:fitsSystemWindows="true" .../>

图 8. 使用 fitsSystemWindows
属性。
请注意,您定义的上下内边距值会被替换,以使所有内容都能容纳在系统窗口中。解决此问题的方法是使用 InsetDrawable
替换内边距值。
创建 inset_favourite.xml
文件来定义内边距值:
<inset xmlns:android="http://schemas.android.com/apk/res/android" android:drawable="@drawable/ic_favourite" android:insetTop="5dp" android:insetBottom="5dp" />
从 activity_main.xml
中移除内边距:
<ImageButton android:id="@+id/lovebtn" android:src="@drawable/inset_favourite"android:paddingTop="5dp"android:paddingBottom="5dp"android:fitsSystemWindows="true" .../>

图 9. 使用 InsetDrawables
。
以编程方式管理下巴
如果您需要获得比使用 XML 以声明方式定义布局时更多的控制权限,可以通过编程方式调整布局。如需获取下巴的尺寸,请将 View.OnApplyWindowInsetsListener
附加到布局的最外层视图。
请将以下代码添加到主 activity 文件中:
Kotlin
private var chinSize: Int = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // find the outermost element findViewById<View>(R.id.outer_container).apply { // attach a View.OnApplyWindowInsetsListener setOnApplyWindowInsetsListener { v, insets -> chinSize = insets.systemWindowInsetBottom // The following line is important for inner elements which react to insets v.onApplyWindowInsets(insets) insets } } }
Java
private int chinSize; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // find the outermost element final View container = findViewById(R.id.outer_container); // attach a View.OnApplyWindowInsetsListener container.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() { @Override public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) { chinSize = insets.getSystemWindowInsetBottom(); // The following line is important for inner elements which react to insets v.onApplyWindowInsets(insets); return insets; } }); }