因爲項目須要,用Flutter重構了以前用Android作過的日曆組件,總體效果感受不錯,流暢度甚至超過原來的,這裏須要提一下官網的作法,以下:html
var date = DateTime.now();
return showDatePicker(
context: context,
initialDate: date,
firstDate: date,
lastDate: date.add(
Duration(days: 30),
),
);
複製代碼
官方的作法就是showDatePicker實現的,支持MD和IOS的風格,但據我瞭解,只支持單選,不支持開始和結束日期的區間選擇,體驗也與我須要的效果不一致,因此通過考慮以後,仍是決定本身寫一個。前端
下面分段對部分代碼進行描述。vue
下面就是一個日曆選擇組件的調用方式:node
return CalendarList(
firstDate: DateTime(2019, 8),
lastDate: DateTime(2020, 8),
selectedStartDate: DateTime(2019, 8, 28),
selectedEndDate: DateTime(2019, 9, 2),
onSelectFinish: (selectStartTime, selectEndTime) {
List<DateTime> result = <DateTime>[];
result.add(selectStartTime);
if (selectEndTime != null) {
result.add(selectEndTime);
}
Navigator.pop(context, result);
},
);
複製代碼
其中firstDate和lastDate是選擇的月份列表,本例中,從2019年8月開始算起,結束時間是2020年8月,而後又有2個參數selectedStartDate和selectedEndDate,這2個參數是給定的默認選中區間,本例中默認選中了2019/8/28和2019/9/2之間的全部日期,默認選中通常是記錄用戶上次選中的結果。onSelectFinish就是選完以後的回調,以上這些參數是根據實際業務能夠靈活設置的。git
這塊其實很簡單,CalendarList自己就支持從底部滑出,調用的方法是showModalBottomSheet,代碼以下:github
showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return Container(
height: 600.0,
child: FullScreenDemo(),
);
},
).then((result) {
setState(() {
selectResult2 = result;
});
});
複製代碼
其中日曆放在了FullScreenDemo裏,經過Container包一層設置一個高度,而後就能夠經過showModalBottomSheet方法從底部滑出。bash
經過上面的講述,咱們瞭解瞭如何使用CalendarList組件,那麼咱們看看源碼裏面具體作了哪些。筆者在實現該功能時把MonthView做爲SliverList的一個build item。放置到CustomScrollView的Sliver裏面,這裏複習一下,Sliver的做用其實就是「粘合劑」的做用,把多個組件粘合起來造成一個滾動區域,佈局以下:函數
CustomScrollView(
slivers: <Widget>[
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
int month = index + monthStart;
DateTime calendarDateTime = DateTime(yearStart, month);
return _getMonthView(calendarDateTime);
},
childCount: count,
),
),
],
),
複製代碼
在BuildContext中,經過index與monthStart想加,計算出日曆,即8,9,10,11...這些月份,須要注意的是DateTime裏面傳入的month參數若是超過了12,則前面的年會自動「進位」(Flutter設置的太貼心了),好了,在_getMonthView裏面,咱們看看return了一個什麼樣的Widget,代碼以下:佈局
Widget _getMonthView(DateTime dateTime) {
int year = dateTime.year;
int month = dateTime.month;
return MonthView(
context: context,
year: year,
month: month,
padding: HORIZONTAL_PADDING,
dateTimeStart: selectStartTime,
dateTimeEnd: selectEndTime,
todayColor: Colors.deepOrange,
onSelectDayRang: (dateTime) => onSelectDayChanged(dateTime),
);
}
複製代碼
好,這裏就是傳入了MonthView,設置了年、月,dateTimeStart,dateTimeEnd,today高亮顏色這些參數。下面,咱們看看MonthView裏面又作了啥學習
MonthView其實就是真正繪製每月有多少個星期,而後每一個星期的7天展現,經過每行(Row)放置7個DayNumber組件,根據每週循環出整個月的數據,代碼片斷以下:
dayRowChildren.add(
DayNumber(
size: widget.itemWidth,
day: day,
isToday: isToday,
isDefaultSelected: isDefaultSelected,
todayColor: widget.todayColor,
onDayTap: (day) {
selectedDate = DateTime(widget.year, widget.month, day);
widget.onSelectDayRang(selectedDate);
},
),
);
if ((day - 1 + firstWeekdayOfMonth) % DateTime.daysPerWeek == 0 ||
day == daysInMonth) {
dayRows.add(
Row(
children: List<DayNumber>.from(dayRowChildren),
),
);
dayRowChildren.clear();
}
複製代碼
這樣,一個日曆就出來了,不過光有這些是不行的,由於還沒開始作選擇器,即(單選,多選,反選,取消這些),須要高亮出來,高亮的邏輯大體以下:
DateTime moment = DateTime(widget.year, widget.month, day);
final bool isToday = dateIsToday(moment);
bool isDefaultSelected = false;
if (widget.dateTimeStart == null &&
widget.dateTimeEnd == null &&
selectedDate == null) {
isDefaultSelected = false;
}
if (widget.dateTimeStart == selectedDate &&
widget.dateTimeEnd == null &&
selectedDate?.day == day &&
day > 0) {
isDefaultSelected = true;
}
if (widget.dateTimeStart != null && widget.dateTimeEnd != null) {
isDefaultSelected = (moment.isAtSameMomentAs(widget.dateTimeStart) ||
moment.isAtSameMomentAs(widget.dateTimeEnd)) ||
moment.isAfter(widget.dateTimeStart) &&
moment.isBefore(widget.dateTimeEnd) &&
day > 0
? true
: false;
}
複製代碼
上述代碼能夠說是一部分核心邏輯,會根據CalendarList傳入的選擇區間經過DateTime moment進行篩選,若是是在區間範圍內,則選中該區間,猜猜怎麼讓DayNumber高亮起來? OK,其實知道了高亮區間以後,在DayNumber裏就能夠傳入默認選中isDefaultSelected,下面,咱們看看DayNumber又作了啥
和CalendarList,MonthView比起來,DayNumber就是小弟了,具體的繪製代碼以下:
Widget _dayItem() {
return Container(
width: widget.size - itemMargin * 2,
height: widget.size - itemMargin * 2,
margin: EdgeInsets.all(itemMargin),
alignment: Alignment.center,
decoration: (isSelected && widget.day > 0)
? BoxDecoration(color: Colors.blue)
: widget.isToday ? BoxDecoration(color: widget.todayColor) : null,
child: Text(
widget.day < 1 ? '' : widget.day.toString(),
textAlign: TextAlign.center,
style: TextStyle(
color: (widget.isToday || isSelected) ? Colors.white : Colors.black87,
fontSize: 15.0,
fontWeight: FontWeight.normal,
),
),
);
}
複製代碼
其中Container裏面聲明瞭decoration,經過BoxDecoration設置了背景色,代碼中把選中的效果優先於today高亮色,這樣就能夠覆蓋當天的顏色,具體的Day則是Text繪製的。
經過上面的描述,咱們瞭解了Calendar,MonthView,DayNumber三者的關係,核心代碼差很少就這些吧。
下面,咱們再看看單選,多選,反選,取消這些邏輯是怎麼實現的
代碼有點長,先貼出來,而後咱們分析一下:
// 選項處理回調
void onSelectDayChanged(dateTime) {
if (selectStartTime == null && selectEndTime == null) {
selectStartTime = dateTime;
} else if (selectStartTime != null && selectEndTime == null) {
selectEndTime = dateTime;
// 若是選擇的開始日期和結束日期相等,則清除選項
if (selectStartTime == selectEndTime) {
setState(() {
selectStartTime = null;
selectEndTime = null;
});
return;
}
// 若是用戶反選,則交換開始和結束日期
if (selectStartTime?.isAfter(selectEndTime)) {
DateTime temp = selectStartTime;
selectStartTime = selectEndTime;
selectEndTime = temp;
}
} else if (selectStartTime != null && selectEndTime != null) {
selectStartTime = null;
selectEndTime = null;
selectStartTime = dateTime;
}
setState(() {
selectStartTime;
selectEndTime;
});
}
複製代碼
onSelectDayChanged其實就是對用戶點擊DayNumber行爲的事件回調,這是一個典型的子組件調用父組件改變其狀態的代碼段,經過selectStartTime和selectEndTime是否爲null判斷用戶的點擊行爲落在哪一個if else裏面,經過setState從新設置開始和結束日期,這樣就能夠「刷新」MonthView裏面的DayNumber選擇範圍,好了,大體的核心源碼就分析到這裏。
本例中相關的代碼放在
github地址:github.com/heruijun/fl…
此例已經做爲補充內容添加至個人《Flutter從0到1構建大前端應用》一書的源碼中,是一個知識點比較多的綜合案例,再版時會根據讀者意見考慮加入到書中講解。
你們好,下面插播一條廣告,我是《Flutter從0到1構建大前端應用》的做者,感謝已經購買的讀者,此書屬於入門上手的書籍,以簡單明瞭的代碼實例說明問題,也便於讀者查閱相關內容。
從Flutter基礎開始講解,結合實際案例,讓讀者逐步掌握Flutter的核心內容,實戰項目篇又經過2個實戰項目讓讀者除了掌握Flutter相關知識以外,對node、mongo,vue作了一些介紹,可讓更多的讀者擁抱目前最火的大前端技術。
京東購買連接:item.jd.com/12546599.ht…