AnimatedSwitcher with IndexedStack
Asked Answered
H

7

17

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.

Hogen answered 28/8, 2019 at 14:1 Comment(0)
H
7

If you need to use IndexedStack.

You can add custom animation and trigger it on changing tabs, like that: enter image description here

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,
    );
  }
}
Harrington answered 29/8, 2019 at 3:33 Comment(5)
Yeah i know that's the reason why it's not switching. But i need to preserve the state of the tabs, and the only way i've found to do that was using an IndexedStack. Do you know any alternatives to IndexedStack that are maintain the state of the tabs and that is compatible with AnimatedSwitcher?Hogen
You can save tab state at upper level and pass it down from there. What kind of data you need to store?Harrington
Most tabs contain for example a listview, for which i want to preserve the scroll position. If i were to save the tab state at upper level, how would i do that if my tabPage is the main route using namedRoutes?Hogen
Persistent state and state management in itself are the big themes. It is possible that using indexedState could works well in your case.Harrington
@Harrington thanks so much, you save my time, can we have reveal effect instead?Fsh
K
44

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,  
  ),  

Killerdiller answered 30/12, 2019 at 18:27 Comment(2)
You save my day! Thank you!Condominium
You are my Hero. Very good work! Flutter should implement that in their Framework! Appreciate it, thank you very much.Hirundine
H
7

If you need to use IndexedStack.

You can add custom animation and trigger it on changing tabs, like that: enter image description here

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,
    );
  }
}
Harrington answered 29/8, 2019 at 3:33 Comment(5)
Yeah i know that's the reason why it's not switching. But i need to preserve the state of the tabs, and the only way i've found to do that was using an IndexedStack. Do you know any alternatives to IndexedStack that are maintain the state of the tabs and that is compatible with AnimatedSwitcher?Hogen
You can save tab state at upper level and pass it down from there. What kind of data you need to store?Harrington
Most tabs contain for example a listview, for which i want to preserve the scroll position. If i were to save the tab state at upper level, how would i do that if my tabPage is the main route using namedRoutes?Hogen
Persistent state and state management in itself are the big themes. It is possible that using indexedState could works well in your case.Harrington
@Harrington thanks so much, you save my time, can we have reveal effect instead?Fsh
G
3

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()
    ],
  ),
);
Gangrene answered 10/10, 2021 at 0:39 Comment(0)
P
2

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

Preface answered 5/7, 2021 at 22:41 Comment(0)
E
0

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.

Eyeleen answered 23/4, 2020 at 7:26 Comment(1)
This doens't work as intended because it does not keep state between switched widgets in IndexedStackPolysepalous
C
0

Here is how you could solved this:

  1. First create a new widget using the code bellow.

  2. This widget uses PageView which will give you a nice sliding animation by default but you could customise the animation if you want.

  3. 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],
            );
          },
        );
      }
    }
    
Convulse answered 30/7, 2023 at 13:32 Comment(0)
D
0

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!

Debbradebby answered 3/8, 2023 at 17:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.