如何處理手勢衝突 | 手勢導航連載 (三)

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

咱們將在近期爲你們帶來一個關於 "手勢導航" 的系列連載,本文是手勢導航連載的第三篇,若是您但願查看前兩篇文章,請點擊下方連接 :java

上一篇文章中,咱們討論完了從邊到邊繪製應用內容。從這一篇文章開始咱們將介紹如何處理您的應用和 Android 10 中新引入的系統交互手勢之間的衝突。android

首先讓咱們來理解一下什麼是 "手勢衝突 (gesture conflict)"。咱們來看一個例子,好比下面這個音樂播放應用,該應用容許用戶經過拖動進度條 (SeekBar) 來快進或快退當前歌曲。git

不幸的是, 進度條太靠近主屏手勢區域 (Home Screen Gesture Area),因此當用戶在該區域滑動時,系統把它錯誤地判斷爲用戶是要執行快速切換應用的操做,這也會讓用戶感到困惑。支持手勢導航的任何屏幕邊緣區域均可能發生相似狀況。有不少可能致使衝突的例子,例如: 導航抽屜 ( DrawerLayout)、多圖展現 ( ViewPager)、進度條 ( SeekBar),甚至在列表上進行 滑動操做也有可能出現衝突。

那麼,如何解決這個問題呢?咱們準備了一張流程圖幫助你們快速作出決策:github

△ 請點擊圖片放大查看

註解: 非粘性沉浸模式: 用戶能夠經過在系統欄上滑動來退出沉浸模式。 粘性沉浸模式: 用戶能夠經過在系統欄上滑動來暫時退出沉浸模式bash

這裏咱們向您進一步解釋一下流程圖裏的內容。app

問題 1: 應用須要隱藏導航欄或狀態欄嗎?ide

流程圖裏的第一個問題,詢問您應用的主要使用場景是否須要隱藏導航和/或狀態欄。所謂 "隱藏",是指讓它們根本不可見。這並不意味着讓您的應用實現從邊到邊的全屏狀態。佈局

須要隱藏的緣由可能包括:優化

通常來講,遊戲、視頻播放器、照片應用、繪圖應用等會在這個問題中回答 "是"。

問題 2: 主要的 UI 須要在交互區域內/附近使用滑動操做嗎?

這個問題是在詢問,應用的界面是否在手勢導航交互區域內或附近包含任何須要用戶滑動操做的組件。(包括在後退和返回主屏按鈕區域滑動)

很多遊戲一般會在此處回答 "是",由於:

  • 遊戲屏幕上的控件每每很是靠近屏幕左/右邊緣,或靠近屏幕底部。
  • 某些遊戲須要在屏幕上滑動操做一個元素,而這個元素可能出如今屏幕的任何位置,例如平臺動做類的遊戲。

除了遊戲以外,有一些常見的 UI 也可能在這裏回答 "是":

  • 圖片裁切 UI,其中用於裁切圖片的控制點可能位於屏幕左/右邊緣附近。
  • 繪圖應用,用戶能夠在屏幕畫布上繪圖 (天然也是滑動操做)。

問題 3: 經常使用的視圖/控件位於手勢交互區域內/附近嗎?

這個問題應該簡單一些。注意,這個問題也包括那些佔據屏幕較大區域,且包括了手勢交互區域的視圖/控件。好比 DrawerLayout 或尺寸較大的 ViewPager。

問題 4: 該視圖/控件須要滑動拖動交互嗎?

這個緊接着問題 3 。在問題 3 中回答 "是" 的視圖,是否須要用戶在其上滑動或拖拽?

有很多用例會在本題回答 "是": 包括前面提到的進度條、底部彈出菜單 (Bottom Sheet) 或者能夠經過滑動打開的彈出菜單 (PopupMenu)。

問題 5: 該視圖/控件大部分位於手勢交互區域內嗎?

緊接着問題 4,進一步確認該視圖是否徹底或大部分位於手勢交互區域內。

若是您的視圖放置在一個可滾動操做的容器 (如 RecyclerView) 中,那麼請這麼理解這個問題: 該視圖是否徹底或大部分位於手勢交互區域中?若是用戶能夠將視圖滾動到手勢交互區域以外,則應該視爲沒有交互衝突。

您也許已經注意到,在流程圖中多圖顯示控件 (ViewPager) 在此處回答 "否"。這是由於與整個視圖的寬度相比,屏幕左右側的手勢交互區域寬度相對較小 (默認爲每邊 20dp)。通常來講手機豎持時屏幕寬度約爲 360dp,也就是說,在約爲 320dp 的範圍內,用戶的滑動操做不受影響 (佔總寬度的近 90%)。即便考慮加上了內外邊距的狀況,用戶仍然能夠正常經過滑動操做來翻看裏面的圖片。

