Shake/jitter animation for TabBar Flutter

Question

Asked by Phil D on November 08, 2021 (source).

enter image description here

Trying to get desired output of the Animation above. Tried with AnimatedBuilder along with Transform of Matrix of z-axis for this animation. But failed to do so. Here's my code. I couldn't get the angle, shrinking or the shaking part right.

MyHomePage


class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
  late TabController _tabController;
  late void Function() _memberCaller;
  late void Function() _voucherCaller;
  late void Function() _rewardCaller;
  late void Function() _drinksCaller;

  double degree = 0;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 4, vsync: this);
  }

  @override
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

  void _callMethoCaller(int index) {
    switch (index) {
      case 0:
        _memberCaller.call();
        break;
      case 1:
        _voucherCaller.call();
        break;
      case 2:
        _rewardCaller.call();
        break;
      case 3:
        _drinksCaller.call();
        break;
    }
  }

  @override
  Widget build(BuildContext context) {
    List<Widget> _tabs(int index) => [
          AnimationTab(
              iconName: Icons.card_giftcard,
              label: 'Membership',
              functionCaller: (void Function() method) {
                _memberCaller = method;
              }),
          AnimationTab(
              iconName: Icons.confirmation_num_outlined,
              label: 'Voucher',
              functionCaller: (void Function() method) {
                _voucherCaller = method;
              }),
          AnimationTab(
              iconName: Icons.emoji_events_outlined,
              label: 'Rewards',
              functionCaller: (void Function() method) {
                _rewardCaller = method;
              }),
          AnimationTab(
              iconName: Icons.wine_bar_outlined,
              label: 'Drinks',
              functionCaller: (void Function() method) {
                _drinksCaller = method;
              }),
        ];

    return Scaffold(
      appBar: AppBar(
        centerTitle: true,
        bottom: PreferredSize(
          child: Container(
            child: TabBar(
              onTap: _callMethoCaller,
              controller: _tabController,
              labelPadding: EdgeInsets.only(top: 5.0, bottom: 2.0),
              indicatorColor: Colors.black,
              tabs: List.generate(4, (index) => _tabs(index)[index]),
            ),
            decoration: BoxDecoration(
              color: Colors.white,
              boxShadow: const [
                BoxShadow(
                    color: Colors.white,
                    spreadRadius: 5.0,
                    offset: Offset(0, 3))
              ],
            ),
          ),
          preferredSize: Size.fromHeight(30),
        ),
      ),
      body: TabBarView(
        controller: _tabController,
        children: const [
          Center(child: Text('1')),
          Center(child: Text('2')),
          Center(child: Text('3')),
          Center(child: Text('4')),
        ],
      ),
    );
  }
}

AnimationTab

class AnimationTab extends StatefulWidget {
  final String label;
  final IconData iconName;
  final FunctionCaller functionCaller;

  const AnimationTab({
    Key? key,
    required this.iconName,
    required this.label,
    required this.functionCaller,
  }) : super(key: key);

  @override
  _AnimationTabState createState() => _AnimationTabState();
}

class _AnimationTabState extends State<AnimationTab>
    with TickerProviderStateMixin {
  late AnimationController _animationController;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      vsync: this,
      duration: Duration(milliseconds: 500),
    );

    final _curvedAnimation = CurvedAnimation(
        parent: _animationController,
        curve: Curves.bounceIn,
        reverseCurve: Curves.bounceOut);

    _animation = TweenSequence<double>([
      TweenSequenceItem<double>(tween: Tween(begin: 0, end: 12.5), weight: 1),
      TweenSequenceItem<double>(tween: Tween(begin: 12.5, end: 0), weight: 1),
      TweenSequenceItem<double>(tween: Tween(begin: 0, end: -12.5), weight: 1),
    ]).animate(_curvedAnimation)
      ..addListener(() {
        setState(() {});
      })
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          _animationController.reverse();
        }
      });
  }

  void _animationExecution() {
    _animationController.forward();
  }

  @override
  Widget build(BuildContext context) {
    const _tabTextStyle = TextStyle(
        fontWeight: FontWeight.w300, fontSize: 12, color: Colors.black);

    widget.functionCaller.call(_animationExecution);

    return AnimatedBuilder(
      animation: _animationController,
      builder: (ctx, _) {
        return Transform(
          transform: Matrix4.rotationZ(math.pi * _animation.value / 180),
          alignment: Alignment.center,
          child: Tab(
            icon: Icon(widget.iconName, color: Colors.black),
            child: Text(widget.label, style: _tabTextStyle),
          ),
        );
      },
    );
  }
}

Answer

Question answered by pskink (source).

you dont need those TweenSequences, "status listeners" etc, also instead of Matrix4.rotationZ use ordinary Transform.rotate, check this:

class TabTest extends StatefulWidget {
  @override
  _TabTestState createState() => _TabTestState();
}

class _TabTestState extends State<TabTest> with TickerProviderStateMixin {
  late TabController tabController;
  late List<AnimationController> animationControllers;

  @override
  void initState() {
    super.initState();
    tabController = TabController(length: 4, vsync: this)
      ..addListener(_listener);
    animationControllers = List.generate(4, (i) => AnimationController(
      vsync: this,
      duration: Duration(milliseconds: 750),
      reverseDuration: Duration(milliseconds: 350),
    ));
  }

  @override
  Widget build(BuildContext context) {
    // timeDilation = 5;
    return Scaffold(
      appBar: AppBar(
        bottom: TabBar(
          tabs: List.generate(4, (i) => AnimatedBuilder(
              animation: animationControllers[i],
              builder: (context, child) {
                final child = Tab(
                  icon: Icon(Icons.cast),
                  child: Text('tab $i'),
                );
                final value = animationControllers[i].value;
                if (animationControllers[i].status == AnimationStatus.forward) {
                  final angle = sin(4 * pi * value) * pi * 0.3;
                  return Transform.rotate(angle: angle, child: child);
                } else {
                  final dy = sin(2 * pi * value) * 0.2;
                  return FractionalTranslation(translation: Offset(0, dy), child: child);
                }
              },
            ),
          ),
          controller: tabController,
        ),
      ),
      body: TabBarView(
        children: List.generate(4, (i) =>
          FittedBox(
            child: Text('tab $i'),
          ),
        ),
        controller: tabController,
      ),
    );
  }

  void _listener() {
    if (tabController.indexIsChanging) {
      animationControllers[tabController.previousIndex].reverse();
    } else  {
      animationControllers[tabController.index].forward();
    }
  }

  @override
  void dispose() {
    super.dispose();
    tabController
      ..removeListener(_listener)
      ..dispose();
    animationControllers.forEach((ac) => ac.dispose());
  }
}
ANIMATION DART FLUTTER MATRIX TRANSFORM
SHARE: