Flutter 组件抽取:日期(DatePicker)、时间(TimePicker)弹窗选择器【仿照】

news/2024/12/2 22:52:50/

简介

仿照《Flutter 仿ios自定义一个DatePicker》实行的日期弹窗选择器(DatePicker)、时间弹窗选择器(TimePicker)

效果

在这里插入图片描述

范例

class _TestPageState extends State<TestPage> {void initState() {super.initState();}Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text('Picker')),body: SingleChildScrollView(child: Column(children: [GestureDetector(child: Container(alignment: Alignment.center,width: 160,height: 60,child: const Text('日期选择(年月日)'),),onTap: () {DatePicker.show(context,startDate: DateTime(2022, 2, 2),selectedDate: DateTime(2023, 3, 3),endDate: DateTime(2025, 5, 5),onSelected: (date) {MsgUtil.toast(date.toString());},);},),GestureDetector(child: Container(alignment: Alignment.center,width: 160,height: 60,child: const Text('日期选择(年月)'),),onTap: () {DatePicker.show(context,hideDay: true,startDate: DateTime(2022, 2),selectedDate: DateTime(2023, 3),endDate: DateTime(2025, 5),onSelected: (date) {MsgUtil.toast(date.toString());},);},),GestureDetector(child: Container(alignment: Alignment.center,width: 160,height: 60,child: const Text('时间选择(时分秒)'),),onTap: () {TimePicker.show(context,startTime: TimeData(11, 11, 11),selectedTime: TimeData(15, 15, 15),endTime: TimeData(22, 22, 22),onSelected: (time) {MsgUtil.toast(time.toString());},);},),GestureDetector(child: Container(alignment: Alignment.center,width: 160,height: 60,child: const Text('时间选择(时分)'),),onTap: () {TimePicker.show(context,hideSecond: true,startTime: TimeData(11, 11),selectedTime: TimeData(15, 15),endTime: TimeData(22, 22),onSelected: (time) {MsgUtil.toast(time.toString());},);},),],),),);}
}

说明(DatePicker)

1、支持选中日期(selectedDate)、开始日期(startDate)、结束日期(endDate)的配置
2、支持“年月日”的选择,也支持“年月”的选择

