详解Flutter如何完全自定义TabBar

来自:网络
时间:2022-08-07
阅读:
目录

前言

在App中TabBar形式交互是非常常见的,但是系统提供的的样式大多数又不能满足我们产品和UI的想法,这篇就记录下在Flutter中我在实现自定义TabBar的一个思路和过程,希望对你也有所帮助~

先看下我最终的效果图:

详解Flutter如何完全自定义TabBar

实现过程

首先我们先看下TabBar的构造方法:

const TabBar({
  Key? key,
  required this.tabs,// tab组件列表
  this.controller,// tabBar控制器
  this.isScrollable = false,// 是否支持滚动
  this.padding,// 内部tab内边距
  this.indicatorColor,// 指示器颜色
  this.automaticIndicatorColorAdjustment = true,// 指示器颜色是否自动跟随主题颜色
  this.indicatorWeight = 2.0,// 指示器高度
  this.indicatorPadding = EdgeInsets.zero,// 指示器padding
  this.indicator,//选择指示器样式
  this.indicatorSize,//选择指示器大小
  this.labelColor,// 选择标签文本颜色
  this.labelStyle,// 选择标签文本样式
  this.labelPadding,// 整体标签边距
  this.unselectedLabelColor,//未选中标签颜色
  this.unselectedLabelStyle,// 未选中标签样式
  this.dragStartBehavior = DragStartBehavior.start,//设置点击水波纹效果 跟随全局点击效果
  this.overlayColor,// 设置水波纹颜色
  this.mouseCursor, // 鼠标指针悬停的效果 App用不到
  this.enableFeedback,// 点击是否反馈声音触觉。
  this.onTap,// 点击Tab的回调
  this.physics,// 滚动边界交互
}) 

TabBar一般和TabView配合使用,TabBarTabView 共有一个控制器从而达到联动的效果,tab数组和tabView数组长度必须一致,不然直接报错。其实这么多方法,主要的就是用来进行tabs字段和指示器相关的样式改变,我们先来看下官方给出的效果:

详解Flutter如何完全自定义TabBar

List<String> tabs = ["Tab1", "Tab2"];
late TabController _tabController =
    TabController(length: tabs.length, vsync: this); //tab 控制器
@override
Widget build(BuildContext context) {
  return Column(
    children: [
      TabBar(
        controller: _tabController,
        tabs: tabs
            .map((value) => Tab(
                  height: 44,
                  text: value,
                ))
            .toList(),
        indicatorColor: Colors.redAccent,
        indicatorWeight: 2,
        labelColor: Colors.redAccent,
        unselectedLabelColor: Colors.black87,
      ),
      Expanded(
          child: TabBarView(
        controller: _tabController,
        children: tabs
            .map((value) => Center(
                  child: Text(
                    value,
                  ),
                ))
            .toList(),
      ))
    ],
  );
}

上面的代码就实现了官方的一个简单的TabBar,你可以改变切换文本的颜色、字重、指示器的颜色、指示器的高度等一些常见的样式。

首先我们看下Tab的源码,其实Tab的源码很简单,一共100多行代码,就是一个继承了PreferredSizeWidget的静态组件。如果我们想要修改Tab样式的话,重写它,修改它即可。

const Tab({
  Key? key,
  this.text,//文本
  this.icon,//图标
  this.iconMargin = const EdgeInsets.only(bottom: 10.0),
  this.height,//tab高度
  this.child,// 自定义组件
}) 
Widget build(BuildContext context) {
  assert(debugCheckHasMaterial(context));

  final double calculatedHeight;
  final Widget label;
  if (icon == null) {
    calculatedHeight = _kTabHeight;
    label = _buildLabelText();
  } else if (text == null && child == null) {
    calculatedHeight = _kTabHeight;
    label = icon!;
  } else {
  // 这里布局默认icon和文本是上下排列的
    calculatedHeight = _kTextAndIconTabHeight;
    label = Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Container(
          margin: iconMargin,
          child: icon,
        ),
        _buildLabelText(),
      ],
    );
  }

  return SizedBox(
    height: height ?? calculatedHeight,
    child: Center(
      widthFactor: 1.0,
      child: label,
    ),
  );
}

接下来我们看下指示器,我们发下如果我们想要改变指示器的宽度,官方提供了indicatorSize:字段,但是这个字段接受一个TabBarIndicatorSize字段,这个字段并不是具体的宽度值,而是一个枚举值,见下只有两种情况,要么跟tab一样宽,要么跟文本一样宽,显然这并不能满足一些产品和UI的需求,比如:宽度要设置成比文本小,指示器离文本再近一点,指示器能不能做成小圆点等等, 那么这时候我们就不可以靠官方的字段来实现了。

详解Flutter如何完全自定义TabBar

enum TabBarIndicatorSize {
// 宽度和tab控件一样宽
  tab,
// 宽度和文本一样宽
  label,
}

接下来重点是对指示器的完全自定义

我们看到TabBar的构造函数里有一个indicator字段来设置指示器的样式,接受一个Decoration装饰盒子,从源码我们看到里面有一个绘制方法,那么我们就可以自己创建一个类继承Decoration自己绘制指示器不就可以了吗?

// 创建装饰盒子
BoxPainter createBoxPainter([ VoidCallback onChanged ]);

// 绘制
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration);

