Mendukung perangkat foldable dan perangkat dua layar dengan Jetpack WindowManager

Codelab praktis ini akan mengajarkan Anda dasar-dasar pengembangan untuk perangkat dua layar dan perangkat foldable. Setelah selesai, Anda akan dapat meningkatkan aplikasi untuk mendukung perangkat, seperti Microsoft Surface Duo dan Samsung Galaxy Z Fold 2.

Prasyarat

Untuk menyelesaikan codelab ini, Anda memerlukan:

  • Pengalaman membuat aplikasi Android
  • Pengalaman dengan Aktivitas, Fragmen, ViewBinding, dan tata letak xml
  • Pengalaman menambahkan dependensi ke project
  • Pengalaman menginstal dan menggunakan emulator perangkat. Untuk codelab ini, Anda akan menggunakan emulator perangkat foldable dan/atau perangkat dua layar.

Yang akan Anda lakukan

  • Membuat aplikasi sederhana dan meningkatkannya agar dapat mendukung perangkat foldable dan perangkat dua layar.
  • Menggunakan Jetpack WindowManager untuk menggunakan perangkat faktor bentuk baru.

Yang Anda butuhkan

  • Android Studio 4.2 atau yang lebih baru.
  • Emulator atau perangkat foldable Jika Anda menggunakan Android Studio 4.2, ada beberapa emulator perangkat foldable yang dapat digunakan, seperti yang ditunjukkan pada gambar di bawah:

7a0db14df3576a82.png

  • Jika ingin menggunakan emulator dua layar, Anda dapat mendownload emulator Microsoft Surface Duo untuk platform Anda (Windows, MacOS, atau GNU/Linux) di sini.

Perangkat foldable menawarkan pengguna ukuran layar yang lebih besar dan antarmuka pengguna yang lebih fleksibel daripada yang sebelumnya tersedia di perangkat seluler. Manfaat lainnya adalah, saat dilipat, ukuran perangkat ini sering kali lebih kecil daripada ukuran tablet pada umumnya, sehingga lebih portabel dan fungsional.

Pada saat codelab ini ditulis, ada dua jenis perangkat foldable:

  • Perangkat foldable satu layar, dengan satu layar yang dapat dilipat. Pengguna dapat menjalankan beberapa aplikasi di layar yang sama secara bersamaan menggunakan mode Multi-Window.
  • Perangkat foldable dua layar, dengan dua layar yang digabungkan menggunakan engsel. Perangkat ini juga dapat dilipat, tetapi memiliki dua region tampilan logis yang berbeda.

affbd6daf04cfe7b.png

Seperti tablet dan perangkat seluler satu layar lainnya, perangkat foldable juga dapat:

  • Menjalankan satu aplikasi di salah satu region tampilan.
  • Menjalankan dua aplikasi secara berdampingan, masing-masing di region tampilan yang berbeda (menggunakan mode Multi-Window).

Tidak seperti perangkat satu layar, perangkat foldable juga mendukung berbagai postur. Postur dapat digunakan untuk menampilkan konten dengan berbagai cara.

f2287b68f32b59e3.png

Perangkat foldable dapat menawarkan postur rentang yang berbeda saat aplikasi direntangkan (ditampilkan) ke seluruh region tampilan (menggunakan semua region tampilan di perangkat foldable dua layar).

Perangkat foldable juga dapat menawarkan postur lipat, seperti mode di atas meja, sehingga Anda bisa mendapatkan pemisahan logis antara bagian layar yang datar dan bagian yang miring ke arah Anda, dan mode tenda yang memungkinkan Anda memvisualisasikan konten seolah-olah perangkat menggunakan gadget stand.

Library Jetpack WindowManager dirancang untuk membantu developer melakukan penyesuaian di aplikasi mereka, dan memanfaatkan pengalaman baru yang disediakan perangkat ini kepada pengguna. Jetpack WindowManager membantu developer aplikasi mendukung faktor bentuk perangkat baru dan menyediakan tampilan API umum untuk berbagai fitur WindowManager pada versi platform yang lama dan yang baru.

Fitur utama

Jetpack WindowManager versi 1.0.0-alpha03 berisi class FoldingFeature yang menjelaskan lipatan di tampilan fleksibel atau engsel di antara dua panel tampilan fisik. API-nya memberikan akses ke informasi penting yang berkaitan dengan perangkat:

Melalui class WindowManager utama, Anda dapat mengakses informasi penting, seperti:

  • getCurrentWindowMetrics(): menampilkan WindowMetrics yang sesuai dengan status layar saat ini. Nilai ini didasarkan pada status aplikasi saat ini sistem.
  • getMaximumWindowMetrics(): menampilkan WindowMetrics terbesar yang sesuai dengan status layar saat ini. Nilai ini didasarkan pada potensi status aplikasi terbesar sistem. Misalnya, untuk aktivitas dalam mode Multi-Aplikasi, metrik yang ditampilkan didasarkan pada batas yang digunakan jika pengguna meluaskan aplikasi untuk menutupi seluruh layar.

Lakukan clone pada repo GitHub atau download kode contoh untuk aplikasi yang akan Anda tingkatkan:

git clone https://github.com/googlecodelabs/android-foldable-codelab

Mendeklarasikan dependensi

Untuk menggunakan Jetpack WindowManager, Anda harus menambahkan dependensi ke Jetpack.

  1. Pertama, tambahkan repositori Google Maven ke project Anda.
  2. Tambahkan dependensi untuk artefak yang ada dalam file build.gradle untuk aplikasi atau modul Anda:
dependencies {
    implementation "androidx.window:window:1.0.0-alpha03"
}

Menggunakan WindowManager

Jetpack WindowManager dapat digunakan denga sangat mudah dengan mendaftarkan aplikasi Anda untuk memantau perubahan konfigurasi.

