Handle different watch shapes

Apps on Wear OS use the same layout techniques as other Android devices but need to be designed with watch-specific constraints.

Note: Don't port the exact functionality and UI from a mobile app to Wear OS and expect a good user experience.

If you design your app for a square watch, content near the corners of the screen might be cropped on round watches. If you are using a scrollable vertical list, this is less of an issue, as the user can scroll to center the content. However, for single screens, it can provide a bad user experience.

Figure 1 shows how one layout looks on square and round screens:

Figure 1. A layout designed for square screens might not work well on round screens.

If you use the following settings for your layout, text displays incorrectly on devices with round screens:

<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>

There are two approaches to solving this problem, which are described in the sections that follow:

  • Use layouts in the Wear OS UI Library for both square and round devices.
    • You can use a BoxInsetLayout to apply different window insets depending on the shape of the device screen. Use this approach when you want a similar layout on both screen shapes without having views cropped near the edges of round screens.
    • You can use a WearableRecyclerView to create a curved layout when you want to display and manipulate a vertical list of items optimized for round screens.
  • Provide alternative layout resources for square and round devices as described in the Providing alternative resources guide. At runtime, Wear OS detects the shape of the device screen and loads the correct layout.

For more information about designing apps, read the Wear OS design guidelines.

Use a BoxInsetLayout

Figure 2. Window insets on a round screen.

The BoxInsetLayout class in the Wear OS UI Library lets you define a single layout that works for both square and round screens. This class applies the required window insets depending on the screen shape and lets you easily align views on the center or near the edges of the screen.

The gray square in figure 2 shows the area where the BoxInsetLayout can automatically place its child views on round screens after applying the required window insets. To be displayed inside this area, child views specify the layout_boxedEdges attribute with the following values:

  • A combination of top, bottom, left, and right. For example, a "left|top" value positions the child's left and top edges inside the gray square in figure 2.
  • The "all" value positions all the child's content inside the gray square in figure 2. This is the most common approach with a ConstraintLayout inside.

On square screens, the window insets are zero, and the layout_boxedEdges attribute is ignored.

Figure 3. A layout definition that works on both square and round screens.

The layout shown in figure 3 uses the <BoxInsetLayout> element and works on square and round screens:

<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/cancel" />

        <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/ok" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</androidx.wear.widget.BoxInsetLayout>

Notice the parts of the layout marked in bold:

  • android:padding="15dp"

    This line assigns padding to the <BoxInsetLayout> element.

  • android:padding="5dp"

    This line assigns padding to the inner ConstraintLayout element.

  • app:layout_boxedEdges="all"

    This line ensures that the ConstraintLayout element and its children are boxed inside the area defined by the window insets on round screens. This line has no effect on square screens.

Use a curved layout

The WearableRecyclerView class in the Wear OS UI Library lets you opt-in to a curved layout optimized for round screens. To enable a curved layout for scrollable lists in your app, see Create lists on Wear OS.

Use different layouts for square and round screens

Another approach to supporting both square and round screens is to provide alternative resources for different screen shapes. Set a resource qualifier to round or notround on layouts, dimensions, or other resource types.

For example, consider organizing your layouts as follows:

  • Use the layout/ directory for layouts that work for both circular and square watches.
  • Use the layout-round/ and layout-notround/ directories for layouts that are specific to the shape of a screen.

You can also use the res/values, res/values-round, and res/values-notround resource directories. By organizing resources in this way, you can share a single layout while changing only specific attributes based on the device type.

Vary the values

One way to build for round and square watches is by using values/dimens.xml and values-round/dimens.xml. By specifying different padding settings, you can create the following layout with a single layout.xml file and the two dimens.xml files:

<dimen name="header_start_padding">36dp</dimen>
<dimen name="header_end_padding">22dp</dimen>
<dimen name="list_start_padding">36dp</dimen>
<dimen name="list_end_padding">22dp</dimen>
Using values-round/dimens.xml

Figure 4. Using values-round/dimens.xml.

<dimen name="header_start_padding">16dp</dimen>
<dimen name="header_end_padding">16dp</dimen>
<dimen name="list_start_padding">10dp</dimen>
<dimen name="list_end_padding">10dp</dimen>
Using values/dimens.xml

Figure 5. Using values/dimens.xml.

Experiment with different values to see what works best.

Use XML to compensate for the chin

Some watches have an inset, also known as a “chin,” in an otherwise circular screen. If you don't compensate for the chin, some of your design may be obscured by it.

For example, suppose you have the following design:

Basic heart design

Figure 6. Basic heart design.

This activity_main.xml snippet defines its layout:

<FrameLayout
  ...
  <androidx.wear.widget.RoundedDrawable
    android:id="@+id/androidbtn"
    android:src="@drawable/ic_android"
    .../>
   <ImageButton
    android:id="@+id/lovebtn"
    android:src="@drawable/ic_favorite"
    android:paddingTop="5dp"
    android:paddingBottom="5dp"
    android:layout_gravity="bottom"
    .../>
</FrameLayout>

If you do nothing, part of the design will disappear behind the chin:

Basic heart design

Figure 7. No fix applied.

You can use the fitsSystemWindows attribute to set the padding to avoid the chin. The following activity_main.xml snippet shows the use of fitsSystemWindows:

<ImageButton
  android:id="@+id/lovebtn"
  android:src="@drawable/ic_favorite"
  android:paddingTop="5dp"
  android:paddingBottom="5dp"
  android:fitsSystemWindows="true"
  .../>
Using fitsSystemWindows

Figure 8. Using the fitsSystemWindows attribute.

The top and bottom padding values that you defined are now overridden to make everything fit in the system window. To fix this, replace the padding values using an InsetDrawable.

Create an inset_favorite.xml file to define the padding values:

<inset
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:drawable="@drawable/ic_favorite"
  android:insetTop="5dp"
  android:insetBottom="5dp" />

Remove the padding from the activity_main.xml file:

<ImageButton
  android:id="@+id/lovebtn"
  android:src="@drawable/inset_favorite"
  android:paddingTop="5dp"
  android:paddingBottom="5dp"
  android:fitsSystemWindows="true"
  .../>
Using InsetDrawables

Figure 9. Using an InsetDrawable.

Manage the chin programmatically

If you require more control than what is possible declaratively using XML, you can programmatically adjust your layout. To obtain the size of the chin, attach a View.OnApplyWindowInsetsListener to the outermost view of your layout.

Add the following to your activity_main.xml file:

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 that 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 that react to insets
            v.onApplyWindowInsets(insets);
            return insets;
        }
    });
}