當我寫了我的上一篇文章關於Jetpack Compose時,我曾在那裡提到Jetpack Compose缺少一些(在我看來)基本的元件,其中之一就是 Tooltip。

當時,沒有內建的可組合來顯示Tooltip,而在網上 circulating 有一些替代方案。這些方案的問題是,一旦Jetpack Compose推出新的版本,這些方案可能會損壞。所以這不是理想的選擇,社區只能他希望未來某個时候可以添加對 Tooltip 的支持。

我很高兴地告訴大家,自Compose Material 3 1.1.0版本以來,我們现在已经有了內建的Tooltip支持。👏

雖然這本身已經很好了,但自那版本發布以來已經过了一年多。並且在隨後的版本中,與Tooltips相關的API也發生了很大的變化。

如果你查看更改記錄,你會看到公用的和內部API的變化。所以請注意,當你閱讀這篇文章時,由於與Tooltips相關的都被標記為ExperimentalMaterial3Api::class,所以事情可能繼續在變化。

❗️ 本文使用的 material 3 版本是 1.2.1,該版本於 2024 年 3 月 6 日發布。

Tooltip 類型

現在我們支持兩種不同類型的 Tooltip:

  1. 普通 Tooltip

  2. 豐富媒體提示

簡易提示

您可以使用第一種方式提供有關圖示按鈕的信息,否則可能不清楚。例如,您可以使用簡易提示向用戶解釋圖示按鈕代表什麼。

要向應用程式添加提示,您可以使用TooltipBox組合。此組合接受幾個參數:

fun TooltipBox(
    positionProvider: PopupPositionProvider,
    tooltip: @Composable TooltipScope.() -> Unit,
    state: TooltipState,
    modifier: Modifier = Modifier,
    focusable: Boolean = true,
    enableUserInput: Boolean = true,
    content: @Composable () -> Unit,
)

如果您之前使用過組合,其中一些參數可能會很熟悉。我將突出一下這裡具有特定用例的參數:

  • positionProvider – 屬於 PopupPositionProvider 類型,用於計算提示的位置。

  • tooltip – 這是您設計提示外觀的地方。

  • state – 這保存與特定提示實例相關聯的狀態。它公開方法,如顯示/隱藏提示,並在實例化其中一個時,您可以聲明提示是否應該是持久的(即是否應該一直顯示在屏幕上,直到用戶在提示外執行點擊操作)。

  • 內容 – 這是 tooltip 將顯示在上方/下方的 UI。

以下是一個填滿所有相關參數的 BasicTooltipBox 實例化的例子:

@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable
fun BasicTooltip() {
    val tooltipPosition = TooltipDefaults.rememberPlainTooltipPositionProvider()
    val tooltipState = rememberBasicTooltipState(isPersistent = false)

    BasicTooltipBox(positionProvider = tooltipPosition,
        tooltip =  { Text("Hello World") } ,
        state = tooltipState) {
        IconButton(onClick = { }) {
            Icon(imageVector = Icons.Filled.Favorite, 
                 contentDescription = "Your icon's description")
        }
    }
}

Jetpack Compose 有一個內置類叫做 TooltipDefaults。您可以使用這個類來幫助您實例化组成 TooltipBox 的參數。例如,您可以使用 TooltipDefaults.rememberPlainTooltipPositionProvider 正確地將 tooltip 定位在锚點元素的相關位置。

Rich Tooltip

一個 rich media tooltip 比一個 plain tooltip 佔用更多的空間,並且可以用於提供關於圖標按鈕功能性的更多上下文。當 tooltip 顯示時,您可以向其添加按鈕和鏈接以提供進一步的解释或定義。

它的實例化方式與 plain tooltip 類似,都在 TooltipBox 內,但您使用 RichTooltip 可 composable。

TooltipBox(positionProvider = tooltipPosition,
        tooltip = {
                  RichTooltip(
                      title = { Text("RichTooltip") },
                      caretSize = caretSize,
                      action = {
                          TextButton(onClick = {
                              scope.launch {
                                  tooltipState.dismiss()
                                  tooltipState.onDispose()
                              }
                          }) {
                              Text("Dismiss")
                          }
                      }
                  ) {
                        Text("This is where a description would go.")
                  }
        },
        state = tooltipState) {
        IconButton(onClick = {
            /* 图标按钮的点击事件 */
        }) {
            Icon(imageVector = tooltipIcon,
                contentDescription = "Your icon's description",
                tint = iconColor)
        }
    }

关于 Rich tooltip 有一些要注意的事情:

  1. Rich tooltip 支持 caret。

  2. 您可以向 tooltip 加入一個動作(也就是一個按鈕),提供給使用者選擇查找更多資訊的選項。

  3. 您可以加入邏輯來關閉 tooltip。

邊界案例

當您選擇將您的tooltip 狀態標記為持久時,這意味著一旦使用者與顯示您的 tooltip 的 UI 進行互動,它將保持可见直到使用者在屏幕上按其他地方。

如果您查看上方 rich tooltip 的範例,您可能會發現我們已經在 tooltip 被點擊後加入了一個按鈕來關閉 tooltip。

當使用者按下那個按鈕時会发生一個問題。由於關閉動作是在 tooltip 上執行的,如果使用者想要在這個 tooltip 所呼叫的 UI 项目中執行另一次長按,則 tooltip 不会再顯示。這意味著 tooltip 的狀態在關閉後是持久的。那麼,我們应该如何解決這個問題?

為了“重置”tooltip 的狀態,我們必須呼叫透過 tooltip 狀態暴露的onDispose方法。一旦我們这样做,tooltip 狀態就会被重置,當使用者在 UI 项目中執行長按時,tooltip 將再次顯示。

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RichTooltip() {
    val tooltipPosition = TooltipDefaults.rememberRichTooltipPositionProvider()
    val tooltipState = rememberTooltipState(isPersistent = true)
    val scope = rememberCoroutineScope()

    TooltipBox(positionProvider = tooltipPosition,
        tooltip = {
                  RichTooltip(
                      title = { Text("RichTooltip") },
                      caretSize = TooltipDefaults.caretSize,
                      action = {
                          TextButton(onClick = {
                              scope.launch {
                                  tooltipState.dismiss()
                                  tooltipState.onDispose()  /// <---- 這裡
                              }
                          }) {
                              Text("Dismiss")
                          }
                      }
                  ) {

                  }
        },
        state = tooltipState) {
        IconButton(onClick = {  }) {
            Icon(imageVector = Icons.Filled.Call, contentDescription = "Your icon's description")
        }
    }
}

另一個情境是,當工具提示被外部點擊而非透過用戶動作呼叫自身進行關閉時,工具提示狀態不會重置。這種情況下,工具提示在后台呼叫關閉方法,並將工具提示狀態設定為已關閉。如果長按UI元件以再次查看工具提示,則不會發生任何事情。

我們呼叫工具提示的 onDispose 方法的邏輯並未觸發,那麼我們如何重置工具提示的狀態呢?

目前我還沒能解決這個問題。這可能與工具提示的MutatorMutex有關。也许在未來的版本中,將有 API 處理這件事。我注意到,如果屏幕上存在其他工具提示,並且它們被點擊,這會重置先前點擊的工具提示。

如果您想查看這裡的代碼,您可以前往這個 GitHub 倉庫

如果您想在應用程序中查看工具提示,您可以這裡查看。

參考資料