【Flutter 元件集錄】Tooltip 與 Overlay

張風捷特烈發表於2021-08-31
前言:

這是我參與8月更文挑戰的第 31 天,活動詳情檢視:8月更文挑戰。為應掘金的八月更文挑戰,我準備在本月挑選 31 個以前沒有介紹過的元件,進行全面分析和屬性介紹。這些文章將來會作為 Flutter 元件集錄 的重要素材。希望可以堅持下去,你的支援將是我最大的動力~

本系列元件文章列表
1.NotificationListener2.Dismissible3.Switch
4.Scrollbar5.ClipPath6.CupertinoActivityIndicator
7.Opacity8.FadeTransition9. AnimatedOpacity
10. FadeInImage11. Offstage12. TickerMode
13. Visibility14. Padding15. AnimatedContainer
16.CircleAvatar17.PhysicalShape18.Divider
19.Flexible、Expanded 和 Spacer 20.Card21.SizedBox
22.ConstrainedBox23.Stack24.Positioned
25.OverflowBox26.SizedOverflowBox27. DecoratedBox
28. BackdropFilter29.ImageFiltered 與 ColorFiltered30.Draggable 與 DragTarget
31.Tooltip 與 Overlay

1. 認識 Tooltip 及使用

今天是八月更文的最後一天,帶大家看一下 Tooltip 元件的實現,從而引出 Overlay 元件的使用方式。 Tooltip 元件主要的作用是在滑鼠懸浮長按手勢下觸發訊息提示。它繼承自 StatefulWidget ,其中必須傳入 String 型別的 message ,還有很多其他的引數用於配置。

final String message;
複製程式碼

如下是 Tooltip 預設的效果,可以套在任意元件上,當滑鼠懸浮長按手勢時,會在下方顯示提示資訊。

Tooltip(
  message: "寶塔鎮河妖",
  child: Icon(Icons.info_outline)
);
複製程式碼

preferBelow 屬性為 false ,提示資訊就會顯示在上方。

Tooltip(
  preferBelow: false,
  message: "寶塔鎮河妖",
  child: Icon(Icons.info_outline)
);
複製程式碼

通過 verticalOffset 可以設定豎直偏移,此偏移量可為負數。此值為 0 時,提示框底部與元件中心對齊

Tooltip(
  preferBelow: false,
  verticalOffset: 12,
  message: "寶塔鎮河妖",
  child: Icon(Icons.info_outline)
);
複製程式碼

通過 paddingmargin 可以設定內外邊距:

Tooltip(
  preferBelow: false,
  padding: EdgeInsets.symmetric(horizontal: 40,vertical: 5),
  margin: EdgeInsets.all(10),
  message: "寶塔鎮河妖",
  child: Icon(Icons.info_outline)
);
複製程式碼

通過 decorationtextStyle 可以設定 盒子裝飾文字樣式

Tooltip(
  preferBelow: false,
  verticalOffset: 15,
  message: "寶塔鎮河妖",
  textStyle: TextStyle(
    color: Colors.red,
    shadows: [
      Shadow(
        color: Colors.white,
        offset: Offset(1, 1),
      ),
    ],
  ),
  decoration: BoxDecoration(boxShadow: [
    BoxShadow(
      color: Colors.orangeAccent,
      offset: Offset(1, 1),
      blurRadius: 8,
    )
  ]),
  child: Icon(Icons.info_outline)
);
複製程式碼

有時候我們並不希望滑鼠一進入就顯示提示,waitDuration 表示滑鼠進入時,需要等待多長時間再顯示提示框。showDuration 表示長按時,需要等待多長時間再顯示提示框。

Tooltip(
  // 略同...
  waitDuration:const Duration(seconds: 2),
  showDuration:const Duration(seconds: 2),
  child: Icon(Icons.info_outline)
);
複製程式碼

Tooltip 元件的屬性就是這些,下面我們來看一下它的原始碼實現。


2. Tooltip 原始碼簡看

Tooltip 作為一個 StatefulWidget,自然是會維護一個狀態類進行元件構建,狀態週期等邏輯處理。如下是 _TooltipState 的類定義。看到它混入了 SingleTickerProviderStateMixin,表示該狀態類中會使用動畫。

