首頁 > 軟體

詳解Flutter如何完全自定義TabBar

2022-04-19 10:01:38

前言

在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內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com