Android アプリを Chrome OS 向けに最適化する

Chromebook で Android アプリを実行できることで、ユーザーはアプリの膨大なエコシステムと新機能を利用できるようになります。これはデベロッパーにとって朗報と言えますが、期待に沿ったユーザビリティや優れたユーザー エクスペリエンスを実現するには、アプリに対して各種の最適化を行う必要があります。この Codelab では、こうした最適化の最も一般的な方法について説明します。

f60cd3eb5b298d5d.png

作業内容

機能的な Android アプリを作成し、Chrome OS 向けのベスト プラクティスと最適化を実証します。作成するアプリの機能は次のとおりです。

以下を含むキーボード入力の処理

  • Enter キー
  • 矢印キー
  • Ctrl+ と Ctrl+Shift+ のショートカット
  • 現在選択されているアイテムに対する視覚的フィードバック

以下を含むマウス入力の処理

  • 右クリック
  • マウスオーバー効果
  • ツールチップ
  • ドラッグ&ドロップ

アーキテクチャ コンポーネントを使用する目的

  • 状態の維持
  • UI の自動更新

52240dc3e68f7af8.png

学習内容

  • Chrome OS でのキーボード入力とマウス入力を処理するためのベスト プラクティス
  • Chrome OS 固有の最適化
  • ViewModelLiveData のアーキテクチャ コンポーネントの基本的な実装

必要なもの

GitHub からリポジトリのクローンを作成します。

git clone https://github.com/googlecodelabs/optimized-for-chromeos

または、リポジトリの zip ファイルをダウンロードして解凍します。

ZIP をダウンロード

プロジェクトをインポートする

  • Android Studio を開きます
  • [Import Project] または [File] > [New] > [Import Project] を選択します
  • プロジェクトのクローンを作成した場所、またはプロジェクトを解凍した場所に移動します
  • プロジェクト optimized-for-chromeos をインポートします
  • startcomplete の 2 つのモジュールがあることに注意してください

アプリを試す

  • start モジュールをビルドして実行します
  • 最初はトラックパッドのみを使用します
  • 恐竜をクリックします
  • シークレット メッセージを送信します
  • 「Drag Me」テキストをドラッグしてみるか、「Drop Things Here」領域にファイルをドロップしてみます
  • キーボードを使用してメッセージの操作や送信を試します
  • タブレット モードでアプリを使用してみます
  • デバイスの回転やウィンドウのサイズ変更を試します

アプリの使い勝手を確認する

このアプリは簡易的に作成されており、不具合があるように見える部分は容易に解決できますが、ユーザー エクスペリエンスはよくありません。これを改善しましょう。

a40270071a9b5ac3.png

キーボードを使用してシークレット メッセージをいくつか入力してみると、Enter キーでは何も実行できないことに気づくでしょう。これではユーザーがストレスを感じてしまいます。

以下のサンプルコードを使用し、キーボード アクションの処理に関するドキュメントを読めば、この問題を解決できます。

MainActivity.kt(onCreate)

// Enter key listener
edit_message.setOnKeyListener(View.OnKeyListener { v, keyCode, keyEvent ->
    if (keyEvent.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_ENTER) {
        button_send.performClick()
        return@OnKeyListener true
    }
    false
})

実際に試してみましょう。キーボードのみを使用してメッセージを送信できれば、ユーザー エクスペリエンスは向上します。

キーボードのみを使用してこのアプリを操作できたらよいと思いませんか。目の前のキーボードを操作していてもアプリが応答しないのは、ユーザーにとって使い勝手が悪く、ストレスにつながります。

矢印キーや Tab キーでビューを操作できるようにする最も簡単な方法は、ビューをフォーカス可能にすることです。

レイアウト ファイルを調べて、Button タグと ImageView タグを確認します。focusable 属性が false に設定されていることに注意してください。XML でこれを true に変更します。

activity_main.xml

android:focusable="true"

または、プログラムで次のように操作します。

MainActivity.kt

button_send.setFocusable(true)
image_dino_1.setFocusable(true)
image_dino_2.setFocusable(true)
image_dino_3.setFocusable(true)
image_dino_4.setFocusable(true)

