Mendukung perangkat foldable dan perangkat dua layar dengan Jetpack WindowManager

1. Sebelum memulai

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

Prasyarat

Untuk menyelesaikan codelab ini, Anda memerlukan:

Yang akan Anda lakukan

Membuat aplikasi sederhana yang:

  • Menampilkan fitur perangkat
  • Mendeteksi saat aplikasi berjalan di perangkat foldable atau perangkat dua layar
  • Menentukan status perangkat
  • Menggunakan Jetpack WindowManager untuk menggunakan perangkat faktor bentuk baru.

Yang Anda butuhkan

Android Emulator v30.0.6+ menyertakan dukungan perangkat foldable dengan sensor engsel virtual dan tampilan 3D. Ada beberapa emulator perangkat foldable yang dapat Anda gunakan, seperti yang ditunjukkan pada gambar di bawah:

ca76200cc00b6ce6.png

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

2. Perangkat satu layar vs perangkat foldable

Perangkat foldable menawarkan ukuran layar yang lebih besar dan antarmuka pengguna yang lebih fleksibel daripada yang sebelumnya tersedia di perangkat seluler. 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 area tampilan logis yang berbeda.

9ff347a7c8483fed.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.

bac1d8089687c0c2.png

Perangkat foldable dapat menawarkan postur bentang yang berbeda saat aplikasi dibentangkan (ditampilkan) ke seluruh area tampilan (menggunakan semua area 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 menghadap Anda, dan mode semi lipat yang memungkinkan Anda memvisualisasikan konten seolah-olah perangkat menggunakan gadget stan.

3. Jetpack WindowManager

Library Jetpack WindowManager membantu developer aplikasi mendukung faktor bentuk perangkat baru dan menyediakan platform API umum untuk berbagai fitur WindowManager pada versi platform lama dan baru.

Fitur utama

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

Dengan menggunakan antarmuka WindowInfoTracker, Anda dapat mengakses windowLayoutInfo() untuk mengumpulkan Flow dari WindowLayoutInfo yang berisi semua DisplayFeature yang tersedia.

4. Penyiapan

Buat project baru dan pilih template "Empty Activity":

a5ce5c7fb033ec4c.png

Biarkan semua parameter seperti default-nya.

Mendeklarasikan dependensi

Untuk menggunakan Jetpack WindowManager, tambahkan dependensi dalam file build.gradle untuk aplikasi atau modul:

app/build.gradle

dependencies {
    ext.windowmanager_version = "1.1.0"

    implementation "androidx.window:window:$windowmanager_version"
    androidTestImplementation "androidx.window:window-testing:$windowmanager_version"

    // Needed to use lifecycleScope to collect the WindowLayoutInfo flow
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2"
}

Menggunakan WindowManager

Fitur jendela dapat diakses melalui antarmuka WindowInfoTracker WindowManager.

Buka file sumber MainActivity.kt dan panggil WindowInfoTracker.getOrCreate(this@MainActivity) untuk melakukan inisialisasi instance WindowInfoTracker yang terkait dengan aktivitas saat ini:

MainActivity.kt

import androidx.window.layout.WindowInfoTracker

private lateinit var windowInfoTracker: WindowInfoTracker

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

        windowInfoTracker = WindowInfoTracker.getOrCreate(this@MainActivity)
}

Dengan instance WindowInfoTracker, dapatkan informasi tentang status jendela perangkat yang berlaku saat ini.

5. Menyiapkan UI aplikasi

Dari Jetpack WindowManager, dapatkan informasi tentang metrik jendela, tata letak, dan konfigurasi layar. Tampilkan informasi ini di tata letak aktivitas utama, dengan menggunakan TextView untuk setiap elemen tersebut.

Buat ConstraintLayout, dengan tiga TextView, yang berpusat di layar.

Buka file activity_main.xml dan tempelkan konten berikut:

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"
        tools:text="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"
        tools:text="Layout change"
        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"
        tools:text="Using one logic/physical display - unspanned"
        android:textSize="20sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/layout_change" />

</androidx.constraintlayout.widget.ConstraintLayout>

Sekarang kita akan menghubungkan elemen UI ini dalam kode menggunakan view binding. Untuk melakukannya, kita mulai mengaktifkannya di file build.gradle aplikasi:

