TabBar scroll/slide 导致显示不准确

TabBar scroll/slide making inaccuracy of displaying

我使用 Container 的颜色来制作类似指示器的颜色,而不是使用 TabBar 的指示器,因为我必须为 Container.[=24 实现一些动画=]

TabController索引改变时,setState在监听器中被调用。在 TabBar 上尝试 scroll/slide,TabBar 没有正确更改索引,因为 listener 没有听 TabBar.[=24 的动画=]

我试过使用 tabcontroller.animation.addListener 方法,但没有任何解决方法可以控制滚动运动。

下面的附加视频演示了点击和 scroll/slide 应用于 TabBar

TabBar-Scroll/Slide

代码:

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) {
    List<IconData> _tabIconData = [
      Icons.card_giftcard,
      Icons.confirmation_num_outlined,
      Icons.emoji_events_outlined,
      Icons.wine_bar_outlined,
    ];

    List<String> _tabLabel = [
      'Tab1',
      'Tab2',
      'Tab3',
      'Tab4',
    ];

    Widget _tab({
      required IconData iconData,
      required String label,
      required bool isSelectedIndex,
      // required double widthAnimation,
      // required heightAnimation,
    }) {
      const _tabTextStyle = TextStyle(
          fontWeight: FontWeight.w300, fontSize: 12, color: Colors.black);
      return AnimatedContainer(
        duration: Duration(milliseconds: 300),
        padding: EdgeInsets.only(bottom: 2.0),
        height: 55,
        width: double.infinity, //_animContainerWidth - widthAnimation,
        decoration: BoxDecoration(
          border: Border(
            bottom: BorderSide(
              color: isSelectedIndex ? Colors.black : Colors.transparent,
              width: 2.0,
            ),
          ),
        ),
        child: Tab(
          iconMargin: EdgeInsets.only(bottom: 5.0),
          icon: Icon(iconData, color: Colors.black),
          child: Text(label, style: _tabTextStyle),
        ),
      );
    }

    List<Widget> _animationGenerator() {
      return List.generate(
        4,
        (index) => ClipRRect(
          child: AnimatedBuilder(
              animation: _animationControllers[index],
              builder: (ctx, _) {
                final value = _animationControllers[index].value;
                final angle = math.sin(value * math.pi * 2) * math.pi * 0.04;
                return Transform.rotate(
                    angle: angle,
                    child: _tab(
                      iconData: _tabIconData[index],
                      label: _tabLabel[index],
                      isSelectedIndex: _tabController.index == index,
                    ));
              }),
        ),
      );
    }

    return Scaffold(
      appBar: PreferredSize(
        preferredSize: Size.fromHeight(100),
        child: AppBar(
          iconTheme: Theme.of(context).iconTheme,
          title: Text(
            'Tab Bar',
            style: TextStyle(
              color: Colors.black,
              fontWeight: FontWeight.w400,
            ),
          ),
          centerTitle: true,
          bottom: PreferredSize(
            preferredSize: Size.fromHeight(20),
            child: Container(
              child: TabBar(
                controller: _tabController,
                labelPadding: EdgeInsets.only(top: 5.0, bottom: 2.0),
                indicatorColor: Colors.transparent,
                tabs: _animationGenerator(),
              ),
              decoration: BoxDecoration(
                color: Colors.white,
                boxShadow: [
                  BoxShadow(
                      color: Colors.white,
                      spreadRadius: 5.0,
                      offset: Offset(0, 3))
                ],
              ),
            ),
          ),
        ),
      ),
      body: TabBarView(
        controller: _tabController,
        children: List.generate(
            4,
            (index) => FittedBox(
                  child: Text('Tab $index'),
                )),
      ),
    );
  }

  void _listener() {
    if (_tabController.indexIsChanging) {
      setState(() {}); // To refresh color for Container bottom Border
      _animationControllers[_tabController.previousIndex].reverse();
    } else {
      _animationControllers[_tabController.index].forward();
    }
  }

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

