Membuat aplikasi Anda fold aware

Dengan layar besar yang dibentangkan dan bentuk lipatan unik memungkinkan pengalaman pengguna baru di perangkat foldable. Agar aplikasi Anda menjadi fold aware, gunakan library Jetpack WindowManager yang menyediakan platform API untuk fitur jendela perangkat foldable, seperti lipatan dan engsel. Jika aplikasi Anda fold-aware, aplikasi dapat menyesuaikan tata letaknya untuk menghindari penempatan konten penting di area lipatan atau engsel serta menggunakan lipatan dan engsel sebagai pemisah alami.

Informasi jendela

Antarmuka WindowInfoTracker di Jetpack WindowManager mengekspos informasi tata letak jendela. Metode windowLayoutInfo() antarmuka menampilkan aliran data WindowLayoutInfo yang memberi tahu aplikasi Anda tentang status lipat perangkat foldable. Metode getOrCreate() WindowInfoTracker membuat instance WindowInfoTracker.

WindowManager menyediakan dukungan untuk mengumpulkan data WindowLayoutInfo menggunakan Alur Kotlin dan callback Java.

Alur Kotlin

Untuk memulai dan menghentikan pengumpulan data WindowLayoutInfo, Anda dapat menggunakan coroutine berbasis siklus proses yang dapat dimulai ulang tempat blok kode repeatOnLifecycle dijalankan saat siklus proses setidaknya dalam status STARTED dan dihentikan saat siklus proses dalam status STOPPED. Eksekusi blok kode otomatis dimulai ulang saat siklus proses dalam status STARTED lagi. Pada contoh berikut, blok kode mengumpulkan dan menggunakan data WindowLayoutInfo:

class DisplayFeaturesActivity : AppCompatActivity() {

    private lateinit var binding: ActivityDisplayFeaturesBinding

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

        binding = ActivityDisplayFeaturesBinding.inflate(layoutInflater)
        setContentView(binding.root)

        lifecycleScope.launch(Dispatchers.Main) {
            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                WindowInfoTracker.getOrCreate(this@DisplayFeaturesActivity)
                    .windowLayoutInfo(this@DisplayFeaturesActivity)
                    .collect { newLayoutInfo ->
                        // Use newLayoutInfo to update the layout.
                    }
            }
        }
    }
}

Callback Java

Lapisan kompatibilitas callback yang disertakan dalam dependensi androidx.window:window-java memungkinkan Anda mengumpulkan update WindowLayoutInfo tanpa menggunakan Flow Kotlin. Artefak ini menyertakan class WindowInfoTrackerCallbackAdapter yang menyesuaikan WindowInfoTracker untuk mendukung pendaftaran callback (dan pembatalan pendaftaran) untuk menerima update WindowLayoutInfo, misalnya:

public class SplitLayoutActivity extends AppCompatActivity {

    private WindowInfoTrackerCallbackAdapter windowInfoTracker;
    private ActivitySplitLayoutBinding binding;
    private final LayoutStateChangeCallback layoutStateChangeCallback =
            new LayoutStateChangeCallback();

   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);

       binding = ActivitySplitLayoutBinding.inflate(getLayoutInflater());
       setContentView(binding.getRoot());

       windowInfoTracker =
                new WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(this));
   }

   @Override
   protected void onStart() {
       super.onStart();
       windowInfoTracker.addWindowLayoutInfoListener(
                this, Runnable::run, layoutStateChangeCallback);
   }

   @Override
   protected void onStop() {
       super.onStop();
       windowInfoTracker
           .removeWindowLayoutInfoListener(layoutStateChangeCallback);
   }

   class LayoutStateChangeCallback implements Consumer<WindowLayoutInfo> {
       @Override
       public void accept(WindowLayoutInfo newLayoutInfo) {
           SplitLayoutActivity.this.runOnUiThread( () -> {
               // Use newLayoutInfo to update the layout.
           });
       }
   }
}

Dukungan RxJava

Jika Anda sudah menggunakan RxJava (versi 2 atau 3), Anda dapat memanfaatkan artefak yang memungkinkan penggunaan Observable atau Flowable untuk mengumpulkan update WindowLayoutInfo tanpa menggunakan Flow Kotlin.

