大画面のクックブック

Android は 5 つ星の大画面アプリに必要な材料をすべて提供しています。このクックブックのレシピでは、開発における特定の問題を解決するために、最適な材料を選択して組み合わせています。各レシピには、おすすめの方法、質の高いコードサンプル、詳細な手順が含まれているので、大画面に対応する際にお役立てください。

星による評価

レシピは、大画面のアプリの品質に関するガイドラインにどの程度準拠しているかに基づいて評価されます。

5 つ星評価 レベル 1(大画面によって差別化)の条件を満たしている
4 つ星評価 レベル 2(大画面向けに最適化)の条件を満たしている
3 つ星評価 レベル 3(大画面に対応)の条件を満たしている
2 つ星評価 大画面向けの機能をいくつか提供しているが、大画面のアプリの品質に関するガイドラインの条件は満たしていない
1 つ星評価 特定のユースケースのニーズは満たしているが、大画面を適切にサポートしていない

Chromebook カメラのサポート

3 つ星評価

Google Play で Chromebook ユーザーにも興味を持ってもらいましょう。

御社のカメラアプリがカメラの基本機能のみで動作できるなら、Chromebook も対象になります。ハイエンド スマートフォンに搭載されるカメラの高度な機能をうっかり指定すると、アプリストアによって Chromebook ユーザーへのインストールが阻止されるので、指定しないようにしましょう。

Chromebook には、前面(ユーザーの方を向いた)カメラが搭載されており、ビデオ会議、スナップショットなどの用途に適しています。ただし、Chromebook には背面(外向き)カメラは搭載されていないものがあります。また、Chromebook の前面カメラは通常、オートフォーカスやフラッシュをサポートしていません。

ベスト プラクティス

多用途のカメラアプリの場合は、カメラの構成(前面カメラ、背面カメラ、USB で接続された外部カメラがあるかどうか)によらず、すべてのデバイスをサポートします。

できるだけ多くのデバイスに対してアプリがアプリストアで表示されるようにするには、アプリが使用するすべてのカメラ機能を必ず宣言し、それらの機能が必須かどうかを明示してください。

要素

  • CAMERA 権限: アプリにデバイスのカメラへのアクセスを許可します。
  • <uses-feature> マニフェスト要素: アプリで使用する機能をアプリストアに通知します。
  • required 属性: アプリが特定の機能なしで動作できるかどうかをアプリストアに指定します。

手順

概要

CAMERA 権限を宣言します。カメラの基本サポートを提供するカメラ機能を宣言します。機能ごとに必須かどうかを指定します。

1. CAMERA 権限を宣言する

アプリ マニフェストに次の権限を追加します。

<uses-permission android:name="android.permission.CAMERA" />
2. カメラの基本機能を宣言する

アプリ マニフェストに以下の機能を追加します。

<uses-feature android:name="android.hardware.camera.any" android:required="false" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
<uses-feature android:name="android.hardware.camera.flash" android:required="false" />
3. 各機能が必須かどうかを指定する

android.hardware.camera.any 機能に android:required="false" を設定すると、あらゆる種類の内蔵カメラや外部カメラが搭載されたデバイスだけでなく、カメラが搭載されていないデバイスからもアプリにアクセスできるようになります。

その他の機能については、android:required="false" を設定することで、背面カメラ、オートフォーカス、またはフラッシュを備えていない Chromebook のようなデバイスがアプリストアでアプリにアクセスできるようにします。

結果

Chromebook ユーザーも、Google Play などのアプリストアからアプリをダウンロードしてインストールできます。また、フル機能のカメラをサポートしているデバイス(スマートフォンなど)で、カメラの機能の制限はなくなります。

アプリでサポートされているカメラ機能を明示的に設定し、アプリに必須の機能を指定することで、できるだけ多くのデバイスでアプリが使用できるようになりました。

参考情報

詳しくは、<uses-feature> ドキュメントのカメラのハードウェア機能をご覧ください。

スマートフォンではアプリの向きが制限されるが、大画面デバイスでは制限されない

2 つ星評価

御社のアプリは縦向きのスマートフォンで最適に動作するため、アプリを縦向きのみに制限しているとします。しかし、横向きの大画面ではより多くの操作を行える可能性があります。

