Wear でレイアウトを定義する

Wear OS アプリはハンドヘルドの Android デバイスと同じレイアウト手法を使用していますが、特定の制約を考慮してデザインする必要があります。ハンドヘルド アプリの機能や UI を移植しても、優れた操作性は期待できません。

優れたウェアラブル アプリをデザインする方法について詳しくは、Wear OS のデザイン ガイドラインをご覧ください。

Wear OS アプリのレイアウトを作成する際には、デバイスの画面が正方形か円形かを明らかにする必要があります。円形の Wear OS デバイスでは、画面の角付近のコンテンツが切り取られることがあります。そのため、正方形の画面用にデザインされたレイアウトは、円形のデバイスでは表示の問題が生じる可能性があります。

たとえば図 1 は、正方形と円形の画面でレイアウトがどのように見えるかを示したものです。

図 1. 正方形の画面用にデザインされたレイアウトが円形の画面ではうまく機能しない例

このように、レイアウトに以下の設定を使用した場合、円形の画面のデバイスではテキストが正しく表示されません。

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:id="@+id/text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/hello_square" />
    </LinearLayout>
    

この問題に対するアプローチとして、次の 2 つがあります。

  1. 正方形と円形の両方のデバイスに Wear UI ライブラリのレイアウトを使用します。
    • BoxInsetLayout - このレイアウトでは、デバイスの画面の形状に応じてさまざまなウィンドウ インセットを適用します。このアプローチは、どちらの画面形状でもほぼ同じレイアウトを使用しつつ、円形の画面の角付近のビューが切り取られないようにしたい場合に使用します。
    • 曲線レイアウト - このレイアウトは、円形の画面向けに最適化された縦方向のアイテムリストを表示および操作したい場合に使用します。
  2. リソースの指定ガイドで説明されているように、正方形と円形のデバイス用に代替レイアウト リソースを指定します。Wear は実行時に、デバイスの画面の形状を検出して正しいレイアウトをロードします。

このライブラリを使用して Android Studio プロジェクトをコンパイルするには、[Extras] で Google Repository パッケージが Android SDK Manager にインストールされていることを確認します。また、wear モジュールの build.gradle ファイルに以下の依存関係を含めます。

    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        compile 'com.android.support:wear:26.0.0'
    }
    

BoxInsetLayout を使用する

図 2. 円形の画面上のウィンドウ インセット

Wear UI ライブラリの BoxInsetLayout クラスを使用すると、正方形と円形の両方の画面で機能する単一のレイアウトを定義できます。このクラスでは、画面の形状に応じて必要なウィンドウ インセットを適用し、画面の中央または端付近のビューを容易に調整することができます。

注: BoxInsetLayout は、ウェアラブル サポート ライブラリのサポートが終了した類似のクラスの後継クラスです。

図 2 のグレーの正方形は、BoxInsetLayout が必要なウィンドウ インセットを適用した後に、円形の画面上に子ビューを自動的に配置できる領域を示しています。この領域内に表示されるようにするには、子ビューで以下の値が設定された boxedEdges 属性を指定します。

  • topbottomleftright の組み合わせ。たとえば、値 "left|top" は、子の左端と上端を図 2 のグレーの正方形内に配置します。
  • "all" は、子のすべてのコンテンツを図 2 のグレーの正方形内に配置します。

正方形の画面ではウィンドウ インセットがゼロのため、boxedEdges 属性は無視されます。

図 3. 正方形と円形の両方の画面で機能するレイアウト定義

