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
相关推荐
编程小Y3 分钟前
Bash 替换机制
开发语言·chrome·bash
我要学脑机5 分钟前
一个图谱映射到功能网络yeo7或17的解决方案
开发语言·网络·php
名字被你们想完了7 分钟前
Flutter 实现一个容器内部元素可平移、缩放和旋转等功能(八)
前端·flutter
weibkreuz9 分钟前
React开发者工具的下载及安装@4
前端·javascript·react
renke33649 分钟前
Flutter 2025 测试工程体系:从单元测试到生产验证,构建高可靠、可交付、零回归的工程质量防线
flutter
坐吃山猪12 分钟前
Python之PDF小工具
开发语言·python·pdf
代码栈上的思考13 分钟前
MyBatis——动态SQL讲解
java·开发语言·数据库
如果你好13 分钟前
一文了解 Cookie、localStorage、sessionStorage的区别与实战案例
前端·javascript
kirk_wang14 分钟前
Flutter 鸿蒙项目 Android Studio 点击 Run 失败 ohpm 缺失
flutter·android studio·harmonyos
RichardMiao14 分钟前
axios 的 withCredentials 到底做了什么?
前端·javascript·http