詳解Flutter如何完全自定義TabBar
前言
在App中TabBar形式交互是非常常見(jiàn)的,但是系統(tǒng)提供的的樣式大多數(shù)又不能滿足我們產(chǎn)品和UI的想法,這篇就記錄下在Flutter中我在實(shí)現(xiàn)自定義TabBar的一個(gè)思路和過(guò)程,希望對(duì)你也有所幫助~
先看下我最終的效果圖:

實(shí)現(xiàn)過(guò)程
首先我們先看下TabBar的構(gòu)造方法:
const TabBar({
Key? key,
required this.tabs,// tab組件列表
this.controller,// tabBar控制器
this.isScrollable = false,// 是否支持滾動(dòng)
this.padding,// 內(nèi)部tab內(nèi)邊距
this.indicatorColor,// 指示器顏色
this.automaticIndicatorColorAdjustment = true,// 指示器顏色是否自動(dòng)跟隨主題顏色
this.indicatorWeight = 2.0,// 指示器高度
this.indicatorPadding = EdgeInsets.zero,// 指示器padding
this.indicator,//選擇指示器樣式
this.indicatorSize,//選擇指示器大小
this.labelColor,// 選擇標(biāo)簽文本顏色
this.labelStyle,// 選擇標(biāo)簽文本樣式
this.labelPadding,// 整體標(biāo)簽邊距
this.unselectedLabelColor,//未選中標(biāo)簽顏色
this.unselectedLabelStyle,// 未選中標(biāo)簽樣式
this.dragStartBehavior = DragStartBehavior.start,//設(shè)置點(diǎn)擊水波紋效果 跟隨全局點(diǎn)擊效果
this.overlayColor,// 設(shè)置水波紋顏色
this.mouseCursor, // 鼠標(biāo)指針懸停的效果 App用不到
this.enableFeedback,// 點(diǎn)擊是否反饋聲音觸覺(jué)。
this.onTap,// 點(diǎn)擊Tab的回調(diào)
this.physics,// 滾動(dòng)邊界交互
}) TabBar一般和TabView配合使用,TabBar 和 TabView 共有一個(gè)控制器從而達(dá)到聯(lián)動(dòng)的效果,tab數(shù)組和tabView數(shù)組長(zhǎng)度必須一致,不然直接報(bào)錯(cuò)。其實(shí)這么多方法,主要的就是用來(lái)進(jìn)行tabs字段和指示器相關(guān)的樣式改變,我們先來(lái)看下官方給出的效果:

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(),
))
],
);
}上面的代碼就實(shí)現(xiàn)了官方的一個(gè)簡(jiǎn)單的TabBar,你可以改變切換文本的顏色、字重、指示器的顏色、指示器的高度等一些常見(jiàn)的樣式。
首先我們看下Tab的源碼,其實(shí)Tab的源碼很簡(jiǎn)單,一共100多行代碼,就是一個(gè)繼承了PreferredSizeWidget的靜態(tài)組件。如果我們想要修改Tab樣式的話,重寫(xiě)它,修改它即可。
const Tab({
Key? key,
this.text,//文本
this.icon,//圖標(biāo)
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 {
// 這里布局默認(rèn)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,
),
);
}接下來(lái)我們看下指示器,我們發(fā)下如果我們想要改變指示器的寬度,官方提供了indicatorSize:字段,但是這個(gè)字段接受一個(gè)TabBarIndicatorSize字段,這個(gè)字段并不是具體的寬度值,而是一個(gè)枚舉值,見(jiàn)下只有兩種情況,要么跟tab一樣寬,要么跟文本一樣寬,顯然這并不能滿足一些產(chǎn)品和UI的需求,比如:寬度要設(shè)置成比文本小,指示器離文本再近一點(diǎn),指示器能不能做成小圓點(diǎn)等等, 那么這時(shí)候我們就不可以靠官方的字段來(lái)實(shí)現(xiàn)了。

