Mengoptimalkan Aplikasi Android untuk Chrome OS

Dengan kemampuan menjalankan aplikasi Android di Chromebook, ekosistem aplikasi yang sangat besar dan fungsi baru yang beragam kini tersedia bagi pengguna. Meskipun ini baik bagi developer, pengoptimalan aplikasi tertentu diperlukan untuk memenuhi ekspektasi kegunaan dan memberikan pengalaman pengguna yang luar biasa. Lab kode ini memandu Anda melalui pengoptimalan paling umum.

f60cd3eb5b298d5d.png

Yang akan Anda buat

Anda akan membuat aplikasi Android fungsional yang menunjukkan praktik terbaik dan pengoptimalan untuk Chrome OS. Aplikasi Anda akan:

Menangani input keyboard termasuk

  • Tombol enter
  • Tombol panah
  • Pintasan Ctrl- dan Ctrl-Shift-
  • Masukan visual untuk item yang saat ini dipilih

Menangani input mouse termasuk

  • Klik kanan
  • Efek saat kursor diarahkan
  • Tooltip
  • Tarik lalu lepas

Menggunakan Komponen Arsitektur untuk

  • Mempertahankan status
  • Mengupdate UI secara otomatis

52240dc3e68f7af8.png

Yang akan Anda pelajari

  • Praktik terbaik untuk menangani input keyboard dan mouse di Chrome OS
  • Pengoptimalan khusus Chrome OS
  • Penerapan dasar Komponen Arsitektur ViewModel dan LiveData

Yang Anda butuhkan

Clone repositori dari GitHub

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

...atau download file zip repositori, lalu ekstrak file tersebut

Download Zip

Mengimpor Project

  • Buka Android Studio
  • Pilih Import Project atau File > New > Import Project
  • Buka tempat Anda meng-clone atau mengekstrak project
  • Impor project optimized-for-chromeos
  • Perhatikan bahwa ada dua modul, mulai dan lengkap

Mencoba Aplikasi

  • Buat dan jalankan modul mulai
  • Mulailah menggunakan trackpad saja
  • Klik dinosaurus
  • Kirim beberapa pesan rahasia
  • Cobalah untuk menyeret teks "Drag Me" atau letakkan file ke area "Drop Things Here"
  • Coba gunakan keyboard untuk membuka dan mengirim pesan
  • Coba gunakan aplikasi dalam mode tablet
  • Coba putar perangkat atau ubah ukuran jendela

Bagaimana menurut Anda?

Meskipun aplikasi ini sangat mendasar, dan bagian yang terasa rusak mudah diatasi, pengalaman pengguna sangatlah buruk. Mari kita perbaiki!

a40270071a9b5ac3.png

Jika Anda mengetik beberapa pesan rahasia menggunakan keyboard, Anda akan melihat bahwa tombol Enter tidak melakukan apa pun. Hal ini dapat membuat pengguna jengkel.

Kode contoh di bawah dan dokumentasi Menangani Tindakan Keyboard seharusnya cukup membantu.

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

Uji sekarang! Kemampuan mengirim pesan hanya dengan menggunakan keyboard adalah pengalaman pengguna yang jauh lebih baik.

Bukankah bagus melihat aplikasi ini hanya menggunakan keyboard? Demikian pula, akan menjadi pengalaman buruk jika pengguna memiliki keyboard tetapi aplikasi tidak meresponsnya. Hal ini menjengkelkan.

Salah satu cara termudah untuk membuat tampilan dapat dibuka melalui tombol panah dan tab adalah dengan membuatnya dapat difokuskan.

Periksa file tata letak dan lihat tag Button dan ImageView. Perhatikan bahwa atribut focusable disetel ke salah. Ubah ke benar dalam XML:

activity_main.xml

android:focusable="true"

Atau secara terprogram:

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)

Cobalah. Anda seharusnya dapat menggunakan tombol panah dan tombol enter untuk memilih dinosaurus. Namun bergantung pada versi OS, layar, dan pencahayaan, Anda mungkin tidak dapat melihat item mana yang saat ini dipilih. Untuk mengatasi hal ini, tetapkan resource latar belakang untuk gambar ke 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)

Biasanya, Android berfungsi dengan baik dalam memutuskan View yang berada di atas, di bawah, di sebelah kiri, atau di sebelah kanan View yang saat ini difokuskan. Seberapa bagus kinerjanya di aplikasi ini? Pastikan untuk menguji tombol panah dan tombol tab. Coba navigasi antara kolom pesan dan tombol kirim dengan tombol panah. Sekarang pilih triceratops dan tekan tab. Apakah fokus berubah ke tampilan yang Anda harapkan?

Dalam contoh ini, terdapat hal-hal yang (sengaja) sedikit tidak sesuai. Sebagai pengguna, gangguan kecil dalam masukan input ini dapat terasa sangat menjengkelkan.

Untuk menyesuaikan perilaku tombol panah/tab secara umum, Anda dapat menggunakan hal berikut:

Tombol panah

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

Tombol Tab

android:nextFocusForward="@id/next_view"

Atau secara terprogram:

Tombol panah

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

Tombol Tab

myView.nextFocusForwardId - R.id.next_view

Untuk contoh ini, urutan fokus dapat diperbaiki dengan:

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

Sekarang Anda dapat memilih dinosaurus, tetapi bergantung pada layar, kondisi pencahayaan, tampilan, dan visi Anda, mungkin akan sulit melihat sorotan untuk item yang dipilih. Misalnya, pada gambar di bawah warna default adalah abu-abu di atas abu-abu.

c0ace19128e548fe.png

Untuk memberikan masukan visual yang lebih menonjol bagi pengguna Anda. Tambahkan berikut ke res/nilai/styles.xml di bagian AppTheme:

res/values/styles.xml

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

23a53d405efe5602.png

Warna merah muda itu mungkin bagus, tetapi jenis sorotan pada gambar di atas mungkin terlalu menonjol dari harapan Anda dan terlihat berantakan jika semua gambar tidak memiliki dimensi yang sama. Dengan menggunakan drawable daftar status, Anda dapat membuat drawable batas yang hanya muncul saat item dipilih.

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>

Sekarang ganti baris highlightValue/setBackgroundResource dari langkah sebelumnya dengan resource latar belakang box_border baru ini:

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

Pengguna keyboard mengharapkan pintasan umum berbasis Ctrl berfungsi. Jadi sekarang Anda akan menambahkan pintasan Urungkan (Ctrl-Z) dan Ulangi (Ctrl-Shift-Z) ke aplikasi.

Pertama, buat tumpukan histori klik sederhana. Bayangkan pengguna telah melakukan 5 tindakan serta menekan Ctrl-Z dua kali sehingga tindakan 4 dan 5 berada di tumpukan ulangi dan 1, 2, dan 3 pada tumpukan urungkan. Jika pengguna menekan Ctrl-Z lagi, tindakan 3 berpindah dari tumpukan urungkan ke tumpukan ulangi. Jika mereka kemudian menekan Ctrl-Shift-Z, tindakan 3 berpindah dari tumpukan ulangi ke tumpukan urungkan.

9d952ca72a5640d7.png

Di bagian atas class utama, tentukan tindakan klik yang berbeda dan buat tumpukan menggunakan 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

Setiap kali pesan dikirim atau dinosaurus diklik, tambahkan tindakan tersebut ke tumpukan urungkan. Saat tindakan baru dilakukan, hapus tumpukan ulangi. Update pemroses klik Anda sebagai berikut:

MainActivity.kt

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

...

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

Sekarang petakan tombol pintasan. Dukungan untuk perintah Ctrl- dan, di Android O dan yang lebih baru, untuk perintah Alt- dan Shift- dapat ditambahkan menggunakan dispatchKeyShortcutEvent.

MainActivity.kt (dispatchKeyShortcutEvent)

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

Mari menjadi pemilih dalam kasus ini. Untuk menetapkan bahwa hanya Ctrl-Z yang memicu callback dan bukan Alt-Z atau Shift-Z, gunakan hasModifiers. Operasi tumpukan urungkan diisi di bawah ini.

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

Lakukan pengujian. Apakah semuanya bekerja seperti yang diharapkan? Sekarang tambahkan Ctrl-Shift-Z menggunakan OR dengan tanda pengubah.

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

Untuk banyak antarmuka, pengguna menganggap bahwa klik kanan dengan mouse atau mengetuk dua kali pada trackpad akan memunculkan menu konteks. Di aplikasi ini, kita akan menyediakan menu konteks ini sehingga pengguna dapat mengirim gambar dinosaurus keren ini kepada teman baik.

8b8c4a377f5e743b.png

Membuat menu konteks akan mencakup fungsionalitas klik kanan secara otomatis. Dalambanyak kasus, inilah yang Anda butuhkan. Terdapat 3 bagian untuk penyiapan ini:

Memberi tahu UI bahwa tampilan ini memiliki menu konteks

Gunakan registerForContextMenu pada setiap tampilan tempat Anda ingin menyediakan menu konteks, dalam hal ini 4 gambar.

MainActivity.kt

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

Menentukan tampilan menu konteks

Desain menu dalam XML yang berisi semua opsi konteks yang Anda butuhkan. Untuk melakukannya, cukup tambahkan "Share".

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>

Kemudian, di class aktivitas utama, ganti onCreateContextMenu dan masukkan file 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)
}

