詳解 | 為可摺疊裝置構建響應式 UI

Android開發者發表於2021-09-26
為可摺疊裝置和大屏裝置優化您的應用

Android 裝置的螢幕尺寸日新月異,隨著平板和可摺疊裝置的普及度越來越高,在開發響應式使用者介面時,瞭解您應用的視窗尺寸和狀態顯得尤為重要。Jetpack WindowManager 現已進入 beta 測試階段,這個庫提供了與 Android 框架中 WindowManager 比較相似的功能,包括了對支援響應式 UI、檢測螢幕改變的回撥介面卡和測試視窗 API 的支援。但 Jetpack WindowManager 還新增了對可摺疊裝置和 ChromeOS 這類視窗環境的支援。

新的 WindowManager API 包含了以下內容:

  • WindowLayoutInfo: 包含了視窗的顯示特性,例如該視窗是否可摺疊或包含鉸鏈
  • FoldingFeature: 讓您能夠監聽可摺疊裝置的摺疊狀態得以判斷裝置的姿態
  • WindowMetrics: 提供當前視窗或全部視窗的顯示指標

Jetpack WindowManager 不與 Android 繫結,這讓 API 能夠迅速地迭代以支援快速發展的市場,還讓開發者們能夠通過更新庫而不必等待 Android 版本更新來獲得支援。

現在,Jetpack WindowManager 庫已進入 beta 測試階段,我們鼓勵所有開發者來使用 Jetpack WindowManager,其與裝置無關 API、測試 API 以及它引入的 WindowMetrics,使您的應用能夠輕鬆響應視窗尺寸的變化。已經進入 beta 測試階段,意味著您可以安心地專注於在這些裝置上打造激動人心的體驗,Jetpack WindowManager 最低支援到 API 14。

關於 Jetpack WindowManager

Jetpack WindowManager 是一個以 Kotlin 優先的現代化庫,它支援不同形態的新裝置,並提供 "類 AppCompat" 的功能以構建具有響應式 UI 的應用。

摺疊狀態

支援可摺疊裝置是 Jetpack WindowManager 庫最直觀的功能。當裝置的摺疊狀態變化時,應用將收到相應的事件,進而更新 UI 介面以支援新的使用者互動。

△ 在 Samsung Galaxy Z Fold2 上執行的 Google Duo

△ 在 Samsung Galaxy Z Fold2 上執行的 Google Duo

您可以通過 Google Duo 學習案例 來了解如何支援可摺疊裝置。

摺疊狀態有兩種,分別是 FLAT (展平) 和 HALF_OPENED (半開)。對於 FLAT,您可以認為表面是完全平整開啟的,儘管有些情況下它有可能被鉸鏈分割。對於 HALF_OPENED,視窗中有至少兩個邏輯區域。我們在下方用圖片說明了每種狀態各自可能的情況。

△ 摺疊狀態: FLAT 和 HALF-OPENED

△ 摺疊狀態: FLAT) 和 HALF-OPENED)

在應用活躍的狀態下,可以通過 Kotlin 資料流收集事件來獲得摺疊狀態改變的資訊。

我們通過 lifecycleScope 來控制事件收集的開始和結束,正如文章《設計 repeatOnLifeCycle API 背後的故事》和示例程式碼所述:

lifecycleScope.launch(Dispatchers.Main) {
    // 傳遞給 repeatOnLifecycle 的程式碼塊將在生命週期進入 STARTED 時執行
    // 並在生命週期為 STOPPED 時取消
    // repeatOnLifecycle 將會在生命週期再次進入 STARTED 時自動重啟程式碼塊
    lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
        // 當生命週期處於 STARTED 時安全地從 windowInfoRepository 中收集資料
        // 當生命週期進入 STOPPED 時停止收集資料
        windowInfoRepository.windowLayoutInfo
            .collect { newLayoutInfo ->
                updateStateLog(newLayoutInfo)
                updateCurrentState(newLayoutInfo)
            }
    }
}

