Flutter備忘録: TextFieldに入力された文字列の一部のスタイルを変更する方法
Googleのストアで公開しているAndoridアプリの『コメ欄』Liteは、これまでAndroid SDKベースの開発環境下でJava/Kotlin言語で開発していましたが、現在は、GoogleのFlutter(フラッター)というアプリ開発環境下に移行しようと作業を進めています。
Java/Kotlin言語で開発した『コメ欄』Liteでは、URLの入力の際に、TextFieldを使っていますが、入力された文字列の正しくない部分だけを赤字で表示するJava/Kotlin言語のコードが組み込まれています。 このコードをFlutter版のコードに変換する時に、一筋縄には行かなかったのでここに備忘録として記録しておきます。
Android SDKからFlutterへ移植
Android SDKはその名の通り、Android専用のアプリを制作するソフト開発キット(SDK)で、Androidアプリしか制作する事しか出来ません※。 iOS用のアプリを制作したい場合は別にiOS SDKを使ってSwift又はObject-C言語で直接開発するか、Swift/Object-Cに互換性のある開発環境を使う必要があります。
上記のKotlinもAndroid SDKがKotlinに対応している上に、KotlinがiOS SDKのAPIを呼ぶ事も出来ます。
なので、Kotlin上で、Android特有の機能の場合はAndroid SDKのAPI、iOS特有の場合はiOS SDKのAPIを呼ぶ仕組みを使うとAndroidとiOSのアプリの同時開発が可能になります。。。
でも、この方法だと実際は2種類のアプリを同じ開発アプリの1つのプロジェクトで管理しているだけで開発にはAndroid SDKとiOS SDKの両方の知識が必要になります。
そこで今回、移行を進めているFlutterですが、AndroidとiOSだけでなく、WindowsやLinuxのアプリとWebブラウザで動くJavaScriptアプリを、OSごとに区分けする事無く1つのコードで同時に制作する事が出来ます (例外有)。
※ 追記
AndroidXというAndroid SDKに含まれるAPIがiOSもサポートし始めたというツイートが流れてきたので将来的にはAndroid SDKでもiPhone/iPadのアプリを同時に直接開発することが可能になるかもしれません…
PSA: AndroidX officially went multiplatform on Wednesday.
— Louis CAD (@Louis_CAD) July 31, 2022
For now, it only affects a small number of artifacts, but that's just the beginning. 👀 👀 👀
That means we can expect exciting times ahead for Kotlin!#AndroidDev pic.twitter.com/qP22yvb985
『コメ欄』のUI (ユーザーインターフェイス)は、マテリアルデザイン (Material Design)を基礎にしてデザインしましたが、新しい開発環境のFlutterもマテリアルデザインを標準でサポートしているのでUIの仕様変更といった必要は無いとは思っていましたが、FlutterがUIのデザインに適していると言われる「Declarative(宣言型) Programming」を採用しているのに対して、これまでは「Imperative (命令型) Programming」というタイプのプログラミングばかりして来たので、宣言型のプログラミングでどの様にアプリケーションステータスを管理するかといった違うプログラミング様式を学んでいます。
Android SDKとFlutter共にマテリアルデザインに対応しているので最終的なアプリの見た目は同じ様になるはずですが、中身のコードは書式や様式が異なる為、既にあるAndroid SDK版の『コメ欄』のデザイン又はコードを元に、Flutterだとこうやってやれば。。とFlutter/Dartのコードを作成、思いつかない場合はインターネットで調べるといった作業の繰り返しになっています。
で、最近、Android SDK版の『コメ欄』で使われている、マテリアルデザインの「TextField」というUIコンポーネントに組み込んだ動作をFlutterで再現する際に、少し手こずった動作があったので次に備忘録として残しておきます。
マテリアルデザインのTextField
TextFieldは編集可能なテキストを表示出来るUIで、マテリアルデザインのサイト(英語)にあるTextFieldの説明は次のリンクから開く事が出来ます 👉URL。
TextFieldには、背景色が違うFilled、囲み線が付くOutlineという2タイプがありますが、『コメ欄』では主にOutlineタイプのTextFieldを使っています。
右上にある例はAndroid版『コメ欄』のコメントオーバーレイのURLを入力する画面のスクリーンショットですが、「URL設定名」と「URL/スクリーンID」というタイトルが表示されているOutlinerタイプのTextField、2つが表示されています。
この例では「URL/スクリーンID」というタイトルがあるTextFieldは、オーバーレイ用のURLを入力する為のUIになっていますが、「https://ja.twitcas...」又は「https://twitcas..」という始まりの文字列が必要になリます。 なので、このTextFieldには入力されたテキストの入力間違いを検出するコードが加えられていて、間違えている部分以降を赤色で表示するという仕様になっています。
間違えている部分を赤色で表示: Android SDKでは比較的簡単に可能
TextFieldに入力された文字列の書式を部分的に変換する仕様はマテリアルデザイン本来のデザインにはありませんが、Android SDKの場合だとAndroid SDKでは一般的な方法を使って、この仕様を比較的簡単に実装する事が出来ます。
Android SDKだと、マテリアルデザインのTextFieldはTextInputLayoutクラスとこのレイアウト内に配置される
TextInputEditTextクラスで実装されます。
XMLでレイアウトを設定する場合は次の様な部分がXMLレイアウト内に必要になります。
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/label">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
</com.google.android.material.textfield.TextInputLayout>
入力された文字列はTextInputEditTextで表示されますが、TextInputEditTextはEditTextのサブクラスでもある為、継承している
addTextChangerListenerメソッドを使って入力された文字列をTextWacherに渡すというAndroid SDKでは一般的な方法で、入力されたテキストをEditableクラスとして取得出来ます。
取得したEditableはsetSpanメソッドなどを使って、そのまま書式を変更する事が出来ます。
詳しい説明は端折りますが、次の様なKotlinコードで入力された文字列を
といった関数に渡す事が出来ます。
view.findViewById<TextInputLayout>(R.id.textField)?. also { inputLayout ->
this.mUrlTextField = inputLayout
inputLayout.editText?. also { editText ->
editText.setText(this.urlValueIn)
editText.text?. also { editableText ->
updateTextChanged(editableText)
}
editText.addTextChangedListener (object: TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
}
override fun afterTextChanged(s: Editable?) {
updateTextChanged(s)
}
})
}
}
updateTextChanged関数内で変数「s」に渡される文字列の書式エラーの位置を検出した後、位置を整数変数のstartの値として設定してから同じ関数内で次のKotlinコードを実行すると、エラーの位置以降の文字を赤色にする事が出来ます。
s?. let{ it.setSpan(ForegroundColorSpan(Color.RED), start, it.length, Spannable.SPAN_INCLUSIVE_EXCLUSIVE) }
Flutterでは?
Flutterの場合だとTextFieldは、同名のTextFieldクラスで実装されています。
TextFieldクラスのリファレンスページに目を通すと、入力があった際に呼び出されるonChange、onEditComplete、onSubmittedといったコールバック(callback)用のプロパティはあるのですが、入力文字列を引数として読み込んだり、処理した文字列を戻り値として返す様な仕様にはなっていません。
ページを読む限りではTextFieldクラスのcontrollerプロパティにTextEditingControllerの値を渡すとTextEditingController.textを使う事で文字列の読み書きが可能になるというFlutterで一般的な仕様になっていますが、これだどと文字列だけで、書式を扱う事は出来ません。。
インターネット検索で「flutter textField change color」と検索した処、検索結果には文字すべての色や、背景色、文字の下の表示される線の色を変更するといった方法がトップに出てくるばかりでした。
「some」や「partially」などといったキーワードを付け加えてみると「すべてではなくいくつかの単語の色を変えたい」といった質問がいくつか引っ掛かりましたが、別のUIをそれっぽく改造するといった回答だったり、かなり長いコードを追加しないといけなかったり、プラググインパッケージを使うといった回答が多く目に着きました。Stackoverflow見つけた質問の場合だと、そのほとんどは質問者が正解の回答を選んでいない状態で放置されている状態の質問ばかりでした。
その中に、
- TextSpanを使う
- TextEditingControllerをカスタマイズする
という別々の回答があったのですが、それだけだと意味が分からず (Flutter初心者です)、そのまま検索していたのですが、Flutterのissueトラッカーの「Add WidgetSpan support for TextField/RenderEditable (TextField/RenderEditableにWidgetSpanのサポートを追加する) #30688」に付いていた回答に「TextEditingControllerのbuildTextSpan
をオーバーライドすれば良い」とあったの見た時にもしやと思い、閃きました。
buildTextSpanはTextEditingControllerのメソッドの一つで、リファレンスページにあるImplementaion (実装ソース)も次の様に割とシンプルです。
TextSpan buildTextSpan({required BuildContext context, TextStyle? style , required bool withComposing}) {
assert(!value.composing.isValid || !withComposing || value.isComposingRangeValid);
// If the composing range is out of range for the current text, ignore it to
// preserve the tree integrity, otherwise in release mode a RangeError will
// be thrown and this EditableText will be built with a broken subtree.
if (!value.isComposingRangeValid || !withComposing) {
return TextSpan(style: style, text: text);
}
final TextStyle composingStyle = style?.merge(const TextStyle(decoration: TextDecoration.underline))
?? const TextStyle(decoration: TextDecoration.underline);
return TextSpan(
style: style,
children: <TextSpan>[
TextSpan(text: value.composing.textBefore(value.text)),
TextSpan(
style: composingStyle,
text: value.composing.textInside(value.text),
),
TextSpan(text: value.composing.textAfter(value.text)),
],
);
}
コード内のいたる所にTextSpanがありますが、buildTextSpan内で渡された文字列(text)に書式(style)を加えていて、、編集中でない時は引数として渡されたstyleをそのまますべての文字列に加えるのと、編集中は編集中の部分に下線を加えて表示するといった仕様になっています。
例: TextFieldに入力された文字列の一部を赤字で表示する方法 (buildTextSpanをオーバーライド)
結局はTextEditingControllerのbuildTextSpanをオーバーライドすれば良いみたいですが、例が特に見当たらなかったので次に:
class MyEditController extends TextEditingController {
MyEditController({String? text}): super(text: text);
int _errorOffset = -1;
set errorOffset(int value) {
_errorOffset = value;
}
void resetErrorOffset() {
_errorOffset = -1;
} @override
TextSpan buildTextSpan({required BuildContext context, TextStyle? style, required bool withComposing,}) {
assert(!value.composing.isValid || !withComposing || value.isComposingRangeValid);
if (!value.isComposingRangeValid || !withComposing) {
if (0> _errorOffset) {
return TextSpan(style: style, text: text);
}
final textPre = text.substring(0, _errorOffset);
final textPost = text.substring(_errorOffset, text.length);
return TextSpan(
text: textPre,
style: style,
children: <TextSpan>[
TextSpan(
text: textPost,
style: style?.copyWith(color: Colors.redAccent,
),
),
],
);
}
final TextStyle composingStyle = style?.merge(const TextStyle(decoration:TextDecoration.underline))
?? const TextStyle(decoration: TextDecoration.underline);
return TextSpan(
style: style,
children: <TextSpan>[
TextSpan(text: value.composing.textBefore(value.text),),
TextSpan(
style: composingStyle,
text: value.composing.textInside(value.text),
),
TextSpan(text: value.composing.textAfter(value.text),),
],
);
}
}
TextEditingControllerを継承したMyEditControllerのインスタンスをTextFieldのcontrollerプロパティに渡して、MyEditController.errorOffsetでエラーの位置を設定するとその位置以降の文字が赤色で表示されます。
但し、入力中は赤色表示は反映されません・・
終わりに
ネット検索で:
- TextSpanを使う
- TextEditingControllerをカスタマイズする
といった解決法が結果に出ていましたが、どれも今回の解決法としては正解なのですが、少し抽象的で理解しにくかったので簡単な例を備忘録として記録しておきました。
コメント
コメントを投稿