実際に試してみましょう。矢印キーと Enter キーを使用して恐竜を選択できるはずですが、OS のバージョン、画面、照明設定によっては、現在選択されているアイテムがわからない場合があります。この問題を解決するには、画像の背景リソースを R.attr.selectableItemBackground に設定します。

MainActivity.kt(onCreate)

val highlightValue = TypedValue()
theme.resolveAttribute(R.attr.selectableItemBackground, highlightValue, true)

image_dino_1.setBackgroundResource(highlightValue.resourceId)
image_dino_2.setBackgroundResource(highlightValue.resourceId)
image_dino_3.setBackgroundResource(highlightValue.resourceId)
image_dino_4.setBackgroundResource(highlightValue.resourceId)

通常、Android は、どの View が現在フォーカスしている View の上、下、左、右にあるかを適切に判断できます。このアプリでも機能するかどうかについては、矢印キーと Tab キーの両方をテストして確認できます。メッセージ欄と送信ボタンの間を矢印キーで移動してみます。トリケラトプスを選択して Tab キーを押します。意図したビューにフォーカスが移ることを確認します。

今回のサンプルでは(意図的に)少しずれるようにしてあります。入力フィードバックでのこうした小さな問題も、ユーザーにとってはストレスになります。

通常、矢印キーと Tab キーの動作を手動で調整するには、次のコマンドを使用します。

矢印キー

android:nextFocusLeft="@id/view_to_left"
android:nextFocusRight="@id/view_to_right"
android:nextFocusUp="@id/view_above"
android:nextFocusDown="@id/view_below"

Tab キー

android:nextFocusForward="@id/next_view"

または、プログラムで次のように操作します。

矢印キー

myView.nextFocusLeftId = R.id.view_to_left
myView.nextFocusRightId = R.id.view_to_right
myView.nextFocusTopId = R.id.view_above
myView.nextFocusBottomId = R.id.view_below

Tab キー

myView.nextFocusForwardId - R.id.next_view

この例では、フォーカスの順序を次のように修正できます。

MainActivity.kt

edit_message.nextFocusForwardId = R.id.button_send
edit_message.nextFocusRightId = R.id.button_send
button_send.nextFocusForwardId = R.id.image_dino_1
button_send.nextFocusLeftId = R.id.edit_message
image_dino_2.nextFocusForwardId = R.id.image_dino_3
image_dino_3.nextFocusForwardId = R.id.image_dino_4

これで恐竜を選択できるようになりましたが、画面、照明条件、ビュー、視野によっては、選択したアイテムがハイライト表示されにくい場合があります。たとえば下の画像の場合、デフォルトでは灰色の背景に灰色でハイライトされています。

c0ace19128e548fe.png

視覚的に目立つフィードバックをユーザーに提供しましょう。AppTheme の res/values/styles.xml に次の行を追加します。

res/values/styles.xml

<item name="colorControlHighlight">@color/colorAccent</item>

23a53d405efe5602.png

このピンク色を使用する場合でも、上の画像のようなハイライト表示はコントラストが強すぎるかもしれません。すべての画像が同じサイズでない場合は、見栄えもよくありません。状態リスト ドローアブルを使用すると、選択したアイテムに対してのみ表示される枠線ドローアブルを作成できます。

res/drawable/box_border.xml

<?xml version="1.0" encoding="UTF-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
   <item android:state_focused="true">
       <shape android:padding="2dp">
           <solid android:color="#FFFFFF" />
           <stroke android:width="1dp" android:color="@color/colorAccent" />
           <padding android:left="2dp" android:top="2dp" android:right="2dp"
               android:bottom="2dp" />
       </shape>
   </item>
</selector>

ここで、前のステップの highlightValue/setBackgroundResource 行を、以下の新しい box_border 背景リソースに置き換えます。

MainActivity.kt(onCreate)

image_dino_1.setBackgroundResource(R.drawable.box_border)
image_dino_2.setBackgroundResource(R.drawable.box_border)
image_dino_3.setBackgroundResource(R.drawable.box_border)
image_dino_4.setBackgroundResource(R.drawable.box_border)

77ac1e50cdfbea01.png

631df359631b28bb.png