app/build.gradle

android {
   // Other configurations

   buildFeatures {
      viewBinding true
   }
}

Sinkronkan project Gradle seperti yang disarankan oleh Android Studio dan gunakan view binding di MainActivity.kt dengan menggunakan kode berikut:

MainActivity.kt

class MainActivity : AppCompatActivity() {
    private lateinit var windowInfoTracker: WindowInfoTracker
    private lateinit var binding: ActivityMainBinding

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

        windowInfoTracker = WindowInfoTracker.getOrCreate(this@MainActivity)
    }
}

6. Memvisualisasikan informasi WindowMetrics

Di metode onCreate MainActivity, panggil fungsi untuk mendapatkan dan menampilkan informasi WindowMetrics. Tambahkan panggilan obtainWindowMetrics() di dalam metode onCreate:

MainActivity.kt

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

   windowInfoTracker = WindowInfoTracker.getOrCreate(this@MainActivity)

   obtainWindowMetrics()
}

Implementasikan metode obtainWindowMetrics:

MainActivity.kt

import androidx.window.layout.WindowMetricsCalculator

private fun obtainWindowMetrics() {
   val wmc = WindowMetricsCalculator.getOrCreate()
   val currentWM = wmc.computeCurrentWindowMetrics(this).bounds.flattenToString()
   val maximumWM = wmc.computeMaximumWindowMetrics(this).bounds.flattenToString()
   binding.windowMetrics.text =
       "CurrentWindowMetrics: ${currentWM}\nMaximumWindowMetrics: ${maximumWM}"
}

Dapatkan instance WindowMetricsCalculator melalui fungsi pendampingnya getOrCreate().

Dengan menggunakan instance WindowMetricsCalculator tersebut, tetapkan informasi ke dalam windowMetrics TextView. Gunakan nilai yang ditampilkan oleh fungsi computeCurrentWindowMetrics.bounds dan computeMaximumWindowMetrics.bounds.

Nilai ini memberikan informasi berguna tentang metrik area yang ditempati oleh jendela.

Jalankan aplikasi. Di emulator perangkat dua layar (seperti gambar di bawah), Anda akan mendapatkan CurrentWindowMetrics yang sesuai dengan dimensi perangkat yang dicerminkan emulator. Anda juga dapat melihat metrik saat aplikasi berjalan dalam mode satu layar:

f6f0deff678fd722.png

Jika aplikasi dibentangkan ke seluruh layar, metrik jendela akan berubah seperti pada gambar di bawah, sehingga kini menampilkan area jendela yang lebih besar yang digunakan oleh aplikasi:

f1ce73d7198b4990.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 membentang di seluruh layar fisik dan saat aplikasi berjalan dalam mode multi-aplikasi:

d00e53154f32d7df.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 khusus yang ditempati aplikasi berjalan (bagian atas) pada mode layar terpisah, dan Anda juga dapat melihat bahwa metrik maksimum menampilkan area tampilan maksimum yang dimiliki perangkat.

Metrik yang disediakan oleh WindowMetricsCalculator sangat berguna untuk menentukan area jendela yang digunakan atau dapat digunakan oleh aplikasi.

7. Memvisualisasikan informasi FoldingFeature

Sekarang, daftar untuk menerima perubahan tata letak jendela beserta karakteristik dan batas DisplayFeatures emulator atau perangkat.

Untuk mengumpulkan informasi dari WindowInfoTracker#windowLayoutInfo(), gunakan lifecycleScope yang ditentukan untuk setiap objek Lifecycle. Setiap coroutine yang diluncurkan dalam cakupan ini akan dibatalkan jika Lifecycle dihancurkan. Anda dapat mengakses cakupan coroutine siklus proses melalui properti lifecycle.coroutineScope atau lifecycleOwner.lifecycleScope.

Di metode onCreate MainActivity, panggil fungsi untuk mendapatkan dan menampilkan informasi WindowInfoTracker. Awali dengan menambahkan panggilan onWindowLayoutInfoChange() di dalam metode onCreate:

MainActivity.kt

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

   windowInfoTracker = WindowInfoTracker.getOrCreate(this@MainActivity)

   obtainWindowMetrics()
   onWindowLayoutInfoChange()
}