問題 6: 該視圖/控件是否和強制系統手勢交互區域重疊?

最後一個問題詢問該控件是否位於系統強制手勢導航交互區域內。若是您讀過咱們以前的文章,應該會記得 "強制系統手勢交互區" 是指系統手勢始終被優先處理的屏幕區域。

對 Android 10 來講,強制交互區域只有一個,那就是屏幕底部。該區域內的滑動操做能讓用戶返回主屏或訪問最近使用的其餘應用。這個強制交互區域可能會在未來的平臺版本中發生變化,但如今咱們只須要考慮屏幕底部便可。

出現這種重疊的常見的例子:

  • 非模態的底部彈出菜單,由於這種菜單經常會在屏幕底部摺疊爲一個較小的視圖,並且還須要滑動操做。
  • 屏幕底部的水平頁面切換,例如軟鍵盤裏選擇不一樣表情包的 UI。

OK,如今我已經解釋了流程圖中的問題,下面咱們來詳細說說流程圖中給出的解決方案。

解決方案 1: 無需處理手勢衝突

最簡單的 "解決方案" ,只須要……什麼都不作!

固然,也許您還能夠 (參考接下來的幾種解決方案) 作點優化,但在啓用了手勢導航的應用中,您應該不會遇到大問題。

若是流程圖爲您選擇了 "什麼都不作" 的答案,但您依然以爲應用的使用有問題,請務必反饋給咱們

解決方案 2: 將該視圖/控件移出手勢交互區域

咱們在上一篇文章有提到,能夠用 Insets 區域來告知應用系統手勢區域在屏幕中的位置。咱們能夠用來解決手勢衝突的一種方法是,將出現衝突的視圖移出手勢導航交互區域。這對於屏幕底部附近的視圖尤爲重要,由於該區域是系統強制手勢交互區域,而且應用沒法在該區域使用熱區切出 API。

這裏讓咱們回到以前提到的音樂播放器示例。它包含一個位於屏幕底部的進度條,容許用戶快進和快退歌曲。

可是,當用戶嘗試快進和快退歌曲時,會發生這種狀況:

發生這種狀況是由於,屏幕底部的系統手勢交互區域與進度條重疊了,而在這裏系統手勢優先級更高。系統手勢區域以下圖所示:

△ 從藍色區域向屏幕中間滑動至關於 "返回" 按鈕;從紅色區域向上滑動則是返回主屏,注意紅色區域即爲系統強制手勢交互區域

簡單的解法

這個問題最簡單的解決方案是,添加一些內/外邊距,將進度條向上推到手勢區域以外。就像這樣:

△ 進度條向上移動後再也不出現衝突

爲了實現這一點,咱們須要使用 API 29 和 Jetpack Core 庫 v1.2.0 (當前爲 alpha 版) 中提供的新系統交互熱區 API。以下方代碼,咱們給進度條增長了底邊距,增長的值正好是系統強制交互區的高度:

ViewCompat.setOnApplyWindowInsetsListener(seekBar) { view, insets ->
    // We'll set the views bottom padding to be the same // value as the system gesture insets bottom value view.updatePadding( bottom = insets.systemGestureInsets.bottom ) insets } 複製代碼

您也能夠查閱咱們發佈的另外一篇博文,咱們在那裏探討了一些讓 WindowInsets 更易於使用的方法。

更優的解法

在作完上一步後,您可能會以爲問題已經解決了。對於某些佈局,這極可能是最終解決方案。可是在上面的修改後,進度條下方有不少空間被浪費掉了,使得 UI 在外觀上的完成度降低。所以,除了直接修改視圖的邊距,咱們還能夠修改佈局,以免出現空間浪費:

△ 將進度條移到視圖的頂部
在這裏,咱們將進度條移到了播放控件的頂部,徹底移出了手勢交互區域。並且這樣作還使得咱們再也不須要額外插入太多無用的邊距。

但請注意,咱們依然須要在播放控件底部插入一個內邊距,其值等於系統欄的高度,這樣可使歌曲名稱等文本不會被系統導航條 (即屏幕底部的那條 "橫線") 遮蓋。

解決方案 3: 使用手勢區域排除 API

咱們在上一篇文章中有提到 "應用能夠從系統手勢區域中切出一部分用來響應本身的手勢交互"。這就是 Android 10 中新引入的手勢區域排除 API。

應用能夠經過 Android 10 中新增的系統手勢區域排除 API 來讓系統邊緣的一部分區域不響應系統手勢。系統提供了兩種不一樣的功能來 "切出" 交互區域: View.setSystemGestureExclusionRects() Window.setSystemGestureExclusionRects()。使用哪一種取決於您的應用: 若是您使用的是 Android View,則建議首選 View API,不然請使用 Window API。

這兩個 API 之間的主要區別在於,Window API 會以窗口 (Window) 座標系計算矩形。若是使用的是 View API,則會以視圖的座標系進行操做。View API 會幫您解決座標空間之間換算的問題。

讓咱們再次回到以前提到的音樂播放器示例,咱們如今把播放進度條挪到了控件上方,而且撐滿了整個屏幕寬度。這時屏幕底部的系統手勢交互衝突已經解決了,但屏幕左右兩側的 "後退" 操做依然和進度條有衝突:

在上圖中,因爲進度條的播放頭正好位於右側手勢區內,所以系統認爲用戶正在用手勢執行 "返回" 操做,所以顯示了 "向後" 的箭頭。這時就會讓用戶感到困惑,由於他們可能並不想後退。出現這種衝突時,咱們就可使用上面提到的手勢區域排除 API 來解決。

手勢區域排除 API 一般會在兩個地方被調用: 當視圖被佈局時 (onLayout),或是當視圖被繪製時 (onDraw)。您的視圖會傳入一個 List ,其中包含應該切出 (即不響應系統手勢) 的矩形區域。如前所述,這些矩形須位於視圖本身的座標系中。

一般,您會建立一個相似於下面的方法,該方法會在 onLayout() 和/或 onDraw() 時被調用:

private val gestureExclusionRects = mutableListOf<Rect>()

private fun updateGestureExclusion() {
    // Skip this call if we're not running on Android 10+ if (Build.VERSION.SDK_INT < 29) return // First, lets clear out any existing rectangles gestureExclusionRects.clear() // Now lets work out which areas should be excluded. For a SeekBar this will // be the bounds of the thumb drawable. thumb?.also { t -> gestureExclusionRects += t.copyBounds() } // If we had other elements in this view near the edges, we could exclude them // here too, by adding their bounds to the list // Finally pass our updated list of rectangles to the system systemGestureExclusionRects = gestureExclusionRects } 複製代碼
  • 上例的完整代碼:

gist.github.com/chrisbanes/…

作完這個 "切出" 操做後,在屏幕邊緣附近進行快進/快退操做就沒有問題了:

注意: SeekBar 實際上會在 Android 10 中自動爲您執行上述切出操做,所以您無需在 Seekbar 中這麼作。這裏只是做爲示例向您展現處理衝突的作法。

限制條件

儘管手勢區域排除 API 彷佛是解決全部手勢衝突的完美方案,但實際上並不是如此。經過使用這個 API,您實際上在聲明應用的手勢比 "返回" 等系統操做更重要。這個作法咱們只建議您在沒有其餘解決方案時採用。

因爲這個 API 會必定程度上破壞用戶習慣的操做,所以系統作出了限制: 屏幕的每一個邊緣最多隻能被應用切除 200dp。

開發者聽到這個限制時,常會提出如下問題:

爲何要有限制?

咱們認爲,開發者須要儘可能確保用戶使用一致的操做來與系統進行交互,如從邊緣向內滑動進行返回。注意是在整個設備上,而不只僅是在一個應用中保持一致性。這個限制看似嚴厲,但若是一個應用可以讓屏幕的整個邊緣都不響應系統手勢,就會讓用戶感到困惑,這個應用也極有可能被用戶卸載。

再次強調,系統導航必須始終保持一致性和可用性。

爲何是 200dp?

200dp 背後的決策邏輯很是簡單。正如咱們前面提到的,手勢區域排除 API 只有在萬不得已的狀況下才可使用,所以咱們計算了可能須要應用這套機制的觸摸對象的面積。觸摸對象的最小推薦尺寸是 48dp。咱們取 4個觸摸對象,即 4 × 48dp = 192dp。再加入一點富餘量,即爲 200dp。

若是開發者要求在邊緣上切出 200dp 以上的區域會怎樣?

答案是,系統只會兌現您的要求中位於最下方的 200dp,以下圖所示:

△ 開發者請求切出 50 + 50 + 125 + 50 dp 的區域,但系統只兌現最下面的總計 200dp

個人視圖不在屏幕內,是否也會受到這個限制?

不會,系統僅計算屏幕範圍內的切出矩形。一樣,若是視圖只有一部分顯示在屏幕內,則僅計算所請求矩形的屏幕內可見部分。

請關注下一篇連載

讀完本文您可能會問: 爲何咱們尚未講流程圖的右半部分?這是由於右半部分適用於那些須要全屏繪製內容的應用,咱們將在下一篇手勢導航連載中爲您繼續講解,敬請保持關注。

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

相關文章
相關標籤/搜索