キーボード ユーザーは、一般的な Ctrl+ ベースのショートカット機能を使用できることを期待しています。ここで、元に戻す(Ctrl+Z)とやり直し(Ctrl+Shift+Z)のショートカットをアプリに追加します。

まず、シンプルなクリック履歴のスタックを作成します。ユーザーが 5 つのアクションを実行してから Ctrl+Z を 2 回押した結果、アクション 4 と 5 がやり直しスタックにあり、アクション 1、2、3 が元に戻すスタックにある場合を考えます。ユーザーが Ctrl+Z をもう一度押すと、アクション 3 は、元に戻すスタックからやり直しスタックに移動します。その後で Ctrl+Shift+Z を押すと、アクション 3 は、やり直しスタックから元に戻すスタックに移動します。

9d952ca72a5640d7.png

メインクラスの上部でさまざまなクリック アクションを定義し、ArrayDeque を使用してスタックを作成します。

MainActivity.kt

private var undoStack = ArrayDeque<Int>()
private var redoStack = ArrayDeque<Int>()

private val UNDO_MESSAGE_SENT = 1
private val UNDO_DINO_CLICKED = 2

メッセージの送信や恐竜のクリックのたびに、そのアクションを、元に戻すスタックに追加します。新しいアクションを行うときは、やり直しスタックをクリアしてください。クリック リスナーを次のように更新します。

MainActivity.kt

//In button_send onClick listener
undoStack.push(UNDO_MESSAGE_SENT)
redoStack.clear()

...

//In ImageOnClickListener
undoStack.push(UNDO_DINO_CLICKED)
redoStack.clear()

次に、ショートカット キーを実際にマッピングします。現在、Ctrl+ コマンドがサポートされており、Android O 以降では、dispatchKeyShortcutEvent を使用して Alt+ コマンドと Shift+ コマンドを追加できます。

MainActivity.kt(dispatchKeyShortcutEvent)

override fun dispatchKeyShortcutEvent(event: KeyEvent): Boolean {
    if (event.getKeyCode() == KeyEvent.KEYCODE_Z) {
        // Undo action
        return true
    }
    return super.dispatchKeyShortcutEvent(event)
}

このケースは慎重に考えましょう。コールバックをトリガーできるのは Ctrl+Z のみであり、Alt+Z または Shift+Z はトリガーできないことを強調するには、hasModifiers を使用してください。元に戻すスタック操作を以下に示します。

MainActivity.kt(dispatchKeyShortcutEvent)

override fun dispatchKeyShortcutEvent(event: KeyEvent): Boolean {
    // Ctrl-z == Undo
    if (event.keyCode == KeyEvent.KEYCODE_Z && event.hasModifiers(KeyEvent.META_CTRL_ON)) {
        val lastAction = undoStack.poll()
        if (null != lastAction) {
            redoStack.push(lastAction)

            when (lastAction) {
                UNDO_MESSAGE_SENT -> {
                    messagesSent--
                    text_messages_sent.text = (Integer.toString(messagesSent))
                }

                UNDO_DINO_CLICKED -> {
                    dinosClicked--
                    text_dinos_clicked.text = Integer.toString(dinosClicked)
                }

                else -> Log.d("OptimizedChromeOS", "Error on Ctrl-z: Unknown Action")
            }

            return true
        }
    }
    return super.dispatchKeyShortcutEvent(event)
}

実際に試して、期待どおりに動作するか確認してみましょう。次に、修飾子フラグを持つ OR を使用して Ctrl+Shift+Z を追加します。

MainActivity.kt(dispatchKeyShortcutEvent)

// Ctrl-Shift-z == Redo
if (event.keyCode == KeyEvent.KEYCODE_Z &&
    event.hasModifiers(KeyEvent.META_CTRL_ON or KeyEvent.META_SHIFT_ON)) {
    val prevAction = redoStack.poll()
    if (null != prevAction) {
        undoStack.push(prevAction)

        when (prevAction) {
            UNDO_MESSAGE_SENT -> {
                messagesSent++
                text_messages_sent.text = (Integer.toString(messagesSent))
            }

            UNDO_DINO_CLICKED -> {
                dinosClicked++
                text_dinos_clicked.text = Integer.toString(dinosClicked)
            }

            else -> Log.d("OptimizedChromeOS", "Error on Ctrl-Shift-z: Unknown Action")
        }

        return true
    }
}