Gunakan implementasi fungsi tersebut untuk mendapatkan informasi setiap kali konfigurasi tata letak baru berubah.

Tentukan tanda tangan dan kerangka fungsi.

MainActivity.kt

private fun onWindowLayoutInfoChange() {
}

Dengan parameter yang diterima fungsi, WindowInfoTracker, dapatkan data WindowLayoutInfo-nya. WindowLayoutInfo berisi daftar DisplayFeature yang terletak dalam jendela. Misalnya, engsel atau lipatan layar dapat membentang di sepanjang jendela, yang mungkin masuk akal untuk memisahkan konten visual dan elemen interaktif menjadi dua grup (misalnya, detail daftar atau kontrol tampilan).

Hanya fitur yang ada dalam batas jendela saat ini yang dilaporkan. Posisi dan ukurannya dapat berubah jika jendela dipindahkan atau diubah ukurannya di layar.

Melalui lifecycleScope yang ditentukan di dalam dependensi lifecycle-runtime-ktx, dapatkan flowWindowLayoutInfo yang berisi daftar semua fitur tampilan. Tambahkan isi onWindowLayoutInfoChange:

MainActivity.kt

import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch

private fun onWindowLayoutInfoChange() {
    lifecycleScope.launch(Dispatchers.Main) {
        lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
            windowInfoTracker.windowLayoutInfo(this@MainActivity)
                .collect { value ->
                    updateUI(value)
                }
        }
    }
}

Fungsi updateUI dipanggil dari collect . Implementasikan fungsi ini untuk menampilkan dan mencetak informasi yang diterima dari flow WindowLayoutInfo. Periksa apakah data WindowLayoutInfo memiliki fitur tampilan atau tidak. Jika ya, fitur tampilan akan berinteraksi dengan UI aplikasi. Jika data WindowLayoutInfo tidak memiliki fitur tampilan, aplikasi akan berjalan di dalam perangkat atau mode satu layar atau dalam mode multi-aplikasi.

MainActivity.kt

import androidx.window.layout.WindowLayoutInfo

private fun updateUI(newLayoutInfo: WindowLayoutInfo) {
    binding.layoutChange.text = newLayoutInfo.toString()
    if (newLayoutInfo.displayFeatures.isNotEmpty()) {
        binding.configurationChanged.text = "Spanned across displays"
    } else {
        binding.configurationChanged.text = "One logic/physical display - unspanned"
    }
}

Jalankan aplikasi. Di emulator perangkat dua layar, Anda akan memiliki:

a6f6452155742925.png

WindowLayoutInfo kosong. Kolom ini memiliki List<DisplayFeature> yang kosong. Namun, jika Anda memiliki emulator dengan engsel di tengah, mengapa Anda tidak mendapatkan informasi dari WindowManager?

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

Dari informasi tersebut, Anda dapat mengetahui mode yang dijalankan oleh aplikasi (mode satu layar atau membentang di seluruh layar) sehingga Anda dapat membuat perubahan di UI/UX, dan memberikan pengalaman pengguna yang lebih baik yang disesuaikan dengan konfigurasi tertentu.

Pada perangkat yang tidak memiliki dua tampilan fisik (biasanya tidak memiliki engsel fisik), aplikasi dapat berjalan berdampingan menggunakan multi-aplikasi. Pada perangkat ini, saat aplikasi berjalan dalam mode multi-aplikasi, aplikasi akan bertindak seperti halnya pada satu layar di contoh sebelumnya. Dan saat aplikasi berjalan menggunakan semua layar, aplikasi akan bertindak sama seperti saat aplikasi dibentangkan. Lihat gambar berikutnya:

eacdd758eefb6c3c.png

Jika aplikasi berjalan dalam mode multi-aplikasi, WindowManager akan menyediakan List<LayoutInfo> kosong.

Singkatnya, Anda akan mendapatkan data WindowLayoutInfo tepat saat aplikasi yang berjalan menempati semua tampilan logika, yang memotong fitur perangkat (lipatan atau engsel). Dalam kasus lain, Anda tidak akan mendapatkan informasi apa pun. 32e4190913b452e4.png

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

586f15def7d23ffd.png