図 3 のレイアウトでは <BoxInsetLayout> 要素が使用されています。このレイアウトは正方形と円形の画面で機能します。

    <android.support.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">

        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:padding="5dp"
            app:boxedEdges="all">

            <TextView
                android:gravity="center"
                android:layout_height="wrap_content"
                android:layout_width="match_parent"
                android:text="@string/sometext"
                android:textColor="@color/black" />

            <ImageButton
                android:background="@null"
                android:layout_gravity="bottom|left"
                android:layout_height="50dp"
                android:layout_width="50dp"
                android:src="@drawable/ok" />

            <ImageButton
                android:background="@null"
                android:layout_gravity="bottom|right"
                android:layout_height="50dp"
                android:layout_width="50dp"
                android:src="@drawable/cancel" />
        </FrameLayout>
    </android.support.wear.widget.BoxInsetLayout>
    

レイアウトのうち、太字でマークした部分に注目してみましょう。

  • android:padding="15dp"

    この行では、<BoxInsetLayout> 要素にパディングを割り当てています。円形のデバイスのウィンドウ インセットは 15 dp より大きいため、このパディングは正方形の画面にのみ適用されます。

  • android:padding="5dp"

    この行では、内部の FrameLayout 要素にパディングを割り当てています。このパディングは正方形と円形の両方の画面に適用されます。ボタンとウィンドウ インセット間のパディングの合計は、正方形の画面では 20 dp(15+5)、円形の画面では 5 dp です。

  • app:boxedEdges="all"

    この行により、円形の画面のウィンドウ インセットによって定義されている領域内で FrameLayout 要素とその子がボックス化されます。この行による正方形の画面への影響はありません。

曲線レイアウトを使用する

Wear UI ライブラリの WearableRecyclerView クラスを使用すると、円形の画面向けに最適化された曲線レイアウトを有効にすることができます。アプリでスクロール可能なリストの曲線レイアウトを有効にする場合は、曲線レイアウトの作成をご覧ください。

正方形と円形の画面で異なるレイアウトを使用する

Wear デバイスには正方形と円形の画面があります。アプリはどちらのデバイス構成にも対応できる必要があります。そのためには、代替リソースを指定する必要があり、-round および -notround リソース修飾子を、レイアウト、ディメンション、その他のリソースタイプに適用します。

たとえば、次のようにレイアウトを整理することを検討します。

  • layout/ ディレクトリに、円形と正方形の両方のスマートウォッチで機能するレイアウトを格納します。
  • layout-round/layout-notround/ の各ディレクトリに、画面の形状に固有のレイアウトを格納します。

また、res/valuesres/values-roundres/values-notround の各リソース ディレクトリを使用することも可能です。このようにリソースを整理することで、1 つのレイアウトを共有し、デバイスのタイプに基づいて特定の属性のみを変更することができます。

値を変更する

values/dimens.xmlvalues-round/dimens.xml を使用すると、円形と正方形のスマートウォッチ用のレイアウトを簡単に作成できます。別々のパディング設定を指定すると、1 つの layout.xml ファイルと 2 つの dimens.xml ファイルを使用して次のレイアウトを作成できます。

    <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>
    
values-round/dimens.xml を使用

図 4. 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>
    
values/dimens.xml を使用

図 5. values/dimens.xml を使用

さまざまな値でテストして、最適なレイアウトを確認する必要があります。

XML を使用して「あご」を補正する

一部のスマートウォッチには、円形の画面にインセット(「あご」とも呼ばれます)が含まれています。補正しないと、デザインの一部があごで隠れることがあります。

たとえば、次のようなデザインがあるとします。

基本的なハートのデザイン

図 6. 基本的なハートのデザイン

次の activity_main.xml のスニペットでは、そのレイアウトを定義しています。

    <FrameLayout
      ...>
      <android.support.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"
      .../>
    
fitsSystemWindows を使用

図 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"
      .../>
    
InsetDrawables を使用

図 9. InsetDrawables を使用

プログラマティックにあごを管理する

XML を使用して宣言的に実行できることよりも詳細な制御が必要な場合は、レイアウトをプログラマティックに調整することができます。あごのサイズを取得するには、View.OnApplyWindowInsetsListener をレイアウトの最も外側のビューにアタッチする必要があります。

以下を MainActivity.java に追加します。

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;
            }
        });
    }