多くのインターフェースでは、ユーザーはマウスの右クリックやトラックパッドのダブルタップでコンテキスト メニューが表示されることを想定しています。このアプリでもコンテキスト メニューを提供して、ユーザーが恐竜の画像を友人に送信できるようにします。

8b8c4a377f5e743b.png

コンテキスト メニューを作成すると、右クリック機能が自動的に追加されます。多くの場合、必要なのはこの機能だけです。この設定は次の 3 つの部分に分かれています。

このビューにコンテキスト メニューがあることを UI に通知する

コンテキスト メニューが必要なビュー(ここでは 4 つの画像)ごとに registerForContextMenu を使用します。

MainActivity.kt

registerForContextMenu(image_dino_1)
registerForContextMenu(image_dino_2)
registerForContextMenu(image_dino_3)
registerForContextMenu(image_dino_4)

コンテキスト メニューの外観を定義する

必要なコンテキスト オプションをすべて含めた XML のメニューを設計します。「共有」を追加するだけで、すべて含めることができます。

res/menu/context_menu.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/menu_item_share_dino"
        android:icon="@android:drawable/ic_menu_share"
        android:title="@string/menu_share" />
</menu>

次に、メイン Activity クラス内で onCreateContextMenu をオーバーライドし、XML ファイルを渡します。

MainActivity.kt

override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) {
    super.onCreateContextMenu(menu, v, menuInfo)
    val inflater = menuInflater
    inflater.inflate(R.menu.context_menu, menu)
}

特定のアイテムが選択されたときに実行するアクションを定義する

最後に、onContextItemSelected をオーバーライドして、実行するアクションを定義します。ここでは、簡単な Snackbar のみを表示し、画像が正常に共有されたことをユーザーに伝えます。

MainActivity.kt

override fun onContextItemSelected(item: MenuItem): Boolean {
    if (R.id.menu_item_share_dino == item.itemId) {
        Snackbar.make(findViewById(android.R.id.content),
            getString(R.string.menu_shared_message), Snackbar.LENGTH_SHORT).show()
        return true
    } else {
        return super.onContextItemSelected(item)
    }
}

実際に試してみましょう。画像を右クリックしたら、コンテキスト メニューが表示されるはずです。

MainActivity.kt

myView.setOnContextClickListener {
    // Display right-click options
    true
}

カーソルを合わせたときに表示されるツールチップ テキストを追加することで、UI の仕組みを理解できるようユーザーを支援したり、詳細情報を提供したりできます。

17639493329a9d1a.png

setTootltipText() メソッドを使用して、恐竜の名前の付いた画像ごとにツールチップを追加します。

MainActivity.kt

// Add dino tooltips
TooltipCompat.setTooltipText(image_dino_1, getString(R.string.name_dino_hadrosaur))
TooltipCompat.setTooltipText(image_dino_2, getString(R.string.name_dino_triceratops))
TooltipCompat.setTooltipText(image_dino_3, getString(R.string.name_dino_nodosaur))
TooltipCompat.setTooltipText(image_dino_4, getString(R.string.name_dino_afrovenator))

ポインティング デバイスが特定のビューの上にあるときに、そのビューに視覚的なフィードバック効果を加えると、より使いやすくなります。

このようなフィードバックを追加するには、以下のコードを使用して、送信ボタンにマウスのカーソルを合わせたときに、ボタンの色が緑色に変わるようにします。

MainActivity.kt(onCreate)

button_send.setOnHoverListener(View.OnHoverListener { v, event ->
    val action = event.actionMasked

    when (action) {
        ACTION_HOVER_ENTER -> {
            val buttonColorStateList = ColorStateList(
                arrayOf(intArrayOf()),
                intArrayOf(Color.argb(127, 0, 255, 0))
            )
            button_send.setBackgroundTintList(buttonColorStateList)
            return@OnHoverListener true
        }

        ACTION_HOVER_EXIT -> {
            button_send.setBackgroundTintList(null)
            return@OnHoverListener true
        }
    }

    false
})

さらにマウスオーバー効果を 1 つ追加する: ドラッグ可能な TextView に関連付けられている背景画像を変更して、テキストがドラッグ可能であることをユーザーがわかるようにします。