両方に対応するには、つまり、小さい画面ではアプリを縦向きに制限し、大画面では横向きを可能にするには、どうすればよいでしょうか。

おすすめの方法

優れたアプリは、ユーザーの好み(デバイスの向きなど)を尊重してくれます。

大画面のアプリの品質に関するガイドラインでは、アプリですべてのデバイス設定(縦向きと横向き、マルチ ウィンドウ モード、折りたたみ式デバイスの折りたたみ / 展開状態など)をサポートすることを推奨しています。アプリはさまざまな設定に対してレイアウトとユーザー インターフェースを最適化する必要があります。また、設定の変更時に状態を保存して復元する必要があります。

このレシピは、大画面をサポートするための一時的な手段です。アプリを改善してあらゆるデバイス設定を完全にサポートできるようになるまで、このレシピをご利用ください。

要素

  • screenOrientation: デバイスの向きの変化にアプリがどう反応するかを指定できるアプリ マニフェストの設定
  • Jetpack WindowManager: アプリ ウィンドウのサイズとアスペクト比を判別できるライブラリのセット。API レベル 14 との下位互換性があります。
  • Activity#setRequestedOrientation(): 実行時にアプリの向きを変更できるメソッド

手順

概要

デフォルトでは、アプリがアプリ マニフェストで向きの変更を扱えるようにします。実行時にアプリのウィンドウ サイズを判別します。アプリ ウィンドウが小さい場合は、マニフェストの向きの設定をオーバーライドして、アプリの向きを制限します。

1. アプリ マニフェストで向きの設定を指定する

アプリ マニフェストの screenOrientation 要素を宣言しないか(この場合、画面の向きはデフォルトで unspecified になります)、画面の向きを fullUser に設定できます。ユーザーがセンサーベースの回転をロックしていない場合、アプリはすべてのデバイスの向きをサポートします。

<activity
    android:name=".MyActivity"
    android:screenOrientation="fullUser">

unspecifiedfullUser の違いはわずかながらも重要です。screenOrientation の値を宣言しない場合、システムによって画面の向きが選択されます。画面の向きの定義に使用されるポリシーはデバイスによって異なる場合があります。一方、fullUser を指定すると、ユーザーがデバイスに定義した動作により近くなります。ユーザーがセンサーベースの回転をロックしている場合、アプリはユーザーの設定に従います。それ以外の場合は、4 種類の画面の向き(縦向き、横向き、逆の縦向き、逆の横向き)のいずれかが可能です。android:screenOrientation をご覧ください。

2. 画面サイズを判別する

ユーザーが許可したすべての向きサポートするようにマニフェストが設定されている場合、画面サイズに基づいてアプリの向きをプログラムで指定することができます。

Jetpack WindowManager ライブラリをモジュールの build.gradle ファイルまたは build.gradle.kts ファイルに追加します。

Kotlin

implementation("androidx.window:window:version")
implementation("androidx.window:window-core:version")

Groovy

implementation 'androidx.window:window:version'
implementation 'androidx.window:window-core:version'

Jetpack WindowManagerWindowMetricsCalculator#computeMaximumWindowMetrics() メソッドを使用して、デバイスの画面サイズを WindowMetrics オブジェクトとして取得します。このウィンドウ指標をウィンドウ サイズクラスと比較すると、画面の向きを制限するタイミングを判断できます。

ウィンドウ サイズクラスは、小さい画面と大画面の間のブレークポイントを提供します。

WindowWidthSizeClass#COMPACT ブレークポイントと WindowHeightSizeClass#COMPACT ブレークポイントを使用して、画面サイズを決定します。

Kotlin

/** Determines whether the device has a compact screen. **/
fun compactScreen() : Boolean {
    val metrics = WindowMetricsCalculator.getOrCreate().computeMaximumWindowMetrics(this)
    val width = metrics.bounds.width()
    val height = metrics.bounds.height()
    val density = resources.displayMetrics.density
    val windowSizeClass = WindowSizeClass.compute(width/density, height/density)

    return windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT ||
        windowSizeClass.windowHeightSizeClass == WindowHeightSizeClass.COMPACT
}

Java

