私の最後のJetpack Composeに関する記事を書いたとき、Jetpack Composeには(私の意見では)基本的なコンポーネントのいくつかが欠けていると述べました。その中の一つはツールチップです。

当時、ツールチップを表示する内置的なコンポーザブルが存在しなかったし、その代わりにいくつかの代替方案がオンラインで回っていました。これらの解決方案の問題は、Jetpack Composeの新しいバージョンがリリースされると、これらの解決方案が壊れるかもしれないということでした。したがって、理想的ではなく、コミュニティは将来的にツールチップのサポートが追加されることを望んでいました。

私はCompose Material 3のバージョン1.1.0以降、内置的なツールチップサポートを持つことを嬉しく思っています。👏

これ自体は素晴らしいことですが、そのバージョンのリリースから1年を超えました。そして、その後のバージョンで、ツールチップに関連するAPIは大きく変更されました。

チangelogを見ると、公的APIと内部APIがどのように変更されたかを確認できます。そのため、この記事を読むとき、すべてのツールチップに関するものはまだExperimentalMaterial3Api::classのアノテーションで示されていると考えていただきます。

❗️この記事で使用されたMaterial 3のバージョンは、2024年3月6日にリリースされた1.2.1です。

ツールチップのタイプ

現在、2つの異なるタイプのツールチップのサポートがあります。

  1. プレーンツールチップ

  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 – ツールチップがどのように表示されるかのUI設計を行う場所です。

  • state – 特定のツールチップインスタンスに関連付けられる状態を保持します。これには、ツールチップを表示したり非表示にしたり、ツールチップが画面上に表示されるまでにユーザーがクリックアクションを行わない限り表示されることを宣言することができるように、インスタンス化する際に persistent かどうか指定できます。

  • コンテンツ – これはトーストが上または下に表示する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を使用して、アンカー要素に対してトーストを正しく配置することができます。

リッチトースト

リッチメディアトーストは、プレーントーストよりももっとスペースを占め、アイコンボタンの機能性に関するより多くのコンテキストを提供するために使用されます。トーストが表示されたときに、ボタンとリンクを追加して、より詳細な説明や定義を提供できます。

これは、プレーントーストと同様に、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)
        }
    }

リッチトーストについて注意すべき点:

  1. リッチトーストはキャレットのサポートがあります。

  2. ツールチップにアクション(つまりボタン)を追加することで、ユーザーがより多くの情報を探るオプションを提供することができます。

  3. ツールチップを閉じるロジックを追加することができます。

境界ケース

ツールチップの状態を永続的にマークすることを選ぶと、ユーザーがツールチップを表示するUIと対話すると、画面の他の場所を押すまでツールチップが表示され続けます。

上記の豊富なツールチップの例を見た場合、ツールチップをクリック後に閉じるボタンを追加したことに気づいたかもしれません。

ユーザーがそのボタンを押すと起こる問題があります。ツールチップで.dismissアクションが実行されるため、ユーザーがこのツールチップを呼び出すUIアイテムで長押しを再度行いたいとき、ツールチップは再表示されません。これは、ツールチップが閉じられた状態が永続的に保持されていることを意味します。これをどう解決するのでしょうか?

ツールチップの状態を”リセット”するために、ツールチップ状態に公開されている.onDisposeメソッドを呼び出す必要があります。これを行うと、ツールチップの状態がリセットされ、ユーザーがUIアイテムで長押しを行うと、ツールチップが再度表示されます。

@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リポジトリをご覧ください。

アプリケーション内でツールチップを見たい場合は、こちらをご覧ください。

参考文献