代码(DatePicker)

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';typedef OnSelected = Function(DateTime date);class DatePicker extends StatefulWidget {static void show(BuildContext context, {DateTime? startDate,DateTime? endDate,DateTime? selectedDate,bool hideDay = false,Function()? onCancel,required OnSelected onSelected,}) async {showModalBottomSheet(context: context,backgroundColor: Colors.transparent,builder: (BuildContext context) {return ClipRRect(borderRadius: const BorderRadius.only(topLeft: Radius.circular(8),topRight: Radius.circular(8),),child: DatePicker._(selectedDate: selectedDate,startDate: startDate,endDate: endDate,onSelected: onSelected,hideDay: hideDay,),);},).then((value) => onCancel?.call());}const DatePicker._({this.selectedDate,this.startDate,this.endDate,this.hideDay = false,required this.onSelected,});final DateTime? selectedDate;final DateTime? startDate;final DateTime? endDate;final bool hideDay;final OnSelected onSelected;State createState() => _DatePickerState();
}class _DatePickerState extends State<DatePicker> {late FixedExtentScrollController yearScrollController;late FixedExtentScrollController monthScrollController;late FixedExtentScrollController dayScrollController;List<String> yearList = []; // 年数组List<String> monthList = []; // 月数组List<String> dayList = []; // 天数组int yearIndex = 0; // 年的索引int monthIndex = 0; // 月的索引int dayIndex = 0; //天的索引late DateTime startDate;late DateTime endDate;late DateTime selectedDate;final double itemExtent = 44;/// 初始化数据void initData() {// 初始化年份数for (int i = startDate.year; i <= endDate.year; i++) {yearList.add(i.toString());}int selectYear = selectedDate.year;int selectMonth = selectedDate.month;// 初始化月份数monthList = getMonthList(selectYear);// 初始化天数dayList = getDayList(selectYear, selectMonth);// 初始化时间索引final List uniqueYearList = Set.from(yearList).toList();final List uniqueMonthList = Set.from(monthList).toList();final List uniqueDayList = Set.from(dayList).toList();// 获取索引setState(() {yearIndex = uniqueYearList.indexOf("${selectedDate.year}");monthIndex = uniqueMonthList.indexOf("${selectedDate.month}");dayIndex = uniqueDayList.indexOf("${selectedDate.day}");});// 设置Picker初始值yearScrollController = FixedExtentScrollController(initialItem: yearIndex);monthScrollController = FixedExtentScrollController(initialItem: monthIndex);dayScrollController = FixedExtentScrollController(initialItem: dayIndex);}List<String> getMonthList(int selectYear) {List<String> monthList = [];if (selectYear == startDate.year) {for (int i = startDate.month; i <= 12; i++) {monthList.add(i.toString());}} else if (selectYear == endDate.year) {for (int i = 1; i <= endDate.month; i++) {monthList.add(i.toString());}} else {for (int i = 1; i <= 12; i++) {monthList.add(i.toString());}}return monthList;}List<String> getDayList(int selectYear, int selectMonth) {List<String> dayList = [];int days = getDayCount(selectYear, selectMonth);if (selectYear == startDate.year && selectMonth == startDate.month) {for (int i = startDate.day; i <= days; i++) {dayList.add(i.toString());}} else if (selectYear == endDate.year && selectMonth == endDate.month) {for (int i = 1; i <= endDate.day; i++) {dayList.add(i.toString());}} else {for (int i = 1; i <= days; i++) {dayList.add(i.toString());}}return dayList;}int getDayCount(int year, int month) {int dayCount = DateTime(year, month + 1, 0).day;return dayCount;}/// 选中年月后更新天void updateDayList() {int year = int.parse(yearList[yearIndex]);int month = int.parse(monthList[monthIndex]);setState(() {dayIndex = 0;dayList = getDayList(year, month);if (dayScrollController.positions.isNotEmpty) {dayScrollController.jumpTo(0);}});}/// 选中年后更新月void updateMonthList() {int selectYear = int.parse(yearList[yearIndex]);setState(() {monthIndex = 0;monthList = getMonthList(selectYear);if (monthScrollController.positions.isNotEmpty) {monthScrollController.jumpTo(0);}});}void initState() {super.initState();startDate = widget.startDate ?? DateTime(1970, 1, 1);endDate = widget.endDate ?? DateTime(2099, 1, 1);selectedDate = widget.selectedDate ?? DateTime.now();if (endDate.difference(startDate).inSeconds < 0) {endDate = startDate;}if (selectedDate.difference(startDate).inSeconds < 0) {selectedDate = startDate;}if (selectedDate.difference(endDate).inSeconds > 0) {selectedDate = endDate;}initData();}void dispose() {yearScrollController.dispose();monthScrollController.dispose();dayScrollController.dispose();super.dispose();}Widget build(BuildContext context) {return Container(color: Colors.white,child: Column(crossAxisAlignment: CrossAxisAlignment.start,mainAxisSize: MainAxisSize.min,children: <Widget>[Container(color: Colors.white,width: double.maxFinite,height: 200,child: Stack(alignment: AlignmentDirectional.center,children: [Container(width: MediaQuery.of(context).size.width - 32,height: itemExtent - 8,decoration: BoxDecoration(color: const Color(0xFFF1F1F1),borderRadius: BorderRadius.circular(12),),),Positioned(left: 20,right: 20,top: 0,bottom: 0,child: Row(mainAxisSize: MainAxisSize.min,children: <Widget>[Expanded(child: yearPickerView()),Expanded(child: monthPickerView()),widget.hideDay? const SizedBox(): Expanded(child: dayPickerView()),],),),],),),Container(color: Colors.white,height: 68,padding: const EdgeInsets.only(left: 16, right: 16),child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween,children: <Widget>[Expanded(child: TextButton(child: const Text('取 消'),onPressed: () {Navigator.pop(context, false);},),),const SizedBox(width: 12),Expanded(child: TextButton(child: const Text('确 定'),onPressed: () {Navigator.pop(context, true);widget.onSelected.call(DateTime(int.parse(yearList[yearIndex]),int.parse(monthList[monthIndex]),int.parse(dayList[dayIndex]),));},),),],),),SizedBox(height: MediaQuery.of(context).padding.bottom),],),);}/// 年PickerWidget yearPickerView() {return buildPickerBorder(child: CupertinoPicker(scrollController: yearScrollController,looping: false,selectionOverlay: const Center(),onSelectedItemChanged: (index) {setState(() {yearIndex = index;});updateMonthList();updateDayList();},itemExtent: itemExtent,children: buildYearWidget(),),);}/// 月PickerWidget monthPickerView() {return buildPickerBorder(child: CupertinoPicker(scrollController: monthScrollController,looping: false,selectionOverlay: const Center(),onSelectedItemChanged: (index) {setState(() {monthIndex = index;});updateDayList();},itemExtent: itemExtent,children: buildMonthWidget(),),);}/// 日PickerWidget dayPickerView() {return buildPickerBorder(child: CupertinoPicker(scrollController: dayScrollController,looping: false,selectionOverlay: const Center(),onSelectedItemChanged: (index) {setState(() {dayIndex = index;});},itemExtent: itemExtent,children: buildDayWidget(),),);}/// 年WidgetList<Widget> buildYearWidget() {List<Widget> widgets = [];for (var i = 0; i < yearList.length; i++) {widgets.add(Center(child: Text('${yearList[i]}年',style: getTextStyle(i == yearIndex),maxLines: 1,),),);}return widgets;}/// 月WidgetList<Widget> buildMonthWidget() {List<Widget> widgets = [];for (var i = 0; i < monthList.length; i++) {widgets.add(Center(child: Text(// monthList[i].padLeft(2, '0')'${monthList[i]}月',style: getTextStyle(i == monthIndex),maxLines: 1,),),);}return widgets;}/// 日WidgetList<Widget> buildDayWidget() {List<Widget> widgets = [];for (var i = 0; i < dayList.length; i++) {widgets.add(Center(child: Text(// dayList[i].padLeft(2, '0')'${dayList[i]}日',style: getTextStyle(i == dayIndex),maxLines: 1,),),);}return widgets;}TextStyle getTextStyle(bool bold) {return TextStyle(color: Colors.black,fontSize: 20,height: 1,fontWeight: bold ? FontWeight.w600 : FontWeight.w400,);}Widget buildPickerBorder({required Widget child}) {return Stack(alignment: AlignmentDirectional.center,children: [/*Container(width: 90,height: itemExtent - 8,decoration: BoxDecoration(color: const Color(0xffEFEFF0),borderRadius: BorderRadius.circular(10),),),*/child,],);}
}

