Flutter:自定义Tab切换,订单列表页tab,tab吸顶

1、自定义tab切换

view

js 复制代码
<Widget>[
  // 好评
  <Widget>[
    TDImage(assetUrl: 'assets/img/order4.png',width: 36.w,height: 36.w,),
    SizedBox(width: 10.w,),
    TextWidget.body('好评',size: 24.sp,color: controller.tabIndex == 0 ? AppTheme.colorfff : AppTheme.color999,),
  ].toRow(mainAxisAlignment: MainAxisAlignment.center)
  .card(color: controller.tabIndex == 0 ? AppTheme.error : AppTheme.colorfff,shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.w)))
  .tight(width: 160.w,height: 68.w)
  .onTap(() {
    controller.onTapOrderStatus(0);
  }),

  SizedBox(width: 20.w,),

  // 中评
  <Widget>[
    TDImage(assetUrl: 'assets/img/order5.png',width: 36.w,height: 36.w,),
    SizedBox(width: 10.w,),
    TextWidget.body('中评',size: 24.sp,color: controller.tabIndex == 1 ? AppTheme.colorfff : AppTheme.color999,),
  ].toRow(mainAxisAlignment: MainAxisAlignment.center)
  .card(color: controller.tabIndex == 1 ? AppTheme.error : AppTheme.colorfff,shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.w)))
  .tight(width: 160.w,height: 68.w)
  .onTap(() {
    controller.onTapOrderStatus(1);
  }),
  
  SizedBox(width: 20.w,),
  
  // 差评
  <Widget>[
    TDImage(assetUrl: 'assets/img/order6.png',width: 36.w,height: 36.w,),
    SizedBox(width: 10.w,),
    TextWidget.body('差评',size: 24.sp,color: controller.tabIndex == 2 ? AppTheme.colorfff : AppTheme.color999,),
  ].toRow(mainAxisAlignment: MainAxisAlignment.center)
  .card(color: controller.tabIndex == 2 ? AppTheme.error : AppTheme.colorfff,shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.w)))
  .tight(width: 160.w,height: 68.w)
  .onTap(() {
    controller.onTapOrderStatus(2);
  }),
].toRow(),

controller

js 复制代码
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class OrderEvaluateController extends GetxController{
  OrderEvaluateController();
  
  // tab 索引
  int tabIndex = 0;
  // 状态改变
  onTapOrderStatus(int index){
    tabIndex = index;
    // print('tabIndex: $tabIndex');
    update(["order_evaluate"]);
  }

  @override
  void onClose() {
    super.onClose();
    tabController.dispose();
  }
}

2、订单列表tab,可以指定tabIndex的初始位置

view

js 复制代码
import 'package:demo/common/index.dart';
import 'package:ducafe_ui_core/ducafe_ui_core.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pull_to_refresh_flutter3/pull_to_refresh_flutter3.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';
import 'index.dart';

class OrderListPage extends GetView<OrderListController> {
  const OrderListPage({super.key});

  // tab切换
  Widget _buildTab() {
    return TDTabBar(
      controller: controller.tabController,
      tabs: controller.tabNames.map((e) => TDTab(text: e)).toList(),
      onTap: (index) {
        controller.onTapOrderStatus(index);
      },
      backgroundColor: Colors.white,
      showIndicator: true,
      indicatorColor: const Color(0xffe01e1e),
      labelColor: const Color(0xffe01e1e),
    );
  }

  // list 列表
  Widget _buildList() {
    return SmartRefresher(
      controller: controller.refreshController,
      enablePullUp: true, // 启用上拉加载
      onRefresh: controller.onRefresh, // 下拉刷新回调
      onLoading: controller.onLoading, // 上拉加载回调
      footer: const SmartRefresherFooterWidget(), // 底部加载更多组件
      header: const SmartRefresherHeaderWidget(), // 顶部下拉刷新组件
      child: ListView.builder(
        itemCount: 10,
        itemBuilder: (context, index) {
          return _buildItem();
        },
      ),
    );
  }

  // 商品item
  Widget _buildItem() {
    return Text('item')
  }

  // 主视图
  Widget _buildView() {
    return <Widget>[
      _buildTab(),
      Expanded(child: _buildList()),
    ].toColumn();
  }

  @override
  Widget build(BuildContext context) {
    return GetBuilder<OrderListController>(
      init: OrderListController(),
      id: "order_list",
      builder: (_) {
        return Scaffold(
          backgroundColor: const Color(0xffF5F6FA),
          appBar: const TDNavBar(
            height: 45,
            title: '我的订单',
            titleFontWeight: FontWeight.w600,
            backgroundColor: Colors.white,
            screenAdaptation: true,
            useDefaultBack: true,
          ),
          body: SafeArea(
            child: _buildView(),
          ),
        );
      },
    );
  }
}

