【Flutter 小知識】震動反饋 HapticFeedback

語言: CN / TW / HK

theme: hydrogen

1. 緣起

這兩天在研究 CupertinoSliverRefreshControl 元件,使用中有個小細節吸引到了我的注意。在下拉達到一定程度時,會有 weng 的一聲震動感。然後翻看原始碼中的具體實現邏輯,在下拉量大於 refreshTriggerPullDistance 時,會觸發 HapticFeedback.mediumImpact(); 方法。看到 Feedback 一詞,也就知道這是震動反饋觸發的方法了。


2. HapticFeedback 類的介紹

HapticFeedback 非常簡單,私有構造,且提供五個靜態方法,很明顯是一個工具類。


其中有五個方法,從體感上來說,五種方法的震感不同,其中 vibratelightImpact 感覺上沒有太大差別。mediumImpact 相對較弱,heavyImpact 很弱,最後 selectionClick 不知道是我手機問題還是什麼,似乎沒有震感。

dart vibrate ≈ lightImpact > mediumImpact > heavyImpact > selectionClick


HapticFeedback 原始碼中介紹提到,這裡的 API 故意設計的比較簡潔,只是呼叫平臺的預設行為,並不能達到精確控制系統震動模組目的。也就是說,這裡就是簡單震動一下,並無法精確控制振幅、震動時長等資訊。

對於 Android 來說,這五個方法分別對應 HapticFeedbackConstants 中的五個常量:

dart vibrate ---- HapticFeedbackConstants.LONG_PRESS lightImpact ---- HapticFeedbackConstants.VIRTUAL_KEY mediumImpact ---- HapticFeedbackConstants.KEYBOARD_TAP heavyImpact ---- HapticFeedbackConstants.CONTEXT_CLICK (API 23+) selectionClick ---- HapticFeedbackConstants.CLOCK_TICK


對於 ios 來是 10+ 之後引入了新的震動反饋特性,很明顯 FlutterHapticFeedback 的方法名稱是參照 ios 來命名的。

dart vibrate ---- kSystemSoundID_Vibrate lightImpact ---- UIImpactFeedbackGenerator-UIImpactFeedbackStyleLight (ios10+) mediumImpact ---- UIImpactFeedbackGenerator-UIImpactFeedbackStyleMedium (ios10+) heavyImpact ---- UIImpactFeedbackGenerator-UIImpactFeedbackStyleHeavy (ios10+) selectionClick ---- UISelectionFeedbackGenerator (ios10+)


3. HapticFeedback 的使用

因為都是靜態方法,所以使用也是非常簡單,呼叫一些即可,比如:

dart HapticFeedback.vibrate();

下面簡單寫個測試介面,通過點選按鈕來觸發不同的震動反饋。藉此也來說一下,如何優雅地實現這種若干個需要觸發事件的按鈕。可能有人看到這個介面,就想到在 Wrap 放一個個的 ElevatedButton 不就行了嗎。如果直接一個個塞進去,程式碼會很長,而且不易管理。

其實這裡的資料關係是 字串和方法(函式)的對映 ,而 方法(函式) 本身也可以作為物件。所以可以使用 Map 來維護資料,為了方便表示函式型別,可以通過 typedef 進行宣告,比如下面的 VoidAsyncFunction

```dart typedef VoidAsyncFunction = Future Function();

final Map feedbackMap = const { 'vibrate': HapticFeedback.vibrate, 'heavyImpact': HapticFeedback.heavyImpact, 'mediumImpact': HapticFeedback.mediumImpact, 'lightImpact': HapticFeedback.lightImpact, 'selectionClick': HapticFeedback.selectionClick, }; ```


這樣在 Wrap 中,通過 feedbackMap 來遍歷 key 列表,生成 ElevatedButton 即可。其中 feedbackMap[name] 就是代表字串名稱對應的函式物件。

dart Wrap( spacing: 5, runSpacing: 5, alignment: WrapAlignment.center, children: feedbackMap.keys .map((String name) => ElevatedButton( onPressed: feedbackMap[name], child: Text(name), ), ).toList(), ),


4. HapticFeedback 中的方法是非同步的

HapticFeedback 中的方法是通過 SystemChannels.platform 執行平臺方法實現功能的。也就是說,觸發 vibrate 並不會立刻震動,向平臺通道傳送訊息是個不確定時長的非同步任務。

dart static Future<void> vibrate() async { await SystemChannels.platform.invokeMethod<void>('HapticFeedback.vibrate'); }

如何你需要確切在震動之後才觸發某段邏輯,可以通過 await 來等待非同步任務完成。比如下面連續四次,間隔 500 ms 的震動。需要在前一次震動方法完成,才能開始下次震動。

dart void run() async { Duration duration = const Duration(milliseconds: 500); await HapticFeedback.vibrate(); await Future.delayed(duration); await HapticFeedback.heavyImpact(); await Future.delayed(duration); await HapticFeedback.mediumImpact(); await Future.delayed(duration); await HapticFeedback.lightImpact(); }

不過一般來說,並沒有必要非常精確地知道震動方法完成的時機,因為這個時間非常短,在 10 ms 左右。像下拉到一定高度給出震動感,並不是很在意確切的時間。

dart int tag = DateTime.now().millisecondsSinceEpoch; await HapticFeedback.vibrate(); int now = DateTime.now().millisecondsSinceEpoch; print(now-tag);


5. HapticFeedback 中各種震動在原始碼中的使用

首先在 androidfuchsia 中,長按事件會觸發 vibrate 震動。iOS 平臺一般不會對長按事件進行反饋。

另外,注意一點,在 InkWellTooltip 元件中才會觸發 forLongPress ,也就是說 GestureDetector 的長按事件是沒有震動反饋的。


lightImpactCupertinoSwitch 元件中被使用,只有在 iOS 平臺才會有反饋。


mediumImpactCupertinoSliverRefreshControlCupertinoScrollbar 元件中被使用:


selectionClickCupertinoPickerLongPressDraggableCupertinoContextMenu 中被使用。

image-20220523085921053.png


最後,heavyImpact 方法沒有在框架中被使用。這就是 HapticFeedback 關於震動反饋的一些小知識,本文就到這裡,謝謝觀看 ~