说明(TimePicker)

1、支持选中时间(selectedtTime)、开始时间(starttTime)、结束时间(endtTime)的配置
2、支持“时分秒”的选择,也支持“时分”的选择
3、自定义时间数据类(TimeData)

代码(TimePicker)

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';typedef OnSelected = Function(TimeData time);
typedef PickerBuilder = Widget Function(BuildContext context);class TimeData {final int hour;final int minute;final int second;TimeData(this.hour, [this.minute = 0, this.second = 0]): assert(hour >= 0 && hour <= 23),assert(minute >= 0 && minute <= 59),assert(second >= 0 && second <= 59);factory TimeData.now() {var now = DateTime.now();return TimeData(now.hour, now.minute, now.second);}bool precede(TimeData other) {return (hour < other.hour) ||(hour == other.hour && minute < other.minute) ||(hour == other.hour && minute == other.minute && second < other.second);}String toString() {return '$hour:$minute:$second';}
}class TimePicker extends StatefulWidget {static void show(BuildContext context, {TimeData? startTime,TimeData? endTime,TimeData? selectedTime,bool hideSecond = false,Function()? onCancel,required OnSelected onSelected,}) async {showModalBottomSheet(context: context,backgroundColor: Colors.transparent,builder: (BuildContext context) {return ClipRRect(borderRadius: const BorderRadius.only(topLeft: Radius.circular(8),topRight: Radius.circular(8),),child: TimePicker._(selectedTime: selectedTime,startTime: startTime,endTime: endTime,hideSecond: hideSecond,onSelected: onSelected,),);},).then((value) => onCancel?.call());}const TimePicker._({this.selectedTime,this.startTime,this.endTime,this.hideSecond = false,required this.onSelected,});final TimeData? selectedTime;final TimeData? startTime;final TimeData? endTime;final bool hideSecond;final OnSelected onSelected;State createState() => _TimePickerState();
}class _TimePickerState extends State<TimePicker> {late FixedExtentScrollController hourScrollController;late FixedExtentScrollController minuteScrollController;late FixedExtentScrollController secondScrollController;List<String> hourList = [];List<String> minuteList = [];List<String> secondList = [];int hourIndex = 0;int minuteIndex = 0;int secondIndex = 0;late TimeData startTime;late TimeData endTime;late TimeData selectedTime;final double itemExtent = 44;/// 初始化数据void initData() {// 初始化时for (int i = startTime.hour; i <= endTime.hour; i++) {hourList.add(i.toString());}int selectHour = selectedTime.hour;int selectMinute = selectedTime.minute;// 初始化分minuteList = getMinuteList(selectHour);// 初始化秒secondList = getSecondList(selectHour, selectMinute);// 初始化时间索引final List uniqueHourList = Set.from(hourList).toList();final List uniqueMinuteList = Set.from(minuteList).toList();final List uniqueSecondList = Set.from(secondList).toList();// 获取索引setState(() {hourIndex = uniqueHourList.indexOf("${selectedTime.hour}");minuteIndex = uniqueMinuteList.indexOf("${selectedTime.minute}");secondIndex = uniqueSecondList.indexOf("${selectedTime.second}");});// 设置Picker初始值hourScrollController = FixedExtentScrollController(initialItem: hourIndex);minuteScrollController = FixedExtentScrollController(initialItem: minuteIndex);secondScrollController = FixedExtentScrollController(initialItem: secondIndex);}List<String> getMinuteList(int selectHour) {List<String> list = [];if (selectHour == startTime.hour) {for (int i = startTime.minute; i <= 59; i++) {list.add(i.toString());}} else if (selectHour == endTime.hour) {for (int i = 0; i <= endTime.minute; i++) {list.add(i.toString());}} else {for (int i = 0; i <= 59; i++) {list.add(i.toString());}}return list;}List<String> getSecondList(int selectHour, int selectMinute) {List<String> list = [];if (selectHour == startTime.hour && selectMinute == startTime.minute) {for (int i = startTime.second; i <= 59; i++) {list.add(i.toString());}} else if (selectHour == endTime.hour && selectMinute == endTime.minute) {for (int i = 0; i <= endTime.second; i++) {list.add(i.toString());}} else {for (int i = 0; i <= 59; i++) {list.add(i.toString());}}return list;}/// 选中时分后更新秒void updateSecondList() {int hour = int.parse(hourList[hourIndex]);int minute = int.parse(minuteList[minuteIndex]);setState(() {secondIndex = 0;secondList = getSecondList(hour, minute);if (secondScrollController.positions.isNotEmpty) {secondScrollController.jumpTo(0);}});}/// 选中时后更新分void updateMinuteList() {int selectHour = int.parse(hourList[hourIndex]);setState(() {minuteIndex = 0;minuteList = getMinuteList(selectHour);if (minuteScrollController.positions.isNotEmpty) {minuteScrollController.jumpTo(0);}});}void initState() {super.initState();DateTime now = DateTime.now();startTime = widget.startTime ?? TimeData(0, 0, 0);endTime = widget.endTime ?? TimeData(23, 59, 59);selectedTime = widget.selectedTime ?? TimeData(now.hour, now.minute, now.second);if (endTime.precede(startTime)) {endTime = startTime;}if (selectedTime.precede(startTime)) {selectedTime = startTime;}if (endTime.precede(selectedTime)) {selectedTime = endTime;}initData();}void dispose() {hourScrollController.dispose();minuteScrollController.dispose();secondScrollController.dispose();super.dispose();}Widget build(BuildContext context) {return Container(color: Colors.white,child: Column(crossAxisAlignment: CrossAxisAlignment.start,mainAxisSize: MainAxisSize.min,children: <Widget>[Container(color: Colors.white,width: double.maxFinite,height: 200,child: Stack(alignment: AlignmentDirectional.center,children: [Container(width: MediaQuery.of(context).size.width - 32,height: itemExtent - 8,decoration: BoxDecoration(color: const Color(0xFFF1F1F1),borderRadius: BorderRadius.circular(12),),),Positioned(left: 20,right: 20,top: 0,bottom: 0,child: Row(mainAxisSize: MainAxisSize.min,children: <Widget>[Expanded(child: hourPickerView()),Expanded(child: minutePickerView()),widget.hideSecond? const SizedBox(): Expanded(child: secondPickerView()),],),),],),),Container(color: Colors.white,height: 68,padding: const EdgeInsets.only(left: 16, right: 16),child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween,children: <Widget>[Expanded(child: TextButton(child: const Text('取 消'),onPressed: () {Navigator.pop(context, false);},),),const SizedBox(width: 12),Expanded(child: TextButton(child: const Text('确 定'),onPressed: () {Navigator.pop(context, true);widget.onSelected.call(TimeData(int.parse(hourList[hourIndex]),int.parse(minuteList[minuteIndex]),int.parse(secondList[secondIndex]),));},),),],),),SizedBox(height: MediaQuery.of(context).padding.bottom),],),);}/// 时PickerWidget hourPickerView() {return buildPickerBorder(child: CupertinoPicker(scrollController: hourScrollController,looping: false,selectionOverlay: const Center(),onSelectedItemChanged: (index) {setState(() {hourIndex = index;});updateMinuteList();updateSecondList();},itemExtent: itemExtent,children: buildHourWidget(),),);}/// 分PickerWidget minutePickerView() {return buildPickerBorder(child: CupertinoPicker(scrollController: minuteScrollController,looping: false,selectionOverlay: const Center(),onSelectedItemChanged: (index) {setState(() {minuteIndex = index;});updateSecondList();},itemExtent: itemExtent,children: buildMinuteWidget(),),);}/// 秒PickerWidget secondPickerView() {return buildPickerBorder(child: CupertinoPicker(scrollController: secondScrollController,looping: false,selectionOverlay: const Center(),onSelectedItemChanged: (index) {setState(() {secondIndex = index;});},itemExtent: itemExtent,children: buildSecondWidget(),),);}/// 时WidgetList<Widget> buildHourWidget() {List<Widget> widgets = [];for (var i = 0; i < hourList.length; i++) {widgets.add(Center(child: Text(// hourList[i].padLeft(2, '0')'${hourList[i]}时',style: getTextStyle(i == hourIndex),maxLines: 1,),),);}return widgets;}/// 分WidgetList<Widget> buildMinuteWidget() {List<Widget> widgets = [];for (var i = 0; i < minuteList.length; i++) {widgets.add(Center(child: Text(// minuteList[i].padLeft(2, '0')'${minuteList[i]}分',style: getTextStyle(i == minuteIndex),maxLines: 1,),),);}return widgets;}/// 秒WidgetList<Widget> buildSecondWidget() {List<Widget> widgets = [];for (var i = 0; i < secondList.length; i++) {widgets.add(Center(child: Text(// secondList[i].padLeft(2, '0')'${secondList[i]}秒',style: getTextStyle(i == secondIndex),maxLines: 1,),),);}return widgets;}TextStyle getTextStyle(bool bold) {return TextStyle(color: Colors.black,fontSize: 20,height: 1,fontWeight: bold ? FontWeight.w600 : FontWeight.w400,);}Widget buildPickerBorder({required Widget child}) {return Stack(alignment: AlignmentDirectional.center,children: [/*Container(width: 90,height: itemExtent - 8,decoration: BoxDecoration(color: const Color(0xffEFEFF0),borderRadius: BorderRadius.circular(10),),),*/child,],);}
}