Mari kita lihat arti setiap kolom:

  • type = TYPE_HINGE: Emulator perangkat dua layar ini menduplikasi perangkat Surface Duo sesungguhnya yang memiliki engsel fisik, dan perangkat inilah yang dilaporkan WindowsManager.
  • Bounds [0, 0 - 1434, 1800]: Mewakili kotak pembatas fitur dalam jendela aplikasi di ruang koordinat jendela. Pada spesifikasi dimensi perangkat Surface Duo, Anda akan melihat bahwa engsel terletak di posisi yang tepat yang dilaporkan oleh batas ini (kiri, atas, kanan, bawah).
  • State: Ada dua nilai berbeda yang mewakili postur (status) perangkat.
  • HALF_OPENED: Engsel perangkat foldable berada di posisi tengah antara keadaan terbuka dan tertutup, dan ada sudut yang tidak datar antara bagian pada layar fleksibel atau antara panel layar fisik.
  • FLAT: Perangkat foldable sepenuhnya terbuka, ruang layar yang ditampilkan kepada pengguna dalam keadaan datar.

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

Jika Anda mengubah postur emulator menggunakan opsi sensor Virtual ke postur Setengah Terbuka, WindowManager akan memberitahukan posisi baru kepada Anda: HALF_OPENED.

cba02ab39d6d346b.png

Menggunakan WindowManager untuk menyesuaikan UI/UX

Seperti yang terlihat dalam gambar yang menunjukkan informasi tata letak jendela, informasi yang ditampilkan terpotong oleh fitur tampilan, hal yang sama juga terjadi di sini:

ff2caf93916f1682.png

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

Seperti yang Anda lihat sebelumnya, saat aplikasi dibentangkan ke seluruh region tampilan yang berbeda, aplikasi Anda juga bersimpangan dengan fitur perangkat, sehingga WindowManager menyediakan Informasi tata letak jendela sebagai status tampilan dan batas layar. Jadi, saat aplikasi dibentangkan adalah saat Anda perlu menggunakan informasi tersebut dan menyesuaikan UI/UX Anda.

Yang akan Anda lakukan berikutnya adalah menyesuaikan UI/UX yang saat ini berada di runtime saat aplikasi Anda dibentangkan sehingga tidak ada informasi penting yang terpotong atau tertutup oleh fitur tampilan. Anda akan membuat tampilan yang menduplikasi fitur tampilan perangkat, dan akan digunakan sebagai referensi untuk membatasi TextView yang terpotong atau tertutup, sehingga tidak ada informasi yang hilang lagi.

Untuk tujuan pembelajaran, beri warna pada tampilan baru ini agar mudah diketahui bahwa tampilan ini secara spesifik berada di tempat yang sama dengan fitur tampilan perangkat yang sesungguhnya dan dengan dimensi yang sama.

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

activity_main.xml

<!-- It's not important where this view is placed by default, it will be positioned dynamically at runtime -->
<View
    android:id="@+id/folding_feature"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:background="@android:color/holo_red_dark"
    android:visibility="gone"
    tools:ignore="MissingConstraints" />

Di MainActivity.kt, buka fungsi updateUI() yang Anda gunakan untuk menampilkan informasi dari WindowLayoutInfo yang ditentukan dan tambahkan panggilan fungsi baru dalam kasus if-else tempat fitur tampilan Anda berada:

MainActivity.kt

private fun updateUI(newLayoutInfo: WindowLayoutInfo) {
   binding.layoutChange.text = newLayoutInfo.toString()
   if (newLayoutInfo.displayFeatures.isNotEmpty()) {
       binding.configurationChanged.text = "Spanned across displays"
       alignViewToFoldingFeatureBounds(newLayoutInfo)
   } else {
       binding.configurationChanged.text = "One logic/physical display - unspanned"
   }
}

Anda telah menambahkan fungsi alignViewToFoldingFeatureBounds yang diterima sebagai parameter WindowLayoutInfo.

Buat fungsi tersebut. Di dalam fungsi, buat ConstraintSet untuk menerapkan batasan baru pada tampilan Anda. Lalu, dapatkan batas fitur tampilan menggunakan WindowLayoutInfo. Karena WindowLayoutInfo menampilkan daftar DisplayFeature yang hanya merupakan antarmuka, transmisikan ke FoldingFeature untuk mendapatkan akses ke semua informasi:

