Android

关注公众号 jb51net

关闭
首页 > 软件编程 > Android > Flutter自定义TabBar

详解Flutter如何完全自定义TabBar

作者:老李code

在App中TabBar形式交互是非常常见的,但是系统提供的的样式大多数又不能满足我们产品和UI的想法,本文记录了在Flutter中如何实现自定义TabBar的一个思路和过程,需要的可以参考一下

前言

在App中TabBar形式交互是非常常见的,但是系统提供的的样式大多数又不能满足我们产品和UI的想法,这篇就记录下在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字段和指示器相关的样式改变,我们先来看下官方给出的效果:

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的需求,比如:宽度要设置成比文本小,指示器离文本再近一点,指示器能不能做成小圆点等等, 那么这时候我们就不可以靠官方的字段来实现了。

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上放置的文本左右添加图标等等。

到此这篇关于详解Flutter如何完全自定义TabBar的文章就介绍到这了,更多相关Flutter自定义TabBar内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文