You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter TabBar点击抖动摇晃动画实现问题,角度收缩抖动效果不符合预期

Tab点击抖动效果

问题修复说明

原有代码核心问题如下:

  • 仅对整个Tab做了Z轴旋转,缺失图标点击时的纵向收缩动画,且误将文字也加入了动画范围(演示效果中文字保持静止)
  • 抖动的Tween序列仅设置了单次左右摆动,没有多段衰减的抖动过程,和预期效果不符
  • 动画曲线选择错误,bounce类曲线不适合模拟快速衰减的左右抖动效果
  • 未做动画控制器的重置处理,多次连续点击会出现动画异常

完整修复代码

首先补充全局的函数类型定义,导入依赖:

import 'package:flutter/material.dart';
import 'dart:math' as math;

typedef FunctionCaller = void Function(void Function());

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;

  @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 = [
      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(
          preferredSize: const Size.fromHeight(60),
          child: Container(
            color: Colors.white,
            child: TabBar(
              onTap: _callMethoCaller,
              controller: _tabController,
              labelPadding: const EdgeInsets.only(top: 5.0, bottom: 2.0),
              indicatorColor: Colors.black,
              tabs: _tabs,
            ),
          ),
        ),
      ),
      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> _rotateAnimation;
  late Animation<double> _scaleAnimation;
  final _tabTextStyle = const TextStyle(
      fontWeight: FontWeight.w300, fontSize: 12, color: Colors.black);

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

    // 旋转抖动动画,多段衰减模拟真实抖动效果
    _rotateAnimation = TweenSequence<double>([
      TweenSequenceItem(tween: Tween(begin: 0, end: 10), weight: 20),
      TweenSequenceItem(tween: Tween(begin: 10, end: -10), weight: 30),
      TweenSequenceItem(tween: Tween(begin: -10, end: 5), weight: 30),
      TweenSequenceItem(tween: Tween(begin: 5, end: -5), weight: 30),
      TweenSequenceItem(tween: Tween(begin: -5, end: 0), weight: 20),
    ]).animate(CurvedAnimation(parent: _animationController, curve: Curves.linear));

    // 纵向收缩动画
    _scaleAnimation = TweenSequence<double>([
      TweenSequenceItem(tween: Tween(begin: 1.0, end: 0.7), weight: 25),
      TweenSequenceItem(tween: Tween(begin: 0.7, end: 1.2), weight: 25),
      TweenSequenceItem(tween: Tween(begin: 1.2, end: 1.0), weight: 50),
    ]).animate(CurvedAnimation(parent: _animationController, curve: Curves.linear));

    widget.functionCaller.call(_animationExecution);
  }

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

  void _animationExecution() {
    // 点击时先重置动画,避免连续点击异常
    if(_animationController.isAnimating) _animationController.stop();
    _animationController.reset();
    _animationController.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Tab(
      // 仅对图标做动画,文字保持静止
      icon: AnimatedBuilder(
        animation: _animationController,
        builder: (ctx, child) {
          return Transform(
            transform: Matrix4.diagonal3Values(1.0, _scaleAnimation.value, 1.0)
              ..rotateZ(math.pi * _rotateAnimation.value / 180),
            alignment: Alignment.center,
            child: child,
          );
        },
        child: Icon(widget.iconName, color: Colors.black),
      ),
      child: Text(widget.label, style: _tabTextStyle),
    );
  }
}

内容的提问来源于stack exchange,提问作者Phil D

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.27 09:06:09