Pertama, lakukan inisialisasi instance WindowManager sehingga Anda bisa mendapatkan akses ke API-nya. Untuk melakukan inisialisasi instance WindowsManager, terapkan kode berikut ke dalam Aktivitas:

private lateinit var wm: WindowManager

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        wm = WindowManager(this)
}

Konstruktor utama hanya mengizinkan satu parameter: konteks visual, seperti Activity atau ContextWrapper di satu Aktivitas. Di balik layar, konstruktor ini akan menggunakan WindowBackend default. Ini adalah kelas server pendukung yang akan memberikan informasi untuk instance ini.

Setelah memiliki instance WindowManager, Anda bisa mendaftarkan callback agar dapat mengetahui waktu perubahan postur terjadi, fitur mana yang dimiliki perangkat, dan batas fitur tersebut (jika ada). Selain itu, seperti yang telah disebutkan sebelumnya, Anda dapat melihat metrik saat ini dan maksimum menurut status sistem saat ini.

  1. Buka Android Studio.
  2. Klik File > New > New Project > Empty Activity untuk membuat project baru.
  3. Klik Next, terima properti dan nilai default, lalu klik Finish.

Sekarang, buat tata letak sederhana sehingga Anda dapat melihat informasi yang akan dilaporkan WindowManager. Untuk itu, Anda harus membuat folder tata letak dan file tata letak tertentu:

  1. Klik File > New > Android resource directory.
  2. Di jendela baru, pilih Resource Type layout, lalu klik OK.
  3. Buka struktur project dan di src/main/res/layout, buat file resource tata letak baru (File > New > Layout resource file) yang disebut activity_main.xml
  4. Buka file, lalu tambahkan konten ini sebagai tata letak Anda:

res/layout/activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<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:id="@+id/constraint_layout"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <TextView
       android:id="@+id/window_metrics"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:padding="20dp"
       android:text="@string/window_metrics"
       android:textSize="20sp"
       app:layout_constraintBottom_toTopOf="@+id/layout_change"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent"
       app:layout_constraintVertical_chainStyle="packed" />

   <TextView
       android:id="@+id/layout_change"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:padding="20dp"
       android:text="@string/layout_change_text"
       android:textSize="20sp"
       app:layout_constrainedWidth="true"
       app:layout_constraintBottom_toTopOf="@+id/configuration_changed"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="@+id/window_metrics" />

   <TextView
       android:id="@+id/configuration_changed"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:padding="20dp"
       android:text="@string/configuration_changed"
       android:textSize="20sp"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="@+id/layout_change" />

</androidx.constraintlayout.widget.ConstraintLayout>

Anda kini telah membuat tata letak sederhana berdasarkan ConstraintLayout dengan tiga TextViews di dalamnya. Tampilan dibatasi agar sejajar dengan bagian tengah induk (dan layar).

  1. Buka file MainActivity.kt dan tambahkan kode berikut:

window_manager/MainActivity.kt

class MainActivity : AppCompatActivity() {
  1. Buat class dalam yang akan membantu Anda menangani hasil dari callback:
inner class LayoutStateChangeCallback : Consumer<WindowLayoutInfo> {
   override fun accept(newLayoutInfo: WindowLayoutInfo) {
       printLayoutStateChange(newLayoutInfo)
   }
}

Fungsi yang digunakan class dalam adalah fungsi sederhana yang akan mencetak informasi yang Anda dapatkan dari WindowManager menggunakan komponen UI (TextView):

private fun printLayoutStateChange(newLayoutInfo: WindowLayoutInfo) {
   binding.layoutChange.text = newLayoutInfo.toString()
   if (newLayoutInfo.displayFeatures.size > 0) {
       binding.configurationChanged.text = "Spanned across displays"
   } else {
       binding.configurationChanged.text = "One logic/physical display - unspanned"
   }
}
  1. Deklarasikan variabel lateinit WindowManager:
private lateinit var wm: WindowManager
  1. Buat variabel yang akan menangani callback menggunakan WindowManager melalui class dalam yang telah Anda buat:
private val layoutStateChangeCallback = LayoutStateChangeCallback()
  1. Tambahkan binding sehingga kita dapat mengakses berbagai tampilan:
private lateinit var binding: ActivityMainBinding
  1. Sekarang, buat fungsi yang memperluas Executor sehingga Anda dapat memberikannya ke callback sebagai parameter pertama, dan akan digunakan saat callback dipanggil. Dalam hal ini, Anda akan membuat fungsi yang berjalan di UI thread. Anda dapat membuat fungsi lain yang tidak berjalan di thread UI, sesuai keinginan Anda.
private fun runOnUiThreadExecutor(): Executor {
   val handler = Handler(Looper.getMainLooper())
   return Executor() {
       handler.post(it)
   }
}
  1. Di onCreate MainActivity, lakukan inisialisasi WindowManager lateinit:
override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   binding = ActivityMainBinding.inflate(layoutInflater)
   setContentView(binding.root)

   wm = WindowManager(this)
}

Kini instance WindowManager memiliki Activity sebagai satu-satunya parameter, dan akan menggunakan implementasi backend WindowManager default.

  1. Temukan fungsi yang telah Anda tambahkan di langkah 5. Tambahkan, langsung setelah header fungsi, baris ini:
binding.windowMetrics.text =
   "CurrentWindowMetrics: ${wm.currentWindowMetrics.bounds.flattenToString()}\n" +
       "MaximumWindowMetrics: ${wm.maximumWindowMetrics.bounds.flattenToString()}"

Di sini, Anda menetapkan nilai TextView window_metrics menggunakan nilai yang dimuat fungsi currentWindowMetrics.bounds.flattenToString() dan maximumWindowMetrics.bounds.flattenToString().