MainActivity.kt(onCreate)

text_drag.setOnHoverListener(View.OnHoverListener { v, event ->
    val action = event.actionMasked

    when (action) {
        ACTION_HOVER_ENTER -> {
            text_drag.setBackgroundResource(R.drawable.hand)
            return@OnHoverListener true
        }

        ACTION_HOVER_EXIT -> {
            text_drag.setBackgroundResource(0)
            return@OnHoverListener true
        }
    }

    false
})

実際に試してみましょう。「Drag Me!」テキストの上にカーソルを合わせると、インターフェースに大きな手の形のグラフィックが表示されるはずです。このような派手なフィードバックでも、ユーザー エクスペリエンスを触覚的なものにします。

詳しくは、View.OnHoverListenerMotionEvent のドキュメントをご覧ください。

デスクトップ環境では、アプリ内に(特に Chrome OS のファイル マネージャーから)アイテムをドラッグ&ドロップするのが一般的です。このステップでは、ファイルまたは書式なしテキスト アイテムを受け取るためのドロップ ターゲットを設定します。Codelab の次のセクションでは、ドラッグ可能なアイテムを実装します。

cfbc5c9d8d28e5c5.gif

まず、空の OnDragListener を作成します。コードを作成する前に、その構造を確認してください。

MainActivity.kt

protected inner class DropTargetListener(private val activity: AppCompatActivity
) : View.OnDragListener {
    override fun onDrag(v: View, event: DragEvent): Boolean {
        val action = event.action

        when (action) {
            DragEvent.ACTION_DRAG_STARTED -> {
                    return true
            }

            DragEvent.ACTION_DRAG_ENTERED -> {
                return true
            }

            DragEvent.ACTION_DRAG_EXITED -> {
                return true
            }

            DragEvent.ACTION_DRAG_ENDED -> {
                return true
            }

            DragEvent.ACTION_DROP -> {
                return true
            }

            else -> {
                Log.d("OptimizedChromeOS", "Unknown action type received by DropTargetListener.")
                return false
            }
        }
    }
}

ドラッグを開始する、ドロップ領域にカーソルを合わせる、アイテムを実際にドロップするなど、さまざまなドラッグ イベントが発生するたびに、onDrag() メソッドが呼び出されます。それぞれのドラッグ イベントの概要は次のとおりです。

  • ACTION_DRAG_STARTED は、アイテムをドラッグするたびにトリガーされます。ターゲットは、受け取ることができる有効なアイテムを探し、自身が受け取る準備ができていることを視覚的に示す必要があります。
  • ACTION_DRAG_ENTEREDACTION_DRAG_EXITED は、アイテムがドラッグされ、そのアイテムがドロップ領域に出入りするときにトリガーされます。アイテムをドロップできることをユーザーに知らせるには、視覚的なフィードバックを提供する必要があります。
  • ACTION_DROP は、アイテムを実際にドロップしたときにトリガーされます。ここでアイテムを処理します。
  • ACTION_DRAG_ENDED は、ドロップが正常に完了するかキャンセルされたときにトリガーされます。UI を通常の状態に戻します。

ACTION_DRAG_STARTED

このイベントは、ドラッグが開始されるたびにトリガーされます。ここで、ターゲットが特定のアイテムを受け取ることができる(true を返す)かできない(false を返す)かを示し、視覚的にユーザーに通知します。ドラッグ イベントには、ドラッグ対象アイテムに関する情報を含む ClipDescription が含まれます。

このドラッグ リスナーがアイテムを受け取ることができるかどうかを確認するには、そのアイテムの MIME タイプを調べます。この例では、背景の色合いを薄緑色に調整することで、ターゲットが有効であることを示します。

MainActivity.kt

DragEvent.ACTION_DRAG_STARTED -> {
    // Limit the types of items that can be received
    if (event.clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN) ||
        event.clipDescription.hasMimeType("application/x-arc-uri-list")) {

        // Greenify background colour so user knows this is a target
        v.setBackgroundColor(Color.argb(55, 0, 255, 0))
        return true
    }

    // If the dragged item is of an unrecognized type, indicate this is not a valid target
    return false
}

ENTERED、EXITED、ENDED

