Flutter 组件抽取:可滑动的 TabBar(ScrollTabBar)

简介

可滑动的 TabBar

效果

在这里插入图片描述

范例

class _TestPageState extends State<TestPage> {
  List<String> tabs = ['一', '二', '三', '四', '五'];

  
  void initState() {
    super.initState();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('ScrollTabBar')),
      body: Column(
        children: [
          ScrollTabBar(
            tabCount: tabs.length,
            visibleCount: tabs.length,
            currentIndex: 2,
            width: MediaQuery.of(context).size.width,
            height: 50,
            backgroundColor: Colors.white,
            animMode: TabAnimMode.middle,
            tabBuilder: (context, index, select) {
              String name = tabs[index];
              return Center(
                child: Text(
                  name,
                  style: TextStyle(
                    fontSize: 20,
                    fontWeight: select ? FontWeight.bold : FontWeight.normal,
                    color: select ? Colors.red : Colors.black,
                  ),
                ),
              );
            },
            dividerBuilder: (context) {
              return const Divider(height: 1, color: Colors.red);
            },
          ),
          const SizedBox(height: 20),
          ScrollTabBar(
            tabCount: tabs.length,
            visibleCount: 3,
            currentIndex: 1,
            width: MediaQuery.of(context).size.width,
            height: 50,
            backgroundColor: Colors.white,
            animMode: TabAnimMode.middle,
            tabBuilder: (context, index, select) {
              String name = tabs[index];
              return Center(
                child: Text(
                  name,
                  style: TextStyle(
                    fontSize: 20,
                    fontWeight: select ? FontWeight.bold : FontWeight.normal,
                    color: select ? Colors.red : Colors.black,
                  ),
                ),
              );
            },
            dividerBuilder: (context) {
              return const Divider(height: 1, color: Colors.red);
            },
          ),
          const SizedBox(height: 20),
          ScrollTabBar(
            tabCount: tabs.length,
            visibleCount: 3,
            currentIndex: 0,
            width: MediaQuery.of(context).size.width,
            height: 50,
            backgroundColor: Colors.white,
            animMode: TabAnimMode.none,
            tabBuilder: (context, index, select) {
              String name = tabs[index];
              return Center(
                child: Text(
                  name,
                  style: TextStyle(
                    fontSize: 20,
                    fontWeight: select ? FontWeight.bold : FontWeight.normal,
                    color: select ? Colors.red : Colors.black,
                  ),
                ),
              );
            },
            dividerBuilder: (context) {
              return const Divider(height: 1, color: Colors.red);
            },
          ),
        ],
      ),
    );
  }
}

说明

1、根据传入宽度(常见为屏幕宽度)平均分配每个 tab 的宽度
2、当传入可见 tab 个数时,展示 “tab 个数减 0.5” 个 tab,tab 宽度为传入宽度除以 “tab 个数减 0.5”
3、点击 tab 时,可选是否自动定位滑动,自动定位滑动时,被点 tab 滑动到 TabBar 中间

代码

import 'package:flutter/material.dart';

typedef TabBuilder = Widget Function(BuildContext context, int index, bool select);
typedef DividerBuilder = Widget Function(BuildContext context);

/// tab 点击后动画交互模式
enum TabAnimMode {
  none, // 0 无动画
  middle, // 1 被点击 tab 居中
}

/// 可滑动 TabBar
class ScrollTabBar extends StatefulWidget {
  final int tabCount;
  final int visibleCount;
  final int currentIndex;

  final double width;
  final double height;
  final Color backgroundColor;
  final TabAnimMode animMode;

  final TabBuilder tabBuilder;
  final Function(int index)? onTabClick;
  final DividerBuilder? dividerBuilder;
  final ScrollTabBarController? controller;

  /// [visibleCount] 展示数量【实际可见数量 + 半个】
  /// [tabBuilder] tab构建函数
  /// [dividerBuilder] TabBar 最下端分隔 divider
  /// [width] TabBar 可见宽度
  /// [height] TabBar 高度
  /// [onTabClick] TabBar 点击回调
  const ScrollTabBar({
    Key? key,
    required this.tabCount,
    this.visibleCount = 3,
    this.currentIndex = 0,
    required this.width,
    required this.height,
    this.backgroundColor = Colors.white,
    this.animMode = TabAnimMode.none,
    required this.tabBuilder,
    this.onTabClick,
    this.dividerBuilder,
    this.controller,
  })  : assert(tabCount > 0),
        assert(visibleCount > 0),
        assert(currentIndex >= 0),
        assert((width > 0) && (height > 0)),
        super(key: key);

  
  State<StatefulWidget> createState() => _ScrollTabBarState();
}

