自定义 Flutter PageView 的指示器动画效果

在 Flutter 开发中,TabBarView 和 PageView 是常见的多页面切换组件。然而,默认的 TabBarView 有一些局限性,比如当 Tab 标签长度不一时,指示器长度不好丝滑的动态调整到固定长度;PageView又没有平滑的指示器过渡动画效果。为了克服这些限制,我们可以自定义组件,结合 PageView 来实现更灵活的布局和动画效果。

本文将详细介绍如何通过自定义组件解决这些问题,并实现一个支持动态指示器长度和滑动动画效果的 PageView

1. 问题分析

1.1  TabBar 的局限

在默认情况下,Flutter 的 TabBar 使用了固定长度的指示器,指示器会均匀分布在每个 Tab 下方。如果我们遇到以下场景:

• 标签长度不一致

• 希望指示器与标签的长度相同

• 希望指示器具有动画过渡效果

那么默认的 TabBar 就无法满足需求。因此,我们需要寻找一种更加灵活的解决方案。

1.2 为什么使用 PageView

相比 TabBarView,PageView 允许我们更加灵活地控制每个页面的过渡效果。使用 PageView 可以实现更平滑的页面切换,并且可以手动控制指示器的动画效果。

2. 自定义 CustomCommonTabBar 组件的实现

为了实现自定义指示器的长度和动画效果,我们需要对 TabBar 进行封装,并通过 PageView 实现更灵活的动画控制。我们将创建一个 CustomCommonTabBar 组件,它能够根据标签的实际长度动态调整指示器的宽度,并在标签切换时,添加指示器的平滑过渡动画。

以下是 CustomCommonTabBar 组件的完整实现:

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';

import '../common/const.dart';
import '../common/res/dimens.dart';
import '../common/res/fontFamily.dart';

typedef ChildWidgetBuilder = Widget Function(
  BuildContext context,
  int index,
  bool isSelected,
);

class CustomCommonTabBar extends StatefulWidget {
  final int currentIndex;
  final PageController controller;
  final ValueChanged<int>? onItemChange;
  final List<String> tabs;
  final double? tabPadding;
  final Widget? suffix;
  final double? bottomPadding;
  final ChildWidgetBuilder? itemBuilder;
  final double? selectedFontSize;
  final double? unSelectedFontSize;

  const CustomCommonTabBar({
    super.key,
    required this.currentIndex,
    required this.controller,
    required this.tabs,
    this.tabPadding,
    this.onItemChange,
    this.suffix,
    this.bottomPadding,
    this.itemBuilder,
    this.selectedFontSize,
    this.unSelectedFontSize,
  });

  @override
  State<CustomCommonTabBar> createState() => _CustomCommonTabBarState();
}

class _CustomCommonTabBarState extends State<CustomCommonTabBar> {
  int currTabIndex = 0;
  double indicatorPosition = 0.0;
  double indicatorWidth = 16.0; // 默认下划线宽度
  final List<GlobalKey> _tabKeys = [];

  @override
  void initState() {
    super.initState();
    currTabIndex = widget.currentIndex;

    // 初始化每个 Tab 的 Key
    _tabKeys.addAll(List.generate(widget.tabs.length, (_) => GlobalKey()));

    WidgetsBinding.instance.addPostFrameCallback((_) {
      _updateIndicator();
    });

    widget.controller.addListener(() {
      int index = widget.controller.page?.round() ?? 0;
      if (currTabIndex != index) {
        currTabIndex = index;
        if (widget.onItemChange != null) {
          widget.onItemChange!(currTabIndex);
        }
        setState(() {
          _updateIndicator();
        });
      }
    });
  }

  // 更新下划线的位置和宽度
  void _updateIndicator() {
    if (_tabKeys[currTabIndex].currentContext != null) {
      final RenderBox renderBox = _tabKeys[currTabIndex]
          .currentContext!
          .findRenderObject() as RenderBox;
      final position = renderBox.localToGlobal(Offset.zero);
      setState(() {
        indicatorPosition =
            position.dx + (renderBox.size.width - indicatorWidth) / 2;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: Theme.of(context).primaryColor,
      ),
      padding: EdgeInsets.only(bottom: widget.bottomPadding ?? 0.0),
      child: Stack(
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.start,
            children: [
              AppConfig.horizontal.horizontalSpace,
              ...List.generate(widget.tabs.length, (index) {
                return Row(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    tabItem(name: widget.tabs[index], index: index),
                    if (widget.tabs.length - 1 != index)
                      widget.tabPadding == null
                          ? Container(
                              width: 40.h,
                            )
                          : Container(
                        width: widget.tabPadding!.h,
                      )
                  ],
                );
              }).toList(),
              Expanded(
                child: widget.suffix ?? const SizedBox(),
              ),
              AppConfig.horizontal.horizontalSpace,
            ],
          ),
          AnimatedPositioned(
            duration: const Duration(milliseconds: 300),
            left: indicatorPosition,
            bottom: 0,
            child: Container(
              width: indicatorWidth, // 根据每个 Tab 设置的宽度
              height: 3.0,
              color: Theme.of(context).colorScheme.error,
            ),
          ),
        ],
      ),
    );
  }

  Widget tabItem({required String name, required int index}) {
    bool isSelected = index == currTabIndex;
    return InkWell(
      key: _tabKeys[index], // 为每个 Tab 设定 Key
      onTap: () {
        if (currTabIndex != index) {
          setState(() {
            currTabIndex = index;
            _updateIndicator();
            if (widget.onItemChange != null) {
              widget.onItemChange!(currTabIndex);
            }
            widget.controller.jumpToPage(currTabIndex);
          });
        }
      },
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Container(
            alignment: Alignment.center,
            padding: const EdgeInsets.only(
              bottom: 8.0,
            ),
            child: Text(
              name,
              style: TextStyle(
                color: isSelected
                    ? Theme.of(context).textTheme.titleLarge!.color!
                    : Theme.of(context).textTheme.titleSmall!.color!,
                fontSize: isSelected
                    ? (widget.selectedFontSize ?? Dimens.font_sp20)
                    : (widget.unSelectedFontSize ?? Dimens.font_sp18),
                fontWeight: isSelected ? Dimens.boldFont : FontWeight.normal,
                fontFamily: isSelected ? FontFamily.Medium : FontFamily.Regular,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

3. 如何使用 CustomCommonTabBar

接下来,我们展示如何在页面中使用这个自定义组件,并处理指示器动画。

Widget tab() {
  return CustomCommonTabBar(
    currentIndex: currentIndex,
    controller: PageController(),
    tabs: [
      'Hong Kong Dollar Remittance',
      'Dollar Remittance',
    ],
    onItemChange: (index) {
      if (mounted) {
        setState(() {
          currentIndex = index;
          getRemittanceInformation();
        });
      }
    },
  );
}

4. 自定义指示器动画效果

在上述组件中,我们通过 AnimatedPositioned 让指示器在页面切换时实现平滑的过渡动画。AnimatedPositioned 是 Flutter 中强大的动画工具,它可以根据属性的变化自动处理过渡动画。

每当 PageController 监听到页面切换时,指示器的宽度和位置都会被重新计算,利用动画来确保过渡效果更加顺滑。

 5.总结

通过自定义 CustomCommonTabBar,我们解决了默认 TabBar 指示器长度固定、动画效果不够灵活的问题。使用 PageView 和 GlobalKey,我们可以精确控制指示器的宽度,并实现平滑的过渡动画。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值