550 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Dart
		
	
	
			
		
		
	
	
			550 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Dart
		
	
	
| import 'package:flutter/cupertino.dart';
 | ||
| import 'package:flutter/material.dart';
 | ||
| 
 | ||
| /// 调用示例:
 | ||
| /// DateTime? picked = await BottomDateTimePicker.showDate(
 | ||
| ///   context,
 | ||
| ///   mode: BottomPickerMode.date, // 或 BottomPickerMode.dateTime(默认)或 BottomPickerMode.dateTimeWithSeconds
 | ||
| ///   allowFuture: true,
 | ||
| ///   allowPast: false, // 是否允许选择过去(false 表示只能选择现在或未来)
 | ||
| ///   minTimeStr: '2025-08-20 08:30:45',
 | ||
| /// );
 | ||
| /// if (picked != null) {
 | ||
| ///   print('用户选择的时间:$picked');
 | ||
| /// }
 | ||
| enum BottomPickerMode {
 | ||
|   dateTime, // 底部弹窗 年月日时分
 | ||
|   date, // 中间弹窗日历
 | ||
|   dateTimeWithSeconds, // 底部弹窗 年月日时分秒
 | ||
| }
 | ||
| 
 | ||
| class BottomDateTimePicker {
 | ||
|   static Future<DateTime?> showDate(
 | ||
|     BuildContext context, {
 | ||
|     bool allowFuture = true,
 | ||
|     bool allowPast = true, // 是否允许选择过去(默认允许)
 | ||
|     String? minTimeStr, // 可选:'yyyy-MM-dd HH:mm:ss'
 | ||
|     BottomPickerMode mode = BottomPickerMode.dateTime,
 | ||
|   }) {
 | ||
|     return showModalBottomSheet<DateTime>(
 | ||
|       context: context,
 | ||
|       backgroundColor: Colors.white,
 | ||
|       isScrollControlled: true,
 | ||
|       shape: const RoundedRectangleBorder(
 | ||
|         borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
 | ||
|       ),
 | ||
|       builder:
 | ||
|           (_) => _InlineDateTimePickerContent(
 | ||
|             allowFuture: allowFuture,
 | ||
|             allowPast: allowPast,
 | ||
|             minTimeStr: minTimeStr,
 | ||
|             mode: mode,
 | ||
|           ),
 | ||
|     );
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| class _InlineDateTimePickerContent extends StatefulWidget {
 | ||
|   final bool allowFuture;
 | ||
|   final bool allowPast;
 | ||
|   final String? minTimeStr;
 | ||
|   final BottomPickerMode mode;
 | ||
| 
 | ||
|   const _InlineDateTimePickerContent({
 | ||
|     Key? key,
 | ||
|     this.allowFuture = true,
 | ||
|     this.allowPast = true,
 | ||
|     this.minTimeStr,
 | ||
|     this.mode = BottomPickerMode.dateTime,
 | ||
|   }) : super(key: key);
 | ||
| 
 | ||
|   @override
 | ||
|   State<_InlineDateTimePickerContent> createState() =>
 | ||
|       _InlineDateTimePickerContentState();
 | ||
| }
 | ||
| 
 | ||
| class _InlineDateTimePickerContentState
 | ||
|     extends State<_InlineDateTimePickerContent> {
 | ||
|   // 数据源
 | ||
|   final List<int> years = List.generate(101, (i) => 1970 + i);
 | ||
|   final List<int> months = List.generate(12, (i) => i + 1);
 | ||
|   final List<int> hours = List.generate(24, (i) => i);
 | ||
|   final List<int> minutes = List.generate(60, (i) => i);
 | ||
|   final List<int> seconds = List.generate(60, (i) => i); // 新增秒数据源
 | ||
| 
 | ||
|   // 动态天数列表(根据年月变化)
 | ||
|   late List<int> days;
 | ||
| 
 | ||
|   // Controllers
 | ||
|   late FixedExtentScrollController yearCtrl;
 | ||
|   late FixedExtentScrollController monthCtrl;
 | ||
|   late FixedExtentScrollController dayCtrl;
 | ||
|   late FixedExtentScrollController hourCtrl;
 | ||
|   late FixedExtentScrollController minuteCtrl;
 | ||
|   late FixedExtentScrollController secondCtrl; // 新增秒控制器
 | ||
| 
 | ||
|   // 当前选中值
 | ||
|   late int selectedYear;
 | ||
|   late int selectedMonth;
 | ||
|   late int selectedDay;
 | ||
|   late int selectedHour;
 | ||
|   late int selectedMinute;
 | ||
|   late int selectedSecond; // 新增秒选中值
 | ||
| 
 | ||
|   DateTime? _minTime; // 解析后的最小允许时间(如果有)
 | ||
| 
 | ||
|   @override
 | ||
|   void initState() {
 | ||
|     super.initState();
 | ||
| 
 | ||
|     // 解析 minTimeStr(若提供)
 | ||
|     _minTime = _parseMinTime(widget.minTimeStr);
 | ||
| 
 | ||
|     // 初始时间:取 now 与 _minTime 的较大者(但要考虑 allowPast)
 | ||
|     final now = DateTime.now();
 | ||
|     DateTime initial = now;
 | ||
| 
 | ||
|     // 如果指定了最小时间并且比 now 晚,则以最小时间为初始
 | ||
|     if (_minTime != null && _minTime!.isAfter(initial)) {
 | ||
|       initial = _minTime!;
 | ||
|     }
 | ||
| 
 | ||
|     // 如果不允许选择过去,则确保 initial 至少为 now(或当天的 00:00,取决于模式)
 | ||
|     if (!widget.allowPast) {
 | ||
|       if (widget.mode == BottomPickerMode.date) {
 | ||
|         final today = DateTime(now.year, now.month, now.day);
 | ||
|         if (initial.isBefore(today)) initial = today;
 | ||
|       } else {
 | ||
|         if (initial.isBefore(now)) initial = now;
 | ||
|       }
 | ||
|     }
 | ||
| 
 | ||
|     // 根据模式调整初始值
 | ||
|     if (widget.mode == BottomPickerMode.date) {
 | ||
|       initial = DateTime(initial.year, initial.month, initial.day);
 | ||
|     } else if (widget.mode == BottomPickerMode.dateTime) {
 | ||
|       initial = DateTime(
 | ||
|         initial.year,
 | ||
|         initial.month,
 | ||
|         initial.day,
 | ||
|         initial.hour,
 | ||
|         initial.minute,
 | ||
|       );
 | ||
|     }
 | ||
|     // dateTimeWithSeconds 模式保持完整的时间
 | ||
| 
 | ||
|     selectedYear = initial.year;
 | ||
|     selectedMonth = initial.month;
 | ||
|     selectedDay = initial.day;
 | ||
|     selectedHour = initial.hour;
 | ||
|     selectedMinute = initial.minute;
 | ||
|     selectedSecond = initial.second;
 | ||
| 
 | ||
|     // 初始化天数列表
 | ||
|     days = _getDaysInMonth(selectedYear, selectedMonth);
 | ||
| 
 | ||
|     // controllers 初始项索引需在范围内
 | ||
|     yearCtrl = FixedExtentScrollController(
 | ||
|       initialItem: years.indexOf(selectedYear).clamp(0, years.length - 1),
 | ||
|     );
 | ||
|     monthCtrl = FixedExtentScrollController(
 | ||
|       initialItem: (selectedMonth - 1).clamp(0, months.length - 1),
 | ||
|     );
 | ||
|     dayCtrl = FixedExtentScrollController(
 | ||
|       initialItem: (selectedDay - 1).clamp(0, days.length - 1),
 | ||
|     );
 | ||
|     hourCtrl = FixedExtentScrollController(
 | ||
|       initialItem: selectedHour.clamp(0, hours.length - 1),
 | ||
|     );
 | ||
|     minuteCtrl = FixedExtentScrollController(
 | ||
|       initialItem: selectedMinute.clamp(0, minutes.length - 1),
 | ||
|     );
 | ||
|     secondCtrl = FixedExtentScrollController(
 | ||
|       // 初始化秒控制器
 | ||
|       initialItem: selectedSecond.clamp(0, seconds.length - 1),
 | ||
|     );
 | ||
| 
 | ||
|     // 确保初始选择满足约束(例如 minTime 或禁止未来/禁止过去)
 | ||
|     WidgetsBinding.instance.addPostFrameCallback((_) {
 | ||
|       _enforceConstraintsAndUpdateControllers();
 | ||
|     });
 | ||
|   }
 | ||
| 
 | ||
|   // 根据年月获取当月天数
 | ||
|   List<int> _getDaysInMonth(int year, int month) {
 | ||
|     final lastDay = DateUtils.getDaysInMonth(year, month);
 | ||
|     return List.generate(lastDay, (i) => i + 1);
 | ||
|   }
 | ||
| 
 | ||
|   // 解析 'yyyy-MM-dd HH:mm:ss' 返回 DateTime 或 null
 | ||
|   DateTime? _parseMinTime(String? s) {
 | ||
|     if (s == null || s.trim().isEmpty) return null;
 | ||
|     try {
 | ||
|       final trimmed = s.trim();
 | ||
|       final parts = trimmed.split(' ');
 | ||
|       final dateParts = parts[0].split('-').map((e) => int.parse(e)).toList();
 | ||
|       final timeParts =
 | ||
|           (parts.length > 1)
 | ||
|               ? parts[1].split(':').map((e) => int.parse(e)).toList()
 | ||
|               : [0, 0, 0];
 | ||
|       final year = dateParts[0];
 | ||
|       final month = dateParts[1];
 | ||
|       final day = dateParts[2];
 | ||
|       final hour = (timeParts.isNotEmpty) ? timeParts[0] : 0;
 | ||
|       final minute = (timeParts.length > 1) ? timeParts[1] : 0;
 | ||
|       final second = (timeParts.length > 2) ? timeParts[2] : 0;
 | ||
|       return DateTime(year, month, day, hour, minute, second);
 | ||
|     } catch (e) {
 | ||
|       debugPrint('parseMinTime failed for "$s": $e');
 | ||
|       return null;
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   // 更新天数列表并调整选中日期
 | ||
|   void _updateDays({bool jumpDay = true}) {
 | ||
|     final newDays = _getDaysInMonth(selectedYear, selectedMonth);
 | ||
|     final isDayValid = selectedDay <= newDays.length;
 | ||
| 
 | ||
|     setState(() {
 | ||
|       days = newDays;
 | ||
|       if (!isDayValid) {
 | ||
|         selectedDay = newDays.last;
 | ||
|         if (jumpDay) dayCtrl.jumpToItem(selectedDay - 1);
 | ||
|       }
 | ||
|     });
 | ||
|   }
 | ||
| 
 | ||
|   // 检查并限制时间(模式感知),支持 allowPast 与 allowFuture
 | ||
|   void _enforceConstraintsAndUpdateControllers() {
 | ||
|     final now = DateTime.now();
 | ||
|     final isDateOnly = widget.mode == BottomPickerMode.date;
 | ||
|     final isDateTimeOnly = widget.mode == BottomPickerMode.dateTime;
 | ||
| 
 | ||
|     DateTime picked;
 | ||
|     if (isDateOnly) {
 | ||
|       picked = DateTime(selectedYear, selectedMonth, selectedDay);
 | ||
|     } else if (isDateTimeOnly) {
 | ||
|       picked = DateTime(
 | ||
|         selectedYear,
 | ||
|         selectedMonth,
 | ||
|         selectedDay,
 | ||
|         selectedHour,
 | ||
|         selectedMinute,
 | ||
|       );
 | ||
|     } else {
 | ||
|       picked = DateTime(
 | ||
|         selectedYear,
 | ||
|         selectedMonth,
 | ||
|         selectedDay,
 | ||
|         selectedHour,
 | ||
|         selectedMinute,
 | ||
|         selectedSecond,
 | ||
|       );
 | ||
|     }
 | ||
| 
 | ||
|     // 处理最小时间约束:结合 _minTime 与 allowPast
 | ||
|     DateTime? minRef;
 | ||
|     if (!widget.allowPast) {
 | ||
|       // 不允许选择过去:最小时间至少为 now(或当天 00:00)
 | ||
|       if (isDateOnly) {
 | ||
|         minRef = DateTime(now.year, now.month, now.day);
 | ||
|       } else if (isDateTimeOnly) {
 | ||
|         minRef = DateTime(now.year, now.month, now.day, now.hour, now.minute);
 | ||
|       } else {
 | ||
|         minRef = now;
 | ||
|       }
 | ||
|       // 如果用户也指定了 _minTime 且比 now 晚,则以 _minTime 为准
 | ||
|       if (_minTime != null && _minTime!.isAfter(minRef)) {
 | ||
|         minRef = _minTime;
 | ||
|         // 根据模式调整精度
 | ||
|         if (isDateOnly) {
 | ||
|           minRef = DateTime(minRef!.year, minRef.month, minRef.day);
 | ||
|         } else if (isDateTimeOnly) {
 | ||
|           minRef = DateTime(
 | ||
|             minRef!.year,
 | ||
|             minRef.month,
 | ||
|             minRef.day,
 | ||
|             minRef.hour,
 | ||
|             minRef.minute,
 | ||
|           );
 | ||
|         }
 | ||
|       }
 | ||
|     } else if (_minTime != null) {
 | ||
|       // 允许选择过去,但若指定了 _minTime,则以 _minTime 为最小参考
 | ||
|       minRef = _minTime;
 | ||
|       // 根据模式调整精度
 | ||
|       if (isDateOnly) {
 | ||
|         minRef = DateTime(minRef!.year, minRef.month, minRef.day);
 | ||
|       } else if (isDateTimeOnly) {
 | ||
|         minRef = DateTime(
 | ||
|           minRef!.year,
 | ||
|           minRef.month,
 | ||
|           minRef.day,
 | ||
|           minRef.hour,
 | ||
|           minRef.minute,
 | ||
|         );
 | ||
|       }
 | ||
|     }
 | ||
| 
 | ||
|     if (minRef != null && picked.isBefore(minRef)) {
 | ||
|       // 把选中项调整为 minRef
 | ||
|       selectedYear = minRef.year;
 | ||
|       selectedMonth = minRef.month;
 | ||
|       selectedDay = minRef.day;
 | ||
|       if (!isDateOnly) {
 | ||
|         selectedHour = minRef.hour;
 | ||
|         selectedMinute = minRef.minute;
 | ||
|         if (!isDateTimeOnly) {
 | ||
|           selectedSecond = minRef.second;
 | ||
|         } else {
 | ||
|           selectedSecond = 0;
 | ||
|         }
 | ||
|       } else {
 | ||
|         selectedHour = 0;
 | ||
|         selectedMinute = 0;
 | ||
|         selectedSecond = 0;
 | ||
|       }
 | ||
| 
 | ||
|       _updateDays(jumpDay: false);
 | ||
|       yearCtrl.jumpToItem(years.indexOf(selectedYear));
 | ||
|       monthCtrl.jumpToItem(selectedMonth - 1);
 | ||
|       dayCtrl.jumpToItem(selectedDay - 1);
 | ||
|       if (!isDateOnly) {
 | ||
|         hourCtrl.jumpToItem(selectedHour);
 | ||
|         minuteCtrl.jumpToItem(selectedMinute);
 | ||
|         if (!isDateTimeOnly) {
 | ||
|           secondCtrl.jumpToItem(selectedSecond);
 | ||
|         }
 | ||
|       }
 | ||
|       return;
 | ||
|     }
 | ||
| 
 | ||
|     // 处理禁止选择未来(当 allowFuture == false)
 | ||
|     if (!widget.allowFuture) {
 | ||
|       DateTime nowRef;
 | ||
|       if (isDateOnly) {
 | ||
|         nowRef = DateTime(now.year, now.month, now.day);
 | ||
|       } else if (isDateTimeOnly) {
 | ||
|         nowRef = DateTime(now.year, now.month, now.day, now.hour, now.minute);
 | ||
|       } else {
 | ||
|         nowRef = now;
 | ||
|       }
 | ||
| 
 | ||
|       if (picked.isAfter(nowRef)) {
 | ||
|         selectedYear = nowRef.year;
 | ||
|         selectedMonth = nowRef.month;
 | ||
|         selectedDay = nowRef.day;
 | ||
|         if (!isDateOnly) {
 | ||
|           selectedHour = nowRef.hour;
 | ||
|           selectedMinute = nowRef.minute;
 | ||
|           if (!isDateTimeOnly) {
 | ||
|             selectedSecond = nowRef.second;
 | ||
|           } else {
 | ||
|             selectedSecond = 0;
 | ||
|           }
 | ||
|         } else {
 | ||
|           selectedHour = 0;
 | ||
|           selectedMinute = 0;
 | ||
|           selectedSecond = 0;
 | ||
|         }
 | ||
| 
 | ||
|         _updateDays(jumpDay: false);
 | ||
|         yearCtrl.jumpToItem(years.indexOf(selectedYear));
 | ||
|         monthCtrl.jumpToItem(selectedMonth - 1);
 | ||
|         dayCtrl.jumpToItem(selectedDay - 1);
 | ||
|         if (!isDateOnly) {
 | ||
|           hourCtrl.jumpToItem(selectedHour);
 | ||
|           minuteCtrl.jumpToItem(selectedMinute);
 | ||
|           if (!isDateTimeOnly) {
 | ||
|             secondCtrl.jumpToItem(selectedSecond);
 | ||
|           }
 | ||
|         }
 | ||
|         return;
 | ||
|       }
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   @override
 | ||
|   void dispose() {
 | ||
|     yearCtrl.dispose();
 | ||
|     monthCtrl.dispose();
 | ||
|     dayCtrl.dispose();
 | ||
|     hourCtrl.dispose();
 | ||
|     minuteCtrl.dispose();
 | ||
|     secondCtrl.dispose(); // 释放秒控制器
 | ||
|     super.dispose();
 | ||
|   }
 | ||
| 
 | ||
|   @override
 | ||
|   Widget build(BuildContext context) {
 | ||
|     final isDateOnly = widget.mode == BottomPickerMode.date;
 | ||
|     final isDateTimeOnly = widget.mode == BottomPickerMode.dateTime;
 | ||
| 
 | ||
|     // 根据模式计算高度
 | ||
|     final height = isDateOnly ? 280 : (isDateTimeOnly ? 330 : 380);
 | ||
| 
 | ||
|     return SizedBox(
 | ||
|       height: height.toDouble(),
 | ||
|       child: Column(
 | ||
|         children: [
 | ||
|           // 顶部按钮
 | ||
|           Padding(
 | ||
|             padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
 | ||
|             child: Row(
 | ||
|               mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | ||
|               children: [
 | ||
|                 TextButton(
 | ||
|                   onPressed: () => Navigator.of(context).pop(),
 | ||
|                   child: const Text("取消", style: TextStyle(color: Colors.grey)),
 | ||
|                 ),
 | ||
|                 TextButton(
 | ||
|                   onPressed: () {
 | ||
|                     DateTime result;
 | ||
|                     if (isDateOnly) {
 | ||
|                       result = DateTime(
 | ||
|                         selectedYear,
 | ||
|                         selectedMonth,
 | ||
|                         selectedDay,
 | ||
|                       );
 | ||
|                     } else if (isDateTimeOnly) {
 | ||
|                       result = DateTime(
 | ||
|                         selectedYear,
 | ||
|                         selectedMonth,
 | ||
|                         selectedDay,
 | ||
|                         selectedHour,
 | ||
|                         selectedMinute,
 | ||
|                       );
 | ||
|                     } else {
 | ||
|                       result = DateTime(
 | ||
|                         selectedYear,
 | ||
|                         selectedMonth,
 | ||
|                         selectedDay,
 | ||
|                         selectedHour,
 | ||
|                         selectedMinute,
 | ||
|                         selectedSecond,
 | ||
|                       );
 | ||
|                     }
 | ||
|                     Navigator.of(context).pop(result);
 | ||
|                   },
 | ||
|                   child: const Text("确定", style: TextStyle(color: Colors.blue)),
 | ||
|                 ),
 | ||
|               ],
 | ||
|             ),
 | ||
|           ),
 | ||
|           const Divider(height: 1),
 | ||
| 
 | ||
|           // 可见的滚轮列
 | ||
|           Expanded(
 | ||
|             child: Row(
 | ||
|               children: [
 | ||
|                 // 年
 | ||
|                 _buildPicker(
 | ||
|                   controller: yearCtrl,
 | ||
|                   items: years.map((e) => e.toString()).toList(),
 | ||
|                   onSelected: (idx) {
 | ||
|                     setState(() {
 | ||
|                       selectedYear = years[idx];
 | ||
|                       _updateDays();
 | ||
|                       _enforceConstraintsAndUpdateControllers();
 | ||
|                     });
 | ||
|                   },
 | ||
|                 ),
 | ||
| 
 | ||
|                 // 月
 | ||
|                 _buildPicker(
 | ||
|                   controller: monthCtrl,
 | ||
|                   items:
 | ||
|                       months.map((e) => e.toString().padLeft(2, '0')).toList(),
 | ||
|                   onSelected: (idx) {
 | ||
|                     setState(() {
 | ||
|                       selectedMonth = months[idx];
 | ||
|                       _updateDays();
 | ||
|                       _enforceConstraintsAndUpdateControllers();
 | ||
|                     });
 | ||
|                   },
 | ||
|                 ),
 | ||
| 
 | ||
|                 // 日
 | ||
|                 _buildPicker(
 | ||
|                   controller: dayCtrl,
 | ||
|                   items: days.map((e) => e.toString().padLeft(2, '0')).toList(),
 | ||
|                   onSelected: (idx) {
 | ||
|                     setState(() {
 | ||
|                       final safeIdx = idx.clamp(0, days.length - 1);
 | ||
|                       selectedDay = days[safeIdx];
 | ||
|                       _enforceConstraintsAndUpdateControllers();
 | ||
|                     });
 | ||
|                   },
 | ||
|                 ),
 | ||
| 
 | ||
|                 // 若不是 dateOnly,则显示时分两列
 | ||
|                 if (!isDateOnly)
 | ||
|                   _buildPicker(
 | ||
|                     controller: hourCtrl,
 | ||
|                     items:
 | ||
|                         hours.map((e) => e.toString().padLeft(2, '0')).toList(),
 | ||
|                     onSelected: (idx) {
 | ||
|                       setState(() {
 | ||
|                         selectedHour = hours[idx];
 | ||
|                         _enforceConstraintsAndUpdateControllers();
 | ||
|                       });
 | ||
|                     },
 | ||
|                   ),
 | ||
| 
 | ||
|                 if (!isDateOnly)
 | ||
|                   _buildPicker(
 | ||
|                     controller: minuteCtrl,
 | ||
|                     items:
 | ||
|                         minutes
 | ||
|                             .map((e) => e.toString().padLeft(2, '0'))
 | ||
|                             .toList(),
 | ||
|                     onSelected: (idx) {
 | ||
|                       setState(() {
 | ||
|                         selectedMinute = minutes[idx];
 | ||
|                         _enforceConstraintsAndUpdateControllers();
 | ||
|                       });
 | ||
|                     },
 | ||
|                   ),
 | ||
| 
 | ||
|                 // 如果是 dateTimeWithSeconds 模式,显示秒列
 | ||
|                 if (widget.mode == BottomPickerMode.dateTimeWithSeconds)
 | ||
|                   _buildPicker(
 | ||
|                     controller: secondCtrl,
 | ||
|                     items:
 | ||
|                         seconds
 | ||
|                             .map((e) => e.toString().padLeft(2, '0'))
 | ||
|                             .toList(),
 | ||
|                     onSelected: (idx) {
 | ||
|                       setState(() {
 | ||
|                         selectedSecond = seconds[idx];
 | ||
|                         _enforceConstraintsAndUpdateControllers();
 | ||
|                       });
 | ||
|                     },
 | ||
|                   ),
 | ||
|               ],
 | ||
|             ),
 | ||
|           ),
 | ||
|         ],
 | ||
|       ),
 | ||
|     );
 | ||
|   }
 | ||
| 
 | ||
|   Widget _buildPicker({
 | ||
|     required FixedExtentScrollController controller,
 | ||
|     required List<String> items,
 | ||
|     required ValueChanged<int> onSelected,
 | ||
|   }) {
 | ||
|     return Expanded(
 | ||
|       child: CupertinoPicker.builder(
 | ||
|         scrollController: controller,
 | ||
|         itemExtent: 40,
 | ||
|         childCount: items.length,
 | ||
|         onSelectedItemChanged: onSelected,
 | ||
|         itemBuilder: (context, index) {
 | ||
|           return Center(child: Text(items[index]));
 | ||
|         },
 | ||
|       ),
 | ||
|     );
 | ||
|   }
 | ||
| }
 |