如何在Flutter中实现带固定锚点的时间选择滑块组件?
Flutter 时间选择滑块组件问题解决
需求与问题
需要实现一款水平时间选择滑块组件:当代表一刻钟、半小时、整小时的进度条处于顶部锚点(倒三角标记)区域时,进度条切换为紫色。目前已完成基础布局,但存在两个问题:
- 无法获取当前锚点对应的选中时间值
- 无法动态切换处于锚点区域的进度条颜色
当前实现代码
import 'package:flutter/material.dart'; import 'dart:developer' as dev; class FixedPinTimePicker extends StatefulWidget { FixedPinTimePicker({super.key, required this.width}); double width; @override State<FixedPinTimePicker> createState() => _FixedPinTimePickerState(); } Widget hourBar() { return Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Container( height: 100, width: 5, color: Colors.grey, ), ); } Widget halfHourBar() { return Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Container( height: 70, width: 5, color: Colors.grey, ), ); } Widget quarterHourBar() { return Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Container( height: 50, width: 5, color: Colors.grey, ), ); } Widget getBarBasedOnIndex(int index) { if (index % 4 == 0) { return halfHourBar(); } else if (index % 2 == 0) { return hourBar(); } else { return quarterHourBar(); } } String getTimeBasedOnIndex(int index) { final int hour = index ~/ 2; final int minute = index % 2 == 0 ? 0 : 30; return index % 2 == 0 ? '$hour:$minute' : ''; } class _FixedPinTimePickerState extends State<FixedPinTimePicker> { ScrollController _scrollController = ScrollController(); @override Widget build(BuildContext context) { dev.log("Width: ${widget.width}"); return SizedBox( height: 150, child: Stack( children: [ ListView.builder( controller: _scrollController, scrollDirection: Axis.horizontal, itemCount: 49, itemBuilder: (context, index) { dev.log("Controller: ${_scrollController.offset}"); return Stack( children: [ getBarBasedOnIndex(index), Positioned( bottom: 0, child: Text( getTimeBasedOnIndex(index), style: TextStyle( color: Colors.black, fontSize: 20, ), ), ), ], ); }, ), Positioned( top: 0, right: widget.width / 2 - 40, child: CustomPaint( painter: InvertedTrianglePainter(), size: Size(30, 60), )), ], ), ); } } // 补充自定义倒三角绘制类 class InvertedTrianglePainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final paint = Paint()..color = Colors.purple; final path = Path() ..moveTo(size.width / 2, 0) ..lineTo(size.width, size.height) ..lineTo(0, size.height) ..close(); canvas.drawPath(path, paint); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; }
解决方案
1. 动态切换进度条颜色
通过监听滚动事件,计算当前锚点位置对应的列表项索引,动态为该索引的进度条设置紫色,其余保持灰色。
2. 获取选中时间值
根据计算出的选中索引,调用原有的getTimeBasedOnIndex方法转换为时间字符串,实时更新选中时间。
修改后的完整代码
import 'package:flutter/material.dart'; import 'dart:developer' as dev; class FixedPinTimePicker extends StatefulWidget { FixedPinTimePicker({super.key, required this.width}); final double width; @override State<FixedPinTimePicker> createState() => _FixedPinTimePickerState(); } Widget hourBar(Color color) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Container( height: 100, width: 5, color: color, ), ); } Widget halfHourBar(Color color) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Container( height: 70, width: 5, color: color, ), ); } Widget quarterHourBar(Color color) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Container( height: 50, width: 5, color: color, ), ); } Widget getBarBasedOnIndex(int index, Color color) { if (index % 4 == 0) { return halfHourBar(color); } else if (index % 2 == 0) { return hourBar(color); } else { return quarterHourBar(color); } } String getTimeBasedOnIndex(int index) { final int hour = index ~/ 2; final int minute = index % 2 == 0 ? 0 : 30; return index % 2 == 0 ? '$hour:$minute' : ''; } class _FixedPinTimePickerState extends State<FixedPinTimePicker> { final ScrollController _scrollController = ScrollController(); int? _selectedIndex; String? _selectedTime; @override void initState() { super.initState(); // 添加滚动监听 _scrollController.addListener(_onScroll); // 初始化选中位置(默认屏幕中心) WidgetsBinding.instance.addPostFrameCallback((_) { _onScroll(); }); } void _onScroll() { setState(() { // 计算每个列表项的宽度:左右padding各20 + 容器宽度5 = 45 const itemWidth = 45.0; // 屏幕水平中心位置 final screenCenter = widget.width / 2; // 计算当前滚动偏移下,屏幕中心对应的列表项索引 _selectedIndex = (_scrollController.offset + screenCenter) ~/ itemWidth; // 边界处理,防止索引越界 _selectedIndex = _selectedIndex!.clamp(0, 48); // 更新选中时间 _selectedTime = getTimeBasedOnIndex(_selectedIndex!); dev.log("选中时间: $_selectedTime"); }); } @override void dispose() { // 移除监听并释放控制器 _scrollController.removeListener(_onScroll); _scrollController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { dev.log("Width: ${widget.width}"); return SizedBox( height: 150, child: Stack( children: [ ListView.builder( controller: _scrollController, scrollDirection: Axis.horizontal, itemCount: 49, itemBuilder: (context, index) { // 判断当前项是否为选中项,设置对应颜色 final barColor = _selectedIndex == index ? Colors.purple : Colors.grey; return Stack( children: [ getBarBasedOnIndex(index, barColor), Positioned( bottom: 0, child: Text( getTimeBasedOnIndex(index), style: TextStyle( color: Colors.black, fontSize: 20, ), ), ), ], ); }, ), Positioned( top: 0, right: widget.width / 2 - 15, // 调整位置使三角对准中心 child: CustomPaint( painter: InvertedTrianglePainter(), size: const Size(30, 60), ), ), // 可选:显示当前选中时间 if (_selectedTime?.isNotEmpty == true) Positioned( bottom: 0, left: widget.width / 2 - 40, child: Text( _selectedTime!, style: const TextStyle( color: Colors.purple, fontSize: 24, fontWeight: FontWeight.bold, ), ), ), ], ), ); } } class InvertedTrianglePainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final paint = Paint()..color = Colors.purple; final path = Path() ..moveTo(size.width / 2, 0) ..lineTo(size.width, size.height) ..lineTo(0, size.height) ..close(); canvas.drawPath(path, paint); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; }
关键修改说明
- 进度条颜色动态化:将三个进度条函数修改为接收
Color参数,在itemBuilder中根据选中索引判断颜色。 - 滚动监听与选中索引计算:通过
ScrollController的监听事件,计算屏幕中心对应的列表项索引,实时更新选中状态。 - 选中时间获取:根据选中索引调用
getTimeBasedOnIndex生成时间字符串,可直接使用或展示。 - 锚点位置调整:修正了倒三角的定位,确保其准确对准屏幕中心。
内容的提问来源于stack exchange,提问作者Siddhesh Kumbhar
相关产品推荐
相关产品推荐