Nilai ini memberikan informasi berguna tentang metrik area yang ditempati oleh jendela. Seperti yang diilustrasikan gambar di bawah, di emulator dua layar, Anda mendapatkan CurrentWindowMetrics yang cocok dengan dimensi perangkat yang diduplikasi. Anda juga dapat melihat metrik saat aplikasi berjalan dalam mode satu layar:

b032c729d6dce292.png

Di bawah ini Anda juga dapat melihat perubahan metrik saat aplikasi dibentangkan ke seluruh layar, sehingga metrik tersebut kini memperlihatkan area jendela lebih besar yang digunakan aplikasi:

b72ca8a63b65e4c1.png

Metrik jendela saat ini dan maksimum memiliki nilai yang sama, karena aplikasi selalu berjalan dan menggunakan seluruh area tampilan yang tersedia, baik di satu layar maupun dua layar.

Di emulator perangkat foldable dengan lipatan horizontal, nilai akan berbeda saat aplikasi direntangkan ke seluruh tampilan fisik dan menggunakan Multi-Aplikasi:

5cb5270ee0e42320.png

Seperti yang dapat Anda lihat pada gambar di sebelah kiri, kedua metrik memiliki nilai yang sama, karena aplikasi berjalan menggunakan seluruh area tampilan yang tersedia, yaitu tampilan saat ini dan layar maksimum.

Tetapi, pada gambar di sebelah kanan, dengan aplikasi berjalan dalam mode Multi-Aplikasi, Anda dapat melihat bagaimana metrik saat ini menampilkan dimensi area yang dijalankan aplikasi di area tersebut (atas) di mode Multi-Aplikasi, dan Anda juga dapat melihat bagaimana metrik maksimum menampilkan area tampilan maksimum yang dimiliki perangkat.

Metrik yang disediakan oleh WindowManager sangat berguna untuk mengetahui area jendela yang digunakan atau dapat digunakan oleh aplikasi.

Sekarang Anda akan mendaftarkan perubahan tata letak, agar Anda dapat mengetahui fitur perangkat (apakah itu perangkat engsel atau lipat) dan batas fitur.

Fungsi yang harus kita gunakan memiliki tanda tangan ini:

public void registerLayoutChangeCallback (
                Executor executor,
                Consumer<WindowLayoutInfo> callback)

Fungsi ini menggunakan jenis WindowLayoutInfo. Class ini memiliki data yang perlu Anda lihat saat callback dipanggil. Class ini secara internal berisi List< DisplayFeature>, daftar ini akan menampilkan daftar DisplayFeatures yang ditemukan di perangkat yang bersimpangan dengan aplikasi. Daftar ini bisa kosong jika tidak ada fitur tampilan yang bersimpangan dengan aplikasi.

Class ini mengimplementasikan DisplayFeature dan setelah Anda mendapatkan List<DisplayFeature> sebagai hasil, Anda dapat mentransmisikan (item) ke FoldingFeature, tempat Anda akan mempelajari informasi seperti postur perangkat, jenis fitur perangkat, dan batasnya.

Mari lihat bagaimana Anda dapat menggunakan callback ini dan memvisualisasikan informasi yang diberikannya. Dengan kode yang telah Anda tambahkan di langkah sebelumnya (Buat aplikasi sampel):

  1. Ganti metode onAttachedToWindow:
override fun onAttachedToWindow() {
   super.onAttachedToWindow()
  1. Gunakan instance WindowManager yang mendaftar ke callback perubahan tata letak, menggunakan eksekutor yang Anda implementasikan sebelumnya sebagai parameter pertama:
   wm.registerLayoutChangeCallback(
       runOnUiThreadExecutor(),
       layoutStateChangeCallback
   )
}

Mari lihat tampilan informasi yang disediakan oleh callback ini. Jika menjalankan kode ini di emulator dua layar, Anda akan mendapatkan:

49a85b4d10245a9d.png

Seperti yang Anda lihat, WindowLayoutInfo kosong. Ini memiliki List<DisplayFeature> kosong, tetapi jika Anda memiliki emulator dengan engsel di tengahnya, mengapa Anda tidak mendapatkan informasi dari WindowManager?

WindowManager akan memberikan LayoutInfo data (jenis fitur perangkat, batas fitur perangkat, dan postur perangkat) tepat saat aplikasi direntangkan ke seluruh layar (fisik atau tidak). Jadi pada gambar sebelumnya, saat aplikasi berjalan pada mode satu layar, WindowLayoutInfo menjadi kosong.

Dengan mempertimbangkan hal ini, Anda akan mengetahui mode yang dijalankan aplikasi (mode satu layar atau mode rentang) sehingga Anda dapat membuat perubahan di UI/UX, memberikan pengalaman yang lebih baik bagi pengguna, yang telah disesuaikan dengan konfigurasi ini.

Pada perangkat yang tidak memiliki dua tampilan fisik (biasanya tidak memiliki engsel fisik), aplikasi dapat berjalan berdampingan, menggunakan Multi-Aplikasi. Pada perangkat ini, saat berjalan di Multi-Aplikasi, aplikasi akan berfungsi seperti halnya pada satu layar di contoh sebelumnya, dan saat berjalan menggunakan semua layar logika, aplikasi akan berlaku seperti direntangkan. Anda dapat melihatnya di gambar berikut:

ecdada42f6df1fb8.png

Seperti yang Anda lihat, saat berjalan dalam mode Multi-Aplikasi, aplikasi tidak bersimpangan dengan fitur perangkat foldable sehingga WindowManager akan menampilkan List<LayoutInfo> kosong.

Singkatnya, Anda akan mendapatkan data LayoutInfo hanya saat aplikasi bersimpangan dengan fitur perangkat (dilipat atau engsel), dan jika tidak, Anda tidak akan mendapatkan informasi apa pun. 564eb78fc85f6d3e.png