enum TabBarIndicatorSize {
// 寬度和tab控件一樣寬
tab,
// 寬度和文本一樣寬
label,
}接下來(lái)重點(diǎn)是對(duì)指示器的完全自定義
我們看到TabBar的構(gòu)造函數(shù)里有一個(gè)indicator字段來(lái)設(shè)置指示器的樣式,接受一個(gè)Decoration裝飾盒子,從源碼我們看到里面有一個(gè)繪制方法,那么我們就可以自己創(chuàng)建一個(gè)類繼承Decoration自己繪制指示器不就可以了嗎?
// 創(chuàng)建裝飾盒子 BoxPainter createBoxPainter([ VoidCallback onChanged ]); // 繪制 void paint(Canvas canvas, Offset offset, ImageConfiguration configuration);
但是我們看到官方提供一個(gè)UnderlineTabIndicator類,通過(guò)insets參數(shù)可以設(shè)置指示器的邊距從而達(dá)到設(shè)置指示器寬度的效果,但是這并不能固定TabBar的寬度,而且當(dāng)tabBar數(shù)量變化時(shí)或者文本長(zhǎng)度改變,指示器寬度也會(huì)改變,我這里直接對(duì)UnderlineTabIndicator這個(gè)類進(jìn)行了二次改造, 關(guān)鍵代碼:通過(guò)這個(gè)方法我們自定義返回已個(gè)矩形,自定義我們需要的寬度值即可。
Rect _indicatorRectFor(Rect indicator, TextDirection textDirection) {
/// 自定義固定寬度
double w = indicatorWidth;
//中間坐標(biāo)
double centerWidth = (indicator.left + indicator.right) / 2;
return Rect.fromLTWH(
centerWidth, //距離左邊距
// 距離上邊距
indicator.bottom - borderSide.width - indicatorBottom,
w,
borderSide.width,
);
}到這里我們就改變了指示器的寬度以及指示器的下邊距設(shè)置,接下來(lái)我們繼續(xù)看,這個(gè)類創(chuàng)建了個(gè)BoxPainter類,這個(gè)類可以使用畫(huà)筆自定義一個(gè)裝飾效果,
@override
BoxPainter createBoxPainter([VoidCallback? onChanged]) {
return _UnderlinePainter(
this,
onChanged,
tabController?.animation,
indicatorWidth,
);
}
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
// 自定義繪制
}那不就想畫(huà)什么畫(huà)什么了唄,圓點(diǎn)、矩形等什么圖形,但是我們雖然可以自定義畫(huà)矩形了,但是我們要實(shí)現(xiàn)指示器寬度動(dòng)態(tài)變化還需要一個(gè)動(dòng)畫(huà)監(jiān)聽(tīng)器,其實(shí)在我們滑動(dòng)的過(guò)程中,TabController有一個(gè)animation回調(diào)函數(shù),在我們滑動(dòng)的時(shí)候,他會(huì)返回tab位置的偏移量,0~1代表1個(gè)tab的位移。
// 回調(diào)函數(shù) 動(dòng)畫(huà)插值 tab位置的偏移量 Animation<double>? get animation => _animationController?.view;
并且在滑動(dòng)的過(guò)程中指示器是不斷在繪制的,那么就好了,我們只需要將動(dòng)畫(huà)不斷偏移的值賦給畫(huà)筆進(jìn)行繪制不就可以了嗎
完整代碼
import 'package:flutter/material.dart';
/// 修改下劃線自定義
class MyTabIndicator extends Decoration {
final TabController? tabController;
final double indicatorBottom; // 調(diào)整指示器下邊距
final double indicatorWidth; // 指示器寬度
const MyTabIndicator({
// 設(shè)置下標(biāo)高度、顏色
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;
//中間坐標(biāo)
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坐標(biāo)為左上角 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);
// 圓角畫(huà)筆
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(); // 獲取這個(gè)數(shù)字的小數(shù)部分
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,
// 這里控制最長(zhǎng)為多長(zhǎng)
width: indicatorWidth * 6 * y + indicatorWidth,
height: indicatorWidth),
// 圓角
2,
2),
paint);
} else {
canvas.drawLine(indicator.bottomLeft, indicator.bottomRight, paint);
}
}
}上面源碼可直接粘貼到項(xiàng)目里使用,直接賦值給indicator屬性,設(shè)置控制器,即可實(shí)現(xiàn)開(kāi)始的效果圖上的交互了。
總結(jié)
通過(guò)記錄這次實(shí)現(xiàn)過(guò)程,其實(shí)搞明白內(nèi)部原理,我們就可以輕而易舉的實(shí)現(xiàn)各種TabBar的交互,本篇重點(diǎn)是如何實(shí)現(xiàn)自定義,上面的交互只是實(shí)現(xiàn)的一個(gè)例子,通過(guò)這個(gè)例子我們可以實(shí)現(xiàn)更多的其他的樣式,比如給文本添加全背景漸變色、tab上放置的文本左右添加圖標(biāo)等等。
到此這篇關(guān)于詳解Flutter如何完全自定義TabBar的文章就介紹到這了,更多相關(guān)Flutter自定義TabBar內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android中顏色選擇器和改變字體顏色的實(shí)例教程
這篇文章主要介紹了Android中顏色選擇器和改變字體顏色的實(shí)例教程,其中改變字體顏色用到了ColorPicker顏色選擇器,需要的朋友可以參考下2016-04-04
android自定義等級(jí)評(píng)分圓形進(jìn)度條
這篇文章主要為大家詳細(xì)介紹了android自定義等級(jí)評(píng)分圓形進(jìn)度條,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-07-07
Android ListView列表控件的介紹和性能優(yōu)化
這篇文章主要介紹了Android ListView列表控件的介紹和性能優(yōu)化,需要的朋友可以參考下2017-06-06
Android下2d物理引擎Box2d用法簡(jiǎn)單實(shí)例
這篇文章主要介紹了Android下2d物理引擎Box2d用法,實(shí)例分析了在Android平臺(tái)上使用Box2d的基本技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-07-07

