详解Flutter如何完全自定义TabBar

目录
  • 前言
  • 实现过程
  • 完整代码
  • 总结

前言

在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内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Android使用ViewPager实现顶部tabbar切换界面

    类似的功能可以看看: 使用RadioGroup实现底部导航栏 进入正题 效果图: 注:PagerSlidingTabStrip为自定义控件,用于切换界面,此处不便贴出代码 1.主界面xml布局中添加ViewPager控件: <android.support.v4.view.ViewPager android:id="@+id/pager_view" android:layout_width="match_parent" android:layout_heigh

  • 100 行代码实现Flutter自定义TabBar的示例代码

    Flutter 的确很强大,但美中不足的是生态还有待完善,没有出现像前端的 Antd 或 Element 那样全能的基础 UI 库. 由此带来的直接影响是开发效率提不上去,需要耗费大量的时间精力在基础组件的封装上. 官方的 TabBar 不满足需求,又没有合适的轮子,只好自己造轮子啦.接下来带你一步步实现自定义 TabBar-- 一.目标和效果 需求目标是: 这个页面不要 material 左侧统一的返回键和 Title 在右侧有取消按钮,点取消即返回 点击 Tab 可以实现 content 切

  • 如何利用Flutter实现酷狗流畅Tabbar效果

    目录 前言 效果图 分析效果 开发思路 FlutterTabbar解析源码 实现步骤 业务使用 写在最后 实现源码 前言 在2021年末,酷狗发布了最新版11.0.0版本,这是一次重大的UI重构,更新完打开着实让我耳目一新.在原有风格上,整个App变得更加清爽,流畅.其中Tabbar的风格让我非常感兴趣,如果用Flutter来实现,或许是一个很有趣的事情. 效果图 分析效果 研究酷狗Tabbar的动画可以发现,默认状态下在当前Tab的中心处展示圆点,滑动时的效果拆分成两个以下部分: 从单个Tab

  • 详解Flutter如何完全自定义TabBar

    目录 前言 实现过程 完整代码 总结 前言 在App中TabBar形式交互是非常常见的,但是系统提供的的样式大多数又不能满足我们产品和UI的想法,这篇就记录下在Flutter中我在实现自定义TabBar的一个思路和过程,希望对你也有所帮助~ 先看下我最终的效果图: 实现过程 首先我们先看下TabBar的构造方法: const TabBar({ Key? key, required this.tabs,// tab组件列表 this.controller,// tabBar控制器 this.isS

  • 详解Flutter 调用 Android Native 的方法

    Flutter 调用 Android Native 的方法,是通过MethodChannel的方式来实现的: 在Android端: 创建一个Class,实现FlutterPlugin和MethodCallHandler接口 重写onAttachedToEngine(),onDetachedFromEngine(),onMethodCall() onAttachedToEngine()中,根据自定义的CHANNEL_NAME创建MethodChannel, onDetachedFromEngine

  • 详解Flutter Widget

    目录 概述: Widget的本质: 分类: Widget StatelessWidget StatefulWidget State ParentDataWidget RenderObjectWidget 小结 概述: 所有的一切都可以被称为widget 在开发 Flutter 应用过程中,接触最多的无疑就是Widget,是『描述』 Flutter UI 的基本单元,通过Widget可以做到: 描述 UI 的层级结构 (通过Widget嵌套): 定制 UI 的具体样式 (如:font.color等

  • 详解Flutter如何绘制曲线,折线图及波浪动效

    目录 正弦曲线绘制 波浪动效 曲线绘制 折线图 其他说明 总结 简介 上一篇用 Flutter 的 Canvas 画点有趣的图形我们介绍了使用 CustomPaint 绘制自定义形状,可以看到有了图形的平面绘制数学计算方法,我们可以画出所需的形状.本篇我们来介绍线条类图形的绘制,并且结合 Animation 实现了常见的波浪动效.通过本篇,你可以了解到: 正弦曲线的绘制 利用两条正弦曲线加上 Animation 实现波浪动效 曲线的一般绘制方法 折线图绘制 下面是最终实现的效果图,接下来我们一项

  • 详解Android如何实现自定义的动画曲线

    目录 前言 Curve 类定义 实例解析 正弦动画曲线 总结 前言 最近在写动画相关的篇章,经常会用到 Curve 这个动画曲线类,那这个类到底怎么实现的?如果想自己来一个自定义的动画曲线该怎么弄?本篇我们就来一探究竟. 曲线 Curve 类定义 查看源码, Curve 类定义如下: abstract class Curve extends ParametricCurve<double> {   const Curve();   @override   double transform(dou

  • 详解Flutter中的数据传递

    目录 Flutter 中的数据传递 InheritedWidget EventBus 总结 Flutter 中的数据传递 在开发中,数据从一个页面传递到另一个页面事很常用的,在Android 开发中,通常是通过把数据放到 intent 中传递过去.在 Flutter 中,数据是如何传递的呢? 在Flutter 中一切都是Widget,所以数据的传递就成了数据才Widget 中的传递.在之前的学习中,数据从一个Widget 传递到 子 Widget 是通过构造函数,一层一层的往里面传,要是 wid

  • 详解flutter中常用的container layout实例

    目录 简介 Container的使用 旋转Container Container中的BoxConstraints 总结 简介 在上一篇文章中,我们列举了flutter中的所有layout类,并且详细介绍了两个非常常用的layout:Row和Column. 掌握了上面两个基本的layout还是不够的,如果需要应付日常的layout使用,我们还需要掌握多一些layout组件.今天我们会介绍一个功能强大的layout:Container layout. Container的使用 Container是一

  • 详解Flutter 响应式状态管理框架GetX

    目录 一.状态管理框架对比 Provider BLoC GetX 二.基本使用 2.1 安装与引用 2.2 使用GetX改造Counter App 2.3 GetX代码插件 三.其他功能 3.1 路由管理 3.2 依赖关系管理 3.3 工具 3.4 改变主题 3.5 GetConnect 3.6 GetPage中间件 Priority Redirect onPageCalled OnBindingsStart OnPageBuildStart 3.7 全局设置和手动配置 3.8 StateMix

  • 详解Flutter点击空白隐藏键盘的全局做法

    开发原生页面的时候,在处理键盘事件上,通常的需求是,点击输入框外屏幕,要隐藏键盘,同样的,这样的需求也需要在 Flutter 上实现, Android 上的实现方式是在基类 Activity 里实现事件分发,判断触摸位置是否在输入框内. /** * 获取点击事件 */ @CallSuper @Override public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.MotionEv

  • 详解Vue中的自定义指令

    除了默认设置的核心指令( v-model 和 v-show ),Vue 也允许注册自定义指令.在Vue里,代码复用的主要形式和抽象是组件.然而,有的情况下,仍然需要对纯 DOM 元素进行底层操作,这时候就会用到自定义指令.本文将详细介绍Vue自定义指令 指令注册 以一个input元素自动获得焦点为例,当页面加载时,使用autofocus可以让元素将获得焦点 .但是autofocus在移动版Safari上不工作.现在注册一个使元素自动获取焦点的指令 指令注册类似于组件注册,包括全局指令和局部指令两

随机推荐