Apa yang akan terjadi saat Anda merentangkan aplikasi ke seluruh layar? Dalam emulator dua layar, LayoutInfo akan memiliki objek FoldingFeature yang memberikan data tentang fitur perangkat: HINGE, batas fitur tersebut: Rect (0, 0- 1434, 1800), dan postur (status) perangkat: FLAT

13edea3ff94baae4.png

Jenis perangkat, seperti yang disebutkan sebelumnya, dapat menggunakan dua nilai: FOLD dan HINGE, seperti yang diekspos dalam kode sumbernya:

@IntDef({
       TYPE_FOLD,
       TYPE_HINGE,
})
  • type = TYPE_HINGE. Emulator dua layar ini menduplikasi perangkat Surface Duo sesungguhnya yang memiliki engsel fisik, dan perangkat inilah yang dilaporkan WindowsManager.
  • Rect (0, 0 - 1434, 1800), mewakili kotak pembatas fitur dalam jendela aplikasi di ruang koordinat jendela. Jika membaca spesifikasi dimensi perangkat Surface Duo, Anda akan melihat bahwa engsel berada dekat batas yang dilaporkan ini (kiri, atas, kanan, bawah).
  • Ada tiga nilai berbeda yang mewakili postur perangkat (status) perangkat:
  • STATE_HALF_OPENED, engsel perangkat foldable berada di posisi tengah antara keadaan terbuka dan tertutup, dan ada sudut yang tidak datar di antara bagian layar fleksibel atau di antara panel layar fisik.
  • STATE_FLAT, perangkat foldable sepenuhnya terbuka, ruang layar yang ditampilkan kepada pengguna dalam keadaan datar.
  • STATE_FLIPPED, perangkat foldable dibalik dengan bagian layar fleksibel atau layar fisik menghadap ke arah yang berlawanan.
@IntDef({
       STATE_HALF_OPENED,
       STATE_FLAT,
       STATE_FLIPPED,
})

Emulator secara default terbuka 180 derajat sehingga postur yang ditampilkan oleh WindowManager adalah STATE_FLAT.

Jika Anda mengubah postur emulator menggunakan Sensor Virtual ke postur Setengah Terbuka, WindowManager akan memberitahukan posisi baru kepada Anda: STATE_HALF_OPENED.

7cfb0b26d251bd1.png

Anda dapat membatalkan pendaftaran dari callback ini jika tidak membutuhkannya lagi. Cukup panggil fungsi ini dari WindowManager API:

public void unregisterDeviceStateChangeCallback (Consumer<DeviceState> callback)

Tempat yang tepat untuk membatalkan pendaftaran callback adalah di metode onDestroy atau onDetachedFromWindow:

override fun onDetachedFromWindow() {
   super.onDetachedFromWindow()
   wm.unregisterLayoutChangeCallback(layoutStateChangeCallback)
}

Menggunakan WindowManager untuk menyesuaikan UI/UX

Seperti yang telah Anda lihat dalam gambar yang menunjukkan Informasi Tata Letak Jendela, informasi yang ditampilkan dipotong oleh fitur tampilan, seperti yang bisa Anda lihat lagi di sini:

4ee805070989f322.png

Ini bukan pengalaman terbaik yang Anda dapat tawarkan kepada pengguna. Anda dapat menggunakan informasi yang disediakan oleh WindowManager untuk menyesuaikan UI/UX Anda.

Seperti yang Anda lihat sebelumnya, saat aplikasi Anda direntangkan ke seluruh region tampilan yang berbeda, aplikasi Anda juga bersimpangan dengan fitur perangkat, sehingga WindowManager menyediakan Info Tata Letak Window sebagai fitur tampilan dan batas layar. Jadi, saat aplikasi direntangkan adalah saat Anda perlu menggunakan informasi tersebut dan menyesuaikan UI/UX Anda.

Yang akan Anda lakukan berikutnya adalah menyesuaikan UI/UX yang saat ini berjalan dalam waktu proses saat aplikasi Anda direntangkan sehingga tidak ada informasi penting yang terpotong/tertutup oleh fitur tampilan. Anda akan membuat tampilan yang menduplikasi fitur tampilan perangkat, dan akan digunakan sebagai referensi untuk membatasi TextView yang terpotong/tertutup, sehingga tidak ada informasi yang hilang lagi.

Untuk tujuan pembelajaran, Anda akan memberikan warna pada tampilan baru ini, sehingga Anda dapat dengan mudah melihat bahwa informasi secara spesifik berada di tempat yang sama dengan fitur tampilan perangkat yang sesungguhnya, dan dengan dimensi yang sama.

  1. Tambahkan tampilan baru yang akan Anda gunakan sebagai referensi fitur perangkat di activity_main.xml.

res/layout/activity_main.xml

<View
   android:id="@+id/device_feature"
   android:layout_width="0dp"
   android:layout_height="0dp"
   android:background="@android:color/holo_red_dark"
   android:visibility="gone" />
  1. Di MainActivity.kt, buka fungsi yang Anda gunakan untuk menampilkan informasi dari callback WindowManager dan tambahkan panggilan fungsi baru dalam kasus if-else tempat fitur tampilan Anda berada:

window_manager/MainActivity.kt

private fun printLayoutStateChange(newLayoutInfo: WindowLayoutInfo) {
   binding.windowMetrics.text =
       "CurrentWindowMetrics: ${wm.currentWindowMetrics.bounds.flattenToString()}\n" +
           "MaximumWindowMetrics: ${wm.maximumWindowMetrics.bounds.flattenToString()}"

   binding.layoutChange.text = newLayoutInfo.toString()
   if (newLayoutInfo.displayFeatures.size > 0) {
       binding.configurationChanged.text = "Spanned across displays"
       alignViewToDeviceFeatureBoundaries(newLayoutInfo)
   } else {
       binding.configurationChanged.text = "One logic/physical display - unspanned"
   }
}