MainActivity.kt

import androidx.constraintlayout.widget.ConstraintSet
import androidx.window.layout.FoldingFeature

private fun alignViewToFoldingFeatureBounds(newLayoutInfo: WindowLayoutInfo) {
   val constraintLayout = binding.constraintLayout
   val set = ConstraintSet()
   set.clone(constraintLayout)

   // Get and translate the feature bounds to the View's coordinate space and current
   // position in the window.
   val foldingFeature = newLayoutInfo.displayFeatures[0] as FoldingFeature
   val bounds = getFeatureBoundsInWindow(foldingFeature, binding.root)

   // Rest of the code to be added in the following steps
}

Tentukan fungsi getFeatureBoundsInWindow() untuk menerjemahkan batas fitur ke ruang koordinat tampilan dan posisi saat ini di jendela.

MainActivity.kt

import android.graphics.Rect
import android.view.View
import androidx.window.layout.DisplayFeature

/**
 * Get the bounds of the display feature translated to the View's coordinate space and current
 * position in the window. This will also include view padding in the calculations.
 */
private fun getFeatureBoundsInWindow(
    displayFeature: DisplayFeature,
    view: View,
    includePadding: Boolean = true
): Rect? {
    // Adjust the location of the view in the window to be in the same coordinate space as the feature.
    val viewLocationInWindow = IntArray(2)
    view.getLocationInWindow(viewLocationInWindow)

    // Intersect the feature rectangle in window with view rectangle to clip the bounds.
    val viewRect = Rect(
        viewLocationInWindow[0], viewLocationInWindow[1],
        viewLocationInWindow[0] + view.width, viewLocationInWindow[1] + view.height
    )

    // Include padding if needed
    if (includePadding) {
        viewRect.left += view.paddingLeft
        viewRect.top += view.paddingTop
        viewRect.right -= view.paddingRight
        viewRect.bottom -= view.paddingBottom
    }

    val featureRectInView = Rect(displayFeature.bounds)
    val intersects = featureRectInView.intersect(viewRect)

    // Checks to see if the display feature overlaps with our view at all
    if ((featureRectInView.width() == 0 && featureRectInView.height() == 0) ||
        !intersects
    ) {
        return null
    }

    // Offset the feature coordinates to view coordinate space start point
    featureRectInView.offset(-viewLocationInWindow[0], -viewLocationInWindow[1])

    return featureRectInView
}

Anda dapat menggunakan informasi tentang batas fitur tampilan untuk menyetel ukuran tinggi yang benar bagi tampilan referensi dan memindahkannya dengan semestinya.

Kode lengkap untuk alignViewToFoldingFeatureBounds adalah:

MainActivity.kt - alignViewToFoldingFeatureBounds

private fun alignViewToFoldingFeatureBounds(newLayoutInfo: WindowLayoutInfo) {
    val constraintLayout = binding.constraintLayout
    val set = ConstraintSet()
    set.clone(constraintLayout)

    // Get and Translate the feature bounds to the View's coordinate space and current
    // position in the window.
    val foldingFeature = newLayoutInfo.displayFeatures[0] as FoldingFeature
    val bounds = getFeatureBoundsInWindow(foldingFeature, binding.root)

    bounds?.let { rect ->
        // Some devices have a 0px width folding feature. We set a minimum of 1px so we
        // can show the view that mirrors the folding feature in the UI and use it as reference.
        val horizontalFoldingFeatureHeight = (rect.bottom - rect.top).coerceAtLeast(1)
        val verticalFoldingFeatureWidth = (rect.right - rect.left).coerceAtLeast(1)

        // Sets the view to match the height and width of the folding feature
        set.constrainHeight(
            R.id.folding_feature,
            horizontalFoldingFeatureHeight
        )
        set.constrainWidth(
            R.id.folding_feature,
            verticalFoldingFeatureWidth
        )

        set.connect(
            R.id.folding_feature, ConstraintSet.START,
            ConstraintSet.PARENT_ID, ConstraintSet.START, 0
        )
        set.connect(
            R.id.folding_feature, ConstraintSet.TOP,
            ConstraintSet.PARENT_ID, ConstraintSet.TOP, 0
        )

        if (foldingFeature.orientation == FoldingFeature.Orientation.VERTICAL) {
            set.setMargin(R.id.folding_feature, ConstraintSet.START, rect.left)
            set.connect(
                R.id.layout_change, ConstraintSet.END,
                R.id.folding_feature, ConstraintSet.START, 0
            )
        } else {
            // FoldingFeature is Horizontal
            set.setMargin(
                R.id.folding_feature, ConstraintSet.TOP,
                rect.top
            )
            set.connect(
                R.id.layout_change, ConstraintSet.TOP,
                R.id.folding_feature, ConstraintSet.BOTTOM, 0
            )
        }

        // Set the view to visible and apply constraints
        set.setVisibility(R.id.folding_feature, View.VISIBLE)
        set.applyTo(constraintLayout)
    }
}

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

