You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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;
}

关键修改说明

  1. 进度条颜色动态化:将三个进度条函数修改为接收Color参数,在itemBuilder中根据选中索引判断颜色。
  2. 滚动监听与选中索引计算:通过ScrollController的监听事件,计算屏幕中心对应的列表项索引,实时更新选中状态。
  3. 选中时间获取:根据选中索引调用getTimeBasedOnIndex生成时间字符串,可直接使用或展示。
  4. 锚点位置调整:修正了倒三角的定位,确保其准确对准屏幕中心。

内容的提问来源于stack exchange,提问作者Siddhesh Kumbhar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 23:40:01