跳转到正文
YGJoon 技术博客

Flutter 实现自定义时间选择器

发布于:
·999 字 · 约 2 分钟·作者:
ListWheelScrollView 做滚轮时间选择器:FixedExtentScrollPhysics 让滚动自动吸附,透视与放大参数调出手感
Flutter 实现自定义时间选择器文章封面
目录

在 Flutter 中,时间选择器是应用程序中常见的组件之一。尽管 Flutter 提供了内置的时间选择器,但有时我们需要实现一个更加个性化的自定义时间选择器。在这篇文章中,我们将使用 ListWheelScrollView 组件来实现一个自定义的时间选择器。

效果

描述

ListWheelScrollView

ListWheelScrollView 是一个用于创建滚动列表的组件,具有旋转效果,适合用于选择器、转盘等场景。通过 ListWheelScrollView,我们可以轻松实现一个既美观又实用的自定义时间选择器。

// ListWheelScrollView 构造函数参数速查(摘自 Flutter 框架源码,仅查阅):
//   key, controller(控制器), physics(滚动物理效果),
//   diameterRatio(直径比例,默认 RenderListWheelViewport.defaultDiameterRatio),
//   perspective(透视比例,默认 RenderListWheelViewport.defaultPerspective),
//   offAxisFraction(偏移量), useMagnifier(放大镜), magnification(放大倍数),
//   overAndUnderCenterOpacity(中心透明度),
//   required itemExtent(每个子项的主轴高度), squeeze(挤压比例),
//   onSelectedItemChanged(选中项改变回调),
//   renderChildrenOutsideViewport, clipBehavior, restorationId, scrollBehavior,
//   required children


// ListWheelScrollView.useDelegate 用法示意(useDelegate 是真实存在的命名构造,
// 下面是参数形态示意,controller/itemBuilder 等需替换为你自己的变量):
// ListWheelScrollView.useDelegate(
//   controller: controller,
//   perspective: 0.01,
//   itemExtent: 50,
//   onSelectedItemChanged: onSelectedItemChanged,
//   physics: const FixedExtentScrollPhysics(),
//   childDelegate: ListWheelChildBuilderDelegate(
//     builder: itemBuilder,
//     childCount: itemCount,
//   ),
// );
```text

## useDelegate 构建的 ListWheelScrollView 的子项

`ListWheelChildBuilderDelegate` 是一个用于构建 `ListWheelScrollView` 子项的委托类。通过 `ListWheelChildBuilderDelegate`,我们可以灵活地定义每个子项的内容,从而实现更加复杂和个性化的选择器。


子项通过container 来构建

```dart
/// 构建子项  
Widget _buildItem(String value, bool isSelected) {
  return Container(
    alignment: Alignment.center,
    child: Text(
      value,
      style: TextStyle(
        color: isSelected
            ? const Color.fromRGBO(28, 7, 57, 0.80)
            : const Color.fromRGBO(28, 7, 57, 0.50),
        fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
      ),
    ),
  );
}

生成时间

/// 生成年
List<int> generateYears() {
  return List.generate(100, (index) => DateTime.now().year - index);
}

/// 生成月
List<int> generateMonths() {
  return List.generate(12, (index) => index + 1);
}

/// 生成天  
List<int> generateDays() {
  int daysInMonth = DateTime(selectedYear, selectedMonth + 1, 0).day;
  return List.generate(daysInMonth, (index) => index + 1);
}
// ListWheelChildBuilderDelegate 构造参数(摘自 Flutter 框架源码,仅查阅):
//   required builder(Widget Function(BuildContext, int),子项构建函数),
//   childCount(子项数量)

final yearController = FixedExtentScrollController(); // 年控制器 
final monthController = FixedExtentScrollController(); // 月控制器
final dayController = FixedExtentScrollController(); // 天控制器

/// 生成天
Widget _buildDayPicker() {
  return _buildPicker(
    controller: controller.dayController,
    itemCount: controller.generateDays().length,
    itemBuilder: (context, index) {
      final day = controller.generateDays()[index];
      return _buildItem(day.toString(), day == controller.selectedDay);
    },
    onSelectedItemChanged: (index) {
      controller.selectedDay = controller.generateDays()[index];
      controller.onDateChanged();
    },
  );
}

/// 生成年
Widget _buildYearPicker() {
  return _buildPicker(
    controller: controller.yearController,
    itemCount: controller.generateYears().length,
  );
}


/// 生成边框
Widget _buildPickerBorder({double? top, double? bottom}) {
return Positioned(
  top: top,
  bottom: bottom,
  left: 0,
  right: 0,
  child: Container(
    height: .5.w,
    color: const Color.fromRGBO(202, 177, 255, 1),
  ),
);
}
```text
## 关键布局代码

```dart
  Widget _buildDatePicker() {
    return SizedBox(
      height: 130,
      child: Stack(
        children: [
          _buildPickerBorder(top: 40),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              Expanded(child: _buildYearPicker()),
              Expanded(child: _buildMonthPicker()),
              Expanded(child: _buildDayPicker()),
            ],
          ),
          _buildPickerBorder(bottom: 40),
        ],
      ),
    );
  }

实战案例

这个场景我还没在项目里自研过时间选择器,但”造轮子还是用官方”这个 trade-off 我踩过类似的。正文用 ListWheelScrollView 从零搭了一个年月日滚轮,效果确实可定制,但代价是联动逻辑全得自己写:选了 2 月,天数滚轮要重算(正文里用 DateTime(year, month + 1, 0).day 取当月天数这个技巧是对的),FixedExtentScrollController 的初始 index 要对齐选中值,无障碍和本地化也得自己补。而官方的 CupertinoDatePicker 这些全处理好了,还跟 iOS 原生体验一致。我的决策经验是:除非设计稿的滚轮样式跟原生差了十万八千里,否则优先用 CupertinoDatePicker(或 Material 的 showDatePicker),把时间花在业务上;真要自研,就把”年月日联动、闰年、初始位置”三个用例先写成测试再动手。

参考:Flutter 官方文档 CupertinoDatePicker、ListWheelScrollView 章节


分享这篇文章:


相关文章

上一篇
Flutter 性能测试详解
下一篇
Flutter 应用打包发布详解