Flutter 时间选择组件

在Flutter 应用开发过程中,或多或少的都会涉及到时间选择器相关的内容。Flutter默认提供了DatePicker日期选择器,如果对样式没有特殊的要求,那么可以使用它来进行时间的选择,默认的样式如下所示。
在这里插入图片描述
使用示例代码如下:

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'dart:async';

class DateTimeDemo extends StatefulWidget {
  @override
  _DateTimeDemoState createState() => _DateTimeDemoState();
}

class _DateTimeDemoState extends State<DateTimeDemo> {
  DateTime selectedDate = DateTime.now();
  TimeOfDay selectedTime = TimeOfDay(hour: 9, minute: 30);
  
  Future<void> _selectDate() async {
    final DateTime date = await showDatePicker(
      context: context,
      initialDate: selectedDate,
      firstDate: DateTime(1900),
      lastDate: DateTime(2100),
    );

    if (date == null) return;

    setState(() {
      selectedDate = date;
    });
  }

  Future<void> _selectTime() async {
    final TimeOfDay time = await showTimePicker(
      context: context,
      initialTime: selectedTime,
    );

    if (time == null) return;

    setState(() {
      selectedTime = time;
    });
  } 
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('DateTimeDemo'),
        elevation: 0.0,
      ),
      body: Container(
        padding: EdgeInsets.all(16.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                InkWell(
                  onTap: _selectDate,
                  child: Row(
                    children: <Widget>[
                      Text(DateFormat.yMMMMd().format(selectedDate)),
                      Icon(Icons.arrow_drop_down),
                    ],
                  ),
                ),
                InkWell(
                  onTap: _selectTime,
                  child: Row(
                    children: <Widget>[
                      Text(selectedTime.format(context)),
                      Icon(Icons.arrow_drop_down),
                    ],
                  ),
                ),
              ],
            ),
          ],
        ),
      )
    );
  }
}

可以发现,默认的样式并不是很友好。通常在移动应用开发中,App的涉及多是参考iOS的设计来的,所以这时候,多半需要进行自定义组件了。不管,为了快速的进行开发我们可以选择一些第三方的组件库,如flutter_custom_calendar,此库具有如下的功能:

  • 支持公历,农历,节气,传统节日,常用节假日
  • 日期范围设置,默认支持的最大日期范围为1971.01-2055.12
  • 禁用日期范围设置,比如想实现某范围的日期内可以点击,范围外的日期置灰
  • 支持单选、多选模式,提供多选超过限制个数的回调和多选超过指定范围的回调。
  • 跳转到指定日期,默认支持动画切换
  • 自定义日历Item,支持组合widget的方式和利用canvas绘制的方式
  • 自定义顶部的WeekBar
  • 根据实际场景,可以给Item添加自定义的额外数据,实现各种额外的功能。比如实- 现进度条风格的日历,实现日历的各种标记
  • 支持周视图的展示,支持月份视图和星期视图的展示与切换联动

如下是部分效果图:
在这里插入图片描述
实际使用时,我们需要根据样式对该库进行二次开发,首先,新建一个date_picker_widget.dart文件,然后添加如下代码:

import 'package:flutter/material.dart';
import 'package:flutter_custom_calendar/flutter_custom_calendar.dart';

class DatePickerWidget extends StatefulWidget {
  final ValueSetter<String> onSetter;
  DatePickerWidget({@required this.onSetter});
  @override
  _DatePickerWidgetState createState() => _DatePickerWidgetState();
}

class _DatePickerWidgetState extends State<DatePickerWidget> {
  ValueNotifier<String> text;
  ValueNotifier<String> selectText;

  CalendarController controller ;
  @override
  Widget build(BuildContext context) {
    return  Column(
      children: <Widget>[
        Container(
          color: Colors.white,
          child: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              IconButton(icon: Icon(Icons.navigate_before), onPressed: (){
                controller.moveToPreviousMonth();
              }),
              ValueListenableBuilder(
                  valueListenable: text,
                  builder: (context, value, child) {
                    return Text(text.value);
                  }),
              IconButton(
                  icon: Icon(Icons.navigate_next),
                  onPressed: () {
                    controller.moveToNextMonth();
                  }),
            ],
          ),
        ),
        CalendarViewWidget(
          boxDecoration: BoxDecoration(
              color: Colors.white
          ),
          calendarController: controller,
          weekBarItemWidgetBuilder: () {
            return CustomStyleWeekBarItem();
          },
          dayWidgetBuilder: (dateModel) {
            return CustomStyleDayWidget(dateModel);
          },
        ),
      ],
    );
  }

  List<DateTime> getHighlightedDates() {
    return List<DateTime>.generate(10,
          (int index) => DateTime.now().add(Duration(days: 10 * (index + 1))),
    );
  }

  int maxSelectYear() {
    return DateTime.now().year;
  }

  int maxSelectMounth() {
    return DateTime.now().month;
  }

  int maxSelectDay() {
    return DateTime.now().day;
  }


  @override
  void initState() {
    super.initState();
    controller =  CalendarController(
      maxSelectYear: maxSelectYear(),
      maxSelectMonth: maxSelectMounth(),
      maxSelectDay:maxSelectDay(),
    );

    controller.addMonthChangeListener(
          (year, month) {
        text.value = "$year年$month月";
      },
    );
    controller.addOnCalendarSelectListener((dateModel){
      String month = dateModel.month < 10 ? '0${dateModel.month}': '${dateModel.month}';
      String day = dateModel.day < 10 ? '0${dateModel.day}': '${dateModel.day}';
      this.widget.onSetter('${dateModel.year}-${month}-${day}');
    });

//    controller.addOnCalendarSelectListener((dateModel) {
//      debugPrint(dateModel.toString()+'+++++++++++');
//      //刷新选择的时间
//      selectText.value =
//      "单选模式\n选中的时间:\n${controller.getSingleSelectCalendar()}";
//    });
//
    text = new ValueNotifier("${DateTime.now().year}年${DateTime.now().month}月");
//
//    selectText = new ValueNotifier(
//        "单选模式\n选中的时间:\n${controller.getSingleSelectCalendar()}");
  }
}

class CustomStyleWeekBarItem extends BaseWeekBar {
  final List<String> weekList = ["日","一", "二", "三", "四", "五", "六",];

  @override
  Widget getWeekBarItem(int index) {
    return Container(
      child: Center(
        child: Text(weekList[index]),
      ),
    );
  }
}

class CustomStyleDayWidget extends BaseCustomDayWidget {
  CustomStyleDayWidget(DateModel dateModel) : super(dateModel);

  @override
  void drawNormal(DateModel dateModel, Canvas canvas, Size size) {
    if (!dateModel.isCurrentMonth) {
      return;
    }
    bool isWeekend = dateModel.isWeekend;
    bool isInRange = dateModel.isInRange;

    //顶部的文字
    TextPainter dayTextPainter = new TextPainter()
      ..text = TextSpan(
          text: dateModel.day.toString(),
          style: new TextStyle(
              color: !isInRange
                  ? Colors.grey
                  : isWeekend ? Colors.blue : Colors.black,
              fontSize: 16))
      ..textDirection = TextDirection.ltr
      ..textAlign = TextAlign.center;

    dayTextPainter.layout(minWidth: size.width, maxWidth: size.width);
    dayTextPainter.paint(canvas, Offset(0, 10));

    //下面的文字
    TextPainter lunarTextPainter = new TextPainter()
      ..text = new TextSpan(
          text: dateModel.lunarString,
          style: new TextStyle(
              color: !isInRange
                  ? Colors.grey
                  : isWeekend ? Colors.blue : Colors.grey,
              fontSize: 12))
      ..textDirection = TextDirection.ltr
      ..textAlign = TextAlign.center;

    lunarTextPainter.layout(minWidth: size.width, maxWidth: size.width);
    lunarTextPainter.paint(canvas, Offset(0, size.height / 2));
  }

  @override
  void drawSelected(DateModel dateModel, Canvas canvas, Size size) {
    if (!dateModel.isCurrentMonth) {
      return;
    }
    //绘制背景
    Paint backGroundPaint = new Paint()
      ..color = Colors.blue
      ..strokeWidth = 2;
    double padding = 8;
    canvas.drawCircle(Offset(size.width / 2, size.height / 2),
        (size.width - padding) / 2, backGroundPaint);

    //顶部的文字
    TextPainter dayTextPainter = new TextPainter()
      ..text = TextSpan(
          text: dateModel.day.toString(),
          style: new TextStyle(color: Colors.white, fontSize: 16))
      ..textDirection = TextDirection.ltr
      ..textAlign = TextAlign.center;

    dayTextPainter.layout(minWidth: size.width, maxWidth: size.width);
    dayTextPainter.paint(canvas, Offset(0, 10));

    //下面的文字
    TextPainter lunarTextPainter = new TextPainter()
      ..text = new TextSpan(
          text: dateModel.lunarString,
          style: new TextStyle(color: Colors.white, fontSize: 12))
      ..textDirection = TextDirection.ltr
      ..textAlign = TextAlign.center;

    lunarTextPainter.layout(minWidth: size.width, maxWidth: size.width);
    lunarTextPainter.paint(canvas, Offset(0, size.height / 2));
  }
}

然后,我们新建一个date_picker_dialog.dart的Dialog自定义组件,代码如下:

import 'package:flutter/material.dart';
import 'package:gc_data_app/pages/views/gaps.dart';
import 'package:gc_data_app/res/colors.dart';
import 'package:gc_data_app/routes/app_route.dart';
import 'package:gc_data_app/utils/date_utils.dart';
import 'package:gc_data_app/widgets/date_picker_new_widget.dart';
import 'package:gc_data_app/widgets/date_picker_widget.dart';
import 'package:gc_data_app/widgets/drop_down_widget.dart';
import 'package:toast/toast.dart';

class DatePickerDialog extends StatefulWidget {

  const DatePickerDialog({
    Key key,
    @required this.onSelectedDate,
  }) : super(key: key);

  final Function(String) onSelectedDate;

  @override
  State<StatefulWidget> createState() {
    return CustomTimeState();
  }
}

class CustomTimeState extends State<DatePickerDialog> {

  bool showDate = false;
  var chooseDateStr = '';

  @override
  Widget build(BuildContext context) {
    return Material(
      child: SizedBox(
        height: 475,
        child: Container(
          color: Colors.white,
          child: Column(
            children: <Widget>[
              _buildTitleBar(),
              _buildLine(),
              _buildDatePicker(),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildTitleBar() {
    return Container(
      padding: EdgeInsets.only(top: 10, left: 15, bottom: 10, right: 15),
      child: Row(
        children: <Widget>[
          InkWell(
            child: Text(
              '取消',
              style: TextStyle(
                  fontSize: 14,
                  fontWeight: FontWeight.w300,
                  color: ColorRes.text_little_blue),
            ),
            onTap: () {
              AppRoute.pop(context);
            },
          ),
          Expanded(
            child: Center(
              child: Text(
                '选择日期',
                style: TextStyle(fontSize: 15, fontWeight: FontWeight.w300),
              ),
            ),
          ),
          InkWell(
            child: Text('确认',
                style: TextStyle(
                    fontSize: 14,
                    fontWeight: FontWeight.w300,
                    color: ColorRes.text_little_blue)),
            onTap: () {
              if(chooseDateStr==''){
                Toast.show('选择日期不能为空!', context);
                return ;
              }
              DateTime choose= DateUtils.strToDate(chooseDateStr);
              DateTime limitStart= DateUtils.strToDate('2014-01-01');
              if(choose.isBefore(limitStart)){
                Toast.show('起始日期不能早于2014-01-01', context);
                return ;
              }
              widget.onSelectedDate(chooseDateStr);
              FocusScope.of(context).unfocus();
              Navigator.pop(context);
            },
          )
        ],
      ),
    );
  }

  Widget _buildLine() {
    return Gaps.line;
  }

  Widget _buildDatePicker() {
    return DatePickerWidget(onSetter: (value) {
      showDate = false;
      chooseDateStr = value;
    });
  }
}

实际使用时候,使用showCupertinoModalPopup组件展示出来即可,如下所示。

showCalendarSheet(){
   showCupertinoModalPopup(
      context: context,
      builder: (BuildContext context) {
        return DatePickerDialog(onSelectedDate: (value){
           
        },
        );
      },
    );
  }
xiangzhihong8 CSDN认证博客专家 HTTPS 前端框架 JavaScript
著有《React Native移动开发实战》、《Kotlin入门与实战》、《Weex跨平台开发与实战》、《React Native开发进阶》和《Flutter跨平台开发实战》,正努力完成《Android应用架构实战》
已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 成长之路 设计师:Amelia_0503 返回首页
实付 19.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值