class _ScrollTabBarState<T> extends State<ScrollTabBar> {
  final ScrollController scrollController = ScrollController();

  int _tabCount = 0;
  int _visibleCount = 0;
  int _currentIndex = 0;
  double _tabWidth = 0;

  void resetConfig({
    required int tabCount,
    required int visibleCount,
    required int currentIndex,
  }) {
    _tabCount = tabCount;
    _visibleCount = ((tabCount >= visibleCount) ? visibleCount : 3);
    _currentIndex = ((tabCount > currentIndex) ? currentIndex : 0);
    _tabWidth = (_tabCount <= _visibleCount)
        ? (widget.width / _tabCount)
        : (widget.width / (_visibleCount + 0.5));
  }

  void changeTab(int index) {
    widget.onTabClick?.call(index);
    setState(() {
      _currentIndex = index;
    });
    // tab 点击后交互动画
    if (widget.animMode == TabAnimMode.middle) {
      double offset = (index == 0)
          ? 0
          : ((index * _tabWidth + _tabWidth / 2) - widget.width / 2);
      scrollController.animateTo(
        offset,
        duration: const Duration(milliseconds: 200),
        curve: Curves.easeInOut,
      );
    }
  }

  
  void setState(VoidCallback fn) {
    if (!mounted) return;
    super.setState(fn);
  }

  
  void initState() {
    super.initState();
    resetConfig(
      tabCount: widget.tabCount,
      visibleCount: widget.visibleCount,
      currentIndex: widget.currentIndex,
    );

    widget.controller?.setOnListener(
      getCurrentIndex: () => _currentIndex,
      jumpTo: (index) {
        if ((index == _currentIndex) || (index >= _tabCount)) {
          return;
        }
        changeTab(index);
      },
      reset: ({
        int? tabCount,
        int? visibleCount,
        int? currentIndex,
      }) {
        setState(() {
          resetConfig(
            tabCount: (tabCount == null) ? _tabCount : tabCount,
            visibleCount: (visibleCount == null) ? _visibleCount : visibleCount,
            currentIndex: (currentIndex == null) ? _currentIndex : currentIndex,
          );
        });
      },
    );
  }

  
  Widget build(BuildContext context) {
    return Container(
      color: widget.backgroundColor,
      width: widget.width,
      height: widget.height,
      child: Stack(
        children: [
          Positioned(
            left: 0,
            right: 0,
            bottom: 0,
            child: (widget.dividerBuilder == null)
                ? const SizedBox()
                : widget.dividerBuilder!(context),
          ),
          ListView.builder(
            physics: const ClampingScrollPhysics(),
            controller: scrollController,
            scrollDirection: Axis.horizontal,
            itemCount: _tabCount,
            itemBuilder: (context, index) {
              return GestureDetector(
                behavior: HitTestBehavior.opaque,
                onTap: () => changeTab(index),
                child: SizedBox(
                  width: _tabWidth,
                  child: widget.tabBuilder(
                      context, index, (index == _currentIndex)),
                ),
              );
            },
          ),
        ],
      ),
    );
  }
}

typedef JumpTo = void Function(int index);
typedef ResetTabBar = void Function({
  int? tabCount,
  int? visibleCount,
  int? currentIndex,
});

class ScrollTabBarController {
  void setOnListener({
    Function? getCurrentIndex,
    required JumpTo? jumpTo,
    required ResetTabBar? reset,
  }) {
    _jumpTo = jumpTo;
    _reset = reset;
  }

  /// 获取当前index
  Function? _getCurrentIndex;

  int get currentIndex {
    if (_getCurrentIndex != null) {
      return _getCurrentIndex!();
    }
    return 0;
  }

  JumpTo? _jumpTo;

  /// 跳转指定 tab
  void jumpTo(int index) {
    if (_jumpTo != null) {
      _jumpTo!(index);
    }
  }

  ResetTabBar? _reset;

  /// 重置TabBar
  void reset({
    int? tabCount,
    int? visibleCount,
    int? currentIndex,
  }) {
    if (tabCount != null) assert(tabCount > 0);
    if (visibleCount != null) assert(visibleCount > 0);
    if (currentIndex != null) assert(currentIndex >= 0);
    if (_reset != null) {
      _reset!.call(
        tabCount: tabCount,
        visibleCount: visibleCount,
        currentIndex: currentIndex,
      );
    }
  }
}

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值