Lapisan kompatibilitas yang disediakan oleh dependensi androidx.window:window-rxjava2 dan androidx.window:window-rxjava3 menyertakan metode WindowInfoTracker#windowLayoutInfoFlowable() dan WindowInfoTracker#windowLayoutInfoObservable(), yang memungkinkan aplikasi Anda menerima update WindowLayoutInfo, misalnya:

class RxActivity: AppCompatActivity {

    private lateinit var binding: ActivityRxBinding

    private var disposable: Disposable? = null
    private lateinit var observable: Observable<WindowLayoutInfo>

   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);

       binding = ActivitySplitLayoutBinding.inflate(getLayoutInflater());
       setContentView(binding.getRoot());

        // Create a new observable
        observable = WindowInfoTracker.getOrCreate(this@RxActivity)
            .windowLayoutInfoObservable(this@RxActivity)
   }

   @Override
   protected void onStart() {
       super.onStart();

        // Subscribe to receive WindowLayoutInfo updates
        disposable?.dispose()
        disposable = observable
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe { newLayoutInfo ->
            // Use newLayoutInfo to update the layout
        }
   }

   @Override
   protected void onStop() {
       super.onStop();

        // Dispose the WindowLayoutInfo observable
        disposable?.dispose()
   }
}

Fitur layar perangkat foldable

Class WindowLayoutInfo Jetpack WindowManager membuat fitur jendela layar tersedia sebagai daftar elemen DisplayFeature.

FoldingFeature adalah jenis DisplayFeature yang memberikan informasi tentang layar perangkat foldable, termasuk informasi berikut:

  • state: Status lipatan perangkat, FLAT atau HALF_OPENED
  • orientation: Orientasi lipatan atau engsel, HORIZONTAL atau VERTICAL
  • occlusionType: Apakah lipatan atau engsel menyembunyikan bagian layar, NONE atau FULL
  • isSeparating: Apakah lipatan atau engsel membuat dua area tampilan logis, benar (true) atau salah (salah)

Perangkat foldable HALF_OPENED selalu melaporkan isSeparating sebagai benar (true) karena layar dipisahkan menjadi dua area tampilan. Selain itu, isSeparating selalu bernilai benar (true) pada perangkat dua layar saat aplikasi membentang pada kedua layar.

Properti bounds FoldingFeature (diwarisi dari DisplayFeature) mewakili persegi panjang pembatas fitur lipat seperti lipatan atau engsel. Batas dapat digunakan untuk memosisikan elemen di layar yang terkait dengan fitur.

Gunakan state FoldingFeature untuk menentukan apakah perangkat menggunakan postur di atas meja atau buku, dan sesuaikan tata letak aplikasi Anda, misalnya:

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    lifecycleScope.launch(Dispatchers.Main) {
        // The block passed to repeatOnLifecycle is executed when the lifecycle
        // is at least STARTED and is cancelled when the lifecycle is STOPPED.
        // It automatically restarts the block when the lifecycle is STARTED again.
        lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
            // Safely collects from windowInfoRepo when the lifecycle is STARTED
            // and stops collection when the lifecycle is STOPPED
            WindowInfoTracker.getOrCreate(this@MainActivity)
                .windowLayoutInfo(this@MainActivity)
                .collect { layoutInfo ->
                    // New posture information
                    val foldingFeature = layoutInfo.displayFeatures
                        .filterIsInstance()
                        .firstOrNull()
                    when {
                            isTableTopPosture(foldingFeature) ->
                                enterTabletopMode(foldingFeature)
                            isBookPosture(foldingFeature) ->
                                enterBookMode(foldingFeature)
                            isSeparating(foldingFeature) ->
                            // Dual-screen device
                            if (foldingFeature.orientation == HORIZONTAL) {
                                enterTabletopMode(foldingFeature)
                            } else {
                                enterBookMode(foldingFeature)
                            }
                            else ->
                                enterNormalMode()
                        }
                }

        }
    }
}

@OptIn(ExperimentalContracts::class)
fun isTableTopPosture(foldFeature : FoldingFeature?) : Boolean {
    contract { returns(true) implies (foldFeature != null) }
    return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
            foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
}

@OptIn(ExperimentalContracts::class)
fun isBookPosture(foldFeature : FoldingFeature?) : Boolean {
    contract { returns(true) implies (foldFeature != null) }
    return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
            foldFeature.orientation == FoldingFeature.Orientation.VERTICAL
}