initState 回撥中,會初始化 _controller 動畫控制器,可以看出 Tooltip 的提示框會伴隨一個透明度的漸變動畫。然後對滑鼠 mouseTracker 和觸點 pointerRouter 進行監聽。

dispose 回撥中移除監聽和銷燬動畫控制器。

build 方法中可以看出提示框的預設表現會受 ThemeTooltipTheme 的資料影響,對暗黑主體也進行了適配。

會通過 GestureDetector 來監聽長按事件,如果檢測到滑鼠的連線,外層會套上 MouseRegion 進行監聽滑鼠的移入移出事件。

最終顯示的是使用者傳入的 child 元件,那提示框是如何彈出和消失的呢?現在焦點就可以放在 _showTooltip_hideTooltip 如何控制提示框的顯隱。


3.Overlay 在 Tooltip 原始碼的應用

在移動端中,長按會彈出提示框,從原始碼中可以看出,核心的方法是 ensureTooltipVisible

void _handleLongPress() {
  _longPressActivated = true;
  final bool tooltipCreated = ensureTooltipVisible();
  if (tooltipCreated)
    Feedback.forLongPress(context);
}
複製程式碼

_TooltipState 中維護了兩個計時器 _hideTimer_showTimer 來處理延遲。開始會取消並置空 _showTimer 計時器,這樣保證不會在計時器完成時再出現一個框。如果 _entry 非空,表示提示框已經存在,會取消並置空 _hideTimer 計時器,並執行動畫。此處返回 false ,表示已經存在,開啟失敗。
否則會執行 _createNewEntry 建立新的 Entry 並執行動畫。

Timer? _hideTimer;
Timer? _showTimer;
OverlayEntry? _entry;

bool ensureTooltipVisible() {
  _showTimer?.cancel();
  _showTimer = null;
  if (_entry != null) {
    // Stop trying to hide, if we were.
    _hideTimer?.cancel();
    _hideTimer = null;
    _controller.forward();
    return false; // Already visible.
  }
  _createNewEntry();
  _controller.forward();
  return true;
}
複製程式碼

_createNewEntry 中,先通過 Overlay.of 獲取 OverlayState 物件,再通過 context.findRenderObject 獲取 RenderBox 得到元件的位置。最後建立 OverlayEntry_entry 賦值,並將_entry通過 overlayState 插入,其中主體的內容就是 overlay 元件。

void _createNewEntry() {
  final OverlayState overlayState = Overlay.of( //1. 得到 overlayState
    context,
    debugRequiredFor: widget,
  )!;
  final RenderBox box = context.findRenderObject()! as RenderBox;
  final Offset target = box.localToGlobal(
    box.size.center(Offset.zero),
    ancestor: overlayState.context.findRenderObject(),
  );

  final Widget overlay = Directionality( 
    textDirection: Directionality.of(context),
    child: _TooltipOverlay(//...元件暫略
  );
   //2. 建立 _entry
  _entry = OverlayEntry(builder: (BuildContext context) => overlay);
   //3. 插入 _entry
  overlayState.insert(_entry!);
  SemanticsService.tooltip(widget.message);
}
複製程式碼

我們為 Tooltip 傳入的大多數引數都是用於構建 _TooltipOverlay 的,下面是它的原始碼。可以看出,它通過 FadeTransition 進行透明漸變動畫,通過 CustomSingleChildLayout 進行提示框的定位。並且 IgnorePointer 表示提示框是忽略點選事件的。

這樣 Overlay 的彈出就看完了,至於 Overlay 的移除,只需要 _entry?.remove(); 即可。

void _removeEntry() {
  _hideTimer?.cancel();
  _hideTimer = null;
  _showTimer?.cancel();
  _showTimer = null;
  _entry?.remove();
  _entry = null;
}
複製程式碼

Overlay 元件本身的使用並不複雜,這是 Tooltip 中有延遲和動畫的處理,讓顯隱的邏輯複雜了一些。這些在計時器的控制人常開發中也是值得我們學習的。雖然是很小的一個元件,但其中包含了很多知識,這種小巧的元件很適合我們去細細品讀。到這裡本系列就完結了,在毫無存稿的情況下,連更 31 天實屬不易,感謝大家的支援,後會有期~

相關文章