Flutter Tabbar 自定义选中下标 自定义Indicator
思考
Flutter中的Tabbar
为我们提供了十分方便的下标控制器indicator
,只不过系统提供的只能设置颜色,尺寸等有限的参数,面对复杂的UI设计搞,系统提供的参数可能就没法实现了,这时候我们就需要自己想办法自己去实现这个下标了。
方案一
我们可以使用Stack
这种布局,配合对tabController.animation
这个动画的监听,通过堆叠widget的方式,来实现Tabbar的下标。不过这种方法的问题是,需要自己去管理下标的状态,位置,可复用性也不强。
方案二
找到系统实现indicator
的方法,把他替换掉,这样子也就不用自己管理下标的位置了。
我们选择第二种方式来展开。
跟踪代码
我们知道可以通过设置indicatorColor
来修改系统indicator
的颜色。所以看看indicatorColor
在哪里被用到就知道系统是怎么设置indicator
了。
TabBar源码地址/tabs.dart
进到tabs.dart
文件直接搜索indicatorColor
,在下面这个地方找到了对他的调用
class _TabBarState extends State<TabBar> {
······
Decoration get _indicator {
······
Color color = widget.indicatorColor ?? Theme.of(context).indicatorColor;
······
return UnderlineTabIndicator(
borderSide: BorderSide(
width: widget.indicatorWeight,
color: color,
),
);
}
······
}
看起来是用来创建了一个_indicator
对象,我们继续搜索_indicator
,看看在哪儿用到了。
class _TabBarState extends State<TabBar> {
······
void _initIndicatorPainter() {
_indicatorPainter = !_controllerIsValid ? null : _IndicatorPainter(
controller: _controller!,
indicator: _indicator,
indicatorSize: widget.indicatorSize ?? TabBarTheme.of(context).indicatorSize,
indicatorPadding: widget.indicatorPadding,
tabKeys: _tabKeys,
old: _indicatorPainter,
);
}
······
}
找到了,代码也很好懂,是在_initIndicatorPainter
方法里面用来创建_IndicatorPainter
了,那么这个_IndicatorPainter
是什么呢,继续跟踪。
class _IndicatorPainter extends CustomPainter {
_IndicatorPainter({
required this.controller,
required this.indicator,
required this.indicatorSize,
required this.tabKeys,
required _IndicatorPainter? old,
required this.indicatorPadding,
}) : assert(controller != null),
assert(indicator != null),
super(repaint: controller.animation) {}
这下子就豁然开朗了,_IndicatorPainter
是继承自CustomPainter
类,原来Tabbar
原本的那条选中线,是用CustomPainter
画出来的。既然是CustomPainter
,我们直接看CustomPainter#paint
方法就好了。
······
@override
void paint(Canvas canvas, Size size) {
_needsPaint = false;
_painter ??= indicator.createBoxPainter(markNeedsPaint);
final double index = controller.index.toDouble();
final double value = controller.animation!.value;
final bool ltr = index > value;
final int from = (ltr ? value.floor() : value.ceil()).clamp(0, maxTabIndex).toInt();
final int to = (ltr ? from + 1 : from - 1).clamp(0, maxTabIndex).toInt();
final Rect fromRect = indicatorRect(size, from);
final Rect toRect = indicatorRect(size, to);
_currentRect = Rect.lerp(fromRect, toRect, (value - from).abs());
assert(_currentRect != null);
final ImageConfiguration configuration = ImageConfiguration(
size: _currentRect!.size,
textDirection: _currentTextDirection,
);
_painter!.paint(canvas, _currentRect!.topLeft, configuration);
}
······
最后一行是调用了_painter!.paint
方法来绘制选中下标的,如果我们在外面能自己控制这里canvas
的绘制,不就想要什么样的下标都有了吗。
扩展
关于canvas的绘制,如果不熟悉可以看看以下资料,我百度上随便找来的,
https://juejin.cn/post/6844903805000089608
编写ExtendedTabs
我们不动flutter的源码,把flutter的tabs.dart文件复制出来,名字改为extened_tabs.dart单独修改。文件直接复制出来,会报一些错,我们一步步修改他们。
在复制tabs.dart的时候,可能会遇到空安全的问题,导致整个页面都是错误。建议将项目的dart版本升级到2.12+来支持空安全,或者自己手动去除tabs.dart里面的空安全
1.引用报错,extened_tabs.dart文件里面引用了一些源码里面的其他widget,我们项目目录下肯定是访问不到的,我们直接删了,然后引用import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart' show DragStartBehavior;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
// import 'app_bar.dart';
// import 'colors.dart';
// import 'constants.dart';
// import 'debug.dart';
// import 'ink_well.dart';
// import 'material.dart';
// import 'material_localizations.dart';
// import 'material_state.dart';
// import 'tab_bar_theme.dart';
// import 'tab_controller.dart';
// import 'tab_indicator.dart';
// import 'theme.dart';
2.修改类名。为了不跟系统的冲突,我们把TabBar
改名为ExtendedTabbar
,需要注意同时要修改文件中其他用到的地方,比如
class _TabBarState extends State<ExtendedTabbar>
3.删除不用的类。extened_tabs文件中除了Tabbar,还有其他的一些公共类,为了不跟系统的冲突,我们把他们删掉。我这边只删了_TabBarState
后面的所有类。大纲看起来是这个样子:
4。我们使用typedef
定义一个绘制的回调方法。需要的参数有Canvas
和对应的Rect
typedef CustomIndicatorPaint(Canvas canvas, Rect currentRect);
5.在_IndicatorPainter
类中加上这个参数,并在CustomPainter#paint
方法中使用它。
class _IndicatorPainter extends CustomPainter {
_IndicatorPainter({
this.customIndicatorPaint,
······
}) : assert(controller != null),
assert(indicator != null),
super(repaint: controller.animation) {
······
final CustomIndicatorPaint? customIndicatorPaint;
······
@override
void paint(Canvas canvas, Size size) {
······
assert(_currentRect != null);
//判断一下,如果传进来的customIndicatorPaint不为空就调用customIndicatorPaint方法,否则还是使用系统的绘制
if (customIndicatorPaint != null) {
customIndicatorPaint!(canvas, _currentRect!);
} else {
final ImageConfiguration configuration = ImageConfiguration(
size: _currentRect!.size,
textDirection: _currentTextDirection,
);
_painter!.paint(canvas, _currentRect!.topLeft, configuration);
}
}
······
}
这样子_IndicatorPainter
这个类就修改好了,但是_IndicatorPainter
是个私有类,我们还需要从ExtendedTabbar
中将customIndicatorPaint
参数传进来。
6.修改ExtendedTabbar
,将customIndicatorPaint
从业务层传过来。
ExtendedTabbar
class ExtendedTabbar extends StatefulWidget implements PreferredSizeWidget {
······
const ExtendedTabbar({
······
this.physics,
this.customIndicatorPaint,
}) : assert(tabs != null),
assert(isScrollable != null),
assert(dragStartBehavior != null),
assert(indicator != null ||
(indicatorWeight != null && indicatorWeight > 0.0)),
assert(indicator != null || (indicatorPadding != null)),
super(key: key);
······
}
接下来再修改_TabBarState
,毕竟_IndicatorPainter
是在这里面被创建的
_TabBarState
class _TabBarState extends State<ExtendedTabbar> {
······
void _initIndicatorPainter() {
_indicatorPainter = !_controllerIsValid
? null
: _IndicatorPainter(
······
old: _indicatorPainter,
customIndicatorPaint: widget.customIndicatorPaint,
);
}
······
}
OK,大功告成,我们去用一下试试看。
7.使用方法
Container(
width: double.infinity,
height: 68,
child: ExtendedTabbar(
controller: _tabController,
indicatorColor: Colors.red,
//重点看这里
customIndicatorPaint: (canvas, currentRect) {
Paint paint = Paint()
..isAntiAlias = true
..color = Colors.green
..strokeCap = StrokeCap.round
..strokeWidth = 2
..style = PaintingStyle.stroke;
canvas.drawLine(
currentRect.centerLeft, currentRect.bottomCenter, paint);
canvas.drawLine(
currentRect.bottomCenter, currentRect.centerRight, paint);
},
tabs: tabs
.map((e) => Text(
e,
style: TextStyle(color: Colors.black),
))
.toList(),
),
),