67b41810704d0011.png

Di emulator perangkat dua layar (di atas, kiri), Anda dapat melihat cara TextView yang menampilkan konten di berbagai tampilan dan yang terpotong oleh engsel tidak terpotong lagi sehingga tidak ada informasi yang hilang.

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

Jika Anda bertanya-tanya di mana fitur tampilan di emulator perangkat dua layar (karena ini adalah perangkat jenis engsel), tampilan yang mewakili fitur tersebut tertutup engsel. Namun, jika aplikasi berubah dari posisi membentang ke posisi tidak membentang, Anda akan melihat posisinya sama dengan fitur yang memiliki tinggi dan lebar yang benar.

1a309ab775c49a6a.png

8. Artefak Jetpack WindowManager lainnya

Selain artefak utama, WindowManager juga dilengkapi dengan artefak berguna lainnya yang akan membantu Anda berinteraksi dengan komponen secara berbeda, dengan mempertimbangkan lingkungan saat ini yang Anda gunakan saat membangun aplikasi.

Artefak Java

Jika Anda menggunakan bahasa pemrograman Java, bukan Kotlin, atau jika memproses peristiwa melalui callback adalah pendekatan yang lebih baik untuk arsitektur Anda, artefak Java WindowManager dapat berguna karena menyediakan API yang cocok untuk Java guna mendaftarkan dan membatalkan pendaftaran pemroses untuk peristiwa melalui callback.

Artefak RxJava

Jika sudah menggunakan RxJava (versi 2 atau 3), Anda dapat menggunakan artefak tertentu yang akan membantu Anda menjaga konsistensi dalam kode, baik Anda menggunakan Observables maupun Flowables.

9. Menguji menggunakan Jetpack WindowManager

Menguji postur perangkat foldable di emulator atau perangkat apa pun dapat sangat berguna untuk menguji cara penempatan elemen UI di sekitar FoldingFeature.

Untuk mencapainya, WindowManger dilengkapi dengan artefak yang sangat berguna untuk uji instrumentasi.

Mari kita lihat cara menggunakannya.

Bersama dengan dependensi WindowManager utama, kami telah menambahkan artefak pengujian dalam file build.gradle aplikasi: androidx.window:window-testing

Artefak window-testing dilengkapi dengan TestRule baru yang berguna, yang disebut WindowLayoutInfoPublisherRule, yang akan membantu menguji pemakaian aliran nilai WindowLayoutInfo. WindowLayoutInfoPublisherRule memungkinkan Anda mendorong berbagai nilai WindowLayoutInfo sesuai permintaan.

Untuk menggunakannya, dan membuat contoh yang bisa membantu Anda menguji UI dengan artefak baru ini, perbarui class pengujian yang dibuat oleh template Android Studio. Ganti semua kode di class ExampleInstrumentedTest dengan kode berikut:

ExampleInstrumentedTest.kt

import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.window.testing.layout.WindowLayoutInfoPublisherRule
import org.junit.Rule
import org.junit.rules.RuleChain
import org.junit.rules.TestRule
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class MainActivityTest {
    private val activityRule = ActivityScenarioRule(MainActivity::class.java)
    private val publisherRule = WindowLayoutInfoPublisherRule()

    @get:Rule
    val testRule: TestRule

    init {
        testRule = RuleChain.outerRule(publisherRule).around(activityRule)
    }
}

Aturan yang disebutkan telah dirantai dengan ActvityScenarioRule.