Menentukan tindakan yang akan diambil saat item tertentu dipilih

Terakhir, tentukan tindakan yang akan diambil dengan mengganti onContextItemSelected. Di sini, cukup tampilkan Snackbar singkat yang memberi tahu pengguna bahwa gambar berhasil dibagikan.

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

Uji sekarang! Jika Anda mengklik kanan sebuah gambar, menu konteks akan muncul.

MainActivity.kt

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

Menambahkan teks tooltip yang muncul saat mengarahkan kursor adalah cara mudah untuk membantu pengguna memahami cara kerja UI atau untuk memberikan informasi tambahan.

17639493329a9d1a.png

Tambahkan tooltip untuk setiap foto dengan nama dinosaurus menggunakan metode 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))

Menambahkan efek masukan visual ke tampilan tertentu saat perangkat penunjuk mengarahkan kursor ke atasnya dapat bermanfaat.

Untuk menambahkan jenis masukan ini, gunakan kode di bawah agar tombol Send berubah menjadi hijau saat mouse diarahkan ke atasnya.

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

Tambahkan satu efek lain saat kursor diarahkan: Ubah gambar latar belakang yang terkait dengan TextView yang dapat ditarik, sehingga pengguna tahu bahwa teks tersebut dapat ditarik.

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

Uji sekarang! Anda akan melihat gambar tangan besar muncul saat mengarahkan mouse ke teks "Drag Me!". Bahkan masukan yang mencolok ini membuat pengalaman pengguna lebih taktil.

Untuk informasi selengkapnya, lihat dokumentasi View.OnHoverListener dan MotionEvent.

Dalam lingkungan desktop, tarik lalu lepas item ke dalam aplikasi adalah hal yang wajar, terutama dari file manager Chrome OS. Pada langkah ini, siapkan target operasi lepas yang dapat menerima file atau item teks biasa. Di bagian codelab berikutnya, kita akan mengimplementasikan item yang dapat ditarik.

cfbc5c9d8d28e5c5.gif

Pertama, buat OnDragListener kosong. Periksa strukturnya sebelum mulai membuat kode:

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

Metode onDrag() dipanggil setiap kali salah satu dari beberapa peristiwa tarik yang berbeda terjadi: memulai operasi tarik, mengarahkan kursor ke zona operasi lepas, atau ketika item benar-benar dilepaskan. Berikut adalah ringkasan dari berbagai peristiwa tarik:

  • ACTION_DRAG_STARTED dipicu saat item apa pun ditarik. Target Anda harus mencari item valid yang dapat diterima dan memberikan indikasi visual bahwa targetnya sudah siap.
  • ACTION_DRAG_ENTERED dan ACTION_RETRY_EXITED dipicu saat item ditarik dan item memasuki/keluar dari zona operasi lepas. Anda harus memberikan masukan visual untuk memberi tahu pengguna bahwa mereka dapat melepaskan item.
  • ACTION_DROP dipicu saat item benar-benar dilepaskan. Proses item di sini.
  • ACTION_DRAG_ENDED dipicu saat pelepasan berhasil dilakukan atau dibatalkan. Mengembalikan UI ke keadaan normal.

ACTION_DRAG_STARTED

Peristiwa ini dipicu setiap kali operasi tarik dimulai. Tunjukkan di sini apakah target dapat menerima item tertentu (menampilkan benar) atau tidak (menampilkan salah) dan memberi tahu pengguna secara visual. Peristiwa tarik akan berisi ClipDescription yang berisi informasi tentang item yang sedang ditarik.

Untuk menentukan apakah pemroses tarik ini dapat menerima item, periksa jenis MIME item. Dalam contoh ini, tunjukkan bahwa target tersebut adalah target yang valid dengan menambahkan tint hijau terang di latar belakang.

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, dan ENDED

ENTERED dan EXITED adalah tempat info via sentuhan/visual muncul. Dalam contoh ini, perdalam warna hijau saat item diarahkan ke zona target untuk menunjukkan bahwa pengguna dapat melepaskannya. Di ENDED, reset UI ke status normal tanpa tarik lalu lepas.

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

Ini adalah peristiwa yang terjadi ketika item benar-benar dilepaskan pada target. Di sinilah pemrosesan diselesaikan.

Catatan: File Chrome OS harus diakses menggunakan ContentResolver.

Dalam demo ini, target dapat menerima objek teks biasa atau file. Untuk teks biasa, tampilkan teks di TextView. Jika berupa file, salin 200 karakter pertama dan tampilkan.

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

Setelah DropTargetListener disiapkan, lampirkan ke tampilan yang diinginkan untuk menerima item yang dilepas.

MainActivity.kt