http://www.ppmy.cn/news/59173.html

相关文章

每日一题141——字符串中的最大奇数

给你一个字符串 num &#xff0c;表示一个大整数。请你在字符串 num 的所有 非空子字符串 中找出 值最大的奇数 &#xff0c;并以字符串形式返回。如果不存在奇数&#xff0c;则返回一个空字符串 "" 。 子字符串 是字符串中的一个连续的字符序列。 示例 1&#xff1…

MySQL索引概述

MySQL索引概述 当表中的数据量到达几十万甚至上百万的时候&#xff0c;SQL查询所花费的时间会很长&#xff0c;导致业务超时出错&#xff0c;此时就需要用索引来加速SQL查询。 由于索引也是需要存储成索引文件的&#xff0c;因此对索引的使用也会涉及磁盘I/O操作。如果索引创建…

探索三维世界【2】:Three.js 的 Texture 纹理

缤纷三维世界大揭秘&#xff1a;探索 Three.js 的 Texture 纹理 1、Texture纹理2、TextureLoader 纹理加载器2.1、创建纹理加载器2.2、纹理属性设置2.3、设置纹理渲染2.4、打光 3、完整代码与展示 1、Texture纹理 Texture 是 three.js 中的“纹理”概念。纹理是指将一张图像映…

Addictive Multiplicative in NN

