詳解Flutter如何完全自定義TabBar
前言
在App中TabBar形式交互是非常常見的,但是系統(tǒng)提供的的樣式大多數又不能滿足我們產品和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的靜態(tài)組件。如果我們想要修改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,
),
);
}接下來我們看下指示器,我們發(fā)下如果我們想要改變指示器的寬度,官方提供了indicatorSize:字段,但是這個字段接受一個TabBarIndicatorSize字段,這個字段并不是具體的寬度值,而是一個枚舉值,見下只有兩種情況,要么跟tab一樣寬,要么跟文本一樣寬,顯然這并不能滿足一些產品和UI的需求,比如:寬度要設置成比文本小,指示器離文本再近一點,指示器能不能做成小圓點等等, 那么這時候我們就不可以靠官方的字段來實現了。

enum TabBarIndicatorSize {
// 寬度和tab控件一樣寬
tab,
// 寬度和文本一樣寬
label,
}接下來重點是對指示器的完全自定義
我們看到TabBar的構造函數里有一個indicator字段來設置指示器的樣式,接受一個Decoration裝飾盒子,從源碼我們看到里面有一個繪制方法,那么我們就可以自己創(chuàng)建一個類繼承Decoration自己繪制指示器不就可以了嗎?
// 創(chuàng)建裝飾盒子 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,
);
}到這里我們就改變了指示器的寬度以及指示器的下邊距設置,接下來我們繼續(xù)看,這個類創(chuàng)建了個BoxPainter類,這個類可以使用畫筆自定義一個裝飾效果,
@override
BoxPainter createBoxPainter([VoidCallback? onChanged]) {
return _UnderlinePainter(
this,
onChanged,
tabController?.animation,
indicatorWidth,
);
}
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
// 自定義繪制
}那不就想畫什么畫什么了唄,圓點、矩形等什么圖形,但是我們雖然可以自定義畫矩形了,但是我們要實現指示器寬度動態(tài)變化還需要一個動畫監(jiān)聽器,其實在我們滑動的過程中,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內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Android開發(fā)中synchronized的三種使用方式詳解
這篇文章主要介紹了Android開發(fā)中synchronized的三種使用方式,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習吧2023-04-04
Android WebView使用方法詳解 附js交互調用方法
這篇文章主要為大家詳細介紹了Android WebView使用方法詳解,文中附js交互調用方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-05-05
Android ListView實現上拉加載更多和下拉刷新功能
這篇文章主要為大家詳細介紹了Android ListView實現上拉加載更多和下拉刷新功能,介紹了ListView刷新原理及實現方法,感興趣的小伙伴們可以參考一下2016-05-05

