TextField のスクロールバーを制御する
TextField を maxLines: null + expands: true で使うと、textarea のような複数行入力欄として利用できるが、入力欄の高さを固定していると入力量に応じて縦方向にスクロールが発生する。
このスクロールバーは、素朴に Scrollbar ウィジェットで囲んでも狙った見た目にはならない。
本記事では、 ScrollConfiguration と RawScrollbar を組み合わせることで、 TextField 内部のスクロールバーの見た目を直接コントロールする方法を紹介する。
Scrollbarウィジェットで囲む場合
Section titled “Scrollbarウィジェットで囲む場合”まず比較対象として、素直に Scrollbar ウィジェットで TextField を囲む実装を見る。
class CustomTextField extends StatelessWidget { const CustomTextField({ super.key, required this.controller, required this.scrollController, });
final TextEditingController controller; final ScrollController scrollController;
@override Widget build(BuildContext context) { return SizedBox( height: 100, child: Scrollbar( controller: scrollController, thumbVisibility: true, interactive: true, thickness: 6, radius: const Radius.circular(8), child: TextField( controller: controller, scrollController: scrollController, keyboardType: TextInputType.multiline, maxLines: null, expands: true, textAlignVertical: TextAlignVertical.top, decoration: const InputDecoration( hintText: '入力してください', contentPadding: EdgeInsets.all(10), border: OutlineInputBorder(), ), ), ), ); }}
Scrollbar(Material 版)はthumbVisibility/trackVisibility/thickness/radius/interactiveは公開しているが、つまみの色を直接指定するプロパティ(thumbColor相当)は用意されていない- 色を変えたい場合は
ScrollbarTheme/ThemeData.scrollbarTheme経由でアプリ全体・スコープ単位に指定する必要がある
- 色を変えたい場合は
TextFieldは複数行入力時、内部のEditableTextが独自のScrollableを持っており、そのScrollableは自身を囲むScrollConfigurationのスクロールバー構築処理を使ってスクロールバーを自動的に用意する- そのため外側から
Scrollbarを被せても、「外側のラップ」と「内部で自動生成される分」の両方が絡み合い、狙った通りの太さ・色・余白にならないことがある
- そのため外側から
- 結果として、「1 個の
Scrollbarウィジェットを被せるだけ」では、色を含めた見た目の細かい調整がしづらい- また
TextFieldのcontentPaddingの影響等で、スクロールバーの表示領域が狭まってしまう
- また
ScrollConfiguration + RawScrollbarでカスタムする
Section titled “ScrollConfiguration + RawScrollbarでカスタムする”そこで、 TextField 配下のスクロール可能領域が使うスクロールバー構築処理そのものを差し替える。
class CustomTextField extends StatelessWidget { const CustomTextField({ super.key, required this.controller, required this.scrollController, });
final TextEditingController controller; final ScrollController scrollController;
@override Widget build(BuildContext context) { return SizedBox( height: 100, child: ScrollConfiguration( behavior: const _TextareaScrollBehavior(), child: TextField( controller: controller, scrollController: scrollController, keyboardType: TextInputType.multiline, maxLines: null, expands: true, textAlignVertical: TextAlignVertical.top, decoration: const InputDecoration( hintText: '入力してください', contentPadding: EdgeInsets.only( left: 10, top: 10, right: 0, bottom: 10, ), border: OutlineInputBorder(), ), ), ), ); }}
class _TextareaScrollBehavior extends MaterialScrollBehavior { const _TextareaScrollBehavior();
@override Widget buildScrollbar( BuildContext context, Widget child, ScrollableDetails details, ) { final controller = details.controller;
if (controller == null || axisDirectionToAxis(details.direction) != Axis.vertical) { return child; }
return RawScrollbar( controller: controller, thumbVisibility: true, interactive: true, padding: EdgeInsets.zero,
// カスタムしたことが分かりやすいように青色にする thumbColor: Colors.blueAccent, thickness: 6, radius: const Radius.circular(8), crossAxisMargin: 4,
// スクロールバーとテキストが被らないように余白を設ける child: Padding(padding: const EdgeInsets.only(right: 10), child: child), ); }}
ScrollConfigurationのbehaviorに独自のScrollBehaviorを渡すと、その配下にあるすべてのScrollable(TextField内部のScrollableを含む)がスクロールバーを構築する際にこのbuildScrollbarを経由するようになるMaterialScrollBehaviorを継承しbuildScrollbarをオーバーライドすることで、デフォルトのスクロールバー生成処理を丸ごと差し替えられるdetails.controllerがnull、または縦方向以外のスクロールの場合はそのままchildを返し、余計な変更を加えない- 縦方向のスクロールのときだけ
RawScrollbarを使ってラップする
RawScrollbarは Material のScrollbarと違い、thumbColorを直接指定できるため、つまみの色を含めて見た目を細かく制御できるPaddingで右側に余白を持たせることで、スクロールバーとテキストが重ならないようにしている
- textarea 的な複数行
TextFieldのスクロールバーを見た目まで含めてカスタムしたい場合、単にScrollbarで囲むだけでは調整が難しい ScrollConfigurationのbuildScrollbarをオーバーライドし、対象を縦方向のスクロールに絞った上でRawScrollbarを組み合わせることで、狙った通りのスタイルを適用できる- 逆に、アプリ全体で統一されたスクロールバーで十分な場合は、無理に
ScrollConfigurationをカスタムせずScrollbarThemeで済ませる方がシンプル