當使用者可以看到應用時,應用可以使用其接收到的 WindowLayoutInfo 物件中包含的資訊更新佈局。

FoldingFeature 包括了諸如鉸鏈 方向),及摺疊功能是否建立了兩個邏輯螢幕區域 (isSeparating) 屬性) 這類資訊。我們能使用這些值來檢查裝置是否處於桌面模式 (螢幕半開並且鉸鏈處於水平方向):

△ 裝置處於 TableTop 模式

△ 裝置處於 TableTop 模式

private fun isTableTopMode(foldFeature: FoldingFeature) =
    foldFeature.isSeparating && 
            foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL

或者書本模式 (螢幕半開並且鉸鏈處於垂直方向):

△ 裝置處於 Book 模式

△ 裝置處於 Book 模式

private fun isBookMode(foldFeature: FoldingFeature) =
    foldFeature.isSeparating &&
            foldFeature.orientation == FoldingFeature.Orientation.VERTICAL

請參閱: 可摺疊裝置中的桌面模式,文內示例介紹瞭如何在媒體播放器應用中實現這樣的功能。

注意: 在主執行緒/UI 執行緒中收集事件這點十分重要,這能避免在 UI 和事件處理之間的同步問題。

支援響應式 UI

Android 裝置的螢幕尺寸變化十分頻繁,因此著手設計能夠完全自適應和響應式的 UI 非常重要。Jetpack WindowManager 庫中包含的另一個功能——能夠檢索當前視窗和最大視窗的指標資訊。這和 API 30 當中的 WindowMetrics API 類似,但它向後相容到 API 14。

Jetpack WindowManager 提供了兩種途徑來檢索 WindowMetrics 資訊,通過資料流事件中的流或者通過 WindowMetricsCalculator 類進行同步處理。

當在編寫檢視程式碼時,使用非同步 API 可能比較困難 (比如 onMeasure)),此時可以使用 WindowMetricsCalculator。

val windowMetrics = 
    WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(activity)

另一個使用場景是用於測試中 (詳見下面的測試一節)。

在處理應用 UI 的高階用法中,通過該庫提供的 WindowInfoRepository#currentWindowMetrics) 能夠在視窗尺寸變更時收到通知,這與是否觸發配置變更無關。

這個例子是關於如何根據可用區域來切換您的佈局:

// 因為 repeatOnLifecycle 是掛起函式,所以建立一個新的協程
lifecycleScope.launch(Dispatchers.Main) {
   // 傳遞給 repeatOnLifecycle 的程式碼塊將在生命週期進入 STARTED 時執行
    // 並在生命週期為 STOPPED 時取消
    // 它將會在生命週期再次進入 STARTED 時自動重啟
   lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
       // 當生命週期處於 STARTED 時安全地從 windowInfoRepository 中收集資料
       // 當生命週期進入 STOPPED 時停止收集資料
       windowInfoRepository.currentWindowMetrics
           .collect { windowMetrics ->
               val currentBounds = windowMetrics.bounds
               Log.i(TAG, "New bounds: {$currentBounds}")
               // 我們可以根據需要在這裡更新佈局
           }
   }
}

回撥介面卡

要在 Java 程式語言中使用這個庫或者使用回撥介面,請在您的應用中新增 androidx.window:window-java 依賴。該元件提供了 WindowInfoRepositoryCallbackAdapter,您可以通過它註冊 (取消註冊) 一個用以接收裝置姿態及視窗指標資訊更新的回撥。

public class SplitLayoutActivity extends AppCompatActivity {

   private WindowInfoRepositoryCallbackAdapter windowInfoRepository;
   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());

       windowInfoRepository =
               new WindowInfoRepositoryCallbackAdapter(WindowInfoRepository.getOrCreate(this));
   }

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

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

   class LayoutStateChangeCallback implements Consumer<WindowLayoutInfo> {
       @Override
       public void accept(WindowLayoutInfo windowLayoutInfo) {
           binding.splitLayout.updateWindowLayout(windowLayoutInfo);
       }
   }
}

測試

