TabBarView 页面无法正确重建
TabBarView page not rebuilding correctly
我试图通过读取 TabController
的索引在 TabBarView
的每一页上显示标签编号。但出于某种原因,即使在日志中打印了正确的值,该值似乎也没有在视觉上正确更新。
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
TabController? _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(
length: 3,
vsync: this,
);
}
_back() {
if (_tabController!.index > 0) {
_tabController!.animateTo(_tabController!.index - 1);
setState(() {});
}
}
_next() {
if (_tabController!.index < _tabController!.length - 1) {
_tabController!.animateTo(_tabController!.index + 1);
setState(() {});
}
}
Widget _tab(int index) {
var value = "Page $index: ${_tabController!.index + 1} / ${_tabController!.length}";
print(value);
return Row(
children: [
TextButton(
onPressed: _back,
child: const Text("Back"),
),
Text(value,
style: const TextStyle(
),
),
TextButton(
onPressed: _next,
child: const Text("Next"),
),
],
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: TabBarView(
controller: _tabController,
children: [
_tab(1),
_tab(2),
_tab(3),
],
)
);
}
}
当从索引 0 导航到索引 1 时,日志中会按预期打印以下内容:
I/flutter (25730): Page 1: 2 / 3
I/flutter (25730): Page 2: 2 / 3
I/flutter (25730): Page 3: 2 / 3
然而实际显示的却是Page 2: 1 / 3
我试过使用 UniqueKey
以及在下一帧调用 setState
,但这没有什么区别。使用硬编码延迟调用 setState
似乎可行,但似乎也是错误的。
考虑到在调用 setState 时重建了所有选项卡,为什么日志中打印的内容与显示的内容不同?假设它与构成 TabBarView
的 PageView
/Scrollable
/Viewport
小部件有关,但到底发生了什么?请注意,即使从第 1 页转到第 2 页,然后转到第 3 页,none 任何页面上的值都在更新,因此即使是屏幕上的小部件也无法正确重建。
来自 flutter animatedTo 方法说明的文档:animatedTo 立即设置索引和上一个索引,然后从当前值到索引播放动画。
调用 _tab 方法后,return来自它的小部件现在位于小部件树中。
每当构建方法再次 运行 时,它的外观就会改变。
每次调用 _tab 方法时,不 return 小部件的代码部分再次 运行s,并且 return 小部件已经在小部件树中.
但是需要再次运行构建方法才能更改小部件。
首次构建小部件时调用构建。但是之后需要重新运行 build 方法 with setState.
我将您的选项卡导航按钮转换为小部件 class。与_tab方法和Widget的对比我们可以更容易理解class.
当从索引 0 导航到索引 1、2、3 时,日志中会打印以下内容:
颤振:第 0 页:1 / 3
颤振:第 0 页:1 / 3
import 'package:flutter/material.dart';
void main() {
runApp(const TestMyApp());
}
class TestMyApp extends StatelessWidget {
const TestMyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage>
with SingleTickerProviderStateMixin {
TabController? _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(
length: 3,
vsync: this,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
children: [
Expanded(
child: TabBarView(
controller: _tabController,
children: [
_tab(1),
_tab(2),
_tab(3),
],
),
),
Expanded(
child: TabNavigationWidget(tabController: _tabController!)),
],
));
}
Widget _tab(int index) {
return Text('$index');
}
}
class TabNavigationWidget extends StatelessWidget {
TabController tabController;
TabNavigationWidget({Key? key, required this.tabController})
: super(key: key);
@override
Widget build(BuildContext context) {
var value = "Page : ${tabController.index + 1} / ${tabController.length}";
print(value);
return Row(
children: [
TextButton(
onPressed: _back,
child: Text("Back ${tabController.index}"),
),
Text(
value,
style: const TextStyle(),
),
TextButton(
onPressed: _next,
child: const Text("Next"),
),
],
);
}
_back() {
if (tabController.index > 0) {
tabController.animateTo(tabController.index - 1);
}
}
_next() {
if (tabController.index < tabController.length - 1) {
tabController.animateTo(tabController.index + 1);
}
}
}
我建议您使用小部件 classes 而不是 _tab() 方法。当调用 setState 方法时,您的 _tab 方法构建了 3 次。
切换页面时可以使用Stream监听tab索引变化。换页时更新索引。
final _tabPageIndicator = StreamController<int>.broadcast();
Stream<int> get getTabPage => _tabPageIndicator.stream;
...
// Update tab index on Stream
_tabPageIndicator.sink.add(_tabController!.index + 1);
然后使用 StreamBuilder,当它正在侦听的 Stream 发生变化时,它会重建。无需使用 setState()
在 StreamBuilder 中重建 Widgets。
StreamBuilder<int>(
stream: getTabPage,
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
if (snapshot.hasData && snapshot.data != null) {
tabIndex = snapshot.data!;
}
return Text(
'Page $index: [$tabIndex / ${_tabController!.length}]',
style: const TextStyle(),
);
}
),
完整样本
import 'dart:async';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
TabController? _tabController;
int tabIndex = 1;
final _tabPageIndicator = StreamController<int>.broadcast();
Stream<int> get getTabPage => _tabPageIndicator.stream;
@override
void initState() {
super.initState();
_tabController = TabController(
length: 3,
vsync: this,
);
// Update tab index on Stream
_tabPageIndicator.sink.add(_tabController!.index + 1);
}
@override
void dispose() {
super.dispose();
// Close Stream when not in use
_tabPageIndicator.close();
}
_back() {
if (_tabController!.index > 0) {
_tabController!.animateTo(_tabController!.index - 1);
// setState(() {
// });
// Update tab index on Stream
_tabPageIndicator.sink.add(_tabController!.index + 1);
}
}
_next() {
if (_tabController!.index < _tabController!.length - 1) {
_tabController!.animateTo(_tabController!.index + 1);
// setState(() {
// });
// Update tab index on Stream
_tabPageIndicator.sink.add(_tabController!.index + 1);
}
}
Widget _tab(int index) {
var value =
"Page $index: ${_tabController!.index + 1} / ${_tabController!.length}";
debugPrint(value);
return Row(
children: [
TextButton(
onPressed: _back,
child: const Text("Back"),
),
// StreamBuilder rebuilds every time there's a change on Stream
StreamBuilder<int>(
stream: getTabPage,
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
if (snapshot.hasData && snapshot.data != null) {
tabIndex = snapshot.data!;
}
return Text(
'Page $index: [$tabIndex / ${_tabController!.length}]',
style: const TextStyle(),
);
}),
TextButton(
onPressed: _next,
child: const Text("Next"),
),
],
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: TabBarView(
controller: _tabController,
children: [
_tab(1),
_tab(2),
_tab(3),
],
));
}
}
我了解的太少widget tree
欢迎指正和更新答案
所有选项卡最初都在构建,而 _tabController!.index
是 0
。 _next
方法会等待 animateTo
完成动画,然后调用 setState
。使用 setState
重建 build
下的 UI 但 TabBarView
不会重建,直到我们告诉它它正在发生变化。
widget tree is smart enough while updating the UI. -
在创建 widget, without providing key 时,它会生成 objectRuntimeType
密钥,并且在调用 setState
.
时不会更改(与提供密钥相同)
虽然在这里,更新取决于键,并且 TabBarView
和 TabBarView
的 widget tree(key) 没有什么不同,但我认为我什么都没发生,我们看不到任何更新UI.
接下来是 adding listener
Register a closure to be called when the object changes.
我们可以在 TabController
上添加侦听器以侦听更改并在 setState
内添加侦听器以更新 UI。您还可以从 _back
和 _next
方法中删除 setState
。
_tabController = TabController(
length: 3,
vsync: this,
)..addListener(() {
setState(() {});
});
或者只是
Use index
instead of _tabController!.index
while both responsibility is same inside Row.
我终于可以回答我自己的问题了。 _TabBarViewState
的内部逻辑解释了这种奇怪的行为。 TabBarView
在内部使用 PageView
,它根据 TabController
索引的变化进行动画处理。这是该逻辑的一个片段:
final int previousIndex = _controller!.previousIndex;
if ((_currentIndex! - previousIndex).abs() == 1) {
_warpUnderwayCount += 1;
await _pageController.animateToPage(_currentIndex!, duration: kTabScrollDuration, curve: Curves.ease);
_warpUnderwayCount -= 1;
return Future<void>.value();
}
请注意,它会跟踪是否正在使用 _warpUnderwayCount
变量进行动画处理,一旦我们在 TabController
.
此外,_TabBarViewState
维护一个 _children
代表每个页面的小部件列表,该列表首先在 TabBarView
初始化时创建,以后只能由 _TabBarViewState
本身通过调用其 _updateChildren()
函数:
void _updateChildren() {
_children = widget.children;
_childrenWithKey = KeyedSubtree.ensureUniqueKeysForList(widget.children);
}
_TabBarViewState
也覆盖了 didUpdateWidget
函数的默认行为:
@override
void didUpdateWidget(TabBarView oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.controller != oldWidget.controller)
_updateTabController();
if (widget.children != oldWidget.children && _warpUnderwayCount == 0)
_updateChildren();
}
请注意,即使我们通过在 animateTo()
之后调用 setState()
从我们的父有状态小部件提供新的 children
列表,children
的列表将是被 TabBarView
忽略,因为 _warpUnderwayCount
在 didUpdateWidget
被调用时的值为 1
,因此 _updateChildren()
不会被调用内部逻辑如上所示。
我认为这是 TabBarView
小部件的一个限制,它与其内部 PageView
以及可选的 TabBar
小部件协调方面的复杂性有关它与它共享 TabController
.
就解决方案而言,考虑到通过更新其 Key
来重建整个 TabBarView
会取消动画,并且通过调用 setState()
设置新的 children
如果在页面更改动画仍然是 运行 时调用 animateTo()
被忽略,我只能考虑在保存重建 children
所需的所有变量后调用 setState()
和before animateTo()
在下一帧调用。如果在同一帧内调用,children
仍然不会更新,因为 didUpdateWidget
仍然会在动画开始后被调用。这是我的问题的代码,更新了建议的解决方案:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
TabController? _tabController;
int _newIndex = 0;
@override
void initState() {
super.initState();
_tabController = TabController(
length: 3,
vsync: this,
);
}
_back() {
if (_tabController!.index > 0) {
_newIndex = _tabController!.index - 1;
setState(() {});
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
_tabController!.animateTo(_newIndex);
});
}
}
_next() {
if (_tabController!.index < _tabController!.length - 1) {
_newIndex = _tabController!.index + 1;
setState(() {});
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
_tabController!.animateTo(_newIndex);
});
}
}
Widget _tab(int index) {
var value = "Page $index: ${_newIndex + 1} / ${_tabController!.length}";
print(value);
return Row(
children: [
TextButton(
onPressed: _back,
child: const Text("Back"),
),
Text(value,
style: const TextStyle(
),
),
TextButton(
onPressed: _next,
child: const Text("Next"),
),
],
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: TabBarView(
controller: _tabController,
children: [
_tab(1),
_tab(2),
_tab(3),
],
)
);
}
}
我试图通过读取 TabController
的索引在 TabBarView
的每一页上显示标签编号。但出于某种原因,即使在日志中打印了正确的值,该值似乎也没有在视觉上正确更新。
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
TabController? _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(
length: 3,
vsync: this,
);
}
_back() {
if (_tabController!.index > 0) {
_tabController!.animateTo(_tabController!.index - 1);
setState(() {});
}
}
_next() {
if (_tabController!.index < _tabController!.length - 1) {
_tabController!.animateTo(_tabController!.index + 1);
setState(() {});
}
}
Widget _tab(int index) {
var value = "Page $index: ${_tabController!.index + 1} / ${_tabController!.length}";
print(value);
return Row(
children: [
TextButton(
onPressed: _back,
child: const Text("Back"),
),
Text(value,
style: const TextStyle(
),
),
TextButton(
onPressed: _next,
child: const Text("Next"),
),
],
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: TabBarView(
controller: _tabController,
children: [
_tab(1),
_tab(2),
_tab(3),
],
)
);
}
}
当从索引 0 导航到索引 1 时,日志中会按预期打印以下内容:
I/flutter (25730): Page 1: 2 / 3
I/flutter (25730): Page 2: 2 / 3
I/flutter (25730): Page 3: 2 / 3
然而实际显示的却是Page 2: 1 / 3
我试过使用 UniqueKey
以及在下一帧调用 setState
,但这没有什么区别。使用硬编码延迟调用 setState
似乎可行,但似乎也是错误的。
考虑到在调用 setState 时重建了所有选项卡,为什么日志中打印的内容与显示的内容不同?假设它与构成 TabBarView
的 PageView
/Scrollable
/Viewport
小部件有关,但到底发生了什么?请注意,即使从第 1 页转到第 2 页,然后转到第 3 页,none 任何页面上的值都在更新,因此即使是屏幕上的小部件也无法正确重建。
来自 flutter animatedTo 方法说明的文档:animatedTo 立即设置索引和上一个索引,然后从当前值到索引播放动画。
调用 _tab 方法后,return来自它的小部件现在位于小部件树中。
每当构建方法再次 运行 时,它的外观就会改变。
每次调用 _tab 方法时,不 return 小部件的代码部分再次 运行s,并且 return 小部件已经在小部件树中. 但是需要再次运行构建方法才能更改小部件。
首次构建小部件时调用构建。但是之后需要重新运行 build 方法 with setState.
我将您的选项卡导航按钮转换为小部件 class。与_tab方法和Widget的对比我们可以更容易理解class.
当从索引 0 导航到索引 1、2、3 时,日志中会打印以下内容: 颤振:第 0 页:1 / 3 颤振:第 0 页:1 / 3
import 'package:flutter/material.dart';
void main() {
runApp(const TestMyApp());
}
class TestMyApp extends StatelessWidget {
const TestMyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage>
with SingleTickerProviderStateMixin {
TabController? _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(
length: 3,
vsync: this,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
children: [
Expanded(
child: TabBarView(
controller: _tabController,
children: [
_tab(1),
_tab(2),
_tab(3),
],
),
),
Expanded(
child: TabNavigationWidget(tabController: _tabController!)),
],
));
}
Widget _tab(int index) {
return Text('$index');
}
}
class TabNavigationWidget extends StatelessWidget {
TabController tabController;
TabNavigationWidget({Key? key, required this.tabController})
: super(key: key);
@override
Widget build(BuildContext context) {
var value = "Page : ${tabController.index + 1} / ${tabController.length}";
print(value);
return Row(
children: [
TextButton(
onPressed: _back,
child: Text("Back ${tabController.index}"),
),
Text(
value,
style: const TextStyle(),
),
TextButton(
onPressed: _next,
child: const Text("Next"),
),
],
);
}
_back() {
if (tabController.index > 0) {
tabController.animateTo(tabController.index - 1);
}
}
_next() {
if (tabController.index < tabController.length - 1) {
tabController.animateTo(tabController.index + 1);
}
}
}
我建议您使用小部件 classes 而不是 _tab() 方法。当调用 setState 方法时,您的 _tab 方法构建了 3 次。
切换页面时可以使用Stream监听tab索引变化。换页时更新索引。
final _tabPageIndicator = StreamController<int>.broadcast();
Stream<int> get getTabPage => _tabPageIndicator.stream;
...
// Update tab index on Stream
_tabPageIndicator.sink.add(_tabController!.index + 1);
然后使用 StreamBuilder,当它正在侦听的 Stream 发生变化时,它会重建。无需使用 setState()
在 StreamBuilder 中重建 Widgets。
StreamBuilder<int>(
stream: getTabPage,
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
if (snapshot.hasData && snapshot.data != null) {
tabIndex = snapshot.data!;
}
return Text(
'Page $index: [$tabIndex / ${_tabController!.length}]',
style: const TextStyle(),
);
}
),
完整样本
import 'dart:async';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
TabController? _tabController;
int tabIndex = 1;
final _tabPageIndicator = StreamController<int>.broadcast();
Stream<int> get getTabPage => _tabPageIndicator.stream;
@override
void initState() {
super.initState();
_tabController = TabController(
length: 3,
vsync: this,
);
// Update tab index on Stream
_tabPageIndicator.sink.add(_tabController!.index + 1);
}
@override
void dispose() {
super.dispose();
// Close Stream when not in use
_tabPageIndicator.close();
}
_back() {
if (_tabController!.index > 0) {
_tabController!.animateTo(_tabController!.index - 1);
// setState(() {
// });
// Update tab index on Stream
_tabPageIndicator.sink.add(_tabController!.index + 1);
}
}
_next() {
if (_tabController!.index < _tabController!.length - 1) {
_tabController!.animateTo(_tabController!.index + 1);
// setState(() {
// });
// Update tab index on Stream
_tabPageIndicator.sink.add(_tabController!.index + 1);
}
}
Widget _tab(int index) {
var value =
"Page $index: ${_tabController!.index + 1} / ${_tabController!.length}";
debugPrint(value);
return Row(
children: [
TextButton(
onPressed: _back,
child: const Text("Back"),
),
// StreamBuilder rebuilds every time there's a change on Stream
StreamBuilder<int>(
stream: getTabPage,
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
if (snapshot.hasData && snapshot.data != null) {
tabIndex = snapshot.data!;
}
return Text(
'Page $index: [$tabIndex / ${_tabController!.length}]',
style: const TextStyle(),
);
}),
TextButton(
onPressed: _next,
child: const Text("Next"),
),
],
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: TabBarView(
controller: _tabController,
children: [
_tab(1),
_tab(2),
_tab(3),
],
));
}
}
我了解的太少widget tree
欢迎指正和更新答案
所有选项卡最初都在构建,而 _tabController!.index
是 0
。 _next
方法会等待 animateTo
完成动画,然后调用 setState
。使用 setState
重建 build
下的 UI 但 TabBarView
不会重建,直到我们告诉它它正在发生变化。
widget tree is smart enough while updating the UI. -
在创建 widget, without providing key 时,它会生成 objectRuntimeType
密钥,并且在调用 setState
.
虽然在这里,更新取决于键,并且 TabBarView
和 TabBarView
的 widget tree(key) 没有什么不同,但我认为我什么都没发生,我们看不到任何更新UI.
接下来是 adding listener
Register a closure to be called when the object changes.
我们可以在 TabController
上添加侦听器以侦听更改并在 setState
内添加侦听器以更新 UI。您还可以从 _back
和 _next
方法中删除 setState
。
_tabController = TabController(
length: 3,
vsync: this,
)..addListener(() {
setState(() {});
});
或者只是
Use
index
instead of_tabController!.index
while both responsibility is same inside Row.
我终于可以回答我自己的问题了。 _TabBarViewState
的内部逻辑解释了这种奇怪的行为。 TabBarView
在内部使用 PageView
,它根据 TabController
索引的变化进行动画处理。这是该逻辑的一个片段:
final int previousIndex = _controller!.previousIndex;
if ((_currentIndex! - previousIndex).abs() == 1) {
_warpUnderwayCount += 1;
await _pageController.animateToPage(_currentIndex!, duration: kTabScrollDuration, curve: Curves.ease);
_warpUnderwayCount -= 1;
return Future<void>.value();
}
请注意,它会跟踪是否正在使用 _warpUnderwayCount
变量进行动画处理,一旦我们在 TabController
.
此外,_TabBarViewState
维护一个 _children
代表每个页面的小部件列表,该列表首先在 TabBarView
初始化时创建,以后只能由 _TabBarViewState
本身通过调用其 _updateChildren()
函数:
void _updateChildren() {
_children = widget.children;
_childrenWithKey = KeyedSubtree.ensureUniqueKeysForList(widget.children);
}
_TabBarViewState
也覆盖了 didUpdateWidget
函数的默认行为:
@override
void didUpdateWidget(TabBarView oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.controller != oldWidget.controller)
_updateTabController();
if (widget.children != oldWidget.children && _warpUnderwayCount == 0)
_updateChildren();
}
请注意,即使我们通过在 animateTo()
之后调用 setState()
从我们的父有状态小部件提供新的 children
列表,children
的列表将是被 TabBarView
忽略,因为 _warpUnderwayCount
在 didUpdateWidget
被调用时的值为 1
,因此 _updateChildren()
不会被调用内部逻辑如上所示。
我认为这是 TabBarView
小部件的一个限制,它与其内部 PageView
以及可选的 TabBar
小部件协调方面的复杂性有关它与它共享 TabController
.
就解决方案而言,考虑到通过更新其 Key
来重建整个 TabBarView
会取消动画,并且通过调用 setState()
设置新的 children
如果在页面更改动画仍然是 运行 时调用 animateTo()
被忽略,我只能考虑在保存重建 children
所需的所有变量后调用 setState()
和before animateTo()
在下一帧调用。如果在同一帧内调用,children
仍然不会更新,因为 didUpdateWidget
仍然会在动画开始后被调用。这是我的问题的代码,更新了建议的解决方案:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
TabController? _tabController;
int _newIndex = 0;
@override
void initState() {
super.initState();
_tabController = TabController(
length: 3,
vsync: this,
);
}
_back() {
if (_tabController!.index > 0) {
_newIndex = _tabController!.index - 1;
setState(() {});
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
_tabController!.animateTo(_newIndex);
});
}
}
_next() {
if (_tabController!.index < _tabController!.length - 1) {
_newIndex = _tabController!.index + 1;
setState(() {});
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
_tabController!.animateTo(_newIndex);
});
}
}
Widget _tab(int index) {
var value = "Page $index: ${_newIndex + 1} / ${_tabController!.length}";
print(value);
return Row(
children: [
TextButton(
onPressed: _back,
child: const Text("Back"),
),
Text(value,
style: const TextStyle(
),
),
TextButton(
onPressed: _next,
child: const Text("Next"),
),
],
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: TabBarView(
controller: _tabController,
children: [
_tab(1),
_tab(2),
_tab(3),
],
)
);
}
}