紅衣佳人白衣友,朝與同歌暮同酒。
世人皆謂慕長安,然吾只戀長安某。git
最近咱們的UI小姐姐給了一份這樣的日曆設計圖 ┭┮﹏┭┮,
能夠上下滑動,支持多日選擇,再次進入日曆頁面能夠選中上次選中的日期,
開始想冒着僥倖的內心去找找網上的開源庫,
無奈找了許久找不到能夠上下滑動的日曆,
故花了三天時間終於寫完了第一版github
默認狀態下是這個樣子:設計模式
選中狀態下是這個樣子:app
TimeUtil提供時間的計算功能類ide
/* * 每一個月對應的天數 * */ static const List<int> _daysInMonth = <int>[ 31, -1, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
/* * 根據年月獲取月的天數 * */ static int getDaysInMonth(int year, int month) { if (month == DateTime.february) { final bool isLeapYear = (year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0); if (isLeapYear) return 29; return 28; } return _daysInMonth[month - 1]; }
/* * 獲得這個月的第一天是星期幾(0 是 星期日 1 是 星期一...) * */ static int computeFirstDayOffset( int year, int month, MaterialLocalizations localizations) { // 0-based day of week, with 0 representing Monday. final int weekdayFromMonday = DateTime(year, month).weekday - 1; // 0-based day of week, with 0 representing Sunday. final int firstDayOfWeekFromSunday = localizations.firstDayOfWeekIndex; // firstDayOfWeekFromSunday recomputed to be Monday-based final int firstDayOfWeekFromMonday = (firstDayOfWeekFromSunday - 1) % 7; // Number of days between the first day of week appearing on the calendar, // and the day corresponding to the 1-st of the month. return (weekdayFromMonday - firstDayOfWeekFromMonday) % 7; }
/* * 每一個月前面空出來的天數 * */ static int numberOfHeadPlaceholderForMonth( int year, int month, MaterialLocalizations localizations) { return computeFirstDayOffset(year, month, localizations); }
/* * 根據當前年月份計算當前月份顯示幾行 * */ static int getRowsForMonthYear(int year, int month, MaterialLocalizations localizations){ int currentMonthDays = getDaysInMonth(year, month); // 每一個月前面空出來的天數 int placeholderDays = numberOfHeadPlaceholderForMonth(year, month, localizations); int rows = (currentMonthDays + placeholderDays)~/7; // 向下取整 int remainder = (currentMonthDays + placeholderDays)%7; // 取餘(最後一行的天數) if (remainder > 0) { rows += 1; } return rows; }
/* * 根據當前年月份計算每一個月後面空出來的天數 * */ static int getLastRowDaysForMonthYear(int year, int month, MaterialLocalizations localizations){ int count = 0; // 當前月份的天數 int currentMonthDays = getDaysInMonth(year, month); // 每一個月前面空出來的天數 int placeholderDays = numberOfHeadPlaceholderForMonth(year, month, localizations); int rows = (currentMonthDays + placeholderDays)~/7; // 向下取整 int remainder = (currentMonthDays + placeholderDays)%7; // 取餘(最後一行的天數) if (remainder > 0) { count = 7-remainder; } return count; }
CalendarViewModel提供日曆要顯示的數據模型ui
class YearMonthModel { int year; int month; YearMonthModel(this.year, this.month); } // 天天對應的數據模型 class DayModel { int year; int month; int dayNum; // 數字類型的幾號 String day; // 字符類型的幾號 bool isSelect; // 是否選中 bool isOverdue; // 是否過時 DayModel(this.year, this.month, this.dayNum, this.day, this.isSelect, this.isOverdue); } // 每一個月對應的數據模型 class CalendarItemViewModel { final List<DayModel> list; final int year; final int month; DayModel firstSelectModel; DayModel lastSelectModel; CalendarItemViewModel({this.list, this.year, this.month, this.firstSelectModel, this.lastSelectModel}); } class CalendarViewModel { List<YearMonthModel> yearMonthList = CalendarViewModel.getYearMonthList(); List<CalendarItemViewModel> getItemList() { List<CalendarItemViewModel> _list = []; yearMonthList.forEach((model){ List<DayModel> dayModelList = getDayModelList(model.year, model.month); _list.add(CalendarItemViewModel(list:dayModelList,year: model.year, month:model.month)); }); return _list; } // 根據年月獲得 月的天天顯示須要的日期 static List<DayModel> getDayModelList(int year, int month) { List<DayModel> _listModel = []; // 今天幾號 int _currentDay = DateTime.now().day; // 今天在幾月 int _currentMonth = DateTime.now().month; // 當前月的天數 int _days = TimeUtil.getDaysInMonth(year, month); String _day = ''; bool _isSelect = false; bool isOverdue = false; int _dayNum = 0; for (int i = 1; i <= _days; i++) { _dayNum = i; if (_currentMonth == month) { //在當前月 if (i < _currentDay) { isOverdue = true; _day = '$i'; } else if (i == _currentDay) { _day = '今'; isOverdue = false; } else { _day = '$i'; isOverdue = false; } } else { _day = '$i'; isOverdue = false; } DayModel dayModel = DayModel(year, month, _dayNum, _day, _isSelect, isOverdue); _listModel.add(dayModel); } return _listModel; } /* * 根據當前年月份計算下面6個月的年月,根據須要能夠實現更多個月的 * */ static List<YearMonthModel> getYearMonthList() { int _month = DateTime.now().month; int _year = DateTime.now().year; List<YearMonthModel> _yearMonthList = <YearMonthModel>[]; for(int i=0; i<6; i++) { YearMonthModel model = YearMonthModel(_year, _month); _yearMonthList.add(model); if(_month == 12) { _month = 1; _year ++; } else { _month ++; } } return _yearMonthList; }
CalendarItem對應的是每一個月的widgetthis
typedef void OnTapDayItem(int year, int month, int checkInTime); class CalendarItem extends StatefulWidget { final CalendarItemViewModel itemModel; final OnTapDayItem dayItemOnTap; CalendarItem(this.dayItemOnTap, this.itemModel); @override _CalendarItemState createState() => _CalendarItemState(); } class _CalendarItemState extends State<CalendarItem> { // 日曆顯示幾行 int _rows = 0; List<DayModel> _listModel = <DayModel>[]; @override void initState() { // TODO: implement initState super.initState(); _listModel = widget.itemModel.list; } @override Widget build(BuildContext context) { double screenWith = MediaQuery.of(context).size.width; // 顯示幾行 _rows = TimeUtil.getRowsForMonthYear(widget.itemModel.year, widget.itemModel.month, MaterialLocalizations.of(context)); return Container( width: screenWith, height: 25.0 + 24.0 + 17.0 + _rows * 52.0 + 32.0 + 13, child: Column( children: <Widget>[ SizedBox( height: 32, ), _yearMonthItem(widget.itemModel.year, widget.itemModel.month), SizedBox( height: 24, ), _weekItem(screenWith), SizedBox( height: 13, ), _monthAllDays(widget.itemModel.year, widget.itemModel.month, context), ], ), ); } /* * 顯示年月的組件,須要傳入年月日期 * */ _yearMonthItem(int year, int month) { return Container( alignment: Alignment.center, height: 25, child: Text( '$year.$month', style: TextStyle( color: ColorUtil.color('212121'), fontSize: 18, fontFamily: 'Avenir-Heavy', ), ), ); } /* * 顯示周的組件,使用了 _weekTitleItem * */ _weekItem(double screenW) { List<String> _listS = <String>[ '日', '一', '二', '三', '四', '五', '六', ]; List<Widget> _listW = []; _listS.forEach((title) { _listW.add(_weekTitleItem(title, (screenW - 40) / 7)); }); return Container( width: screenW - 40, height: 17, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: _listW, ), ); } /* * 周內對應的天天的組件 * */ _weekTitleItem(String title, double width) { return Container( alignment: Alignment.center, width: width, child: Text( title, style: TextStyle( color: ColorUtil.color('757575'), fontSize: 12, fontFamily: 'PingFangSC-Semibold', ), ), ); } _monthAllDays(int year, int month, BuildContext context) { double screenWith = MediaQuery.of(context).size.width; // 當前月前面空的天數 int emptyDays = TimeUtil.numberOfHeadPlaceholderForMonth( year, month, MaterialLocalizations.of(context)); List<Widget> _list = <Widget>[]; for (int i = 1; i <= emptyDays; i++) { _list.add(_dayEmptyTitleItem(context)); } for (int i = 1; i <= _listModel.length; i++) { _list.add(_dayTitleItem(_listModel[i - 1], context)); } List<Row> _rowList = <Row>[ Row( children: _list.sublist(0, 7), ), Row( children: _list.sublist(7, 14), ), Row( children: _list.sublist(14, 21), ), ]; if (_rows == 4) { _rowList.add( Row( children: _list.sublist(21, _list.length), ), ); } else if (_rows == 5) { _rowList.add( Row( children: _list.sublist(21, 28), ), ); _rowList.add( Row( children: _list.sublist(28, _list.length), ), ); } else if (_rows == 6) { _rowList.add( Row( children: _list.sublist(21, 28), ), ); _rowList.add( Row( children: _list.sublist(28, 25), ), ); _rowList.add( Row( children: _list.sublist(35, _list.length), ), ); } return Container( width: screenWith - 40, color: Colors.white, height: 52.0 * _rows, child: Column( children: _rowList, ), ); } /* * number 月的幾號 * isOverdue 是否過時 * */ _dayTitleItem(DayModel model, BuildContext context) { double screenWith = MediaQuery.of(context).size.width; double singleW = (screenWith - 40) / 7; String dayTitle = model.day; if (widget.itemModel.firstSelectModel != null && model.isSelect && model.dayNum == widget.itemModel.firstSelectModel.dayNum) { dayTitle = '入住'; } if (widget.itemModel.lastSelectModel != null && model.isSelect && model.dayNum == widget.itemModel.lastSelectModel.dayNum) { dayTitle = '離開'; } return GestureDetector( onTap: () { if(model.isOverdue) return; _dayTitleItemTap(model); }, child: Stack( children: <Widget>[ Container( width: singleW, height: 52, alignment: Alignment.center, child: Text( dayTitle, style: TextStyle( color: model.isOverdue ? ColorUtil.color('BDBDBD') : ColorUtil.color('212121'), fontSize: 15, fontFamily: 'Avenir-Medium', ), ), ), Positioned( left: 0, right: 0, bottom: 0, child: Visibility( visible: model.isOverdue ? false : model.isSelect, child: Container( height: 4, width: singleW, color: ColorUtil.color('FED836'), )), ), ], ), ); } _dayEmptyTitleItem(BuildContext context) { double screenWith = MediaQuery.of(context).size.width; double singleW = (screenWith - 40) / 7; return Container( width: singleW, height: 52, ); } _dayTitleItemTap(DayModel model) { widget.dayItemOnTap( widget.itemModel.year, widget.itemModel.month, model.dayNum); setState(() {}); } }
CalendarPage是日曆頁面的Widgetspa
/* * Location 標記當前選中日期和以前的日期相比, * left: 是在以前日期以前 * mid: 和以前日期相等 * right:在以前日期以後 * */ enum Location{left,mid,right} typedef void SelectDateOnTap(DayModel checkInTimeModel, DayModel leaveTimeModel); class CalendarPage extends StatefulWidget { final DayModel startTimeModel;// 外部傳入的以前選中的入住日期 final DayModel endTimeModel;// 外部傳入的以前選中的離開日期 final SelectDateOnTap selectDateOnTap;// 肯定按鈕的callback 給外部傳值 CalendarPage({this.startTimeModel,this.endTimeModel,this.selectDateOnTap}); @override _CalendarPageState createState() => _CalendarPageState(); } class _CalendarPageState extends State<CalendarPage> { String _selectCheckInTime = '選擇入住時間'; String _selectLeaveTime = '選擇離開時間'; bool _isSelectCheckInTime = false; // 是否選擇入住日期 bool _isSelectLeaveTime = false; // 是否選擇離開日期 int _checkInDays = 0; // 入住天數 // 保存當前選中的入住日期和離開日期 DayModel _selectCheckInTimeModel = null; DayModel _isSelectLeaveTimeModel = null; List<CalendarItemViewModel> _list = []; @override void initState() { super.initState(); // 加載日曆數據源 _list = CalendarViewModel().getItemList(); // 處理外部傳入的選中日期 if(widget.startTimeModel!=null && widget.endTimeModel!=null) { for(int i=0; i<_list.length; i++) { CalendarItemViewModel model = _list[i]; if(model.month == widget.startTimeModel.month) { _updateDataSource(widget.startTimeModel.year, widget.startTimeModel.month, widget.startTimeModel.dayNum); } if (model.month == widget.endTimeModel.month) { _updateDataSource(widget.endTimeModel.year, widget.endTimeModel.month, widget.endTimeModel.dayNum); } } } } @override Widget build(BuildContext context) { final data = MediaQuery.of(context); // 屏幕寬高 final screenHeight = data.size.height; final screenWidth = data.size.width; return Container( color: Colors.white, width: double.maxFinite, height: screenHeight - 64, child: Stack( children: <Widget>[ Column( children: <Widget>[ SizedBox( height: 86, ), Row( children: <Widget>[ SizedBox( width: 20, ), // 擇入住時間的視圖 _selectTimeItem(context, _selectCheckInTime, Alignment.centerLeft, _isSelectCheckInTime), // 入住天數的視圖 _daysItem(_checkInDays), // 選擇離開時間的視圖 _selectTimeItem(context, _selectLeaveTime, Alignment.centerRight, _isSelectLeaveTime), SizedBox( width: 20, ), ], ), // 月日期的視圖 Container( height: screenHeight - 64 - 80 - 83 - 30, child: ListView.builder( itemBuilder: (BuildContext context, int index) { CalendarItemViewModel itemModel = _list[index]; return CalendarItem( (year, month, checkInTime) { _updateCheckInLeaveTime( year, month, checkInTime); }, itemModel, ); }, itemCount: _list.length, ), ), ], ), Positioned( left: 0, right: 0, bottom: 0, height: MediaQuery.of(context).padding.bottom, child: Container(), ), _bottonSureButton(screenWidth), ], ), ); } /* * content 顯示的日期 * alignment 用來控制文本的對齊方式 * isSelectTime 是否選擇了日期 * */ _selectTimeItem(BuildContext context, String content, Alignment alignment, bool isSelectTime) { final screenWidth = MediaQuery.of(context).size.width; return Container( width: (screenWidth - 40 - 30) / 2, height: 30, alignment: alignment, child: Text( content, style: TextStyle( fontFamily: isSelectTime ? 'Avenir-Heavy' : 'PingFangSC-Regular', fontSize: isSelectTime ? 22 : 18, color: isSelectTime ? ColorUtil.color('212121') : ColorUtil.color('BDBDBD'), ), ), ); } /* * day 入住天數,默認不選擇爲0 * */ _daysItem(int day) { return Container( width: 30, height: 18, alignment: Alignment.center, decoration: BoxDecoration( color: Colors.white, border: Border.all(width: 0.5, color: ColorUtil.color('BDBDBD')), borderRadius: BorderRadius.all(Radius.circular(2)), ), child: Text( '$day晚', style: TextStyle( color: ColorUtil.color('BDBDBD'), fontSize: 12, ), ), ); } /* * 底部肯定按鈕 * */ _bottonSureButton(double screenWidth) { return Positioned( left: 0, right: 0, bottom: MediaQuery.of(context).padding.bottom, height: 80, child: Container( height: 80, width: double.maxFinite, color: Colors.white, alignment: Alignment.center, child: GestureDetector( onTap: _sureButtonTap, child: Container( height: 48, width: screenWidth - 30, decoration: BoxDecoration( color: ColorUtil.color('FED836'), borderRadius: BorderRadius.all(Radius.circular(24.0)), ), child: Center( child: Text( '肯定', style: TextStyle( fontSize: 16, color: Colors.black, fontFamily: 'PingFangSC-Light', ), ), ), ), ), ), ); } /* * 比較後面的日期是比model日期小(left) 仍是相等(mid) 仍是大 (right) * */ _comparerTime(DayModel model, int year, int month, int day){ if(year > model.year) { return Location.right; } else if(year == model.year) { if(model.month < month) { return Location.right; } else if(model.month == month){ if(model.dayNum < day){ return Location.right; } else if(model.dayNum == day){ return Location.mid; } else { return Location.left; } } else { return Location.right; } } else { return Location.left; } } /* * 更新日曆的數據源 * */ _updateDataSource(int year, int month, int checkInTime) { // 左右指針 用來記錄選擇的入住日期和離開日期 DayModel firstModel = null ; DayModel lastModel = null; for(int i=0; i<_list.length; i++) { CalendarItemViewModel model = _list[i]; if(model.firstSelectModel != null){ firstModel = model.firstSelectModel; } if (model.lastSelectModel != null) { lastModel = model.lastSelectModel; } } if (firstModel != null && lastModel != null) { for(int i=0; i<_list.length; i++) { CalendarItemViewModel model = _list[i]; model.firstSelectModel = null; model.lastSelectModel = null; firstModel = null; lastModel = null; for(int i=0; i<model.list.length; i++) { DayModel dayModel = model.list[i]; dayModel.isSelect = false; if(_comparerTime(dayModel, year, month, checkInTime) == Location.mid){ dayModel.isSelect = true; model.firstSelectModel = dayModel; _isSelectCheckInTime = true; _isSelectLeaveTime = false; _selectCheckInTime = '$year.$month.$checkInTime'; _selectCheckInTimeModel = dayModel; } } } _checkInDays = 0; _isSelectLeaveTime = false; _selectLeaveTime = '選擇離開時間'; _isSelectLeaveTimeModel = null; } else if(firstModel != null && lastModel == null) { if(_comparerTime(firstModel, year, month, checkInTime) == Location.left){ for(int i=0; i<_list.length; i++) { CalendarItemViewModel model = _list[i]; model.firstSelectModel = null; model.lastSelectModel = null; firstModel = null; lastModel = null; for(int i=0; i<model.list.length; i++) { DayModel dayModel = model.list[i]; dayModel.isSelect = false; if(_comparerTime(dayModel, year, month, checkInTime) == Location.mid){ dayModel.isSelect = !dayModel.isSelect; model.firstSelectModel = dayModel; _isSelectCheckInTime = dayModel.isSelect ? true : false; _selectCheckInTime = '$year.$month.$checkInTime'; _selectCheckInTimeModel = dayModel; } } } _checkInDays = 0; _isSelectLeaveTime = false; _selectLeaveTime = '選擇離開時間'; _isSelectLeaveTimeModel = null; } else if(_comparerTime(firstModel, year, month, checkInTime) == Location.mid){//點擊了本身 for(int i=0; i<_list.length; i++) { CalendarItemViewModel model = _list[i]; model.lastSelectModel = null; if(model.month == month){ for(int i=0; i<model.list.length; i++) { DayModel dayModel = model.list[i]; if(_comparerTime(dayModel, year, month, checkInTime) == Location.mid){ dayModel.isSelect = !dayModel.isSelect; model.firstSelectModel = dayModel.isSelect ? dayModel : null; _selectCheckInTimeModel = dayModel.isSelect ? dayModel : null; _isSelectCheckInTime = dayModel.isSelect ? true : false; _selectCheckInTime = dayModel.isSelect ? '$year.$month.$checkInTime' : '選擇入住時間'; } } } } _checkInDays = 0; _isSelectLeaveTime = false; _selectLeaveTime = '選擇離開時間'; _isSelectLeaveTimeModel = null; } else if (_comparerTime(firstModel, year, month, checkInTime) == Location.right){ if(month == firstModel.month){ // 統計入住天數 int _calculaterDays = 1; for(int i=0; i<_list.length; i++) { CalendarItemViewModel model = _list[i]; if(model.month == month){ for(int i=0; i<model.list.length; i++) { DayModel dayModel = model.list[i]; if(dayModel.dayNum == checkInTime) { dayModel.isSelect = true; model.lastSelectModel = dayModel; _isSelectLeaveTimeModel = dayModel; _isSelectLeaveTime = true; _selectLeaveTime = '$year.$month.$checkInTime'; }else if(dayModel.dayNum > firstModel.dayNum && dayModel.dayNum<checkInTime){ dayModel.isSelect = true; _calculaterDays++; } } } } _checkInDays = _calculaterDays; } else { // 統計入住天數 int _calculaterDays = 1; for(int i=0; i<_list.length; i++) { CalendarItemViewModel model = _list[i]; if(model.month == firstModel.month){ for(int i=0; i<model.list.length; i++) { DayModel dayModel = model.list[i]; if (dayModel.dayNum > firstModel.dayNum){ dayModel.isSelect = true; _calculaterDays++; } } } else if(model.month>firstModel.month && model.month<month){ for(int i=0; i<model.list.length; i++) { DayModel dayModel = model.list[i]; dayModel.isSelect = true; _calculaterDays++; } } else if(month == model.month){ for(int i=0; i<model.list.length; i++) { DayModel dayModel = model.list[i]; if(dayModel.dayNum < checkInTime){ dayModel.isSelect = true; _calculaterDays++; } else if (dayModel.dayNum == checkInTime) { dayModel.isSelect = true; model.lastSelectModel = dayModel; _isSelectLeaveTimeModel = dayModel; _isSelectLeaveTime = true; _selectLeaveTime = '$year.$month.$checkInTime'; } } } } _checkInDays = _calculaterDays; } } } else if(firstModel == null && lastModel == null){ for(int i=0; i<_list.length; i++) { CalendarItemViewModel model = _list[i]; model.firstSelectModel = null; model.lastSelectModel = null; firstModel = null; lastModel = null; for(int i=0; i<model.list.length; i++) { DayModel dayModel = model.list[i]; dayModel.isSelect = false; if(_comparerTime(dayModel, year, month, checkInTime) == Location.mid){ dayModel.isSelect = true; model.firstSelectModel = dayModel; _isSelectCheckInTime = true; _selectCheckInTimeModel = dayModel; _isSelectLeaveTime = false; _selectCheckInTime = '$year.$month.$checkInTime'; } } } } } /* * 點擊日期的回調事件 * */ _updateCheckInLeaveTime(int year, int month, int checkInTime) { // 更新數據源 _updateDataSource(year, month, checkInTime); // 刷新UI setState(() {}); } /* * 底部肯定按鈕的點擊事件 * */ _sureButtonTap() { if(!_isSelectCheckInTime){ ShowToast().showToast('請選擇入住時間'); return; } else if (!_isSelectLeaveTime){ ShowToast().showToast('請選擇離開時間'); return; } print('${_selectCheckInTimeModel.year},${_selectCheckInTimeModel.month},${_selectCheckInTimeModel.dayNum}'); print('${_isSelectLeaveTimeModel.year},${_isSelectLeaveTimeModel.month},${_isSelectLeaveTimeModel.dayNum}'); print('入住日期:$_selectCheckInTime, 離開時間:$_selectLeaveTime, 共$_checkInDays晚'); // 把日期回調給外部 widget.selectDateOnTap(_selectCheckInTimeModel,_isSelectLeaveTimeModel); Navigator.pop(context); } }
日曆除了UI視圖外,最麻煩的就是選擇日期的各類邏輯
這裏咱們把它抽象
成幾種狀態
之間的轉換:設計
對應的代碼邏輯就是:CalendarPage頁面中更新日曆的數據源的方法(_updateDataSource)指針