/** Determines whether the device has a compact screen. **/
private boolean compactScreen() {
    WindowMetrics metrics = WindowMetricsCalculator.getOrCreate().computeMaximumWindowMetrics(this);
    int width = metrics.getBounds().width();
    int height = metrics.getBounds().height();
    float density = getResources().getDisplayMetrics().density;
    WindowSizeClass windowSizeClass = WindowSizeClass.compute(width/density, height/density);
    return windowSizeClass.getWindowWidthSizeClass() == WindowWidthSizeClass.COMPACT ||
                windowSizeClass.getWindowHeightSizeClass() == WindowHeightSizeClass.COMPACT;
}
    注:
  • 上記の例は、アクティビティのメソッドとして実装されています。そのため、アクティビティは computeMaximumWindowMetrics() の引数で this として逆参照されています。
  • アプリがマルチ ウィンドウ モードで起動される可能性があるため、computeCurrentWindowMetrics() の代わりに computeMaximumWindowMetrics() メソッドが使用されています。マルチ ウィンドウ モードの場合、画面の向きの設定は無視されます。アプリ ウィンドウがデバイスの画面全体になっていない限り、アプリ ウィンドウのサイズを判別して向きの設定をオーバーライドしても意味がありません。

アプリ内で computeMaximumWindowMetrics() メソッドを使用できるように依存関係を宣言する手順については、WindowManager をご覧ください。

3. アプリ マニフェストの設定をオーバーライドする

デバイスの画面サイズが小さいと判断した場合は、Activity#setRequestedOrientation() を呼び出してマニフェストの screenOrientation 設定をオーバーライドできます。

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    requestedOrientation = if (compactScreen())
        ActivityInfo.SCREEN_ORIENTATION_PORTRAIT else
        ActivityInfo.SCREEN_ORIENTATION_FULL_USER
    ...
    // Replace with a known container that you can safely add a
    // view to where the view won't affect the layout and the view
    // won't be replaced.
    val container: ViewGroup = binding.container

    // Add a utility view to the container to hook into
    // View.onConfigurationChanged. This is required for all
    // activities, even those that don't handle configuration
    // changes. You can't use Activity.onConfigurationChanged,
    // since there are situations where that won't be called when
    // the configuration changes. View.onConfigurationChanged is
    // called in those scenarios.
    container.addView(object : View(this) {
        override fun onConfigurationChanged(newConfig: Configuration?) {
            super.onConfigurationChanged(newConfig)
            requestedOrientation = if (compactScreen())
                ActivityInfo.SCREEN_ORIENTATION_PORTRAIT else
                ActivityInfo.SCREEN_ORIENTATION_FULL_USER
        }
    })
}

Java

@Override
protected void onCreate(Bundle savedInstance) {
    super.onCreate(savedInstanceState);
    if (compactScreen()) {
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
    } else {
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_USER);
    }
    ...
    // Replace with a known container that you can safely add a
    // view to where the view won't affect the layout and the view
    // won't be replaced.
    ViewGroup container = binding.container;

    // Add a utility view to the container to hook into
    // View.onConfigurationChanged. This is required for all
    // activities, even those that don't handle configuration
    // changes. You can't use Activity.onConfigurationChanged,
    // since there are situations where that won't be called when
    // the configuration changes. View.onConfigurationChanged is
    // called in those scenarios.
    container.addView(new View(this) {
        @Override
        protected void onConfigurationChanged(Configuration newConfig) {
            super.onConfigurationChanged(newConfig);
            if (compactScreen()) {
                setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
            } else {
                setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_USER);
            }
        }
    });
}

onCreate() メソッドと View.onConfigurationChanged() メソッドにロジックを追加することで、アクティビティがサイズ変更されたりディスプレイ間を移動されたりするたびに(デバイスの回転後、折りたたみ式デバイスが折りたたまれたとき / 広げられたとき、など)、最大ウィンドウ指標を取得して向きの設定をオーバーライドできるようになります。構成の変更が発生するタイミングや、その変更によってアクティビティの再作成が発生するタイミングについて詳しくは、構成の変更に対処するをご覧ください。

結果

アプリは、デバイスの回転に関係なく、小さい画面では縦向きのままになります。大画面では横向きと縦向きをサポートします。

参考情報

常にすべてのデバイス設定をサポートするようアプリをアップグレードする方法については、以下をご覧ください。