@OptIn(ExperimentalContracts::class)
fun isSeparating(foldFeature : FoldingFeature?) : Boolean {
    contract { returns(true) implies (foldFeature != null) }
    return foldFeature?.state == FoldingFeature.State.FLAT && foldFeature.isSeparating
}

Java

private WindowInfoTrackerCallbackAdapter windowInfoTracker;
private final LayoutStateChangeCallback layoutStateChangeCallback =
                new LayoutStateChangeCallback();

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    ...
    windowInfoTracker =
            new WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(this));
}

@Override
protected void onStart() {
    super.onStart();
    windowInfoTracker.addWindowLayoutInfoListener(
            this, Runnable::run, layoutStateChangeCallback);
}

@Override
protected void onStop() {
    super.onStop();
    windowInfoTracker.removeWindowLayoutInfoListener(layoutStateChangeCallback);
}

class LayoutStateChangeCallback implements Consumer<WindowLayoutInfo> {
    @Override
    public void accept(WindowLayoutInfo newLayoutInfo) {
        // Use newLayoutInfo to update the Layout
        List<DisplayFeature> displayFeatures = newLayoutInfo.getDisplayFeatures();
        for (DisplayFeature feature : displayFeatures) {
            if (feature instanceof FoldingFeature) {
                if (isTableTopPosture((FoldingFeature) feature)) {
                    enterTabletopMode(feature);
                } else if (isBookPosture((FoldingFeature) feature)) {
                    enterBookMode(feature);
                } else if (isSeparating((FoldingFeature) feature)) {
                    // Dual-screen device
                    if (((FoldingFeature) feature).getOrientation() ==
                              FoldingFeature.Orientation.HORIZONTAL) {
                        enterTabletopMode(feature);
                    } else {
                        enterBookMode(feature);
                    }
                } else {
                    enterNormalMode();
                }
            }
        }
    }
}

private boolean isTableTopPosture(FoldingFeature foldFeature) {
    return (foldFeature != null) &&
           (foldFeature.getState() == FoldingFeature.State.HALF_OPENED) &&
           (foldFeature.getOrientation() == FoldingFeature.Orientation.HORIZONTAL);
}

private boolean isBookPosture(FoldingFeature foldFeature) {
    return (foldFeature != null) &&
           (foldFeature.getState() == FoldingFeature.State.HALF_OPENED) &&
           (foldFeature.getOrientation() == FoldingFeature.Orientation.VERTICAL);
}

private boolean isSeparating(FoldingFeature foldFeature) {
    return (foldFeature != null) &&
           (foldFeature.getState() == FoldingFeature.State.FLAT) &&
           (foldFeature.isSeparating() == true);
}

Pada perangkat dua layar, gunakan tata letak yang didesain untuk postur di atas meja dan postur buku meskipun status FoldingFeature adalah FLAT.

Jangan tempatkan kontrol UI terlalu dekat dengan lipatan atau engsel jika isSeparating benar (true) karena kontrol akan sulit dijangkau. Gunakan occlusionType untuk menentukan apakah akan menempatkan konten dalam bounds fitur lipat.

Perubahan ukuran jendela

Area tampilan aplikasi dapat berubah sebagai akibat dari perubahan konfigurasi perangkat—misalnya, saat perangkat dilipat atau dibentangkan, diputar, atau jendela diubah ukurannya dalam mode multi-aplikasi.

Class WindowMetricsCalculator Jetpack WindowManager memungkinkan Anda mengambil metrik jendela saat ini dan maksimum. Seperti WindowMetrics platform yang diperkenalkan di API level 30, WindowMetrics WindowManager menyediakan batas jendela, tetapi API kompatibel sampai dengan API level 14.

Gunakan WindowMetrics dalam metode onCreate() atau onConfigurationChanged() aktivitas untuk mengonfigurasi tata letak aplikasi Anda bagi ukuran jendela saat ini, misalnya:

Kotlin

override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)
    val windowMetrics = WindowMetricsCalculator.getOrCreate()
        .computeCurrentWindowMetrics(this@MainActivity)
    val bounds = windowMetrics.getBounds()
    ...
}

Java

@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    final WindowMetrics windowMetrics = WindowMetricsCalculator.getOrCreate()
        .computeCurrentWindowMetrics(this);
    final Rect bounds = windowMetrics.getBounds();
    ...
}

Lihat juga Mendukung berbagai ukuran layar.

Referensi lainnya

Contoh

Codelab