controller

js 复制代码
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pull_to_refresh_flutter3/pull_to_refresh_flutter3.dart';

class OrderListController extends GetxController with GetSingleTickerProviderStateMixin{
  OrderListController();
  // 订单列表
  List items = [];
  // tab 控制器
  late TabController tabController;
  // tab 索引
  int tabIndex = Get.arguments["tabIndex"] ?? 0;
  // tab 名称
  List<String> tabNames = ['全部', '待付款', '待发货', '待收货', '已完成'];
  
  // 订单状态改变
  onTapOrderStatus(int index){
    tabIndex = index;
    // 刷新订单列表
    refreshController.requestRefresh();
    update(["order_list"]);
  }

  @override
  void onInit() {
    super.onInit();
    // 初始化时设置当前索引
    tabController = TabController(
      length: tabNames.length,
      vsync: this,
      initialIndex: tabIndex, // 设置初始索引
    );
    
    // 监听 tab 切换
    tabController.addListener(() {
      if (tabController.indexIsChanging) {
        tabIndex = tabController.index;
        update(["order_list"]);
      }
    });
  }

  @override
  void onClose() {
    super.onClose();
    refreshController.dispose();
    tabController.dispose();
  }
  
  /*
  * 分页
  * refreshController:分页控制器
  * _page:分页
  * _limit:每页条数
  * _loadNewsSell:拉取数据(是否刷新)
  * onLoading:上拉加载新商品
  * onRefresh:下拉刷新
  * */
  final RefreshController refreshController = RefreshController(
    initialRefresh: true,
  );
  // int _page = 1;
  // int _limit = 20;
  Future<bool> _loadNewsSell(bool isRefresh) async {
    return false;
    // var result = await ProductApi.products(ProductsReq(
    //   page:isRefresh ? 1:_page,
    //     prePage:_limit
    // ));
    // if(isRefresh){
    //   _page = 1;
    //   items.clear();
    // }
    // if(result.isNotEmpty){
    //   _page++;
    //   items.addAll(result);
    // }
    // // 是否是空
    // return result.isEmpty;
  }

  // 上拉载入新商品
  void onLoading() async {
    if (items.isNotEmpty) {
      try {
        // 拉取数据是否为空 ? 设置暂无数据 : 加载完成
        var isEmpty = await _loadNewsSell(false);
        isEmpty
            ? refreshController.loadNoData()
            : refreshController.loadComplete();
      } catch (e) {
        refreshController.loadFailed(); // 加载失败
      }
    } else {
      refreshController.loadNoData(); // 设置无数据
    }
    update(["goods_list"]);
  }

  // 下拉刷新
  void onRefresh() async {
    try {
      await _loadNewsSell(true);
      refreshController.refreshCompleted();
    } catch (e) {
      refreshController.refreshFailed();
    }
    update(["goods_list"]);
  }
}

3、tab吸顶,主要记录view中的实现。
下拉刷新和tab切换的方法基本与上方2、的controller一致


view

js 复制代码
import 'package:ducafe_ui_core/ducafe_ui_core.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pull_to_refresh_flutter3/pull_to_refresh_flutter3.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';
import 'package:xiaoshukeji/common/index.dart';
import 'index.dart';

// 1. SliverPersistentHeaderDelegate:必须实现的抽象类
class _StickyTabBarDelegate extends SliverPersistentHeaderDelegate {
  final Widget child;

  _StickyTabBarDelegate({required this.child});

  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    // shrinkOffset: 滚动距离
    // overlapsContent: 是否与其他内容重叠
    return Container(
      color: AppTheme.pageBgColor,
      child: child,
    );
  }

  @override
  double get maxExtent => 92.w; // 最大高度,已知tab高度72+上下padding:10

  @override
  double get minExtent => 92.w; // 最小高度

  @override
  bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) => true;
}


class RankingPage extends GetView<RankingController> {
  const RankingPage({super.key});

  // 头部皇冠位置
  Widget _buildHeader() {
    return <Widget>[

    ].toRow()
    .card(color: AppTheme.pageBgColor)
    .tight(width: 750.w,height: 300.w,);
  }

  // tab,可吸顶
  Widget _buildTab() {
    return <Widget>[
      <Widget>[
        TextWidget.body('日榜', size: 28.sp, weight: FontWeight.w600, color: AppTheme.textColorfff),
      ].toRow(mainAxisAlignment: MainAxisAlignment.center)
      .card(color: AppTheme.primaryYellow)
      .tight(width: 336.w,height: 72.w,),

      <Widget>[
        TextWidget.body('总榜', size: 28.sp, weight: FontWeight.w600, color: AppTheme.textColor6a7),
      ].toRow(mainAxisAlignment: MainAxisAlignment.center)
      .card(color: AppTheme.navBarBgColor)
      .tight(width: 336.w,height: 72.w,),

    ].toRow(mainAxisAlignment: MainAxisAlignment.spaceBetween);
  }