外部キーボードの Space キーによるメディア再生の一時停止と再開

4 つ星評価

大画面向けの最適化には、外部キーボード入力を処理する機能が含まれています。たとえば、Space キーが押された場合には、動画などのメディアの再生が一時停止または再開されます。外部キーボードに接続されることが多いタブレットや、通常は外部キーボードが付属しておりタブレット モードでも使用できる Chromebook で特に便利な機能です。

メディアがウィンドウの唯一の要素(フルスクリーン動画再生など)である場合、アクティビティ レベルで、または Jetpack Compose では画面レベルで、キー操作イベントに対応します。

おすすめの方法

アプリでメディア ファイルを再生する場合は、物理キーボードの Space キーを押すと、再生を一時停止または再開できるようにします。

要素

Compose

  • onPreviewKeyEvent: コンポーネント(またはその子の一つ)がフォーカスされたときにコンポーネントがハードウェア キーイベントをインターセプトできるようにする Modifier
  • onKeyEvent: onPreviewKeyEvent と同様に、コンポーネント(またはその子の一つ)がフォーカスされたときにコンポーネントがハードウェア キーイベントをインターセプトできるようにするModifier

View

  • onKeyUp(): キーが離され、アクティビティ内のビューで処理されないときに呼び出される。

手順

概要

ビューベースのアプリと Jetpack Compose ベースのアプリは、キーボードのキーが押された場合に同様の方法で対応します。アプリはキー操作イベントをリッスンして、イベントをフィルタし、Space キーなどの選択したキー操作に応答する必要があります。

1. キーボードのイベントをリッスンする

View

アプリのアクティビティで、onKeyUp() メソッドをオーバーライドします。

Kotlin

override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
    ...
}

Java

@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
    ...
}

このメソッドは、押されたキーが離されると呼び出されるため、キー入力ごとに 1 回呼び出されます。

Compose

Jetpack Compose では、キー入力を管理する画面上の onPreviewKeyEvent 修飾子または onKeyEvent 修飾子を利用できます。

Column(modifier = Modifier.onPreviewKeyEvent { event ->
    if (event.type == KeyEventType.KeyUp) {
        ...
    }
    ...
})

または

Column(modifier = Modifier.onKeyEvent { event ->
    if (event.type == KeyEventType.KeyUp) {
        ...
    }
    ...
})

2. Space キーの押下をフィルタする

onKeyUp() メソッド、または Compose onPreviewKeyEvent および onKeyEvent 修飾子メソッド内で、KeyEvent.KEYCODE_SPACE をフィルタしてメディア コンポーネントに正しいイベントを送信します。

View

Kotlin

if (keyCode == KeyEvent.KEYCODE_SPACE) {
    togglePlayback()
    return true
}
return false

Java

if (keyCode == KeyEvent.KEYCODE_SPACE) {
    togglePlayback();
    return true;
}
return false;

Compose

Column(modifier = Modifier.onPreviewKeyEvent { event ->
    if (event.type == KeyEventType.KeyUp && event.key == Key.Spacebar) {
        ...
    }
    ...
})

または

Column(modifier = Modifier.onKeyEvent { event ->
    if (event.type == KeyEventType.KeyUp && event.key == Key.Spacebar) {
        ...
    }
    ...
})

結果

これでアプリは Space キーが押された場合に対応して、動画などのメディアを一時停止したり再開したりできるようになります。

参考情報

キーボード イベントとその管理方法について詳しくは、キーボード入力を処理するをご覧ください。

タッチペンのパーム リジェクション

5 つ星評価

大画面でのタッチペンは、生産性と創造性に優れたツールです。しかし、ユーザーがタッチペンを使用して描画、書き込み、またはアプリの操作を行うと、手のひらが画面に触れることがあります。このタッチイベントが、手のひらでの偶発的なタッチとして認識されて無視される前に、アプリに報告される場合があります。

おすすめの方法

アプリは、不要なタッチイベントを識別して無視する必要があります。Android は、MotionEvent オブジェクトをディスパッチすることにより、手のひらでのタッチをキャンセルします。オブジェクトの ACTION_CANCEL または ACTION_POINTER_UPFLAG_CANCELED を確認して、手のひらによる操作を拒否するかどうかを決定します。

