Closed10
動的なTabViewを作る
ピン留めされたアイテム
TabBar
とTabBarView
は、List<Widget> をパラメーターとして持つ。そのため、List.generate()
でリストを定義し、増減時にController
を作り直すことによって動的なTabを作り出している。
とりあえず、この内容を解釈する。
動作例
スワイプでタブ移動。
フローティングボタン押すとタブ追加。
コード
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<String> data = ['Page 0', 'Page 1', 'Page 2'];
int initPosition = 1;
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: CustomTabView(
initPosition: initPosition,
itemCount: data.length,
tabBuilder: (context, index) => Tab(text: data[index]),
pageBuilder: (context, index) => Center(child: Text(data[index])),
onPositionChange: (index){
print('current position: $index');
initPosition = index;
},
onScroll: (position) => print('$position'),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
data.add('Page ${data.length}');
});
},
child: Icon(Icons.add),
),
);
}
}
class CustomTabView extends StatefulWidget {
final int? itemCount;
final IndexedWidgetBuilder? tabBuilder;
final IndexedWidgetBuilder? pageBuilder;
final Widget? stub;
final ValueChanged<int>? onPositionChange;
final ValueChanged<double>? onScroll;
final int? initPosition;
CustomTabView({this.itemCount, this.tabBuilder, this.pageBuilder, this.stub,
this.onPositionChange, this.onScroll, this.initPosition});
@override
_CustomTabsState createState() => _CustomTabsState();
}
class _CustomTabsState extends State<CustomTabView> with TickerProviderStateMixin {
late TabController controller;
late int _currentCount;
late int _currentPosition;
@override
void initState() {
_currentPosition = widget.initPosition!;
controller = TabController(
length: widget.itemCount!,
vsync: this,
initialIndex: _currentPosition,
);
controller.addListener(onPositionChange);
controller.animation!.addListener(onScroll);
_currentCount = widget.itemCount!;
super.initState();
}
@override
void didUpdateWidget(CustomTabView oldWidget) {
if (_currentCount != widget.itemCount) {
controller.animation!.removeListener(onScroll);
controller.removeListener(onPositionChange);
controller.dispose();
if (widget.initPosition != null) {
_currentPosition = widget.initPosition!;
}
if (_currentPosition > widget.itemCount! - 1) {
_currentPosition = widget.itemCount! - 1;
_currentPosition = _currentPosition < 0 ? 0 :
_currentPosition;
if (widget.onPositionChange is ValueChanged<int>) {
WidgetsBinding.instance!.addPostFrameCallback((_){
if(mounted) {
widget.onPositionChange!(_currentPosition);
}
});
}
}
_currentCount = widget.itemCount!;
setState(() {
controller = TabController(
length: widget.itemCount!,
vsync: this,
initialIndex: _currentPosition,
);
controller.addListener(onPositionChange);
controller.animation!.addListener(onScroll);
});
} else if (widget.initPosition != null) {
controller.animateTo(widget.initPosition!);
}
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
controller.animation!.removeListener(onScroll);
controller.removeListener(onPositionChange);
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (widget.itemCount! < 1) return widget.stub ?? Container();
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Container(
alignment: Alignment.center,
child: TabBar(
isScrollable: true,
controller: controller,
labelColor: Theme.of(context).primaryColor,
unselectedLabelColor: Theme.of(context).hintColor,
indicator: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).primaryColor,
width: 2,
),
),
),
tabs: List.generate(
widget.itemCount!,
(index) => widget.tabBuilder!(context, index),
),
),
),
Expanded(
child: TabBarView(
controller: controller,
children: List.generate(
widget.itemCount!,
(index) => widget.pageBuilder!(context, index),
),
),
),
],
);
}
onPositionChange() {
if (!controller.indexIsChanging) {
_currentPosition = controller.index;
if (widget.onPositionChange is ValueChanged<int>) {
widget.onPositionChange!(_currentPosition);
}
}
}
onScroll() {
if (widget.onScroll is ValueChanged<double>) {
widget.onScroll!(controller.animation!.value);
}
}
}
_MyHomePageState
_MyHomePageState
List<String> data = ['Page 0', 'Page 1', 'Page 2']; // Tabの管理
int initPosition = 1; // 選択しているタブの要素
CustomTabView(
initPosition: initPosition,
itemCount: data.length,
tabBuilder: (context, index) => Tab(text: data[index]),
pageBuilder: (context, index) => Center(child: Text(data[index])),
onPositionChange: (index){
print('current position: $index');
initPosition = index;
},
onScroll: (position) => print('$position'),
),
CustomTabView
CustomTabView
class CustomTabView extends StatefulWidget {
final int? initPosition; // 現在選択しているタブの番号(要素)
final int? itemCount; // タブの総数
final IndexedWidgetBuilder? tabBuilder; // 「Tab()」Widgetを作成する
final IndexedWidgetBuilder? pageBuilder; // 「TabBarView()」Widgetを作成する
final Widget? stub; // タブがゼロ(ない)の時に表示するWidget
final ValueChanged<int>? onPositionChange; // どのタブ(要素)に移動したか通知する
final ValueChanged<double>? onScroll; // スクロール量を検知
CustomTabView({this.initPosition, this.itemCount, this.tabBuilder, this.pageBuilder, this.stub,
this.onPositionChange, this.onScroll});
@override
_CustomTabsState createState() => _CustomTabsState();
}
- IndexedWidgetBuilder : インデックスを持つ特定のWidgetを作成する関数のシグネチャ。
- ValueChanged :
void
の関数を実行し、結果を通知する。
_CustomTabsStateを読む
late TabController controller; // TabのControllerを作成
late int _currentCount; // Tabの総数
late int _currentPosition; // 現在選択しているTab
@override
void initState() {
_currentPosition = widget.initPosition!; // 現在選択している番号(タブ)を代入
// TabControllerを定義
controller = TabController(
length: widget.itemCount!, // Controllerで管理するタブの総数
vsync: this, // フレーム関連のトリガー
initialIndex: _currentPosition, // 初期タブを設定
);
controller.addListener(onPositionChange); // オブジェクトの変更時、呼び出されるクロージャーを定義
controller.animation!.addListener(onScroll); // アニメーション実行時、呼び出されるクロージャーを定義
_currentCount = widget.itemCount!; // dataのlengthを代入
super.initState();
});
}
- addListener method : オブジェクトの変更時、呼び出されるクロージャーを定義。
- animation property : アニメーションの数値を返す。範囲は0.0 ~ length-1.0。
indexIsChanging property
このプロパティは、TabBarを選択し画面遷移中ならTrue / それ以外はFalseを返すプロパティです。
詳細
TabController
は、TabBarをタップし遷移するアニメーションをanimateTo method で実施している。つまり、animateTo method
が実行中はTrue / それ以外はFalseを返している。
上記のGifのようにTabBarを選択し、色が変色&遷移アニメーション中はTrueを返す。それ以外はFalseを返す。
addListenerで呼び出されるクロージャー
onPositionChange()
onPositionChange()
/// Tab変更時呼び出され、Tabを管理する配列の値を変更する
void onPositionChange() {
if (controller.indexIsChanging) return; // TabBarを選択し、アニメーション中は処理を拒否する
// 遷移が完了している
_currentPosition = controller.index;
widget.onPositionChange!(_currentPosition); // 現在選択しているTabを更新する
}
onScroll()
onScroll()
/// スクロール時、移動量を反映する
void onScroll() {
widget.onScroll!(controller.animation!.value);
}
WidgetsBinding.instance!.addPostFrameCallbac((_) => );
build完了直後にコールバックするらしい。
詳細
WidgetsBinding mixin
WidgetのレイヤーとFlutterエンジンを紐づける。(ドキュメントの内容を和訳)
WidgetsBinding
を一言で表すなら、ライフサイクルの取得。
instance property
現在のWidgetsBinding
を取得するプロパティ。
addPostFrameCallback method
フレームの終わり頃、コールバックを実行するメソッド。
まとめ
アプリのライフサイクルに介入し、フレームの描画終了を通知する。
didUpdateWidget method
StatefulWidget
で親Widgetの構成が変更される度呼び出される。(ツリー追加時にも実行される。)
プログラム
didUpdateWidget()
/// Widgetの構成が変更時、呼び出される。
@override
void didUpdateWidget(CustomTabView oldWidget) {
super.didUpdateWidget(oldWidget);
if (_currentCount != widget.itemCount) {
// 新規にタブが追加された時に実行
controller.removeListener(onPositionChange); // オブジェクト変更時、呼び出されるクロージャーを削除
controller.animation!.removeListener(onScroll); // アニメーション実行時、呼び出されるクロージャーを削除
controller.dispose(); // コントローラーを削除
_currentPosition = widget.initPosition; // 現在選択しているタブ
// Tab数が0の時に実行する
if (_currentPosition > widget.itemCount - 1) {
_currentPosition = widget.itemCount - 1; // (Tabを管理する)配列の要素-1を代入
_currentPosition = _currentPosition < 0 ? 0 : _currentPosition; // _currentPositionが0以下の時、0を代入。1以上の時、現状維持。
if (widget.onPositionChange is ValueChanged<int>) {
// UI描画後、実行する。(ライフサイクルに介入し、フレーム描画後の処理を定義する)
WidgetsBinding.instance!.addPostFrameCallback((_) {
// Stateオブジェクトがツリーに存在する場合、親Widgetに現在選択しているTabを通知する
if(mounted) widget.onPositionChange!(_currentPosition);
});
}
}
_currentCount = widget.itemCount; // タブの総数を更新する
// TabControllerを作り直す
setState(() {
controller = TabController(
length: widget.itemCount,
vsync: this,
initialIndex: _currentPosition,
);
controller.addListener(onPositionChange);
controller.animation!.addListener(onScroll);
});
} else {
// 初期化処理(ツリー追加時に実行)
controller.animateTo(widget.initPosition); // initPositionの数値の要素のタブに移動する
}
}
build
build()
@override
Widget build(BuildContext context) {
if (widget.itemCount < 1) return widget.stub ?? Container();
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Container(
alignment: Alignment.center,
child: TabBar(
isScrollable: true,
controller: controller,
labelColor: Theme.of(context).primaryColor, // 選択時のラベルの色
unselectedLabelColor: Theme.of(context).hintColor, // 非選択時のラベルの色
indicator: BoxDecoration( // インジケータ(Containerのプロパティで装飾)を定義する
border: Border( // 枠線を定義する
bottom: BorderSide( // 下側の境界線
color: Theme.of(context).primaryColor,
width: 2,
),
),
),
tabs: List.generate(
widget.itemCount, // リストの長さ
(index) => widget.tabBuilder(context, index), // 要素の総数分実行する
),
),
),
Expanded( // 横幅を埋める
child: TabBarView(
controller: controller,
children: List.generate(
widget.itemCount,
(index) => widget.pageBuilder(context, index),
),
),
),
],
);
}
このスクラップは2021/11/27にクローズされました