Anda telah menambahkan fungsi alignViewToDeviceFeatureBoundaries yang diterima sebagai parameter WindowLayoutInfo.

  1. Di dalam fungsi baru, buat ConstraintSet untuk menerapkan batasan baru pada tampilan Anda:
private fun alignViewToDeviceFeatureBoundaries(newLayoutInfo: WindowLayoutInfo) {
   val constraintLayout = binding.constraintLayout
   val set = ConstraintSet()
   set.clone(constraintLayout)
  1. Sekarang, dapatkan batas fitur tampilan menggunakan WindowLayoutInfo:
val rect = newLayoutInfo.displayFeatures[0].bounds
  1. Sekarang, dengan WindowLayoutInfo yang disediakan dalam variabel persegi, tetapkan ukuran tinggi yang benar untuk tampilan referensi Anda:
set.constrainHeight(
   R.id.device_feature,
   rect.bottom - rect.top
)
  1. Sekarang, sesuaikan tampilan Anda dengan lebar fitur tampilan, berdasarkan koordinat kanan - koordinat kiri, sehingga Anda mengetahui lebar fitur tampilan:
set.constrainWidth(R.id.device_feature, rect.right - rect.left)
  1. Tetapkan batasan perataan ke referensi tampilan, sehingga sejajar dengan induknya di awal dan sisi atas:
set.connect(
   R.id.device_feature, ConstraintSet.START,
   ConstraintSet.PARENT_ID, ConstraintSet.START, 0
)
set.connect(
   R.id.device_feature, ConstraintSet.TOP,
   ConstraintSet.PARENT_ID, ConstraintSet.TOP, 0
)

Anda juga dapat menambahkan batasan ini langsung di xml sebagai atribut untuk tampilan, bukan di dalam kode ini.

Selanjutnya, Anda akan ingin mencakup semua penempatan fitur perangkat yang memungkinkan: perangkat yang memiliki fitur tampilan yang ditempatkan secara vertikal (seperti emulator dua layar) dan perangkat yang memilki fitur tampilan yang ditempatkan secara horizontal (seperti emulator perangkat foldable dengan lipatan horizontal).

  1. Untuk skenario pertama, top == 0 menunjukkan bahwa fitur perangkat Anda akan ditempatkan secara vertikal (seperti emulator dua layar kita):
if (rect.top == 0) {
  1. Kini saatnya Anda menerapkan margin ke tampilan referensi, sehingga margin diposisikan di posisi yang sama persis tempat fitur tampilan yang sesungguhnya.
  2. Setelah itu, terapkan batasan ke TextView yang ingin Anda tempatkan dengan lebih baik untuk menghindari fitur tampilan, sehingga batasannya mempertimbangkan fitur:
set.setMargin(R.id.device_feature, ConstraintSet.START, rect.left)
set.connect(
   R.id.layout_change, ConstraintSet.END,
   R.id.device_feature, ConstraintSet.START, 0
)

Fitur tampilan horizontal

Perangkat pengguna Anda mungkin memiliki fitur tampilan yang ditempatkan secara horizontal (seperti emulator perangkat foldable dengan lipatan horizontal).

Bergantung pada UI, Anda mungkin memiliki toolbar atau status bar untuk ditampilkan. Jadi, sebaiknya Anda menghitung tingginya sehingga Anda dapat menyesuaikan representasi fitur tampilan agar pas di UI.

Dalam aplikasi contoh, kami memiliki status bar dan toolbar:

val statusBarHeight = calculateStatusBarHeight()
val toolBarHeight = calculateToolbarHeight()

Implementasi fungsi yang sederhana untuk membuat perhitungan ini (ditempatkan di luar fungsi saat ini) adalah:

private fun calculateToolbarHeight(): Int {
   val typedValue = TypedValue()
   return if (theme.resolveAttribute(android.R.attr.actionBarSize, typedValue, true)) {
       TypedValue.complexToDimensionPixelSize(typedValue.data, resources.displayMetrics)
   } else {
       0
   }
}

private fun calculateStatusBarHeight(): Int {
   val rect = Rect()
   window.decorView.getWindowVisibleDisplayFrame(rect)
   return rect.top
}

Kembali ke fungsi utama di pernyataan else, tempat Anda menangani fitur perangkat horizontal, untuk margin, Anda dapat menggunakan tinggi status bar dan tinggi toolbar, karena batas fitur tampilan tidak mempertimbangkan elemen UI mana pun yang kita miliki, dan diambil dari koordinat (0,0). Anda harus mempertimbangkan elemen ini untuk menempatkan tampilan referensi kita di tempat yang benar:

} else {
   //Device feature is placed horizontally
   val statusBarHeight = calculateStatusBarHeight()
   val toolBarHeight = calculateToolbarHeight()
   set.setMargin(
       R.id.device_feature, ConstraintSet.TOP,
       rect.top - statusBarHeight - toolBarHeight
   )
   set.connect(
       R.id.layout_change, ConstraintSet.TOP,
       R.id.device_feature, ConstraintSet.BOTTOM, 0
   )
}

Langkah berikutnya adalah mengubah visibilitas tampilan referensi menjadi terlihat, sehingga Anda dapat melihatnya dalam sampel (berwarna merah), dan yang paling penting, batasan tersebut diterapkan. Jika tampilan hilang, tidak akan ada batasan untuk diterapkan:

set.setVisibility(R.id.device_feature, View.VISIBLE)

Langkah terakhir adalah menerapkan ConstraintSet yang Anda buat ke ConstraintLayout, untuk menerapkan semua perubahan dan penyesuaian UI:

    set.applyTo(constraintLayout)
}

Sekarang, TextView yang bertentangan dengan fitur tampilan perangkat mempertimbangkan lokasi fitur tersebut sehingga kontennya tidak pernah terpotong atau tertutup:

80993d3695a9a60.png

Di emulator dua layar (kiri), Anda dapat melihat bagaimana TextView yang menampilkan konten di seluruh layar dan yang terpotong oleh engsel tidak terpotong lagi sehingga tidak ada informasi yang hilang.

Dalam emulator perangkat foldable (kanan), Anda akan melihat garis merah terang yang mewakili lokasi fitur tampilan lipat, dan TextView kini ditempatkan di bawah fitur sehingga saat perangkat dilipat (misalnya, dalam 90 derajat dalam postur laptop) tidak ada informasi yang terpengaruh oleh fitur ini.

Jika Anda ingin tahu di mana letak fitur tampilan di emulator dua layar, karena ini adalah perangkat jenis engsel, tampilan yang mewakili fitur tertutup engsel. Namun, jika kita memindahkan aplikasi dari posisi direntangkan ke posisi tidak direntangkan, Anda akan melihat posisinya sama dengan fitur dengan tinggi dan lebar yang benar.

4dbe464ac71b498e.png

Sejauh ini, Anda telah mempelajari perbedaan antara perangkat foldable dan perangkat satu layar.

Salah satu fitur yang disediakan perangkat foldable adalah opsi untuk menjalankan dua aplikasi secara berdampingan sehingga Anda dapat menggunakannya secara efisien. Sebagai contoh, pengguna dapat menampilkan aplikasi email di satu sisi dan aplikasi kalender di sisi lainnya, atau melakukan panggilan video di satu layar dan membuat catatan di sisi lainnya. Ada begitu banyak kemungkinan!

Anda dapat memanfaatkan dua layar hanya menggunakan API yang ada dan disertakan dalam framework Android. Mari kita lihat beberapa peningkatan yang dapat Anda lakukan.

Meluncurkan aktivitas ke jendela berdampingan

Peningkatan ini memungkinkan aplikasi Anda meluncurkan Aktivitas baru di jendela berdampingan, untuk memanfaatkan beberapa area jendela secara bersamaan tanpa harus berupaya lebih.

Bayangkan Anda memiliki tombol yang jika diklik, aplikasi akan meluncurkan Aktivitas baru:

  1. Pertama, buat fungsi yang akan menangani peristiwa klik:

intent/MainActivity.kt

private fun openActivityInAdjacentWindow() {
}
  1. Di dalam fungsi, buat Intent yang akan digunakan untuk meluncurkan Aktivitas baru (dalam hal ini disebut SecondActivity. Ini hanya Aktivitas sederhana dengan TextView sebagai pesan):
val intent = Intent(this, SecondActivity::class.java)
  1. Selanjutnya, tetapkan flag yang akan meluncurkan Aktivitas baru jika layar berdampingan kosong:
intent.addFlags(
   Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT or
       Intent.FLAG_ACTIVITY_NEW_TASK
)

Yang dilakukan flag ini adalah:

  • FLAG_ACTIVITY_NEW_TASK = Jika ditetapkan, aktivitas ini akan menjadi awal tugas baru pada tumpukan histori ini.
  • FLAG_ACTIVITY_LAUNCH_ADJACENT = Flag ini digunakan untuk mode Multi-Aplikasi layar terpisah (dan juga berfungsi untuk perangkat dua layar dengan layar fisik terpisah). Aktivitas baru akan ditampilkan berdampingan dengan aktivitas yang meluncurkannya.

Saat melihat tugas baru, platform ini akan mencoba menggunakan jendela berdampingan untuk mengalokasikannya ke sana. Tugas baru akan diluncurkan di atas tugas Anda saat ini, sehingga Aktivitas baru akan diluncurkan di atas tugas Anda saat ini.

  1. Langkah terakhir adalah meluncurkan aktivitas baru menggunakan Intent yang telah kita buat:
     startActivity(intent)

Hasil aplikasi pengujian akan berperilaku seperti yang Anda lihat dalam animasi di bawah ini, tempat mengklik tombol akan meluncurkan Aktivitas baru di jendela berdampingan yang kosong.

Anda dapat melihatnya berjalan di perangkat dua layar dan di perangkat foldable yang berjalan dalam mode Multi-Aplikasi:

9696f7fa2ee1e35f.gif a2dc98dae26e3045.gif

Tarik lalu lepas

Menambahkan tarik lalu lepas ke aplikasi dapat memberikan fungsi sangat berguna yang akan disukai pengguna Anda. Fungsi ini memungkinkan aplikasi Anda memberikan konten ke aplikasi lain (menerapkan fungsi tarik), menerima konten dari aplikasi lain (menerapkan fungsi lepas), atau dapat menyertakan kedua fitur, sehingga aplikasi Anda dapat memberikan dan menerima konten dari aplikasi lain dan dari aplikasi itu sendiri (misalnya, konten ditempatkan di lokasi yang berbeda di dalam aplikasi yang sama).

Tarik lalu lepas telah tersedia di framework Android sejak API 11, tetapi belum digunakan hingga tersedianya dukungan Multi-Window pada API level 24, yang membuat tarik lalu lepas lebih sesuai, karena Anda dapat menarik lalu melepas elemen antar aplikasi yang berjalan secara berdampingan pada layar yang sama.

Sekarang, dengan diperkenalkannya perangkat foldable yang dapat memiliki lebih banyak area untuk tujuan Multi-Aplikasi atau bahkan dua layar logis yang berbeda, tarik lalu lepas akan terasa lebih sesuai. Skenario yang berguna meliputi aplikasi rencana yang menerima (lepas) teks yang menjadi tugas baru saat dilepas, atau aplikasi kalender yang menerima (lepas) konten ke slot hari/waktu dan menjadi acara, dll.