ENTERED と EXITED は、視覚的フィードバックと触覚フィードバックのロジックの方向です。この例では、アイテムがターゲット ゾーンの上に移動したときに緑色を濃くすることで、ユーザーがアイテムをドロップできることを示します。ENDED では、UI を通常の非ドラッグ&ドロップ状態にリセットします。

MainActivity.kt

DragEvent.ACTION_DRAG_ENTERED -> {
    // Increase green background colour when item is over top of target
    v.setBackgroundColor(Color.argb(150, 0, 255, 0))
    return true
}

DragEvent.ACTION_DRAG_EXITED -> {
    // Less intense green background colour when item not over target
    v.setBackgroundColor(Color.argb(55, 0, 255, 0))
    return true
}

DragEvent.ACTION_DRAG_ENDED -> {
    // Restore background colour to transparent
    v.setBackgroundColor(Color.argb(0, 255, 255, 255))
    return true
}

ACTION_DROP

このイベントは、アイテムが実際にターゲットにドロップされたときに発生します。ここで処理が完了します。

注: Chrome OS ファイルには ContentResolver を使用してアクセスする必要があります。

このデモでは、ターゲットは書式なしテキスト オブジェクトまたはファイルを受信することがあります。書式なしテキストの場合は、TextView にテキストを表示します。ファイルの場合は、最初の 200 文字をコピーして表示します。

MainActivity.kt

DragEvent.ACTION_DROP -> {
    requestDragAndDropPermissions(event) // Allow items from other applications
    val item = event.clipData.getItemAt(0)
    val textTarget = v as TextView

    if (event.clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
        // If this is a text item, simply display it in a new TextView.
        textTarget.setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f)
        textTarget.text = item.text
        // In STEP 10, replace line above with this
        // dinoModel.setDropText(item.text.toString())
    } else if (event.clipDescription.hasMimeType("application/x-arc-uri-list")) {
        // If a file, read the first 200 characters and output them in a new TextView.

        // Note the use of ContentResolver to resolve the ChromeOS content URI.
        val contentUri = item.uri
        val parcelFileDescriptor: ParcelFileDescriptor?
        try {
            parcelFileDescriptor = contentResolver.openFileDescriptor(contentUri, "r")
        } catch (e: FileNotFoundException) {
            e.printStackTrace()
            Log.e("OptimizedChromeOS", "Error receiving file: File not found.")
            return false
        }

        if (parcelFileDescriptor == null) {
            textTarget.setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f)
            textTarget.text = "Error: could not load file: " + contentUri.toString()
            // In STEP 10, replace line above with this
            // dinoModel.setDropText("Error: could not load file: " + contentUri.toString())
            return false
        }

        val fileDescriptor = parcelFileDescriptor.fileDescriptor

        val MAX_LENGTH = 5000
        val bytes = ByteArray(MAX_LENGTH)

        try {
            val `in` = FileInputStream(fileDescriptor)
            try {
                `in`.read(bytes, 0, MAX_LENGTH)
            } finally {
                `in`.close()
            }
        } catch (ex: Exception) {
        }

        val contents = String(bytes)

        val CHARS_TO_READ = 200
        val content_length = if (contents.length > CHARS_TO_READ) CHARS_TO_READ else 0

        textTarget.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10f)
        textTarget.text = contents.substring(0, content_length)
        // In STEP 10, replace line above with this
        // dinoModel.setDropText(contents.substring(0, content_length))
    } else {
        return false
    }
    return true
}

OnDragListener

DropTargetListener を設定したら、ドロップしたアイテムを受け取りたいビューに追加します。

MainActivity.kt

text_drop.setOnDragListener(DropTargetListener(this))

実際に試してみましょう。Chrome OS ファイル マネージャーからファイルをドラッグする必要があります。Chrome OS のテキスト エディタを使用してテキスト ファイルを作成するか、インターネットから画像ファイルをダウンロードすることができます。

ここで、アプリでドラッグ可能なアイテムを設定します。ドラッグ プロセスは通常、ビューを長押しすることでトリガーされます。アイテムをドラッグできるかどうかを示すには、LongClickListener を作成し、転送するデータをシステムに提供してデータ型を指定してください。ここでアイテムをドラッグするときの外観を設定することもできます。

