在 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 章节
