處理視覺衝突 | 手勢導航 (二)

做者 / Chris Banes, Android 開發者關係團隊工程師html

咱們將在近期爲你們帶來一個關於 "手勢導航" 的系列連載,本文是連載的第二篇,若是您但願瞭解其餘手勢導航的話題,請持續關注咱們。android

上一篇文章中,咱們介紹瞭如何將應用構建到全面屏設備。然而有些交互可能致使應用的某些視圖被系統欄遮蓋,致使用戶沒法看見或操做。本文正是爲幫助您解決這個問題而撰寫——如何判斷安全的交互區域。安全

更具體一點來講,本文主要處理與系統 UI 出現視覺重疊的問題。系統 UI 包括屏幕上由系統提供的全部 UI,例如導航欄和狀態欄,另外它還包括諸如通知面板之類的內容。bash

邊襯區 (Insets)

很多 Android 開發者看到邊襯區 (insets) 每每會遠而避之,這個可能來源自他們在 Android Lollipop 時代試圖在狀態欄後面繪製 UI 的經歷,而這個經歷並不那麼使人愉悅。咱們甚至能看到在 StackOverflow 上有個一直熱門的問題就是關於這個的。app

Insets 區域負責描述屏幕的哪些部分會與系統 UI 相交 (intersect),例如導航或狀態欄。若是您的控件出如今了這些區域內,就可能被系統 UI 遮蓋。天然,咱們可使用 insets 區域來嘗試解決視覺衝突,如把視圖從屏幕邊緣向內移動到一個合適的位置。ide

在 Android 上,Insets 區域由 WindowInsets 類表示,在 AndroidX 中則使用 WindowInsetsCompat。在 Android 10 系統中處理應用佈局時,開發者須要知曉 5 個獲取 insets 區域的方法。須要使用哪一種方法取決於具體狀況,接下來就讓咱們逐一說明。佈局

系統窗口邊襯區

方法: getSystemWindowInsets()測試

系統窗口區域是最經常使用到的。自 API 1 以來,它們就以各類形式存在着,而且每當系統 UI 重疊顯示在您的應用上方時,這個方法就會被調用。常見的例子是下拉狀態欄和導航欄,或者彈出屏幕軟鍵盤 (IME)。ui

咱們來看一個使用系統窗口區域的例子。咱們有一個懸浮操做按鈕 (FAB),它位於屏幕右下角,距離屏幕邊緣 16dp (這符合設計指南中的要求)。google

<com.google.android.material.floatingactionbutton.FloatingActionButton
    android:layout_margin="16dp"
    android:layout_gravity="bottom|end" />
複製代碼

Google I/O 的官方應用中就有這種 FAB,在應用被迭代爲全屏應用前它看起來是這個樣子:

在迭代爲全面屏應用後,爲了取得更加沉浸式的體驗,咱們將日程表控件延展進了導航欄的區域。但這時能夠看到 FAB 被導航欄遮住了:

更糟的是,FAB 如今被遮蓋了,就意味着用戶可能沒法點擊它。顯然咱們要解決這種視覺衝突。當系統設置爲使用按鈕導航模式時 (即上圖例子所示),視覺衝突會更加明顯,由於這時導航欄的高度更大。在系統使用手勢導航模式時 (即導航欄變成屏幕底部的一條粗線,也就是導航條),因爲導航條有動態色彩調整功能,這個衝突可能不會那麼明顯。可是請記住,系統 UI 能夠隨時切換爲半透明遮蓋模式,因此咱們有必要完全解決這個問題。

再強調一次,您如今最好在全部的導航模式下測試您的應用。

那麼咱們如何處理這種視覺衝突呢?系統窗口區域在這就能派上用場。這套 insets 描述了系統欄佔據的區域,方便您使用對應的數值將本身的控件從系統欄下面移開。

具體到本例中,FAB 位於底部右側邊緣附近,所以咱們可使用 systemWindowInsets.bottom 和 systemWindowInsets.right 值來增長 FAB 下方和右方的邊距。

增長邊距後看到的效果以下:

本文後面會爲你們介紹具體作法。

簡而言之,系統窗口區域 insets 最適合那些須要點擊的控件,能夠確保系統欄不遮蓋住它們。

可點擊區域

方法: getTappableElementInsets()

接下來是 Android 10 中新增的可點擊區域 insets。它們與上面的系統窗口區域 insets 很是類似。可點擊區域 insets 用來界定可觸發系統點擊行爲 (tap) 的最小區域。注意,使用可點擊區域裏的數值進行佈局時,依然可能致使本身的控件與系統 UI 在視覺上重疊,這一點與系統窗口區域 insets 不一樣,使用後者的值對本身的控件進行位移後能確保不會與系統/導航欄發生視覺重疊。

這裏讓咱們仍然使用 FAB 來舉例:

注意看上圖,在導航欄模式下,FAB 不會進入導航欄佔據的高度 (48dp)。在手勢操做 (導航條) 模式,且開啓了導航條色彩適應後,雖然導航條依然有高度 (即紅色區域 16dp),但它被認爲是 "透明" 的,系統在這 16 dp 的高度內依然容許用戶點擊應用裏的控件,因此在可點擊區域 insets 中,其 bottom 值爲 0dp。如上圖所示,FAB 這時會更靠下一些。

不要在代碼中硬編碼上面提到的值 (48dp / 16 dp),由於導航欄的尺寸是會變更的,請使用 insets 獲取須要的數值。

Insets 其實並無規定 "您應在何處放置本身的控件",因此從理論上講能夠這麼作:

但這個作法顯然很差,由於 FAB 這時很是靠近導航條,雖然依然能夠點擊,但會讓用戶感受迷惑。

從實用的角度出發,在平常開發中我建議使用系統窗口區域 insets,它能夠更好地知足幾乎全部須要使用可點擊區域 insets 的用例。

系統手勢邊襯區

方法: getSystemGestureInsets() & getMandatorySystemGestureInsets()

這是在 Android 10 中新增的: 系統手勢區域邊襯區 (insets)。Android 10 帶來了新的手勢導航模式,容許用戶經過手勢動做,而不是導航按鈕來進行導航:

  1. 從屏幕左/右邊緣向中間滑動,至關於後退按鈕 (Back)。
  2. 從屏幕底部開始向上滑動,可讓用戶切換最近使用的應用 (Recent)。

在系統手勢區域中,系統手勢操做優先於應用本身的手勢操做。您可能已經注意到系統手勢區域有兩個獲取方法。這是由於 getMandatorySystemGestureInsets() 只包含強制性系統手勢區域,是系統手勢區域的子集。這裏咱們分開來講:

系統手勢邊襯區

首先是系統手勢邊襯區。在這些區域內,系統手勢優先於應用手勢。在 Android 10 上,系統手勢區域以下:

△ 左/右側的後退操做區域寬 40dp,下方的主屏操做區域高 60dp
若是您有須要滑動操做的控件出如今了系統手勢區域內,就可使用對應的數值來將這些控件挪開。常見的例子包括底部導航菜單 ( Bottom Sheets)、遊戲裏的滑動交互、多圖展現 ( ViewPager) 等。

強制系統手勢邊襯區

強制系統手勢邊襯區是系統手勢邊襯區的子集,之因此稱之爲 "強制區域",是由於應用沒法修改這些區域 。關於如何修改系統手勢區域,請參考咱們接下來的文章《如何處理手勢衝突 | 手勢導航連載 (三)》。

強制系統手勢邊襯區只包含那些系統保留的區域,在這些區域內系統手勢操做永遠優先。在 Android 10 上,當前惟一的強制區域是屏幕底部的主屏手勢區域,系統保留這個區域就可讓用戶在任什麼時候候均可以退出當前應用:

△ 底部 60dp 即爲強制系統手勢邊襯區

穩定顯示邊襯區

方法: getStableInsets()

這也是咱們今天提到的 5 個 inset 方法的最後一個。嚴格來講,這個方法與手勢導航關係不大,可是爲了知識的完整性,咱們這裏快速介紹一下這個方法。

和系統窗口邊襯區相似,穩定顯示區域是系統 UI 可能在您的應用上顯示的位置。在有些顯示模式下 (好比放鬆模式和沉浸模式),系統 UI 可能會根據狀況在可見與不可見之間切換 (如遊戲、照片瀏覽、視頻播放器等)。這時使用穩定顯示區域就能夠確保本身的控件不會被 "忽然出現" 的系統 UI 擋住。

處理邊襯區衝突

但願您如今對不一樣類型的 insets 區域有了更深的瞭解,下面咱們來看看您須要如何在應用中實際使用它們。

訪問 WindowInsets 主要是經過 setOnApplyWindowInsetsListener 這個方法。咱們來看一下例子,咱們想給某個控件增長一些邊距,讓它不被導航欄遮擋:

ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
    v.updatePadding(bottom = insets.systemWindowInsets.bottom)
    // Return the insets so that they keep going down the view hierarchy
    insets
}
複製代碼

在這裏,咱們僅將系統窗口區域的底部邊距值賦給了控件的底邊距。

注意: 若是您要在 ViewGroup 上執行此操做,則可能要對其進行設置 android:clipToPadding="false"。這是由於默認狀況下,全部視圖都會在填充區域內裁剪圖形。該屬性一般與 RecyclerView 一塊兒使用,咱們將在之後的文章中對其進行詳細介紹。

可是,請確保 Listener 裏的計算操做有冪等性,即屢次進行該計算所獲得的結果應該相同。如下是一個錯誤的例子:

ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
    v.updatePadding(bottom = v.paddingBottom + insets.systemWindowInsets.bottom)
    insets
}
複製代碼

請不要在計算邊距時使用自加運算 (+=)。由於這個計算可能會重複屢次,自加運算會致使結果不符合預期。

使用 Jetpack

使用 insets 時,我建議始終用 Jetpack 中的 WindowInsetsCompat 類,不管您須要的最低 SDK 版本是什麼。多年來,WindowInsets API 已獲得改進和擴展,而 compat 版本在全部的 API 級別上都提供了一致的 API 和行爲。

在 Android 10 中新增的 insets 方面,compat 版本的方法在全部 API 級別的設備上都能獲得正確的結果。要訪問 AndroidX 中的新 API,請確保更新到 androidx.core:core:1.2.0-xxx (目前爲 Alpha 版) 或更高版本。

更進一步

本文提到的是使用 WindowInsets[Compat] API 的最簡單方法,但它們可能會讓您的代碼很是冗長和重複。我在今年早些時候寫了一篇博文,詳細介紹了一些使用綁定轉換操做顯著提升效率的作法。

在本次連載的下一篇文章《如何處理手勢衝突 | 手勢導航連載 (三)》中,咱們將爲你們介紹如何處理應用與系統的手勢導航衝突,敬請保持關注。

點擊這裏進一步瞭解 Android 手勢導航

相關文章
相關標籤/搜索