但是我们看到官方提供一个UnderlineTabIndicator类,通过insets参数可以设置指示器的边距从而达到设置指示器宽度的效果,但是这并不能固定TabBar的宽度,而且当tabBar数量变化时或者文本长度改变,指示器宽度也会改变,我这里直接对UnderlineTabIndicator这个类进行了二次改造, 关键代码:通过这个方法我们自定义返回已个矩形,自定义我们需要的宽度值即可。

Rect _indicatorRectFor(Rect indicator, TextDirection textDirection) {
  /// 自定义固定宽度
  double w = indicatorWidth;
  //中间坐标
  double centerWidth = (indicator.left + indicator.right) / 2;
  return Rect.fromLTWH(
    centerWidth, //距离左边距
    // 距离上边距
    indicator.bottom - borderSide.width - indicatorBottom,
    w,
    borderSide.width,
  );
}

到这里我们就改变了指示器的宽度以及指示器的下边距设置,接下来我们继续看,这个类创建了个BoxPainter类,这个类可以使用画笔自定义一个装饰效果,

@override
BoxPainter createBoxPainter([VoidCallback? onChanged]) {
  return _UnderlinePainter(
    this,
    onChanged,
    tabController?.animation,
    indicatorWidth,
  );
}

void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
// 自定义绘制
}

那不就想画什么画什么了呗,圆点、矩形等什么图形,但是我们虽然可以自定义画矩形了,但是我们要实现指示器宽度动态变化还需要一个动画监听器,其实在我们滑动的过程中,TabController有一个animation回调函数,在我们滑动的时候,他会返回tab位置的偏移量,0~1代表1个tab的位移。

// 回调函数 动画插值 tab位置的偏移量
Animation<double>? get animation => _animationController?.view;

并且在滑动的过程中指示器是不断在绘制的,那么就好了,我们只需要将动画不断偏移的值赋给画笔进行绘制不就可以了吗

完整代码

import 'package:flutter/material.dart';

/// 修改下划线自定义
class MyTabIndicator extends Decoration {
  final TabController? tabController;
  final double indicatorBottom; // 调整指示器下边距
  final double indicatorWidth; // 指示器宽度

  const MyTabIndicator({
    // 设置下标高度、颜色
    this.borderSide = const BorderSide(width: 2.0, color: Colors.white),
    this.tabController,
    this.indicatorBottom = 0.0,
    this.indicatorWidth = 4,
  });

  /// The color and weight of the horizontal line drawn below the selected tab.
  final BorderSide borderSide;

  @override
  BoxPainter createBoxPainter([VoidCallback? onChanged]) {
    return _UnderlinePainter(
      this,
      onChanged,
      tabController?.animation,
      indicatorWidth,
    );
  }

  Rect _indicatorRectFor(Rect indicator, TextDirection textDirection) {
    /// 自定义固定宽度
    double w = indicatorWidth;
    //中间坐标
    double centerWidth = (indicator.left + indicator.right) / 2;
    return Rect.fromLTWH(
      //距离左边距
      tabController?.animation == null ? centerWidth - w / 2 : centerWidth - 1,
      // 距离上边距
      indicator.bottom - borderSide.width - indicatorBottom,
      w,
      borderSide.width,
    );
  }

  @override
  Path getClipPath(Rect rect, TextDirection textDirection) {
    return Path()..addRect(_indicatorRectFor(rect, textDirection));
  }
}

class _UnderlinePainter extends BoxPainter {
  Animation<double>? animation;
  double indicatorWidth;

  _UnderlinePainter(this.decoration, VoidCallback? onChanged, this.animation,
      this.indicatorWidth)
      : super(onChanged);

  final MyTabIndicator decoration;

  @override
  void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
    assert(configuration.size != null);
    // 以offset坐标为左上角 size为宽高的矩形
    final Rect rect = offset & configuration.size!;
    final TextDirection textDirection = configuration.textDirection!;
    // 返回tab矩形
    final Rect indicator = decoration._indicatorRectFor(rect, textDirection)
      ..deflate(decoration.borderSide.width / 2.0);
    // 圆角画笔
    final Paint paint = decoration.borderSide.toPaint()
      ..style = PaintingStyle.fill
      ..strokeCap = StrokeCap.round;
    if (animation != null) {
      num x = animation!.value; // 变化速度 0-0.5-1-1.5-2...
      num d = x - x.truncate(); // 获取这个数字的小数部分
      num? y;
      if (d < 0.5) {
        y = 2 * d;
      } else if (d > 0.5) {
        y = 1 - 2 * (d - 0.5);
      } else {
        y = 1;
      }
      canvas.drawRRect(
          RRect.fromRectXY(
              Rect.fromCenter(
                  center: indicator.centerLeft,
                  // 这里控制最长为多长
                  width: indicatorWidth * 6 * y + indicatorWidth,
                  height: indicatorWidth),
              // 圆角
              2,
              2),
          paint);
    } else {
      canvas.drawLine(indicator.bottomLeft, indicator.bottomRight, paint);
    }
  }
}

上面源码可直接粘贴到项目里使用,直接赋值给indicator属性,设置控制器,即可实现开始的效果图上的交互了。

总结

通过记录这次实现过程,其实搞明白内部原理,我们就可以轻而易举的实现各种TabBar的交互,本篇重点是如何实现自定义,上面的交互只是实现的一个例子,通过这个例子我们可以实现更多的其他的样式,比如给文本添加全背景渐变色、tab上放置的文本左右添加图标等等。

返回顶部
顶部