RevoltPlz
RevoltPlz

Reputation: 225

AnimatedSwitcher with IndexedStack

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

Answers (8)

Kirill
Kirill

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

bobmoff
bobmoff

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

MohKoma
MohKoma

Reputation: 1452

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],
            );
          },
        );
      }
    }
    

Upvotes: 0

Barry
Barry

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

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

Kherel
Kherel

Reputation: 16185

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

Upvotes: 7

diegoveloper
diegoveloper

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

Slava R
Slava R

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

Related Questions