開發者們講到,更健壯的測試 API 對於維護 LTS (長期支援) 是十分關鍵的。讓我們來聊聊如何在普通裝置上測試可摺疊裝置姿態。

現在,我們已經知道 Jetpack WindowManager 庫可以在裝置姿態改變時,向您的應用傳送通知,以便您修改應用的佈局。

該庫在 androidx.window:window-testing 中提供了 WindowLayoutInfoPublisherRule 讓您能夠釋出一個 WindowInfoLayout 以支援測試 FoldingFeature:

import androidx.window.testing.layout.FoldingFeature
import androidx.window.testing.layout.WindowLayoutInfoPublisherRule

我們可以在測試中虛擬一個 FoldingFeature:

val feature = FoldingFeature(
   activity = activity,
   center = center,
   size = 0,
   orientation = VERTICAL,
   state = HALF_OPENED
)
val expected =
   WindowLayoutInfo.Builder().setDisplayFeatures(listOf(feature)).build()

publisherRule.overrideWindowLayoutInfo(expected)

然後使用 WindowLayoutInfoPublisherRule 來發布它:

val publisherRule = WindowLayoutInfoPublisherRule()

publisherRule.overrideWindowLayoutInfo(expected)

最後,使用可用的 Espresso 匹配器 來檢查我們正在測試的 Activity 的佈局是否符合預期。

下面這個測試中釋出了一個處於 HALF_OPENED 狀態並且鉸鏈垂直於螢幕中心的 FoldingFeature:

@Test
fun testDeviceOpen_Vertical(): Unit = testScope.runBlockingTest {
   activityRule.scenario.onActivity { activity ->
       val feature = FoldingFeature(
           activity = activity,
           orientation = VERTICAL,
           state = HALF_OPENED
       )
       val expected =
           WindowLayoutInfo.Builder().setDisplayFeatures(listOf(feature)).build()

       val value = testScope.async {
           activity.windowInfoRepository().windowLayoutInfo.first()
       }
       publisherRule.overrideWindowLayoutInfo(expected)
       runBlockingTest {
           Assert.assertEquals(
               expected,
               value.await()
           )
       }
   }

    // 檢查在有垂直摺疊特性時 start_layout 在 end_layout 的左側
    // 這需要在足夠大的螢幕上執行測試以適應螢幕上的兩個檢視
   onView(withId(R.id.start_layout))
       .check(isCompletelyLeftOf(withId(R.id.end_layout)))
}

檢視示例程式碼

Github 上的 最新示例 展示瞭如何使用 Jetpack WindowManager 庫從 WindowLayoutInfo 流收集資訊,或者通過向 WindowInfoRepositoryCallbackAdapter 註冊回撥來獲取顯示姿態資訊。

該例項還包含一些測試,它們可以在任何裝置或模擬器中執行。

在您的應用中使用 WindowManager

可摺疊裝置及雙屏裝置不再僅僅是實驗性的或前瞻的——大螢幕空間和額外的裝置姿態已經被證實是具有使用者價值的,而且現在有更多的裝置可供您的使用者選擇。可摺疊裝置和雙屏裝置代表了智慧手機的自然進化。對於 Android 開發者來說,這提供了一個進入正在增長的高階市場的機會,感謝裝置製造商們重新開始關注大屏裝置。

我們去年推出了 Jetpack WindowManager alpha01 版本。該庫自那時起開始穩步地發展,早期的反饋讓其有了很大的改進。現在,它已經擁抱了 Android 的 Kotlin 優先理念,從回撥驅動模型逐漸過渡到協程和資料流。隨著 WindowManager 進入測試階段,API 已經穩定,我們強烈建議使用它。

更新並不僅限於此。我們計劃為該庫新增更多功能,並使其發展成為與 AppCompat 解綁的系統 UI 庫,使開發者能夠在所有的 Android 裝置上輕鬆實現現代化的、響應式的 UI。

歡迎您 點選這裡 向我們提交反饋,或分享您喜歡的內容、發現的問題。您的反饋對我們非常重要,感謝您的支援!

相關文章