Flutter如何自定义横向时间选择器 实现带FROM/TO标注的黑色圆角样式
Flutter横向时间选择器实现方案
方案1:基于现有time_range: ^1.0.1插件调整
你可以通过调整插件参数和外层嵌套装饰组件接近目标效果,修改要点如下:
- 恢复
fromTitle、toTitle的文本配置,不要替换为分割线 - 给
TimeRange组件外层嵌套ClipRRect和Container实现全局圆角,调整选中状态的背景样式 - 如对圆角要求高,可复制插件源码到项目内自定义,给时间块增加圆角属性
调整后的核心代码片段:
// 外层嵌套实现圆角 ClipRRect( borderRadius: BorderRadius.circular(16), child: TimeRange( fromTitle: Text( 'FROM', style: TextStyle( fontSize: 14, color: dark, fontWeight: FontWeight.w600, ), ), toTitle: Text( 'TO', style: TextStyle( fontSize: 14, color: dark, fontWeight: FontWeight.w600, ), ), // 其余原有参数保持不变 titlePadding: leftPadding, textStyle: TextStyle( fontWeight: FontWeight.normal, color: dark, ), activeTextStyle: TextStyle( fontWeight: FontWeight.bold, color: Colors.white, ), borderColor: Colors.transparent, activeBorderColor: Colors.transparent, backgroundColor: Colors.transparent, activeBackgroundColor: dark, firstTime: TimeOfDay(hour: 8, minute: 00), lastTime: TimeOfDay(hour: 20, minute: 00), initialRange: _timeRange, timeStep: 10, timeBlock: 30, onRangeCompleted: (range) => setState(() => _timeRange = range), ), )
如果调整后仍无法完全匹配设计,建议使用自定义实现方案,灵活度更高。
方案2:自定义横向时间选择器(推荐,完全匹配样式)
完全自主实现组件,可100%还原设计的圆角、标注、样式效果,实现逻辑如下:
- 按指定步长生成所有可选时间点列表
- 用横向
ListView渲染时间块,未选中状态为透明背景黑色文字,选中状态为黑色圆角背景白色文字 - 通过手势监听处理点击/拖拽选择起始、结束时间
- 在选中区间的首尾位置上方添加
FROM、TO标注文本
核心示例代码:
class CustomTimeRangePicker extends StatefulWidget { final TimeOfDay startTime; final TimeOfDay endTime; final int stepMinutes; final Function(TimeOfDay start, TimeOfDay end) onRangeSelected; const CustomTimeRangePicker({ super.key, required this.startTime, required this.endTime, required this.stepMinutes, required this.onRangeSelected, }); @override State<CustomTimeRangePicker> createState() => _CustomTimeRangePickerState(); } class _CustomTimeRangePickerState extends State<CustomTimeRangePicker> { List<TimeOfDay> timePoints = []; int? startIndex; int? endIndex; @override void initState() { super.initState(); _generateTimePoints(); } void _generateTimePoints() { TimeOfDay current = widget.startTime; while (current.hour < widget.endTime.hour || (current.hour == widget.endTime.hour && current.minute <= widget.endTime.minute)) { timePoints.add(current); current = TimeOfDay(hour: current.hour + (current.minute + widget.stepMinutes) ~/ 60, minute: (current.minute + widget.stepMinutes) % 60); } } @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 标注行 if (startIndex != null && endIndex != null) Row( children: [ SizedBox(width: startIndex! * 60 + 20), const Text('FROM', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600)), SizedBox(width: (endIndex! - startIndex!) * 60 - 40), const Text('TO', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600)), ], ), const SizedBox(height: 8), // 时间块横向列表 SizedBox( height: 50, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: timePoints.length, itemBuilder: (context, index) { bool isSelected = startIndex != null && endIndex != null && index >= startIndex! && index <= endIndex!; return GestureDetector( onTap: () { if (startIndex == null || endIndex != null) { startIndex = index; endIndex = null; } else { endIndex = index > startIndex! ? index : startIndex; startIndex = index < startIndex! ? index : startIndex; widget.onRangeSelected(timePoints[startIndex!], timePoints[endIndex!]); } setState(() {}); }, child: Container( width: 60, margin: const EdgeInsets.symmetric(horizontal: 2), decoration: BoxDecoration( color: isSelected ? const Color(0xFF333A47) : Colors.transparent, borderRadius: BorderRadius.circular(12), ), child: Center( child: Text( timePoints[index].format(context), style: TextStyle( color: isSelected ? Colors.white : const Color(0xFF333A47), fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, ), ), ), ), ); }, ), ), ], ); } }
使用时直接替换原有TimeRange组件即可,可根据设计需求任意调整圆角、颜色、间距、标注位置等样式。
内容的提问来源于stack exchange,提问作者Shanmukha
相关产品推荐
相关产品推荐

