flutter 手写 TabBar

前言:

这几天在使用 flutter TabBar 的时候 我们的设计给我提了一个需求:

如下 Tabbar  第一个元素 左对齐试了下TabBar 的配置,无法实现这个需求,他的 配置是针对所有元素的。而且 这个 TabBar 下面的 滑块在移动的时候 上面的文字会相应的抖动。

看了下 TabBar 的源代码 他的实现是相对复杂的 下面的 滑块是 canvas 实现的。 有可能他要实现的功能比较丰富。

自定义Tabbar 的基本布局

下面是我页面的布局:这样实现起来 里面元素的 样式可以完全自己定义单个配置,想怎么显示都可以。这样就可以不用局限于 自带Tabbar的配置

SingleChildScrollView 解析

完成页面布局相对简单,主要实现底部 滑块的移动,以及 整 SingleChildScrollView 的居中移动是一个关键点

ScrollController 中的几个关键概念:
  • controller.position.viewportDimension:  SingleChildScrollView 视口 的大小
  • position.maxScrollExtent :                     SingleChildScrollView 可以移动的最大范围
  • position.minScrollExtent  :                     SingleChildScrollView 可以移动最小范围 一般是0
  • Row 的长度就是所有元素的长度之和:也就是 position.maxScrollExtent + position.viewportDimension 

Row 的长度之和 为什么是 SingleChildScrollView 最大可移动范围加 position.viewportDimension 的和

SingleChildScrollView 可见区域始终是他的视口大小,不可见的也就是 Row的长度减去视口大小 也就是 maxScrollExtent 可拖动的最大区域

实现 Tabbar

下面是我实现的大致效果:第一个元素左对齐,最后一个元素右对齐,我这边是直接写死的,你们封装一下 在外边直接用就好了。

代码如下:

import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:game/const/app_textStyle.dart';
import 'package:game/utils/app_widget.dart';
import 'package:game/wrap/extension/extension.dart';

class PageTabBar extends StatefulWidget {
  const PageTabBar({Key? key}) : super(key: key);

  @override
  State<PageTabBar> createState() => _PageTabBarState();
}

class _PageTabBarState extends State<PageTabBar> {
  final ScrollController _controller = ScrollController();
  int _selectIndex = 0;
  double _width = 0;

  double _positionX = 0;
  final List<Map> _listMap = [
    {'width': 0, 'name': '一号', 'key': GlobalKey()},
    {'width': 0, 'name': '二二号技师', 'key': GlobalKey()},
    {'width': 0, 'name': '三三三号技师', 'key': GlobalKey()},
    {'width': 0, 'name': '四号技师', 'key': GlobalKey()},
    {'width': 0, 'name': '五五号技师', 'key': GlobalKey()},
    {'width': 0, 'name': '六六六号技师', 'key': GlobalKey()},
    {'width': 0, 'name': '七号技师', 'key': GlobalKey()},
    {'width': 0, 'name': '八八号技师', 'key': GlobalKey()},
    {'width': 0, 'name': '九', 'key': GlobalKey()},
    {'width': 0, 'name': '十号技师', 'key': GlobalKey()},
  ];

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) => _printSize());

    // _controller.addListener(() {
    //   print('_controller.offset:${_controller.offset}');
    // });
  }

  @override
  void dispose() {
    // TODO: implement dispose
    _controller.dispose();
    super.dispose();
  }

  void _printSize() {
    for (Map element in _listMap) {
      final RenderBox box = element['key'].currentContext.findRenderObject();
      element['width'] = box.size.width;
    }
    _width = _listMap[0]['width'];
    _selectItem(0);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppWidget.appBar(title: 'TabBar 测试页面'),
      body: Center(
        child: Container(
          height: 220.cale,
          width: 710.cale,
          color: Colors.deepOrangeAccent,
          child: SingleChildScrollView(
            physics: const BouncingScrollPhysics(
              parent: AlwaysScrollableScrollPhysics(),
            ),
            controller: _controller,
            scrollDirection: Axis.horizontal,
            child: Stack(
              children: [
                Row(
                  children: _listMap
                      .asMap()
                      .map(
                        (key, value) => MapEntry(
                          key,
                          AppWidget.inkWellEffectNone(
                            onTap: () {
                              _selectItem(key);
                            },
                            child: Container(
                              padding: key == 0
                                  ? EdgeInsets.only(right: 25.cale)
                                  : key == _listMap.length - 1
                                      ? EdgeInsets.only(left: 25.cale)
                                      : EdgeInsets.symmetric(
                                          horizontal: 25.cale),
                              key: value['key'],
                              color: Colors.blue.withOpacity(0.1 * key),
                              height: 180.cale,
                              child: Center(
                                child: Text(
                                  value['name'],
                                  style: _selectIndex == key
                                      ? AppTextStyle.textStyle_34_FD3949_Bold
                                      : AppTextStyle.textStyle_30_1A1A1A,
                                ),
                              ),
                            ),
                          ),
                        ),
                      )
                      .values
                      .toList(),
                ),
                AnimatedPositioned(
                  bottom: 0.cale,
                  left: _positionX,
                  duration: const Duration(milliseconds: 250),
                  child: AnimatedContainer(
                    duration: const Duration(milliseconds: 250),
                    width: _width,
                    child: Container(
                      height: 20.cale,
                      margin: EdgeInsets.symmetric(horizontal: 25.cale),
                      width: double.infinity,
                      color: Colors.green,
                    ),
                  ),
                )
              ],
            ),
          ),
        ),
      ),
    );
  }

  void _selectItem(int index) {
    print('index:$index');
    final ScrollPosition position = _controller.position;
    setState(() {
      _selectIndex = index;
      _width = _listMap[index]['width'];
    });

    _positionX = 0;
    List.generate(index, (itemIndex) {
      _positionX += _listMap[itemIndex]['width'];
    });
    //当前展示的元素位置 中心点位置,用户确定 滚动位置
    double viewPosition = _positionX + _listMap[index]['width'] / 2;
    double movePosition = viewPosition - position.viewportDimension / 2;
    movePosition = clampDouble(
        movePosition, position.minScrollExtent, position.maxScrollExtent);
    _controller.animateTo(
      movePosition,
      duration: const Duration(milliseconds: 300),
      curve: Curves.easeOut,
    );
  }
}

可以按需求封装下就能上手使用了

相关推荐

  1. “ 选择排序 ”

    2024-07-16 08:26:02       50 阅读
  2. 爬虫框架

    2024-07-16 08:26:02       64 阅读
  3. 一个vuex?

    2024-07-16 08:26:02       51 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-16 08:26:02       70 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-16 08:26:02       74 阅读
  3. 在Django里面运行非项目文件

    2024-07-16 08:26:02       62 阅读
  4. Python语言-面向对象

    2024-07-16 08:26:02       72 阅读

热门阅读

  1. electron-egg webSocket使用封装

    2024-07-16 08:26:02       19 阅读
  2. Spring的启动过程

    2024-07-16 08:26:02       23 阅读
  3. 并发编程-锁的分类

    2024-07-16 08:26:02       29 阅读
  4. Gmsh教程

    2024-07-16 08:26:02       31 阅读
  5. Redis是什么

    2024-07-16 08:26:02       28 阅读