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(),
        ),
    ),
效果如下:

 
                   
                   
                   
                   
                             这篇博客介绍了如何在Flutter中自定义TabBar的选中下标指示器。作者提出了两种方案,一种是使用Stack和tabController.animation监听,另一种是深入源码找到系统实现并替换。最终选择了后者,通过跟踪代码发现TabBar的指示器是用CustomPainter绘制的。通过扩展Flutter的TabBar源码,创建了一个新的ExtendedTabbar,并在其中添加了自定义绘制回调,允许用户自定义indicator的绘制方式。示例代码展示了如何使用自定义的ExtendedTabbar实现绿色描边的下标效果。
这篇博客介绍了如何在Flutter中自定义TabBar的选中下标指示器。作者提出了两种方案,一种是使用Stack和tabController.animation监听,另一种是深入源码找到系统实现并替换。最终选择了后者,通过跟踪代码发现TabBar的指示器是用CustomPainter绘制的。通过扩展Flutter的TabBar源码,创建了一个新的ExtendedTabbar,并在其中添加了自定义绘制回调,允许用户自定义indicator的绘制方式。示例代码展示了如何使用自定义的ExtendedTabbar实现绿色描边的下标效果。
           
       
           
                 
                 
                 
                 
                 
                
               
                 
                 
                 
                 
                
               
                 
                 扫一扫
扫一扫
                     
              
             
                   2609
					2609
					
 被折叠的  条评论
		 为什么被折叠?
被折叠的  条评论
		 为什么被折叠?
		 
		  到【灌水乐园】发言
到【灌水乐园】发言                                
		 
		 
    
   
    
   
             
            


 
            