前言:
这几天在使用 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, ); } }
可以按需求封装下就能上手使用了