【Flutter解体新書 vol. 2】SelectableText篇

Dart

概要

どうも、@daiki1003です!

約1年前に、TextField.decorationの解体新書という記事をリリースしました。

【Flutter解体新書 vol. 1】TextField.decoration篇
さて、皆さんに一つ質問です。 FlutterのWidgetって、パラメタ多すぎじゃありません?笑 そんな、あなたに朗報です。 このブログを始めとする解体新書シリーズを読めば 何を指定したら何が変わるのか、理解出来る様...

割と好評をいただけているようなので、今回はSelectableTextについて解説したいと思います。

このブログを始めとする解体新書シリーズを読めば
何を指定したら何が変わるのか、理解出来る様になります。
一度で理解できなくとも、辞書の様に困ったら何度でも見に来てください。
あなたの手助けをしてくれることでしょう。 (だといいな)

なお、今回のコードは全て以下で試すことが出来ますので
興味がある方は覗いてみてくださいね!

ちなみに、今後も解体新書シリーズは公開予定でしてこのレポジトリで更新していきます。

GitHub - daiki1003/dismantle_flutter_widgets: Repository for flutter widgets
Repository for flutter widgets. Contribute to daiki1003/dismantle_flutter_widgets development by creating an account on GitHub.

執筆時環境

Flutter 3.3.1 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 4f9d92fbbd (3 days ago) • 2022-09-06 17:54:53 -0700
Engine • revision 3efdf03e73
Tools • Dart 2.18.0 • DevTools 2.15.0

補足

僕自身、正直完全に理解出来ていないところもありそのような箇所は理解次第更新していきたいと思います。

初期設定

では、まずはSelectableTextを表示するところから始めます。
表示は至って簡単で、Textウィジェットの様に第一引数に表示する文字列を渡してあげます。

日本語と英語を混ぜていますが、後にこれがちょっと大事だったりします。

それでは、早速それぞれのプロパティについて変更していきましょう。

まず初めに

以下のプロパティに関しては、Textウィジェットと同じく割と良く使われるため割愛させていただきます。

focusNode, style, strutStyle,
textAlign, textDirection, textScaleFactor,
textHeightBehavior

bool showCursor = false

選択(タップ)した際にカーソルを表示するかどうかを指定します。

また、単語単位で最初か最後にしかカーソルは表示されません。
スクショの例で言うと、2行目において最初の文字であるaか最後の文字であるzの後ろにしか表示されず、
例えばmをタップしたとしてもzの後に表示されます。

double cursorWidth = 2.0

2.010.020.0

カーソルの横幅を指定します。
20の存在感すごいですね。

また、このcursorWidthはテキストの表示に影響を与えるらしく20のスクショでは2行目に収まっていたzが3行目にずれ込んでいますね。

double? cursorHeight

※見やすくするために、前項cursorWidthを7に設定しています。

1.024.0(=fontSize)48.0

カーソルの縦幅を指定します。
50(フォントサイズ)以上ってどういう用途なんだろう…w

double? cursorRadius

0.05.010.0(=cursorWidth/2)

角丸を指定します。
通常のカーソル幅は2.0なことが多く、あまり指定することはなさそうですね。

また、borderRadiuscursorRadiusは短辺の値の1/2以上を指定しても
見た目に変化が起こりません。
今回でいえば、横幅cursorWidthを20に設定しているので最大値を10としています。

BoxWidthStyle selectionWidthStyle = ui.BoxWidthStyle.tight

tight(デフォルト)maxmax(右端)

選択範囲の横幅を指定します。
デフォルトのtightでは、選択した単語と同じ幅になります。
一方、maxにすると面白いことが起こります。
基本は同じなのですが、選択範囲が行末であった場合、SelectableTextの描画範囲の端まで選択状態となります。

BoxHeightStyle selectionHeightStyle = ui.BoxHeightStyle.tight

tightmaxincludeLineSpacingMiddle
includeLineSpacingTopincludeLineSpacingBottomstrut

選択範囲の縦幅を指定します。
tightの場合、文字の占有範囲ではなく描画範囲と同じ高さになるので英語や日本語などが混ざるとガタガタになってしまいますね。
なので、ユーザ自由入力の内容を表示するなど日英等複数の言語が混ざる場合は指定するのが良さそうに思いました。

なお、include系の動作の差異が現状把握しきれいていないです。