Untuk membuat FoldingFeature tiruan, artefak baru ini dilengkapi dengan beberapa fungsi yang sangat berguna untuk melakukannya. Berikut adalah tiruan paling sederhana yang memberikan beberapa nilai default.

Di MainActivity, TextView sejajar dengan bagian kiri fitur lipat. Buat pengujian yang memeriksa apakah tindakan ini sudah diterapkan dengan benar.

Buat pengujian bernama testText_is_left_of_Vertical_FoldingFeature:

ExampleInstrumentedTest.kt

import androidx.window.layout.FoldingFeature.Orientation.Companion.VERTICAL
import androidx.window.layout.FoldingFeature.State.Companion.FLAT
import androidx.window.testing.layout.FoldingFeature
import androidx.window.testing.layout.TestWindowLayoutInfo
import org.junit.Test

@Test
fun testText_is_left_of_Vertical_FoldingFeature() {
   activityRule.scenario.onActivity { activity ->
       val hinge = FoldingFeature(
           activity = activity,
           state = FLAT,
           orientation = VERTICAL,
           size = 2
       )

       val expected = TestWindowLayoutInfo(listOf(hinge))
       publisherRule.overrideWindowLayoutInfo(expected)
   }

   // Add Assertion with EspressoMatcher here

}

Pengujian FoldingFeature memiliki status FLAT dan orientasinya adalah VERTICAL. Kita telah menentukan ukuran tertentu karena ingin FoldingFeature palsu ditampilkan di UI dalam pengujian sehingga kita dapat melihat lokasinya di perangkat.

Kita menggunakan WindowLayoutInfoPublishRule yang kita buat sebelumnya untuk memublikasikan FoldingFeaure palsu, sehingga kita bisa mendapatkannya seperti halnya dengan data WindowLayoutInfo sebenarnya:

Langkah terakhir adalah menguji apakah elemen UI berada di tempat yang seharusnya untuk menghindari FoldingFeature. Untuk melakukannya, cukup gunakan EspressoMatchers, tambahkan pernyataan di akhir pengujian yang baru saja kita buat:

ExampleInstrumentedTest.kt

import androidx.test.espresso.assertion.PositionAssertions
import androidx.test.espresso.matcher.ViewMatchers.withId

onView(withId(R.id.layout_change)).check(
    PositionAssertions.isCompletelyLeftOf(withId(R.id.folding_feature))
)

Pengujian lengkapnya adalah:

ExampleInstrumentedTest.kt

@Test
fun testText_is_left_of_Vertical_FoldingFeature() {
    activityRule.scenario.onActivity { activity ->
        val hinge = FoldingFeature(
            activity = activity,
            state = FoldingFeature.State.FLAT,
            orientation = FoldingFeature.Orientation.VERTICAL,
            size = 2
        )
        val expected = TestWindowLayoutInfo(listOf(hinge))
        publisherRule.overrideWindowLayoutInfo(expected)
    }
    onView(withId(R.id.layout_change)).check(
        PositionAssertions.isCompletelyLeftOf(withId(R.id.folding_feature))
    )
}
val horizontal_hinge = FoldingFeature(
   activity = activity,
   state = FLAT,
   orientation = HORIZONTAL,
   size = 2
)

Sekarang Anda dapat menjalankan pengujian di perangkat atau emulator untuk memeriksa apakah aplikasi berperilaku seperti yang diharapkan. Perhatikan bahwa Anda tidak memerlukan perangkat foldable atau emulator untuk menjalankan pengujian ini.

10. Selamat!

Jetpack WindowManager membantu developer menangani perangkat faktor bentuk baru, seperti perangkat foldable.

Informasi yang disediakan WindowManager sangat membantu dalam menyesuaikan aplikasi Android ke perangkat foldable untuk memberikan pengalaman pengguna yang lebih baik.

Sebagai ringkasan, dalam codelab ini Anda telah mempelajari:

  • Pengertian perangkat foldable
  • Perbedaan antara berbagai perangkat foldable
  • Perbedaan antara perangkat foldable, perangkat satu layar, dan tablet
  • Jetpack WindowManager API
  • Menggunakan Jetpack WindowManager dan menyesuaikan aplikasi ke faktor bentuk perangkat baru
  • Menguji menggunakan Jetpack WindowManager

Pelajari lebih lanjut