目录
前言
在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
配合使用,TabBar
和 TabView
共有一个控制器从而达到联动的效果,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上放置的文本左右添加图标等等。