要素

  • MotionEvent: タッチイベントと移動イベントを表します。イベントを無視するかどうかを決定するために必要な情報が含まれています。
  • OnTouchListener#onTouch(): MotionEvent オブジェクトを受け取ります。
  • MotionEvent#getActionMasked(): モーション イベントに関連付けられたアクションを返します。
  • ACTION_CANCEL: 操作を元に戻す必要があることを示す MotionEvent 定数。
  • ACTION_POINTER_UP: 最初のポインタ以外のポインタが上に移動した(つまり、デバイス画面との接続が喪失した)ことを示す MotionEvent 定数。
  • FLAG_CANCELED: ポインタの上移動により意図しないタップイベントが発生したことを示す MotionEvent 定数。Android 13(API レベル 33)以降の ACTION_POINTER_UP および ACTION_CANCEL イベントに追加されました。

歩数

概要

アプリにディスパッチされた MotionEvent オブジェクトを調べます。MotionEvent API を使用してイベントの特性を判別します。

  • シングル ポインタ イベント - ACTION_CANCEL を確認します。Android 13 以降では、FLAG_CANCELED についても確認します。
  • マルチポインタ イベント - Android 13 以降では、ACTION_POINTER_UPFLAG_CANCELED を確認します。

ACTION_CANCEL イベントと ACTION_POINTER_UP/FLAG_CANCELED イベントに応答します。

1. モーション イベント オブジェクトを取得する

アプリに OnTouchListener を追加します。

Kotlin

val myView = findViewById<View>(R.id.myView).apply {
    setOnTouchListener { view, event ->
        // Process motion event.
    }
}

Java

View myView = findViewById(R.id.myView);
myView.setOnTouchListener( (view, event) -> {
    // Process motion event.
});
2. イベント アクションとフラグを決定する

ACTION_CANCEL を確認します。これは、すべての API レベルでのシングル ポインタ イベントを表します。Android 13 以降では、ACTION_POINTER_UPFLAG_CANCELED. を確認します。

Kotlin

val myView = findViewById<View>(R.id.myView).apply {
    setOnTouchListener { view, event ->
        when (event.actionMasked) {
            MotionEvent.ACTION_CANCEL -> {
                //Process canceled single-pointer motion event for all SDK versions.
            }
            MotionEvent.ACTION_POINTER_UP -> {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
                   (event.flags and MotionEvent.FLAG_CANCELED) == MotionEvent.FLAG_CANCELED) {
                    //Process canceled multi-pointer motion event for Android 13 and higher.
                }
            }
        }
        true
    }
}

Java

View myView = findViewById(R.id.myView);
myView.setOnTouchListener( (view, event) -> {
    switch (event.getActionMasked()) {
        case MotionEvent.ACTION_CANCEL:
            // Process canceled single-pointer motion event for all SDK versions.
        case MotionEvent.ACTION_UP:
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
               (event.getFlags() & MotionEvent.FLAG_CANCELED) == MotionEvent.FLAG_CANCELED) {
                //Process canceled multi-pointer motion event for Android 13 and higher.
            }
    }
    return true;
});
3. 操作を元に戻す

手のひらでのタップを識別した場合、操作による画面上の効果を元に戻すことができます。

手のひらでのタップなどの意図しない入力を元に戻すには、アプリでユーザー操作の履歴を保持する必要があります。Codelab の Android アプリでのタッチペン サポートの強化基本的な描画アプリを実装するで例を紹介しています。

結果

アプリは、Android 13 以降の API レベルにおけるマルチポインタ イベントと、すべての API レベルにおけるシングルポインタ イベントで、手のひらでのタップを識別、拒否できるようになりました。

参考情報

詳しくは次の記事をご覧ください。

WebView の状態管理

3 つ星評価

WebView は、状態管理の高度なシステムを提供する、よく使われるコンポーネントです。WebView は、構成の変更後も状態とスクロール位置を維持する必要があります。WebView は、ユーザーがデバイスを回転させるか折りたたみ式スマートフォンを開くと、スクロール位置を喪失することがあります。そのため、ユーザーは WebView の最上部から前のスクロール位置まで再びスクロールする必要があります。

おすすめの方法