text_drop.setOnDragListener(DropTargetListener(this))

Uji sekarang! Ingat, Anda harus menarik file dari file manager Chrome OS. Anda dapat membuat file teks menggunakan editor teks Chrome OS atau mendownload file gambar dari internet.

Sekarang siapkan item yang dapat ditarik di aplikasi Anda. Proses tarik biasanya dipicu dengan menekan lama tampilan. Untuk menunjukkan bahwa item dapat ditarik, buat LongClickListener yang menyediakan data yang ditransfer kepada sistem dan menunjukkan jenis data. Di sinilah Anda juga mengonfigurasi tampilan item saat sedang ditarik.

Siapkan item tarik teks biasa yang menarik string dari TextView. Tetapkan jenis MIME konten ke ClipDescription.MIMETYPE_TEXT_PLAIN.

Untuk tampilan visual selama tarik, gunakan DragShadowBuilder bawaan untuk tampilan tarik transparan standar. Lihat Memulai Operasi Tarik dalam dokumentasi untuk contoh yang lebih kompleks.

Ingatlah untuk menyetel tanda DRAG_FLAG_GLOBAL untuk menunjukkan bahwa item ini dapat ditarik ke aplikasi lain.

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

Dan sekarang tambahkan LongClickListener ke TextView yang dapat ditarik.

MainActivity.kt (onCreate)

text_drag.setOnLongClickListener(TextViewLongClickListener())

Cobalah! Apakah Anda dapat menarik teks dari TextView?

Aplikasi Anda akan terlihat cukup bagus - dukungan keyboard, dukungan mouse, dan dinosaurus! Namun, dalam lingkungan desktop, pengguna akan sering mengubah ukuran aplikasi, memaksimalkan, meminimalkan, beralih ke mode tablet, dan mengubah orientasi. Apa yang terjadi pada item yang dilepaskan, penghitung pesan yang dikirim, dan penghitung klik?

Siklus Proses Aktivitas penting untuk dipahami saat membuat aplikasi Android. Karena aplikasi menjadi semakin rumit, mengelola status siklus proses dapat menjadi hal yang sulit. Untungnya, Komponen Arsitektur memudahkan penanganan masalah siklus proses dengan cara yang tangguh. Dalam codelab ini, kita akan fokus pada penggunaan ViewModel dan LiveData untuk mempertahankan status aplikasi.

ViewModel membantu mempertahankan data terkait UI di seluruh perubahan siklus proses. LiveData berfungsi sebagai pengamat untuk memperbarui elemen UI secara otomatis.

Pertimbangkan data yang ingin kita lacak di aplikasi ini:

  • Penghitung pesan terkirim (ViewModel, LiveData)
  • Penghitung gambar yang diklik (ViewModel, LiveData)
  • Teks target pelepasan saat ini (ViewModel, LiveData)
  • Tumpukan urungkan/ulangi (ViewModel)

Periksa kode untuk class ViewModel yang menyiapkannya. Pada dasarnya, objek ini berisi pengambil dan penyetel menggunakan pola singleton.

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

Di aktivitas utama Anda, dapatkan ViewModel menggunakan ViewModelProvider. Ini akan melakukan semua keajaiban siklus proses. Misalnya, tumpukan urungkan dan ulangi akan mempertahankan statusnya secara otomatis selama perubahan ukuran, orientasi, dan tata letak.

MainActivity.kt (onCreate)

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

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

Untuk variabel LiveData, buat dan lampirkan objek Observer lalu beri tahu UI cara mengubahnya saat variabel berubah.

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

Setelah pengamat ini diterapkan, kode di semua callback klik dapat disederhanakan untuk hanya mengubah data variabel ViewModel.

Kode di bawah menunjukkan bagaimana Anda tidak perlu memanipulasi objek TextView secara langsung—semua elemen UI dengan pengamat LiveData diperbarui secara otomatis.

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

Terakhir, perbarui perintah urungkan/ulangi untuk menggunakan ViewModel dan LiveData sebagai ganti memanipulasi UI secara langsung.

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

Cobalah! Bagaimana cara mengubah ukurannya sekarang? Apakah Anda menyukai Komponen Arsitektur?

Baca Codelab Siklus Proses Android untuk menjelajahi Komponen Arsitektur yang lebih mendetail. Postingan blog ini adalah referensi yang sangat tepat untuk memahami cara kerja dan interaksi ViewModel dan onSavedInstanceState.

Anda berhasil! Bagus sekali! Anda telah mengenal masalah paling umum yang dihadapi developer saat mengoptimalkan aplikasi Android untuk Chrome OS.

52240dc3e68f7af8.png

Contoh Kode Sumber

Clone repositori dari GitHub

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

...atau download repositori sebagai file Zip

Download Zip