美文网首页
Flutter TabBar 在实际项目中的运用

Flutter TabBar 在实际项目中的运用

作者: 木子雨廷t | 来源:发表于2022-10-26 13:30 被阅读0次
    Tabs在实际的项目开发中,运用的十分广泛,此文根据在实际项目中使用整理了一个demo.再此开源,纯属技术交流,欢迎评论交流.

    TabBar是flutter中非常常用的一个组件,Flutter提供的TabBar几乎可以满足我们大部分的业务需求,而且实现非常简单,我们可以仅用几行代码,就完成一个Tab滑动效果。
    关于TabBar的基本使用,我这里就不介绍了,不熟悉的朋友可以自行百度看看,有很多的Demo。

    下面我们针对TabBar在平时的开发中遇到的一些问题,来看下如何解决。

    一. 解决汉字滑动抖动的问题

    首先,我们来看下TabBar的抖动问题,这个问题发生在我们设置labelStyleunselectedLabelStyle字体大小不一致时,这个需求其实在实际的开发当中也很常见,当我们选中一个Tab时,当然希望选中的标题能够放大,突出一些,但是FlutterTabBar居然会在滑动过程中抖动,开始以为是Debug包的问题,后来发现Release也一样。

    Flutter的Issue中,其实已经有这样的问题了。不过到目前为止,这个问题也没修复,可能在老外的设计中,从来没有这种设计吧。不过Issue中也提到了很多方案来修复这个问题,其中比较好的一个方案,就是通过修改源码来实现,在TabBar源码的_TabStylebuild函数中,将实现改为下面的方案。

    ///根据前后字体大小计算缩放倍率
    final double _magnification = labelStyle!.fontSize! / unselectedLabelStyle!.fontSize!;
    final double _scale = (selected ? lerpDouble(_magnification, 1, animation.value) : lerpDouble(1, _magnification, animation.value))!;
    
    return DefaultTextStyle(
      style: textStyle.copyWith(
        color: color,
        fontSize: unselectedLabelStyle!.fontSize,
      ),
      child: IconTheme.merge(
        data: IconThemeData(
          size: 24.0,
          color: color,
        ),
        child: Transform.scale(
          scale: _scale,
          child: child,
        ),
      ),
    );
    

    这个方案的确可以修复这个问题,不过却需要修改源码,所以,有一些使用成本,那么有没有其它方案呢,其实,Issue中已经给出了问题的来源,实际上就是Text在计算Scala的过程中,由于Baseline不对齐导致的抖动,所以,我们可以换一种思路,将labelStyleunselectedLabelStyle的字体大小设置成一样的,这样就不会抖动啦。

    当然,这样的话需求也就满足不了了。

    其实,我们是将Scala的效果,放到外面来实现,在TabBartabs中,我们将滑动百分比传入,借助隐式动画来实现Scala效果,就可以解决抖动的问题了。

    AnimatedScale(
      scale: 1 + progress * 0.3,
      duration: const Duration(milliseconds: 100),
      child: Text(tabName),
    ),
    
    最终效果图
    解决汉字滑动抖动
    二. 自定义下标宽度和位置

    在实际的开发中,TabBar 往往和indicator 配合在一起进行使用,现在Appindicator设计的也是五花八门,有很多的样式。而在flutterindicator 宽度默认是不能修改的,所以可以支持修改宽度indicator 也是很必要的。flutterUnderlineTabIndicatorTab的默认实现,我们可以将UnderlineTabIndicator源码复制出来然后取一个自己的名字如MyUnderlineTabIndicator在这个类里面修改宽度。代码如下

    import 'package:flutter/material.dart';
    import 'package:flutter/widgets.dart';
    
    class MyUnderlineTabIndicator extends Decoration {
      const MyUnderlineTabIndicator({
        this.borderSide = const BorderSide(width: 2.0, color: Colors.white),
        this.insets = EdgeInsets.zero, required this.wantWidth,
      }) : assert(borderSide != null),
            assert(insets != null);
    
      final BorderSide borderSide;
      final EdgeInsetsGeometry insets;
      final double wantWidth;
    
      @override
      Decoration? lerpFrom(Decoration? a, double t) {
        if (a is MyUnderlineTabIndicator) {
          return MyUnderlineTabIndicator(
            wantWidth:5,
            borderSide: BorderSide.lerp(a.borderSide, borderSide, t),
            insets: EdgeInsetsGeometry.lerp(a.insets, insets, t)!,
          );
        }
        return super.lerpFrom(a, t);
      }
    
      @override
      Decoration? lerpTo(Decoration? b, double t) {
        if (b is MyUnderlineTabIndicator) {
          return MyUnderlineTabIndicator(
            borderSide: BorderSide.lerp(borderSide, b.borderSide, t),
            insets: EdgeInsetsGeometry.lerp(insets, b.insets, t)!, wantWidth: 5,
          );
        }
        return super.lerpTo(b, t);
      }
    
      @override
      _UnderlinePainter createBoxPainter([ VoidCallback? onChanged ]) {
        return _UnderlinePainter(this, onChanged);
      }
    
      Rect _indicatorRectFor(Rect rect, TextDirection textDirection) {
        assert(rect != null);
        assert(textDirection != null);
        final Rect indicator = insets.resolve(textDirection).deflateRect(rect);
        //希望的宽度
        double cw = (indicator.left + indicator.right) / 2;
        return Rect.fromLTWH(cw - wantWidth / 2,
            indicator.bottom - borderSide.width, wantWidth, borderSide.width);
      }
    
      @override
      Path getClipPath(Rect rect, TextDirection textDirection) {
        return Path()..addRect(_indicatorRectFor(rect, textDirection));
      }
    }
    
    class _UnderlinePainter extends BoxPainter {
      _UnderlinePainter(this.decoration, VoidCallback? onChanged)
          : assert(decoration != null),
            super(onChanged);
    
      final MyUnderlineTabIndicator decoration;
    
      @override
      void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
        assert(configuration != null);
        assert(configuration.size != null);
        final Rect rect = offset & configuration.size!;
        final TextDirection textDirection = configuration.textDirection!;
        final Rect indicator = decoration._indicatorRectFor(rect, textDirection).deflate(decoration.borderSide.width / 2.0);
        final Paint paint = decoration.borderSide.toPaint()..strokeCap = StrokeCap.round;
        canvas.drawLine(indicator.bottomLeft, indicator.bottomRight, paint);
      }
    }
    
    

    修改indicator位置

    indicatorWeight: 4,
    indicatorPadding: EdgeInsets.symmetric(vertical: 8),
    

    如果你想要indicator在垂直距离上更接近,那么可以使用indicatorPadding参数,如果你想让indicator更细,那么可以使用indicatorWeight参数。

    最终效果图
    自定义下标宽度和位置
    三. 自定义下标样式

    在实际的开发中很多时候都需要自定义Indicator的样式,刚刚修改Indicator 样式时是将源码UnderlineTabIndicator拷贝出来进行修改,最定义也是一样的道理。
    在源码最后的BoxPainter,就是我们绘制Indicator的核心,在这里根据Offset和ImageConfiguration,就可以拿到当前Indicator的参数,就可以进行绘制了。

    例如我们最简单的,把Indicator绘制成一个圆,实际上只需要修改最后的draw函数,代码如下所示。

    import 'package:flutter/widgets.dart';
    import 'package:flutter/material.dart';
    
    class CustomUnderlineTabIndicator extends Decoration {
    
      const CustomUnderlineTabIndicator({
        this.borderSide = const BorderSide(width: 2.0, color: Colors.white),
        this.insets = EdgeInsets.zero,
      }) : assert(borderSide != null),
            assert(insets != null);
    
      final BorderSide borderSide;
    
      final EdgeInsetsGeometry insets;
    
      @override
      Decoration? lerpFrom(Decoration? a, double t) {
        if (a is CustomUnderlineTabIndicator) {
          return CustomUnderlineTabIndicator(
            borderSide: BorderSide.lerp(a.borderSide, borderSide, t),
            insets: EdgeInsetsGeometry.lerp(a.insets, insets, t)!,
          );
        }
        return super.lerpFrom(a, t);
      }
    
      @override
      Decoration? lerpTo(Decoration? b, double t) {
        if (b is CustomUnderlineTabIndicator) {
          return CustomUnderlineTabIndicator(
            borderSide: BorderSide.lerp(borderSide, b.borderSide, t),
            insets: EdgeInsetsGeometry.lerp(insets, b.insets, t)!,
          );
        }
        return super.lerpTo(b, t);
      }
    
      @override
      BoxPainter createBoxPainter([ VoidCallback? onChanged ]) {
        return _UnderlinePainter(this, onChanged);
      }
    
      Rect _indicatorRectFor(Rect rect, TextDirection textDirection) {
        assert(rect != null);
        assert(textDirection != null);
        final Rect indicator = insets.resolve(textDirection).deflateRect(rect);
        return Rect.fromLTWH(
          indicator.left,
          indicator.bottom - borderSide.width,
          indicator.width,
          borderSide.width,
        );
      }
    
      @override
      Path getClipPath(Rect rect, TextDirection textDirection) {
        return Path()..addRect(_indicatorRectFor(rect, textDirection));
      }
    }
    
    class _UnderlinePainter extends BoxPainter {
      _UnderlinePainter(this.decoration, VoidCallback? onChanged)
          : assert(decoration != null),
            super(onChanged);
    
      final CustomUnderlineTabIndicator decoration;
      final Paint _paint = Paint()
        ..color = Colors.orange
        ..style = PaintingStyle.fill;
      final radius = 6.0;
    
      @override
      void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
        assert(configuration != null);
        assert(configuration.size != null);
        final Rect rect = offset & configuration.size!;
        canvas.drawCircle(
          Offset(rect.bottomCenter.dx, rect.bottomCenter.dy - radius),
          radius,
          _paint,
        );
      }
    }
    
    最终效果图
    自定义下标样式
    四. 自定义背景块样式

    在开发中有时候会遇到带背景块的tabbar,很简单flutter提供有这个类ShapeDecoration可以用来实现这个效果。

    indicator: ShapeDecoration(
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(8),
      ),
      color: Colors.cyan.shade200,
    )
    
    最终效果图
    自定义背景块样式
    五. 动态获取tab

    在实际项目开发中,一般这些tab都是通过后台接口返回的,重点是接口返回是异步的,需要在数据未返回时进行判断返回一个空的Widget。不难实现,直接上代码了。

    import 'package:cached_network_image/cached_network_image.dart';
    import 'package:flutter/material.dart';
    import 'package:flutter_easyloading/flutter_easyloading.dart';
    import '../config/Http_service.dart';
    import '../model/cook_info_model.dart';
    import 'my_underline_tabIndicator.dart';
    
    class DynamicDataTab extends StatefulWidget {
      final String titleStr;
      const DynamicDataTab({Key? key, required this.titleStr}) : super(key: key);
    
      @override
      State<DynamicDataTab> createState() => _DynamicDataTabState();
    }
    
    class _DynamicDataTabState extends State<DynamicDataTab>
        with SingleTickerProviderStateMixin {
      TabController? _tabController;
      List<CookInfoModel> _cookInfoList = CookInfoModelList([]).list;
    
      // 获取数据
      Future _getRecommendData() async {
        EasyLoading.show(status: 'loading...');
        try {
          Map<String, dynamic> result =
              await HttpService.getHomeRecommendData(page: 1, pageSize: 10);
          EasyLoading.dismiss();
          List list = [];
          for (Map item in result['result']['list']) {
            list.add(item['r']);
            print(item['r']);
          }
          CookInfoModelList infoList = CookInfoModelList.fromJson(list);
          setState(() {
            _tabController =
                TabController(length: infoList.list.length, vsync: this);
            _cookInfoList = infoList.list;
          });
        } catch (e) {
          print(e);
          EasyLoading.dismiss();
        } finally {
          EasyLoading.dismiss();
        }
      }
    
      @override
      void initState() {
        super.initState();
        _getRecommendData();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text(widget.titleStr),
          ),
          body: Container(
            width: MediaQuery.of(context).size.width,
            height: MediaQuery.of(context).size.height,
            padding: const EdgeInsets.only(top: 20),
            color: Colors.white,
            child: Column(
              children: [
                _cookInfoList.isEmpty
                    ? PreferredSize(
                        preferredSize: const Size(0, 0), child: Container())
                    : TabBar(
                        controller: _tabController,
                        indicatorColor: Colors.blue,
                        indicatorWeight: 18,
                        isScrollable: true,
                        indicatorPadding: const EdgeInsets.symmetric(vertical: 6),
                        indicator: const MyUnderlineTabIndicator(
                            wantWidth: 30.0,
                            borderSide: BorderSide(
                                width: 6.0,
                                color: Color.fromRGBO(36, 217, 252, 1))),
                        tabs: getTabs()
                            .asMap()
                            .entries
                            .map(
                              (entry) => AnimatedBuilder(
                                animation: _tabController!.animation!,
                                builder: (ctx, snapshot) {
                                  final forward = _tabController!.offset > 0;
                                  final backward = _tabController!.offset < 0;
                                  int _fromIndex;
                                  int _toIndex;
                                  double progress;
                                  // Tab
                                  if (_tabController!.indexIsChanging) {
                                    _fromIndex = _tabController!.previousIndex;
                                    _toIndex = _tabController!.index;
                                    progress = (_tabController!.animation!.value -
                                                _fromIndex)
                                            .abs() /
                                        (_toIndex - _fromIndex).abs();
                                  } else {
                                    // Scroll
                                    _fromIndex = _tabController!.index;
                                    _toIndex = forward
                                        ? _fromIndex + 1
                                        : backward
                                            ? _fromIndex - 1
                                            : _fromIndex;
                                    progress = (_tabController!.animation!.value -
                                            _fromIndex)
                                        .abs();
                                  }
                                  var flag = entry.key == _fromIndex
                                      ? 1 - progress
                                      : entry.key == _toIndex
                                          ? progress
                                          : 0.0;
                                  return buildTabContainer(
                                      entry.value.text ?? '', flag);
                                },
                              ),
                            )
                            .toList(),
                      ),
                Expanded(
                    child: _cookInfoList.isEmpty
                        ? PreferredSize(
                            preferredSize: const Size(0, 0), child: Container())
                        : TabBarView(
                            controller: _tabController, children: getWidgets()))
              ],
            ),
          ),
        );
      }
    
      List<Tab> getTabs() {
        List<Tab> widgetList = [];
        for (int i = 0; i < _cookInfoList.length; i++) {
          CookInfoModel model = _cookInfoList[i];
          if(model.stdname!.length > 5){
            model.stdname = model.stdname?.substring(0,5);
          }
          widgetList.add(Tab(text: model.stdname!.isNotEmpty ? model.stdname : '暂无数据'));
        }
        return widgetList;
      }
    
      List<Widget> getWidgets() {
        List<Widget> widgetList = [];
        for (int i = 0; i < _cookInfoList.length; i++) {
          CookInfoModel model = _cookInfoList[i];
          widgetList.add(
            Container(
              padding: const EdgeInsets.only(left: 20, right: 20, top: 10),
              child: SingleChildScrollView(
                child: Column(
                  children: [
                    Container(
                      width: MediaQuery.of(context).size.width,
                      clipBehavior: Clip.hardEdge,
                      decoration: BoxDecoration(
                        borderRadius: BorderRadius.circular(6),
                        color: Colors.white,
                      ),
                      child: CachedNetworkImage(
                        imageUrl: model.img??"",
                        width: MediaQuery.of(context).size.width,
                        fit: BoxFit.fitWidth,
                      ),
                    ),
                    Text(
                      model.n ?? '',
                      style: const TextStyle(fontSize: 14, color: Colors.black54),
                    )
                  ],
                ),
              )
            ),
          );
        }
        return widgetList;
      }
    
      buildTabContainer(String tabName, double alpha) {
        return Container(
          padding: const EdgeInsets.symmetric(vertical: 1, horizontal: 1),
          child: AnimatedScale(
            scale: 1 + double.parse((alpha * 0.2).toStringAsFixed(2)),
            duration: const Duration(milliseconds: 100),
            child: Text(
              tabName,
              style: const TextStyle(
                  fontSize: 16, fontWeight: FontWeight.w500, color: Colors.black),
            ),
          ),
        );
      }
    }
    
    最终效果图
    动态获取tab
    六. 动态获取tab和tab悬停

    动态获取tab同案例五一样,悬停是通过NestedScrollViewSliverAppBar来实现的,原理不复杂,不直接上代码。

    import 'package:cached_network_image/cached_network_image.dart';
    import 'package:flutter/material.dart';
    import 'package:flutter_easyloading/flutter_easyloading.dart';
    import '../widget/banner.dart';
    import '../config/Http_service.dart';
    import '../model/banner_model.dart';
    import '../model/cook_info_model.dart';
    import 'my_underline_tabIndicator.dart';
    
    class DynamicDataHover extends StatefulWidget {
      final String titleStr;
      const DynamicDataHover({Key? key, required this.titleStr}) : super(key: key);
    
      @override
      State<DynamicDataHover> createState() => _DynamicDataHoverState();
    }
    
    class _DynamicDataHoverState extends State<DynamicDataHover>
        with SingleTickerProviderStateMixin {
      TabController? _tabController;
      List<CookInfoModel> _cookInfoList = CookInfoModelList([]).list;
    
      /// 轮播图数据
      List<BannerModel> _bannerList = BannerModelList([]).list;
    
      // 获取数据
      Future _getRecommendData() async {
        EasyLoading.show(status: 'loading...');
        try {
          Map<String, dynamic> result =
              await HttpService.getHomeRecommendData(page: 1, pageSize: 10);
          EasyLoading.dismiss();
    
          /// 轮播图数据
          BannerModelList bannerModelList =
              BannerModelList.fromJson(result['result']['banner']);
          print('哈哈哈哈哈或$result');
          List list = [];
          for (Map item in result['result']['list']) {
            list.add(item['r']);
            print(item['r']);
          }
          CookInfoModelList infoList = CookInfoModelList.fromJson(list);
          setState(() {
            _tabController =
                TabController(length: infoList.list.length, vsync: this);
            _cookInfoList = infoList.list;
            _bannerList = bannerModelList.list;
          });
        } catch (e) {
          print(e);
          EasyLoading.dismiss();
        } finally {
          EasyLoading.dismiss();
        }
      }
    
      @override
      void initState() {
        super.initState();
        _getRecommendData();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text(widget.titleStr),
          ),
          body: NestedScrollView(
            headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
              return <Widget>[
                SliverAppBar(
                  backgroundColor: Colors.white,
                  elevation: 0,
                  pinned: true,
                  floating: true,
                  /// 去掉返回按钮
                  leading: const Text(''),
                  expandedHeight: 180,
                  flexibleSpace: FlexibleSpaceBar(
                    collapseMode: CollapseMode.pin,
                    background: Container(
                      color: Colors.white,
                      height: double.infinity,
                      child: Column(
                        children: <Widget>[
                          Container(
                            height: 120,
                            width: MediaQuery.of(context).size.width,
                            color: Colors.blue,
                            child: BannerView(
                              bannerList: _bannerList,
                            ),
                          ),
                        ],
                      ),
                    ),
                  ),
                  bottom: _cookInfoList.isEmpty
                      ? PreferredSize(
                          preferredSize: const Size(0, 0), child: Container())
                      : TabBar(
                          controller: _tabController,
                          indicatorColor: Colors.blue,
                          indicatorWeight: 18,
                          isScrollable: true,
                          indicatorPadding:
                              const EdgeInsets.symmetric(vertical: 6),
                          indicator: const MyUnderlineTabIndicator(
                              wantWidth: 30.0,
                              borderSide: BorderSide(
                                  width: 6.0,
                                  color: Color.fromRGBO(36, 217, 252, 1))),
                          tabs: getTabs()
                              .asMap()
                              .entries
                              .map(
                                (entry) => AnimatedBuilder(
                                  animation: _tabController!.animation!,
                                  builder: (ctx, snapshot) {
                                    final forward = _tabController!.offset > 0;
                                    final backward = _tabController!.offset < 0;
                                    int _fromIndex;
                                    int _toIndex;
                                    double progress;
                                    // Tab
                                    if (_tabController!.indexIsChanging) {
                                      _fromIndex = _tabController!.previousIndex;
                                      _toIndex = _tabController!.index;
                                      progress =
                                          (_tabController!.animation!.value -
                                                      _fromIndex)
                                                  .abs() /
                                              (_toIndex - _fromIndex).abs();
                                    } else {
                                      // Scroll
                                      _fromIndex = _tabController!.index;
                                      _toIndex = forward
                                          ? _fromIndex + 1
                                          : backward
                                              ? _fromIndex - 1
                                              : _fromIndex;
                                      progress =
                                          (_tabController!.animation!.value -
                                                  _fromIndex)
                                              .abs();
                                    }
                                    var flag = entry.key == _fromIndex
                                        ? 1 - progress
                                        : entry.key == _toIndex
                                            ? progress
                                            : 0.0;
                                    return buildTabContainer(
                                        entry.value.text ?? '', flag);
                                  },
                                ),
                              )
                              .toList(),
                        ),
                )
              ];
            },
            body: _cookInfoList.isEmpty
                ? PreferredSize(
                    preferredSize: const Size(0, 0), child: Container())
                : TabBarView(controller: _tabController, children: getWidgets()),
          ),
        );
      }
    
      List<Tab> getTabs() {
        List<Tab> widgetList = [];
        for (int i = 0; i < _cookInfoList.length; i++) {
          CookInfoModel model = _cookInfoList[i];
          if (model.stdname!.length > 5) {
            model.stdname = model.stdname?.substring(0, 5);
          }
          widgetList.add(Tab(text: model.stdname!.isNotEmpty ? model.stdname : '暂无数据'));
        }
        return widgetList;
      }
    
      List<Widget> getWidgets() {
        List<Widget> widgetList = [];
        for (int i = 0; i < _cookInfoList.length; i++) {
          CookInfoModel model = _cookInfoList[i];
          widgetList.add(
            Container(
                padding: const EdgeInsets.only(left: 20, right: 20, top: 10,bottom: 15),
                child: SingleChildScrollView(
                  child: Column(
                    children: [
                      Container(
                        width: MediaQuery.of(context).size.width,
                        clipBehavior: Clip.hardEdge,
                        height: 200,
                        decoration: BoxDecoration(
                          borderRadius: BorderRadius.circular(6),
                          color: Colors.white,
                        ),
                        child: CachedNetworkImage(
                          imageUrl: model.img ?? "",
                          width: MediaQuery.of(context).size.width,
                          fit: BoxFit.fitWidth,
                          height: 200,
                        ),
                      ),
                      const SizedBox(height: 15,),
                      Text(
                        model.n ?? '',
                        style: const TextStyle(fontSize: 14, color: Colors.black54),
                      )
                    ],
                  ),
                )),
          );
        }
        return widgetList;
      }
    
      buildTabContainer(String tabName, double alpha) {
        return Container(
          padding: const EdgeInsets.symmetric(vertical: 1, horizontal: 1),
          child: AnimatedScale(
            scale: 1 + double.parse((alpha * 0.2).toStringAsFixed(2)),
            duration: const Duration(milliseconds: 100),
            child: Text(
              tabName,
              style: const TextStyle(
                  fontSize: 16, fontWeight: FontWeight.w500, color: Colors.black),
            ),
          ),
        );
      }
    }
    
    最终效果图
    动态获取tab和tab悬停
    七. 动态获取tab+tab悬停+下拉刷新上拉加载
    最终效果图
    动态获取tab+tab悬停+下拉刷新上拉加载
    八. tab嵌套tab
    最终效果图
    tab嵌套tab
    九. tab自由布局
    tab自由布局
    项目地址请移步: 项目地址
    Flutter timer的使用: 项目地址

    相关文章

      网友评论

          本文标题:Flutter TabBar 在实际项目中的运用

          本文链接:https://www.haomeiwen.com/subject/qogtzrtx.html