Reputation: 225
I have to use IndexedStack to maintain the state of my widgets for my BottomNavigationBar. Now i want to use AnimatedSwitcher (or an alternative) to create an animation when i switch tabs. I'm having issues getting AnimatedSwitcher to trigger on change of IndexedStack. I'm having IndexedStack as the child of AnimatedSwitcher, which obviously causes AnimatedSwitcher to not trigger because the IndexedStack widget doesn't change, only it's child.
body: AnimatedSwitcher(
duration: Duration(milliseconds: 200),
child: IndexedStack(
children: _tabs.map((t) => t.widget).toList(),
index: _currentIndex,
),
)
Is there any way around this issue? By manually triggering the AnimatedSwitcher, or by using a different method to create an animation? I also tried changing the key, but that obviously resulted in it creating a new IndexedStack everytime the a new state was created, and therefor the states of the tabs was lost as well.
Upvotes: 17
Views: 17487
Reputation: 1509
Working solution that maintains state, uses cross fade effect and lazy builds children
/// A lazy-loading [_IndexedStack] that loads [children] accordingly.
class LazyIndexedStack extends StatefulWidget {
const LazyIndexedStack({
super.key,
this.alignment = AlignmentDirectional.topStart,
this.textDirection,
this.sizing = StackFit.loose,
this.index = 0,
this.children = const [],
});
final AlignmentGeometry alignment;
final TextDirection? textDirection;
final StackFit sizing;
final int index;
final List<Widget> children;
@override
LazyIndexedStackState createState() => LazyIndexedStackState();
}
class LazyIndexedStackState extends State<LazyIndexedStack> {
late List<bool> _activated = _initializeActivatedList();
List<bool> _initializeActivatedList() =>
List<bool>.generate(widget.children.length, (i) => i == widget.index);
@override
void didUpdateWidget(covariant LazyIndexedStack oldWidget) {
if (oldWidget.children.length != widget.children.length) {
_activated = _initializeActivatedList();
}
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
// Mark current index as active
_activated[widget.index] = true;
final children = List.generate(_activated.length, (i) {
return _activated[i] ? widget.children[i] : const SizedBox.shrink();
});
return AnimatedIndexedStack(
alignment: widget.alignment,
sizing: widget.sizing,
textDirection: widget.textDirection,
index: widget.index,
children: children,
duration: Times.m200,
firstCurve: Curves.easeIn,
secondCurve: Curves.easeIn,
);
}
}
class AnimatedIndexedStack extends StatelessWidget {
const AnimatedIndexedStack({
super.key,
this.alignment = AlignmentDirectional.topStart,
this.textDirection,
this.sizing = StackFit.loose,
required this.index,
required this.children,
required this.duration,
required this.firstCurve,
required this.secondCurve,
});
final AlignmentGeometry alignment;
final TextDirection? textDirection;
final StackFit sizing;
final int index;
final List<Widget> children;
final Duration duration;
final Curve firstCurve;
final Curve secondCurve;
@override
Widget build(BuildContext context) {
final List<Widget> wrappedChildren = List<Widget>.generate(
children.length,
(int i) {
return AnimatedCrossFade(
duration: duration,
firstChild: children[i],
secondChild: const SizedBox(),
firstCurve: firstCurve,
secondCurve: secondCurve,
crossFadeState: const <bool, CrossFadeState>{
true: CrossFadeState.showFirst,
false: CrossFadeState.showSecond,
}[i == index]!,
layoutBuilder: (Widget top, Key key1, Widget bottom, Key key2) {
return Stack(
clipBehavior: Clip.none,
children: <Widget>[
Positioned.fill(key: key1, child: top),
Positioned.fill(key: key2, child: bottom),
],
);
},
);
},
);
return Stack(
alignment: alignment,
textDirection: textDirection,
fit: sizing,
children: wrappedChildren,
);
}
}
Upvotes: 0
Reputation: 2485
I was a bit disappointed myself that this wasn't supported out of the box with IndexedStack so I took a stab at making an enhanced version that would support custom transitions (with a bit more control) AND still maintain state of the children.
I made a repo with a tiny test app here: https://github.com/IMGNRY/animated_indexed_stack
Any feedback or PR's are welcome!
Upvotes: 0
Reputation: 1452
Here is how you could solved this:
First create a new widget using the code bellow.
This widget uses PageView
which will give you a nice sliding animation by default but you could customise the animation if you want.
To keep the state of the children, Just use AutomaticKeepAliveClientMixin inside each child's widget.
class IndexedStackSlider extends StatefulWidget {
/// Constract
const IndexedStackSlider({
super.key,
required this.currentIndex,
required this.children,
});
/// The current index
final int currentIndex;
/// The list of children
final List<Widget> children;
@override
State<IndexedStackSlider> createState() => _IndexedStackSliderState();
}
class _IndexedStackSliderState extends State<IndexedStackSlider> {
late PageController _pageController;
@override
void initState() {
super.initState();
_pageController = PageController(initialPage: widget.currentIndex);
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
void didUpdateWidget(IndexedStackSlider oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.currentIndex != widget.currentIndex) {
_pageController.animateToPage(
widget.currentIndex,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
}
@override
Widget build(BuildContext context) {
return PageView.builder(
controller: _pageController,
physics:
const NeverScrollableScrollPhysics(), // Disable swipe between pages
itemCount: widget.children.length,
itemBuilder: (context, index) {
return AnimatedBuilder(
animation: _pageController,
builder: (context, child) {
return Align(
alignment: Alignment.topCenter,
child: child,
);
},
child: widget.children[index],
);
},
);
}
}
Upvotes: 0
Reputation: 121
Here's example how to do it:
PageTransitionSwitcher(
duration: Duration(milliseconds: 250),
transitionBuilder: (widget, anim1, anim2) {
return FadeScaleTransition(
animation: anim1,
child: widget,
);
},
child: IndexedStack(
index: _currentIndex,
key: ValueKey<int>(_currentIndex),
children: [
Page1(),
Page2(),
Page3()
],
),
);
Upvotes: 3
Reputation: 1736
Try to add key to your IndexedStack and a transitionBuilder method to your AnimatedSwitcher widget like so...
AnimatedSwitcher(
duration: Duration(milliseconds: 1200),
transitionBuilder: (child, animation) => SizeTransition(
sizeFactor: animation,
child: IndexedStack(
key: ValueKey<int>(navigationIndex.state),
index: navigationIndex.state,
children: _tabs.map((t) => t.widget).toList(),
),
),
child: IndexedStack(
key: ValueKey<int>(navigationIndex.state), //using StateProvider<int>
index: navigationIndex.state,
children: _tabs.map((t) => t.widget).toList(),
),
),
There are also cool other transitions like ScaleTransition, SizeTransition, FadeTransition
Upvotes: 2
Reputation: 16185
If you need to use IndexedStack.
You can add custom animation and trigger it on changing tabs, like that:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
final List<Widget> myTabs = [
Tab(text: 'one'),
Tab(text: 'two'),
Tab(text: 'three'),
];
AnimationController _animationController;
TabController _tabController;
int _tabIndex = 0;
Animation animation;
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
void initState() {
_tabController = TabController(length: 3, vsync: this);
_animationController = AnimationController(
vsync: this,
value: 1.0,
duration: Duration(milliseconds: 500),
);
_tabController.addListener(_handleTabSelection);
animation = Tween(begin: 0.0, end: 1.0).animate(_animationController);
super.initState();
}
_handleTabSelection() {
if (!_tabController.indexIsChanging) {
setState(() {
_tabIndex = _tabController.index;
});
_animationController.reset();
_animationController.forward();
}
}
@override
Widget build(BuildContext context) {
List<Widget> _tabs = [
MyAnimation(
animation: animation,
child: Text('first tab'),
),
MyAnimation(
animation: animation,
child: Column(
children: List.generate(20, (index) => Text('line: $index')).toList(),
),
),
MyAnimation(
animation: animation,
child: Text('third tab'),
),
];
return Scaffold(
appBar: AppBar(),
bottomNavigationBar: TabBar(
controller: _tabController,
labelColor: Colors.redAccent,
isScrollable: true,
tabs: myTabs,
),
body: IndexedStack(
children: _tabs,
index: _tabIndex,
),
);
}
}
class MyAnimation extends AnimatedWidget {
MyAnimation({key, animation, this.child})
: super(
key: key,
listenable: animation,
);
final Widget child;
@override
Widget build(BuildContext context) {
Animation<double> animation = listenable;
return Opacity(
opacity: animation.value,
child: child,
);
}
}
Upvotes: 7
Reputation: 103461
This is a cleaner way to use IndexedStack
with animations , I created a FadeIndexedStack
widget.
https://gist.github.com/diegoveloper/1cd23e79a31d0c18a67424f0cbdfd7ad
Usage
body: FadeIndexedStack(
//this is optional
//duration: Duration(seconds: 1),
children: _tabs.map((t) => t.widget).toList(),
index: _currentIndex,
),
Upvotes: 45
Reputation: 82
Try to add key to your IndexedStack so your code will look like:
body: AnimatedSwitcher(
duration: Duration(milliseconds: 200),
child: IndexedStack(
key: ValueKey<int>(_currentIndex),
children: _tabs.map((t) => t.widget).toList(),
index: _currentIndex,
),
)
The key is changed so AnimatedSwitcher will know that it's child is need to rebuild.
Upvotes: 0