WebView の再作成回数を最小限に抑えます。WebView は状態を管理しやすく、構成変更をできるだけ多く管理することでこの品質を利用できます。Activity の再作成(システムによる構成変更の処理方法)でも WebView が再作成され、WebView の状態が喪失するため、アプリが構成の変更を処理する必要があります。

要素

  • android:configChanges: マニフェストの <activity> 要素の属性。アクティビティによって処理される構成変更をリストします。
  • View#invalidate(): ビューを再描画させるメソッド。WebView によって継承されます。

手順

概要

WebView 状態を保存するには、Activity の再作成をできるだけ回避してから、WebView を無効にして、状態を保持したままサイズ変更できるようにします。

1. アプリの AndroidManifest.xml ファイルに構成変更を追加する

(システムではなく)アプリで処理する構成変更を指定して、アクティビティの再作成を回避します。

<activity
  android:name=".MyActivity"
  android:configChanges="screenLayout|orientation|screenSize
      |keyboard|keyboardHidden|smallestScreenSize" />

2. アプリが構成の変更を受け取るたびに WebView を無効にする

Kotlin

override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)
    webView.invalidate()
}

Java

@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    webview.invalidate();
}

このステップはビューシステムにのみ適用されます。Composable 要素を正しくサイズ変更するために Jetpack Compose が無効にする必要があるものは何もないためです。ただし、正しく管理されていない場合、Compose は WebView を頻繁に再作成します。Accompanist WebView ラッパーを使用して、Compose アプリの WebView の状態を保存および復元します。

結果

アプリの WebView コンポーネントは、サイズ変更から向きの変更、開閉まで、複数の構成変更の後も状態とスクロール位置を保持するようになりました。

参考情報

構成変更とその管理方法について詳しくは、構成の変更に対処するをご覧ください。

RecyclerView の状態管理

3 つ星評価

RecyclerView では、最小限のグラフィック リソースを使用して大量のデータを表示できます。RecyclerView がアイテムのリスト内でスクロールすると、RecyclerView は画面外にスクロールしたアイテムの View インスタンスを再使用し、画面上でのスクロール時に新しいアイテムを作成します。しかし、デバイスの回転などの構成変更により、RecyclerView の状態がリセットされ、ユーザーが RecyclerView アイテムのリストの以前の位置に再びスクロールしなければならない場合があります。

おすすめの方法

RecyclerView は、すべての構成変更において、状態(特にスクロール位置)とリスト要素の状態を維持する必要があります。

要素

歩数

概要

RecyclerView のスクロール位置を保存するように RecyclerView.Adapter の状態復元ポリシーを設定します。RecyclerView のリストアイテムの状態を保存します。RecyclerView アダプターにリストアイテムの状態を追加し、ViewHolder にバインドされたときにリストアイテムの状態を復元します。

1. Adapter の状態復元ポリシーを有効にする

RecyclerView アダプターの状態復元ポリシーを有効にして、構成変更後も RecyclerView のスクロール位置が維持されるようにします。ポリシー仕様をアダプター コンストラクタに追加します。

Kotlin

class MyAdapter() : RecyclerView.Adapter() {
    init {
        stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY
    }
    ...
}

Java

class MyAdapter extends RecyclerView.Adapter {

    public Adapter() {
        setStateRestorationPolicy(StateRestorationPolicy.PREVENT_WHEN_EMPTY);
    }
    ...
}

2. ステートフルなリストアイテムの状態を保存する

EditText 要素を含むアイテムなど、複雑な RecyclerView リストアイテムの状態を保存します。たとえば、EditText の状態を保存するには、テキストの変更をキャプチャする onClick ハンドラと同様のコールバックを追加します。コールバック内で、保存するデータを定義します。

Kotlin

input.addTextChangedListener(
    afterTextChanged = { text ->
        text?.let {
            // Save state here.
        }
    }
)

Java

input.addTextChangedListener(new TextWatcher() {
    
    ...

    @Override
    public void afterTextChanged(Editable s) {
        // Save state here.
    }
});

Activity または Fragment でコールバックを宣言します。ViewModel を使用して状態を保存します。

3. Adapter にリストアイテムの状態を追加する

リストアイテムの状態を RecyclerView.Adapter に追加します。ホスト Activity または Fragment が作成されたときに、アイテムの状態をアダプター コンストラクタに渡します。