  // 数据列表
  Widget _buildDataList() {
    return SliverList(
      delegate: SliverChildBuilderDelegate(
        (context, index) {
          return <Widget>[]
              .toRow()
              .paddingHorizontal(30.w)
              .card(color: AppTheme.blockBgColor)
              .tight(
                width: 690.w,
                height: 120.w,
              )
              .marginOnly(bottom: 20.w);
        },
        childCount: 20,
      ),
    );
  }

  // 主视图
  Widget _buildView() {
    return SmartRefresher(
      controller: controller.refreshController,
      enablePullUp: true,
      onRefresh: controller.onRefresh,
      onLoading: controller.onLoading,
      footer: const SmartRefresherFooterWidget(),
      header: const SmartRefresherHeaderWidget(),
      child: CustomScrollView(
        slivers: [
          // 头部
          _buildHeader().sliverToBoxAdapter().sliverPaddingHorizontal(30.w),
          
          // 2. SliverPersistentHeader:实现吸顶的核心组件
          SliverPersistentHeader(
            pinned: true,  // 设置为 true 实现吸顶
            delegate: _StickyTabBarDelegate(
              child: Container(
                padding: EdgeInsets.symmetric(horizontal: 30.w, vertical: 10.w),
                child: _buildTab(),
              ),
            ),
          ),
          
          // 列表内容
          _buildDataList().sliverPaddingHorizontal(30.w),
        ],
      ),
    );
  }


  @override
  Widget build(BuildContext context) {
    return GetBuilder<RankingController>(
      init: RankingController(),
      id: "ranking",
      builder: (_) {
        return Scaffold(
          backgroundColor: AppTheme.pageBgColor, // 自定义颜色
          appBar: const TDNavBar(
            height: 0,
            titleColor: AppTheme.textColorfff,
            titleFontWeight: FontWeight.w600,
            backgroundColor: AppTheme.pageBgColor,
            screenAdaptation: true, // 是否进行屏幕适配,默认true
            useDefaultBack: false,
          ),
          body: _buildView(),
        );
      },
    );
  }
}
cpp 复制代码
GetTickerProviderStateMixin 和 GetSingleTickerProviderStateMixin 的主要区别在于:
1、GetSingleTickerProviderStateMixin:
用于创建单个 AnimationController
性能更好,因为只维护一个 Ticker
适用场景:只需要一个动画控制器的情况,如单个 TabController
class MyController extends GetxController with GetSingleTickerProviderStateMixin {
  late TabController tabController;
  
  @override
  void onInit() {
    super.onInit();
    tabController = TabController(length: 3, vsync: this);
  }
  
  @override
  void onClose() {
    tabController.dispose();
    super.onClose();
  }
}

2、GetTickerProviderStateMixin:
可以创建多个 AnimationController
适用场景:需要多个独立动画控制器的情况

class MyController extends GetxController with GetTickerProviderStateMixin {
  late AnimationController controller1;
  late AnimationController controller2;
  
  @override
  void onInit() {
    super.onInit();
    controller1 = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    );
    controller2 = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 500),
    );
  }
}


什么时候需要 GetSingleTickerProviderStateMixin:
当你使用 TabController 来控制 tab 切换动画
当你需要实现滑动切换 tab 功能
当你需要更复杂的 tab 切换效果

如果只需要点击切换,不需要 GetSingleTickerProviderStateMixin
相关推荐
TDengine (老段)1 小时前
TDengine 中的关联查询
大数据·javascript·网络·物联网·时序数据库·tdengine·iotdb
Tttian6221 小时前
Python办公自动化(3)对Excel的操作
开发语言·python·excel
再学一点就睡2 小时前
大文件上传之切片上传以及开发全流程之前端篇
前端·javascript
独好紫罗兰3 小时前
洛谷题单2-P5713 【深基3.例5】洛谷团队系统-python-流程图重构
开发语言·python·算法
闪电麦坤954 小时前
C#:base 关键字
开发语言·c#
Mason Lin4 小时前
2025年3月29日(matlab -ss -lti)
开发语言·matlab
DREAM.ZL4 小时前
基于python的电影数据分析及可视化系统
开发语言·python·数据分析
難釋懷4 小时前
JavaScript基础-移动端常见特效
开发语言·前端·javascript
海姐软件测试4 小时前
Postman参数化设置如何设置?
开发语言·jmeter
松树戈4 小时前
Java常用异步方式总结
java·开发语言