Aplikasi harus menerapkan perilaku tarik untuk menjadi konsumen data dan/atau perilaku lepas untuk menjadi pembuat data untuk menggunakan fungsi ini.

Dalam sampel, Anda akan menerapkan tarik di satu aplikasi dan lepas di aplikasi yang berbeda, tetapi tentunya Anda dapat menerapkan tarik lalu lepas di aplikasi yang sama.

Menerapkan tarik

"Aplikasi tarik" hanya akan memiliki TextView dan akan memicu tindakan tarik saat pengguna mengkliknya lama.

  1. Pertama, buat aplikasi baru dengan membuka File > New > New Project > Empty Activity.
  2. Lalu buka activity_main.xml yang telah dibuat. Di sana, ganti tata letak yang ada untuk tata letak ini:

res/layout/activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<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"
   tools:context=".MainActivity">

   <TextView
       android:id="@+id/drag_text_view"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:layout_margin="20dp"
       android:text="@string/drag_text"
       android:textSize="30sp" />
</LinearLayout>
  1. Sekarang buka file MainActivity.kt dan tambahkan tag dan panggil fungsi setOnLongClickListener:

drag/MainActivity.kt

class MainActivity : AppCompatActivity(), View.OnLongClickListener {
   private lateinit var binding: ActivityMainBinding

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       binding = ActivityMainBinding.inflate(layoutInflater)
       setContentView(binding.root)

       binding.dragTextView.tag = "text_view"
       binding.dragTextView.setOnLongClickListener(this)
   }
  1. Sekarang ganti fungsi onLongClick sehingga TextView Anda dapat menggunakan fungsi yang telah diganti untuk peristiwa onLongClickListener.
override fun onLongClick(view: View): Boolean {
  1. Pastikan parameter penerima adalah jenis View yang Anda tambahkan dengan fungsi tarik. Dalam kasus Anda, parameter ini adalah TextView:
return if (view is TextView) {
  1. Buat ClipData.item dari teks yang disimpan TextView:
val text = ClipData.Item(view.text)
  1. Sekarang tentukan MimeType yang akan kita gunakan:
val mimeType = arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN)
  1. Dengan item sebelumnya yang telah Anda buat, buat paket (instance ClipData) yang akan Anda gunakan untuk berbagi data:
val dataToShare = ClipData(view.tag.toString(), mimeType, text)

Memberikan masukan kepada pengguna sangatlah penting, sehingga sebaiknya memberikan informasi visual tentang apa yang ditarik.

  1. Buat bayangan konten yang kita tarik agar pengguna melihat konten di bawah jari mereka saat interaksi tarik berjalan:
val dragShadowBuilder = View.DragShadowBuilder(view)
  1. Sekarang, karena Anda akan ingin mengaktifkan tarik lalu lepas di berbagai aplikasi, pertama-tama Anda harus menentukan kumpulan flag yang akan mengaktifkan fungsi tersebut:
val flags =
   View.DRAG_FLAG_GLOBAL or View.DRAG_FLAG_GLOBAL_URI_READ

Sesuai dengan dokumentasi, flag ini berarti:

  • DRAG_FLAG_GLOBAL: Flag yang menunjukkan bahwa tarik dapat melewati batas jendela.
  • DRAG_FLAG_GLOBAL_URI_READ: Saat flag ini digunakan dengan DRAG_FLAG_GLOBAL, penerima tarik akan dapat meminta akses baca ke URI konten yang ada dalam objek ClipData.
  1. Terakhir, panggil fungsi startDragAndDrop di tampilan dengan komponen yang telah Anda buat, sehingga interaksi tarik dimulai:
view.startDragAndDrop(dataToShare, dragShadowBuilder, view, flags)
  1. Selesaikan dan tutup onLongClick function dan MainActivity:
         true
       } else {
           false
       }
   }
}

Menerapkan lepas

Dalam sampel, Anda membuat aplikasi sederhana yang memiliki fungsi lepas yang terpasang di EditText. Tampilan ini akan menerima data teks (yang dapat berasal dari aplikasi tarik dari TextView-nya).

EditText (atau area lepas) akan mengubah latar belakangnya sesuai dengan tahap tarik kita, sehingga Anda dapat memberikan informasi status interaksi tarik lalu lepas kepada pengguna, dan pengguna dapat melihat kapan mereka dapat melepas konten.

  1. Pertama, buat aplikasi baru dengan membuka File > New > New Project > Empty Activity.
  2. Selanjutnya, buka activity_main.xml yang telah dibuat. Ganti tata letak yang ada untuk yang ini:

res/layout/activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
tools:context=".MainActivity">

<EditText
   android:id="@+id/drop_edit_text"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:background="@android:color/holo_blue_dark"
   android:gravity="top"
   android:hint="@string/drop_text"
   android:textColor="@android:color/white"
   android:textSize="30sp" />

</RelativeLayout>
  1. Sekarang buka file MainActivity.kt dan tambahkan pemroses ke fungsi EditText setOnDragListener:

drop/MainActivity.kt

class MainActivity : AppCompatActivity(), View.OnDragListener {
   private lateinit var binding: ActivityMainBinding

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       binding = ActivityMainBinding.inflate(layoutInflater)
       setContentView(binding.root)
       binding.dropEditText.setOnDragListener(this)
   }
  1. Sekarang ganti fungsi onDrag sehingga EditText, seperti yang ditulis di atas, dapat menggunakan callback yang telah diganti ini untuk fungsi onDragListener.

Fungsi ini akan dipanggil setiap kali DragEvent baru terjadi, seperti ketika jari pengguna memasuki atau keluar dari area lepas, saat pengguna meletakkan tangannya di area lepas sehingga lepas dilakukan, atau meletakkan jarinya di luar area lepas dan membatalkan interaksi tarik lalu lepas.