Kotlin

val adapter = MyAdapter(items, viewModel.retrieveState())

Java

MyAdapter adapter = new MyAdapter(items, viewModel.retrieveState());

4. アダプターの ViewHolder でリストアイテムの状態を復元する

RecyclerView.AdapterViewHolder をアイテムにバインドする際に、アイテムの状態を復元します。

Kotlin

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
    ...
    val item = items[position]
    val state = states.firstOrNull { it.item == item }

    if (state != null) {
        holder.restore(state)
    }
}

Java

@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
    ...
    Item item = items[position];
    Arrays.stream(states).filter(state -> state.item == item)
        .findFirst()
        .ifPresent(state -> holder.restore(state));
}

結果

RecyclerView は、スクロール位置と RecyclerView リスト内のすべてのアイテムの状態を復元できるようになりました。

参考情報

取り外し可能なキーボードの管理

3 つ星評価

取り外し可能なキーボードのサポートにより、大画面デバイスでのユーザーの生産性を最大化できます。Android は、キーボードがデバイスに装着または取り外されるたびに構成の変更をトリガーするため、UI の状態が失われる可能性があります。アプリでは、状態を保存して復元して、システムにアクティビティの再作成を処理させるか、キーボード構成の変更に応じてアクティビティの再作成を制限できます。いずれの場合も、キーボードに関連するすべてのデータは Configuration オブジェクトに格納されます。構成オブジェクトの keyboard フィールドと keyboardHidden フィールドには、キーボードのタイプとその可用性に関する情報が含まれています。

おすすめの方法

大画面向けに最適化されたアプリは、ソフトウェア キーボードやハードウェア キーボードから、タッチペン、マウス、トラックパッドなどの周辺機器まで、あらゆるタイプの入力デバイスをサポートします。

外部キーボードをサポートするには構成の変更が必要です。これは次のいずれかの方法で管理できます。

  1. 現在実行中のアクティビティをシステムに再作成させることで、アプリの状態の管理はデベロッパーが行います。
  2. 構成の変更を自分で管理します(アクティビティは再作成されません)。
    • キーボード関連のすべての構成値を宣言する
    • 構成変更ハンドラを作成する

テキスト入力などの入力のために UI を細かく制御する必要があることが多い生産性向上アプリでは、構成変更の処理にセルフサービス方式を取り入れるとメリットが得られます。

特殊なケースとして、ハードウェア キーボードの取り付けや取り外し時にアプリのレイアウトを変更することがあります。たとえば、ツールや編集ウィンドウ用のスペースを増やす場合などです。

構成の変更をリッスンする信頼できる唯一の方法は、ビューの onConfigurationChanged() メソッドをオーバーライドすることです。そのため、新しい View インスタンスをアプリ アクティビティに追加し、キーボードのアタッチまたはデタッチによる構成の変更にビューの onConfigurationChanged() ハンドラで応答できます。

要素

  • android:configChanges: アプリ マニフェストの <activity> 要素の属性。アプリが管理する構成変更をシステムに通知します。
  • View#onConfigurationChanged(): 新しいアプリ構成の伝播に対応するメソッド。

歩数

概要

configChanges 属性を宣言し、キーボード関連の値を追加します。View をアクティビティのビュー階層に追加し、構成の変更をリッスンします。

1. configChanges 属性を宣言する

すでに管理されている構成変更のリストに keyboard|keyboardHidden の値を追加して、アプリ マニフェストの <activity> 要素を更新します。

<activity
      …
      android:configChanges="...|keyboard|keyboardHidden">

2. ビュー階層に空のビューを追加する

新しいビューを宣言し、ビューの onConfigurationChanged() メソッド内にハンドラコードを追加します。

Kotlin

val v = object : View(this) {
  override fun onConfigurationChanged(newConfig: Configuration?) {
    super.onConfigurationChanged(newConfig)
    // Handler code here.
  }
}

Java

View v = new View(this) {
    @Override
    protected void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        // Handler code here.
    }
};

結果

アプリは、現在実行中のアクティビティを再作成することなく、外部キーボードの接続または接続解除に応答するようになりました。

参考情報

キーボードの接続や接続解除などの構成変更の際にアプリの UI の状態を保存する方法については、UI の状態を保存するをご覧ください。