DragStartBehavior dragStartBehavior

現在、僕の中でこれを変えることによってどういう影響がSelectableTextに出ているのかが把握できておりません🙏
基本的には、ドラッグ開始コールバックに渡す座標をタップ開始位置(.down)とするかドラッグを開始後の次のフレームの位置(.start)とするかの違いになります。
(0, 0)でタップして、(20, 20)に素早く指やカーソルを動かした場合、
.downであれば(0, 0).startであれば(20, 20)が渡されるイメージです。

どなたか、見識のある方教えてください!

bool enableInteractiveSelection = true

ユーザの操作によるテキスト選択の可否を設定します。
falseとすると、ダブルタップや長押しでテキスト選択が出来なくなります。

TextSelectionControls? selectionControls

materialcupertino

選択状態でのコントローラのUI(つまみや「Copy」などの部分)をMaterial UICupertino (iOS)とするかを指定します。
基本的には動作しているOS準拠になると思うので、未指定のままで良いかなとは思います。

これは余談ですが、実行中に動的に変更することは出来ないようです。

GestureTapCallback? onTap

SelectableTextが占有された領域がタップされたことだけが分かります。
引数などはありません。

typedef GestureTapCallback = void Function();

ScrollPhysics? scrollPhysics

スクロールの動作を指定します。
通常であれば、描画領域を超えてコンテンツが存在する場合はスクロールを行い、そうでなければスクロールが
出来ないようになっています。

Never:どんな場合でもスクロールしません。
Always:常にスクロールします。
BouncingiOSのような、スクロール領域をはみ出ようとした場合にバウンスするアニメーションを行います。
ClampingAndroidのような、スクロール領域をはみ出ようとした場合にすぐにピタッと止まる感じになります。

void Function(TextSelection, SelectionChangeCause?)? onSelectionChanged

選択範囲が変更された時に呼ばれるコールバックです。

TextSelection

こちらは、baseOffset, extentOffset, isDirectionalの三つのプロパティを持っています。

SelectionChangeCause

こちらは、enumになっていて、ダブルタップやロングプレス、ドラッグなど選択範囲が変更した際の理由について返却されます。

1234

例えば、

1. 「え」をロングタップして選択状態
2. 先頭のつまみをドラッグして「いうえ」を選択状態
3. 終端のつまみをドラッグして「いうえおか」を選択状態
4. 「こ」をタップして選択解除

と言うストーリーがあったとします。
コールバックは以下のように返って来ます。

1. 
flutter: TextSelection(baseOffset: 3, extentOffset: 4, isDirectional: false)
flutter: SelectionChangedCause.longPress
2.
flutter: TextSelection(baseOffset: 4, extentOffset: 1, isDirectional: false)
flutter: SelectionChangedCause.drag
3.
flutter: TextSelection(baseOffset: 1, extentOffset: 6, isDirectional: false)
flutter: SelectionChangedCause.drag
4.
flutter: TextSelection.collapsed(offset: 10, affinity: TextAffinity.downstream, isDirectional: false)
flutter: SelectionChangedCause.tap

なんとなく想像付きましたでしょうか?
前回の値を覚えておいてoffsetを比較することでどちら側への移動なのかとかは分かりそうですね。

最後に

いかがでしたでしょうか?
基本的にアプリ上に出てくるテキストって選択してコピーとかしたくなることが多い
(Twitterの投稿内容とか)ので、意外とSelectableTextの利用頻度は多いんじゃないかなーなんて思います。

似たようなウィジェットでSelectionAreaと言うのがあるのですが、そちらはchildとして指定したウィジェット
を対象として選択可能にします。

頑張ったねって少しでも思っていただけたら↓からコーヒーを少し奢っていただけると嬉しいです!
もちろん、強制ではありません笑

誰かのお役に立てば。

Twitterフォローお願いします

「次回以降も記事を読んでみたい!」
「この辺分からなかったから質問したい!」

そんな時は、是非Twitter (@daiki1003)Instagram (@ashdik_flutter)のフォローお願いします♪

Twitterコミュニティ参加お願いします

Twitterコミュニティ「Flutter lovers」を開設しました!
参加お待ちしております😁

☕️ Buy me a coffee

また、記事がとても役に立ったと思う人は
コーヒーを奢っていただけると非常に嬉しいです!
@daiki1003

コメント

タイトルとURLをコピーしました