[Flutter] 让TabBar的文字不再瑟瑟发抖

2,858 阅读3分钟

效果对比

p1
上方的文本过度更平滑,文字没有瑟瑟发抖

实现过程

拷贝官方源码进行分析,顺藤摸瓜,删除一些代码排除干扰:

TabBarIndicatorSize

Tab

TabBarView

_TabBarViewState

TabPageSelectorIndicator

TabPageSelector

tabs.dart 中找到了TabBar的实现方法:

///TabBar
class TabBar extends StatefulWidget implements PreferredSizeWidget {
    ...
}

1.修复文字抖动

文字的样式变换是绑定在TabController上的,官方的策略是每次位置变动重新渲染文字样式,但是Flutter的文本尺寸变化并不是平滑过渡的:

class _TabStyle extends AnimatedWidget {
  ...
  @override
  Widget build(BuildContext context) {
      ...

      ///计算文本样式插值
      final TextStyle textStyle = selected
      ? TextStyle.lerp(defaultStyle, defaultUnselectedStyle, animation.value)
      : TextStyle.lerp(defaultUnselectedStyle, defaultStyle, animation.value);

      return DefaultTextStyle(
      ///构建文本样式(问题就出在这里)
      style: textStyle.copyWith(color: color),
      child: IconTheme.merge(
        data: IconThemeData(
          size: 24.0,
          color: color,
        ),
        child: child,
      ),
    );
  }
}

解决思路

  • 禁用文本尺寸变化
  • 用scale实现Tab内容的缩放效果
///根据前后字体大小计算缩放倍率
final double _magnification =
    labelStyle.fontSize / unselectedLabelStyle.fontSize;
final double _scale = selected
    ? lerpDouble(_magnification, 1, animation.value)
    : lerpDouble(1, _magnification, animation.value);

return DefaultTextStyle(
  style: textStyle.copyWith(
    color: color,

    ///钉死文字大小
    fontSize: unselectedLabelStyle.fontSize,
  ),
  child: IconTheme.merge(
    data: IconThemeData(
      size: 24.0,
      color: color,
    ),

    ///添加一个缩放外壳
    child: Transform.scale(
      scale: _scale,
      child: child,
    ),
  ),
);

这样我们就得到了一个切换相对平滑的TabBar

2.自定义点击水波纹

TabBar的点击是用 InkWell 实现的:

///_TabBarState
class _TabBarState extends State<ScaleTabBar> {
    ...
    @override
    Widget build(BuildContext context) {
        ...
        for (int index = 0; index < tabCount; index += 1) {
            ///生成tabs的时候嵌套了一层InkWell
            wrappedTabs[index] = InkWell(
                ...
            );
        }
    }
}

实现思路

  • 给TabBar添加水波纹相关的属性
  • 引用属性
///TabBar
class TabBar extends StatefulWidget implements PreferredSizeWidget {
    ...
    const TabBar({
        ...
        this.splashColor = Colors.transparent,
        this.highlightColor = Colors.transparent,
        this.borderRadius = const BorderRadius.all(Radius.circular(0)),
        ...
    })
  ...
  ///波纹颜色 , 默认为 [Colors.transparent]
  final Color splashColor;

  ///高亮颜色 , 默认为 [Colors.transparent]
  final Color highlightColor;

  ///波纹的圆角大小 , 默认为 0
  final BorderRadius borderRadius;
}
///_TabBarState
class _TabBarState extends State<TabBar> {
    ...
    @override
    Widget build(BuildContext context) {
        ...
        for (int index = 0; index < tabCount; index += 1) {
            wrappedTabs[index] = InkWell(
                ...
                ///波纹样式相关属性
                splashColor: widget.splashColor,
                highlightColor: widget.highlightColor,
                borderRadius: widget.borderRadius,
            );
        }
    }
}

因为设置了默认颜色为 Colors.transparent , 所以默认情况下点击tab不会出现水波纹,需要的时候可自定义,更多效果可自行添加

3.添加更多点击事件

此处以添加双击为例,先来看看自带的onTap是怎么实现的

///TabBar
class TabBar extends StatefulWidget implements PreferredSizeWidget {
    const TabBar({
        ...
        this.onTap,
    })
  ...
  ///点击回调
  final ValueChanged<int> onTap;
}
///_TabBarState
class _TabBarState extends State<TabBar> {
    ...
    ///onTap的具体实现
    void _handleTap(int index) {
        assert(index >= 0 && index < widget.tabs.length);
        ///跳转tab
        _controller.animateTo(index);
        if (widget.onTap != null) {
            ///不为空则触发
            widget.onTap(index);
        }
    }
  
    @override
    Widget build(BuildContext context) {
        ...
        for (int index = 0; index < tabCount; index += 1) {
            ///生成tabs的时候嵌套了一层InkWell
            wrappedTabs[index] = InkWell(
                ...
                ///点击事件
                onTap: () {
                    _handleTap(index);
                },
            );
        }
    }
}

很简单,依葫芦画瓢就行了:

///TabBar
class TabBar extends StatefulWidget implements PreferredSizeWidget {
    const TabBar({
        ...
        this.onDoubleTap,
    })
  ...
  ///双击事件,仅在选中tab生效
  final ValueChanged<int> onDoubleTap;
}
///_TabBarState
class _TabBarState extends State<TabBar> {
    ...
    ///onDoubleTap的具体实现
    void _handleDoubleTap(int index) {
        assert(index >= 0 && index < widget.tabs.length);
        if (widget.onDoubleTap != null && _currentIndex == index) {
            ///当为选中tab时才能触发双击事件
            widget.onDoubleTap(index);
        }
    }
  
    @override
    Widget build(BuildContext context) {
        ...
        for (int index = 0; index < tabCount; index += 1) {
            wrappedTabs[index] = InkWell(
                ...
                ///添加双击事件
                ///注意:有延时的事件会影响onTap的触发速度 , 不建议使用
                ///触发前判断一下 , 如果为null则不设置回调 , 消除对onTap的影响
                onDoubleTap: widget.onDoubleTap == null ? null : () => _handleDoubleTap(index),
            );
        }
    }
}

萌新一枚,没有常写文章,欢迎指出问题或错误
修改后的代码封装为了 ScaleTabBar 控件,已上传Github,使用方法与官方完全一致,希望能帮到你。

项目地址