TextView から文字列を取得する書式なしテキストのドラッグ アイテムを設定します。コンテンツ MIME タイプを ClipDescription.MIMETYPE_TEXT_PLAIN に設定します。

ドラッグ中の視覚効果が必要な場合は、組み込みの DragShadowBuilder を使用して標準的な半透明の見た目になるようにします。より複雑な例については、ドキュメントのドラッグの開始をご覧ください。

このアイテムを他のアプリにドラッグできることを示すために、必ず DRAG_FLAG_GLOBAL フラグを設定するようにしてください。

MainActivity.kt

protected inner class TextViewLongClickListener : View.OnLongClickListener {
    override fun onLongClick(v: View): Boolean {
        val thisTextView = v as TextView
        val dragContent = "Dragged Text: " + thisTextView.text

        //Set the drag content and type
        val item = ClipData.Item(dragContent)
        val dragData = ClipData(dragContent, arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN), item)

        //Set the visual look of the dragged object
        //Can be extended and customized. We use the default here.
        val dragShadow = View.DragShadowBuilder(v)

        // Starts the drag, note: global flag allows for cross-application drag
        v.startDragAndDrop(dragData, dragShadow, null, View.DRAG_FLAG_GLOBAL)

        return false
    }
}

次に、LongClickListener をドラッグ可能な TextView に追加します。

MainActivity.kt(onCreate)

text_drag.setOnLongClickListener(TextViewLongClickListener())

実際に試して、TextView からテキストをドラッグできるか確認してみましょう。

キーボードのサポート、マウスのサポート、恐竜など、アプリの見栄えは良くなっているはずです。ただし、デスクトップ環境では、アプリのサイズ変更、最大化、最大化解除、タブレット モードへの切り替え、画面の向きの変更が頻繁に行われます。ドロップしたアイテム、送信したメッセージ カウンタ、クリック カウンタへの影響はあるでしょうか。

Activity のライフサイクルは、Android アプリの作成時に理解しておく必要があります。アプリが複雑になると、ライフサイクルの状態の管理が難しくなる場合があります。幸いなことに、アーキテクチャ コンポーネントを使用すると、ライフサイクルの問題を確かな方法で簡単に処理できます。この Codelab では、ViewModelLiveData を使用してアプリの状態を維持することに注目します。

ViewModel は、ライフサイクルの変更後も UI 関連データを維持するのに役立ちます。LiveData は、UI 要素を自動的に更新するオブザーバーとして機能します。

このアプリで記録したいデータについて考えてみましょう。

  • 送信済みメッセージ カウンタ(ViewModel、LiveData)
  • 画像クリック カウンタ(ViewModel、LiveData)
  • 現在のドロップ ターゲット テキスト(ViewModel、LiveData)
  • 元に戻す / やり直しスタック(ViewModel)

これらのプロパティを設定する ViewModel クラスのコードを調べます。基本的には、シングルトン パターンを使用したゲッターとセッターが含まれます。

DinoViewModel.kt

class DinoViewModel : ViewModel() {
    private val undoStack = ArrayDeque<Int>()
    private val redoStack = ArrayDeque<Int>()

    private val messagesSent = MutableLiveData<Int>().apply { value = 0 }
    private val dinosClicked = MutableLiveData<Int>().apply { value = 0 }
    private val dropText = MutableLiveData<String>().apply { value = "Drop Things Here!" }

    fun getUndoStack(): ArrayDeque<Int> {
        return undoStack
    }

    fun getRedoStack(): ArrayDeque<Int> {
        return redoStack
    }

    fun getDinosClicked(): LiveData<Int> {
        return dinosClicked
    }

    fun getDinosClickedInt(): Int {
        return dinosClicked.value ?: 0
    }

    fun setDinosClicked(newNumClicks: Int): LiveData<Int> {
        dinosClicked.value = newNumClicks
        return dinosClicked
    }

    fun getMessagesSent(): LiveData<Int> {
        return messagesSent
    }

    fun getMessagesSentInt(): Int {
        return messagesSent.value ?: 0
    }

    fun setMessagesSent(newMessagesSent: Int): LiveData<Int> {
        messagesSent.value = newMessagesSent
        return messagesSent
    }

    fun getDropText(): LiveData<String> {
        return dropText
    }

    fun setDropText(newDropText: String): LiveData<String> {
        dropText.value = newDropText
        return dropText
    }
}

メイン Activity で、ViewModelProvider を使用して ViewModel を取得します。これで、すべてのライフサイクルが実現します。たとえば、元に戻すスタックとやり直しスタックでは、サイズ変更、向きの変更、レイアウトの変更が生じたときに、その状態を自動的に維持します。

MainActivity.kt(onCreate)

// Get the persistent ViewModel
dinoModel = ViewModelProviders.of(this).get(DinoViewModel::class.java)

// Restore our stacks
undoStack = dinoModel.getUndoStack()
redoStack = dinoModel.getRedoStack()

LiveData 変数用に、Observer オブジェクトを作成して追加し、変数が変更されたときの UI の変更方法を指定します。

MainActivity.kt(onCreate)

// Set up data observers
dinoModel.getMessagesSent().observe(this, androidx.lifecycle.Observer { newCount ->
    text_messages_sent.setText(Integer.toString(newCount))
})

dinoModel.getDinosClicked().observe(this, androidx.lifecycle.Observer { newCount ->
    text_dinos_clicked.setText(Integer.toString(newCount))
})

dinoModel.getDropText().observe(this, androidx.lifecycle.Observer { newString ->
    text_drop.text = newString
})

これらのオブザーバーを導入すると、すべてのクリック コールバックのコードが簡略化され、ViewModel 変数データを変更できるようになります。

以下のコードは、TextView オブジェクトを直接操作する必要がないことを示しています。LiveData オブザーバーですべての UI 要素が自動的に更新されます。

MainActivity.kt

internal inner class SendButtonOnClickListener(private val sentCounter: TextView) : View.OnClickListener {
    override fun onClick(v: View?) {
        undoStack.push(UNDO_MESSAGE_SENT)
        redoStack.clear()
        edit_message.getText().clear()

        dinoModel.setMessagesSent(dinoModel.getMessagesSentInt() + 1)
    }
}

internal inner class ImageOnClickListener(private val clickCounter: TextView) : View.OnClickListener {
    override fun onClick(v: View) {
        undoStack.push(UNDO_DINO_CLICKED)
        redoStack.clear()

        dinoModel.setDinosClicked(dinoModel.getDinosClickedInt() + 1)
    }
}

最後に、UI を直接操作するのではなく、ViewModel と LiveData を使用するように、undo または redo コマンドを更新します。

MainActivity.kt

when (lastAction) {
    UNDO_MESSAGE_SENT -> {
        dinoModel.setMessagesSent(dinoModel.getMessagesSentInt() - 1)
    }

    UNDO_DINO_CLICKED -> {
        dinoModel.setDinosClicked(dinoModel.getDinosClickedInt() - 1)
    }

    else -> Log.d("OptimizedChromeOS", "Error on Ctrl-z: Unknown Action")
}

...

when (prevAction) {
    UNDO_MESSAGE_SENT -> {
        dinoModel.setMessagesSent(dinoModel.getMessagesSentInt() + 1)
    }

    UNDO_DINO_CLICKED -> {
        dinoModel.setDinosClicked(dinoModel.getDinosClickedInt() + 1)
    }

    else -> Log.d("OptimizedChromeOS", "Error on Ctrl-Shift-z: Unknown Action")
}

実際に試して、現在のサイズを変更してみましょう。アーキテクチャ コンポーネントを適切に使えますか。

アーキテクチャ コンポーネントについて詳しくは、Android ライフサイクル Codelab をご覧ください。このブログ投稿では、ViewModel と onSavedInstanceState の動作や相互作用を理解するのに役立つリソースをご紹介しています。

以上でこの Codelab は終了です。おつかれさまでした。Chrome OS 向けに Android アプリを最適化する際にデベロッパーが直面する一般的な問題について、十分に理解することができました。

52240dc3e68f7af8.png

サンプル ソースコード

GitHub からリポジトリのクローンを作成します。

git clone https://github.com/googlecodelabs/optimized-for-chromeos

または、リポジトリを ZIP ファイルとしてダウンロードします

ZIP をダウンロード