override fun onDrag(v: View, event: DragEvent): Boolean {
  1. Untuk merespons berbagai DragEvents yang akan dipicu, tambahkan pernyataan when untuk menangani peristiwa yang berbeda:
return when (event.action) {
  1. Tangani ACTION_DRAG_STARTED yang terpicu saat interaksi tarik dimulai. Saat peristiwa ini dipicu, warna area lepas akan berubah sehingga pengguna tahu bahwa EditText Anda menerima konten yang dilepaskan:
DragEvent.ACTION_DRAG_STARTED -> {
       setDragStartedBackground()
       true
}
  1. Tangani peristiwa tarik ACTION_DRAG_ENTERED yang dipicu saat jari memasuki area lepas. Ubah lagi warna latar belakang area lepas untuk menunjukkan kepada pengguna bahwa area lepas sudah siap. (Tentu saja Anda bisa mengabaikan peristiwa ini dan tidak mengubah peristiwa latar belakang, ini hanya untuk tujuan informatif.)
DragEvent.ACTION_DRAG_ENTERED -> {
   setDragEnteredBackground()
   true
}
  1. Tangani peristiwa ACTION_DROP sekarang. Peristiwa ini dipicu saat pengguna meletakkan jari dengan konten tarik di area lepas, sehingga tindakan lepas dapat dilakukan.
DragEvent.ACTION_DROP -> {
   handleDrop(event)
   true
}

Kita akan melihat cara menangani tindakan lepas nanti.

  1. Selanjutnya, tangani peristiwa ACTION_DRAG_ENDED. Peristiwa ini dipicu setelah ACTION_DROP, sehingga tindakan tarik lalu lepas telah selesai.

Ini adalah saat yang tepat untuk memulihkan perubahan yang telah Anda lakukan, misalnya, tempat Anda mengubah latar belakang area lepas menjadi nilai aslinya.

DragEvent.ACTION_DRAG_ENDED -> {
   clearBackgroundColor()
   true
}
  1. Selanjutnya, tangani peristiwa ACTION_DRAG_EXITED. Peristiwa ini dipicu saat pengguna meninggalkan area lepas (saat jari berada di area lepas, tetapi kemudian keluar dari area tersebut).

Di sini, jika Anda mengubah latar belakang untuk memberi tanda memasuki area lepas, ini adalah waktu yang tepat untuk memulihkannya ke nilai sebelumnya.

DragEvent.ACTION_DRAG_EXITED -> {
   setDragStartedBackground()
   true
}
  1. Terakhir, tangani kasus else dari pernyataan when Anda dan tutup fungsi onDrag:
      else -> false
   }
}

Sekarang mari kita lihat bagaimana tindakan lepas ditangani. Sebelum Anda melihat bahwa saat peristiwa ACTION_DROP dipicu, di sinilah kita harus menangani fungsi lepas, sehingga sekarang Anda akan melihat cara melakukannya.

  1. Teruskan DragEvent sebagai parameter karena objek inilah yang menyimpan data tarik:
private fun handleDrop(event: DragEvent) {
  1. Di dalam fungsi, minta izin fungsi tarik lalu lepas. Izin ini diperlukan saat Anda melakukan tindakan tarik lalu lepas di antara aplikasi yang berbeda.
val dropPermissions = requestDragAndDropPermissions(event)
  1. Melalui parameter DragEvent, Anda dapat mengakses item clipData yang telah dibuat sebelumnya di "langkah tarik":
val item = event.clipData.getItemAt(0)
  1. Kini dengan item tarik, akses teks yang menyimpannya dan yang telah dibagikan. Ini adalah teks yang dimiliki TextView Anda dalam contoh tarik:
val dragData = item.text.toString()
  1. Setelah memiliki data nyata yang dibagikan (teks), Anda dapat menetapkannya ke area lepas (EditText) seperti saat Anda menetapkan teks menjadi EditText di kode:
binding.dropEditText.setText(dragData)
  1. Langkah terakhir adalah merilis izin tarik lalu lepas yang diminta. Jika Anda tidak melakukannya setelah tindakan lepas diselesaikan, saat Aktivitas dihancurkan, izin akan dirilis secara otomatis. Tutup fungsi dan class:
      dropPermissions?.release()
   }
}

Setelah Anda menerapkan implementasi lepas ini di aplikasi lepas sederhana, kita dapat menjalankan kedua aplikasi secara berdampingan dan melihat cara kerja tarik lalu lepas.

Pada animasi di bawah, lihat cara kerjanya dan bagaimana peristiwa tarik yang berbeda dipicu, dan yang Anda lakukan saat menanganinya (mengubah latar belakang area lepas bergantung pada DragEvent tertentu dan melepaskan konten):

d66c5c24c6ea81b3.gif

Seperti yang telah kita lihat di blok konten ini, menggunakan Jetpack WindowManager akan membantu kita menangani perangkat faktor bentuk baru, seperti perangkat foldable.

Informasi yang diberikan sangat membantu untuk menyesuaikan aplikasi dengan perangkat ini sehingga kita dapat memberikan pengalaman yang lebih baik saat aplikasi berjalan di perangkat ini.

Sebagai ringkasan, dari keseluruhan codelab, Anda telah mempelajari:

  • Pengertian perangkat foldable.
  • Perbedaan antara berbagai perangkat foldable.
  • Perbedaan antara perangkat foldable dan perangkat satu layar, serta tablet.
  • Jetpack WindowManager. Apa yang disediakan API ini?
  • Menggunakan Jetpack WindowManager dan mengadaptasi aplikasi kita ke perangkat faktor bentuk baru.
  • Meningkatkan aplikasi dengan menambahkan perubahan minimal untuk meluncurkan aktivitas ke jendela berdampingan yang kosong, serta menerapkan tarik lalu lepas yang berfungsi antar aplikasi.

Pelajari lebih lanjut