这是一个由 TabController.animation 驱动的 CustomPaint 小部件的解决方案:

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();
    // timeDilation = 10;
    _tabController = TabController(length: 4, vsync: this)
      ..addListener(_listener);
    _animationControllers = List.generate(4, (i) => AnimationController(
      vsync: this,
      duration: Duration(milliseconds: 750),
    ));
  }

  @override
  Widget build(BuildContext context) {
    List<IconData> _tabIconData = [
      Icons.card_giftcard,
      Icons.confirmation_num_outlined,
      Icons.emoji_events_outlined,
      Icons.wine_bar_outlined,
    ];

    List<String> _tabLabel = [
      'Tab1',
      'Tab2',
      'Tab3',
      'Tab4',
    ];

    List<Color> _tabColor = [
      Color(0xffaa0000),
      Color(0xff00aa00),
      Color(0xff0000aa),
      Colors.black,
    ];

    Widget _tab({
      required IconData iconData,
      required String label,
      required Color color,
      required int index,
      required Animation<double>? animation,
    }) {
      const _tabTextStyle = TextStyle(fontWeight: FontWeight.w300, fontSize: 12, color: Colors.black);
      return CustomPaint(
        painter: TabPainter(
          animation: animation!,
          index: index,
          color: color,
        ),
        child: SizedBox(
          width: double.infinity,
          child: Tab(
            iconMargin: EdgeInsets.only(bottom: 5.0),
            icon: Icon(iconData, color: Colors.black),
            child: Text(label, style: _tabTextStyle),
          ),
        ),
      );
    }

    List<Widget> _animationGenerator() {
      return List.generate(
        4,
        (index) => AnimatedBuilder(
            animation: _animationControllers[index],
            builder: (ctx, _) {
              final value = _animationControllers[index].value;
              final angle = sin(value * pi * 3) * pi * 0.04;
              return Transform.rotate(
                  angle: angle,
                  child: _tab(
                    iconData: _tabIconData[index],
                    label: _tabLabel[index],
                    color: _tabColor[index],
                    index: index,
                    animation: _tabController.animation,
                  ));
            }),
      );
    }

    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.transparent,
        elevation: 0,
        title: Text('Tab Bar',
          style: TextStyle(
            color: Colors.black,
            fontWeight: FontWeight.w400,
          ),
        ),
        centerTitle: true,
        bottom: TabBar(
          controller: _tabController,
          labelPadding: EdgeInsets.only(top: 5.0, bottom: 2.0),
          indicatorColor: Colors.transparent,
          tabs: _animationGenerator(),
        ),
      ),
      body: TabBarView(
        controller: _tabController,
        children: List.generate(4, (index) => FittedBox(
          child: Text('Tab $index'),
        )),
      ),
    );
  }

  void _listener() {
    if (_tabController.indexIsChanging) {
      _animationControllers[_tabController.previousIndex].value = 0;
    } else {
      _animationControllers[_tabController.index].forward();
    }
  }

  @override
  void dispose() {
    super.dispose();
    _tabController
      ..removeListener(_listener)
      ..dispose();
    _animationControllers.forEach((ac) => ac.dispose());
  }
}

class TabPainter extends CustomPainter {
  final Animation<double> animation;
  final int index;
  final Color color;
  final tabPaint = Paint();

  TabPainter({
    required this.animation,
    required this.index,
    required this.color,
  });

  @override
  void paint(ui.Canvas canvas, ui.Size size) {
    // timeDilation = 10;
    if ((animation.value - index).abs() < 1) {
      final rect = Offset.zero & size;
      canvas.clipRect(rect);
      canvas.translate(size.width * (animation.value - index), 0);
      final tabRect = Alignment.bottomCenter.inscribe(Size(size.width, 3), rect);
      canvas.drawRect(tabRect, tabPaint..color = color);
    }
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}