特征交叉是特征工程中的重要环节&#xff0c;在以表格型&#xff08;或结构化&#xff09;数据为输入的建模中起到了很关键的作用。 特征交互的作用&#xff0c;一是尽可能挖掘对目标有效的模式、特征&#xff0c;二是具有较好的可解释性&#xff0c;三是能够将对数据的洞见引…

基于max30102的物联网病房监测系统(中断处理和主题逻辑)

目录 五、中断处理 六、主体框架 对采集数据的初始化 核心功能的实现 烟雾 通信帧格式 wifi接收数据的处理 OLED显示 五、中断处理 void SysTick_Handler(void) {TimingDelay_Decrement(); }void ESP8266_USART_INT_FUN(void) {uint8_t ucCh;if ( USART_GetITStatus (…

CSS(二)-- 选择器的运用(针对基本选择器和复合选择器的详细解析)

目录 1. 选择器的作用 2. 选择器的分类 3. 基本选择器 3.1 标签选择器 3.2 类选择器

手写【深拷贝】

JS中深拷贝的实现 JSON.parse(JSON.stringify())递归实现深拷贝 使用JSON.parse(JSON.stringify()) 实现 无法拷贝 函数、正则、时间格式、原型上的属性和方法等 递归实现深拷贝 es5实现深拷贝 源对象 const obj {name: 张桑,age: 18,hobby: [{name: 篮球,year: 5,loveSta…

Pinia入门-实现简单的用户状态管理

目录 一&#xff1a;pinia 是什么 二&#xff1a;pinia与vuex的区别 三&#xff1a;pinia的基本使用 3.1.安装 3.2.创建一个 pinia 实例 四&#xff1a;Pinia中的Store 4.1.什么是store 4.2.什么时候使用store 4.3.store的基本使用 4.4.state 4.5.getters 4.6.acti…