前面已經透過pubspec.yaml
加入http
, provider
等功能了,我們已經對於新增套件不陌生
今天要來介紹UI相關的套件,先閱讀官方文件,一起嘗試在專案的「月曆頁」中加入月曆套件並且改成我們需要的樣子
好的,那我們就開始吧!
首先將table_calendar加入pubspec.yaml
, 並下載套件
$ flutter pub add table_calendar
$ flutter pub get
在文件中我們看到要引用TableCalendar
至少需要三個參數
因此我們在CalendarPage
先使用TableCalendar
並輸入必要參數
class _CalendarPageState extends State<CalendarPage> {
@override
Widget build(BuildContext context) {
DateTime current = DateTime.now();
return Center(
child: TableCalendar(
firstDay: DateTime.utc(current.year - 2, 01, 01),
lastDay: DateTime.utc(current.year + 2, 12, 31),
focusedDay: DateTime.now(),
));
}
}
目標是距離現今前後兩年的每日圖片
接著加入一些互動的元素:畫面會依照我們選取的日期互動
class _CalendarPageState extends State<CalendarPage> {
DateTime _selectedDay = DateTime.now();
DateTime _focusedDay = DateTime.now();
@override
Widget build(BuildContext context) {
DateTime current = DateTime.now();
return Center(
child: TableCalendar(
firstDay: DateTime.utc(current.year - 2, 01, 01),
lastDay: DateTime.utc(current.year + 2, 12, 31),
focusedDay: _focusedDay,
selectedDayPredicate: (day) {
return isSameDay(_selectedDay, day);
},
onDaySelected: (selectedDay, focusedDay) {
setState(() {
_selectedDay = selectedDay; // 選取的日期
_focusedDay = focusedDay; // 頁面停留的日期
});
},
));
}
現在頁面可以依照我們的點選而變化,接著我們要客製化月曆上的日期UI,顯示日期的同時也要顯示一些標示。為此使用此套件的calendarBuilder
功能,更多詳細設定見 CalendarBuilders class
calendarBuilders: CalendarBuilders(
defaultBuilder: (context, day, focusedDay) =>
Center(child: Text(day.day.toString())),
),
如上例的設定,會讓客製化的UI跟目前的月曆完全一樣
我們要讓日期稍微偏上,並在下方加入一個小圓點,紅色代表未查看過當日的圖片,綠色代表已查看。我們先將每日的方格組件客製化,挖好空格準備依照資料給予property
class CalendarDayCell extends StatefulWidget {
const CalendarDayCell(
{super.key,
required this.day,
this.decoration,
this.hasDot = false,
this.dotColor = Colors.red});
final DateTime day;
final Decoration? decoration;
final bool hasDot;
final Color dotColor;
@override
State<CalendarDayCell> createState() => _CalendarDayCellState();
}
class _CalendarDayCellState extends State<CalendarDayCell> {
@override
Widget build(BuildContext context) {
return Center(
child: Column(children: [
Container(
width: 35.0,
height: 35.0,
padding: const EdgeInsets.all(5.0),
decoration: widget.decoration,
child: Center(
child: Text(
widget.day.day.toString(),
style: const TextStyle(fontSize: 16),
),
),
),
const SizedBox(
height: 2,
),
widget.hasDot
? Container(
width: 5.0,
height: 5.0,
decoration: BoxDecoration(
color: widget.dotColor,
shape: BoxShape.circle,
),
)
: Container(),
]),
);
}
}
在tableCalendar中的calendarBuilder加入剛才的客製化組件
calendarBuilders: CalendarBuilders(
defaultBuilder: (context, day, focusedDay) =>
Center(child: CalendarDayCell(day: day)),
selectedBuilder: (context, day, focusedDay) => Center(
child: CalendarDayCell(
day: day,
decoration: BoxDecoration(
color: Colors.blue[300],
shape: BoxShape.circle,
))),
todayBuilder: (context, day, focusedDay) => CalendarDayCell(
day: day,
decoration: BoxDecoration(
color: Colors.blue[100],
shape: BoxShape.circle,
)))));
加入長按功能:長按之後要導頁到對應的每日圖片頁
onDayLongPressed: ((selectedDay, focusedDay) {
// 長按日期時導向該日的天文圖片頁面
Navigator.push(context, MaterialPageRoute(builder: (context) {
return Scaffold(
appBar: AppBar(
title: Text(
'${selectedDay.year}-${selectedDay.month}-${selectedDay.day}')),
body: const AstroPicture(
title: '',
pictureUrl: '',
desc: '',
note: '',
isFavorite: false));
}));
call API 取得整月份資料,再依照長按的日期注入資料
class CalendarPage extends StatefulWidget {
const CalendarPage({super.key});
@override
State<CalendarPage> createState() => _CalendarPageState();
}
class _CalendarPageState extends State<CalendarPage> {
DateTime _selectedDay = DateTime.now();
DateTime _focusedDay = DateTime.now();
DateTime current = DateTime.now();
bool _isLoading = false;
final Map<String, ApodData> _dailyApodMap = {};
@override
void initState() {
super.initState();
String firstDayofMonth =
formatDate(DateTime(current.year, current.month, 1));
_fetchDailyApodData(firstDayofMonth, formatDate(current));
}
_fetchDailyApodData(String startDate, String endDate) async {
if (_dailyApodMap[startDate] != null && _dailyApodMap[endDate] != null) {
// 已經前後時間的資料,代表當月都抓過了,不必再call api抓一次
return;
}
setState(() {
_isLoading = true;
});
Uri url = Uri.parse(
'$apodEndpoint?api_key=$apiKey&thumbs=true&start_date=$startDate&end_date=$endDate');
final response = await http.get(url, headers: {
'Content-type': 'application/json',
'Accept': 'application/json',
});
final parsedResponse = json.decode(response.body) as List<dynamic>;
List<Map<String, dynamic>> dataList =
parsedResponse.map((data) => data as Map<String, dynamic>).toList();
for (var data in dataList) {
_dailyApodMap[data['date']] = ApodData.fromJson(data);
}
setState(() {
_isLoading = false;
});
}
String formatDate(DateTime datetime) {
return '${datetime.year}-${datetime.month < 10 ? '0${datetime.month}' : datetime.month}-${datetime.day < 10 ? '0${datetime.day}' : datetime.day}';
}
@override
Widget build(BuildContext context) {
return Center(
child: _isLoading
? const CircularProgressIndicator()
: TableCalendar(
headerStyle: const HeaderStyle(formatButtonVisible: false),
firstDay: DateTime.utc(current.year - 2, 01, 01),
lastDay: DateTime.now(),
focusedDay: _focusedDay,
selectedDayPredicate: (day) {
return isSameDay(_selectedDay, day);
},
onPageChanged: (focusedDay) {
setState(() {
_focusedDay = focusedDay;
});
if (focusedDay.month <= current.month) {
String lastDayOfMonth = formatDate(
DateTime(focusedDay.year, focusedDay.month + 1, 0));
_fetchDailyApodData(
formatDate(focusedDay),
DateTime(focusedDay.year, focusedDay.month + 1, 0)
.isAfter(current)
? formatDate(current)
: lastDayOfMonth);
}
},
onDaySelected: (selectedDay, focusedDay) {
setState(() {
_selectedDay = selectedDay; // 選取的日期
_focusedDay = focusedDay; // 頁面停留的日期
});
},
onDayLongPressed: ((selectedDay, focusedDay) {
String date = formatDate(focusedDay);
// 長按日期時導向該日的天文圖片頁面
Navigator.push(context, MaterialPageRoute(builder: (context) {
return Scaffold(
appBar: AppBar(title: Text(formatDate(selectedDay))),
body: AstroPicture(
title: _dailyApodMap[date]?.title ?? '',
pictureUrl: _dailyApodMap[date]?.url ?? '',
desc: _dailyApodMap[date]?.desc ?? '',
note: '',
isFavorite: false));
}));
}),
calendarBuilders: CalendarBuilders(
defaultBuilder: (context, day, focusedDay) =>
Center(child: CalendarDayCell(day: day)),
selectedBuilder: (context, day, focusedDay) => Center(
child: CalendarDayCell(
day: day,
decoration: BoxDecoration(
color: Colors.blue[300],
shape: BoxShape.circle,
))),
todayBuilder: (context, day, focusedDay) => CalendarDayCell(
day: day,
decoration: BoxDecoration(
color: Colors.blue[100],
shape: BoxShape.circle,
)))));
}
}
今天示範了如何應用第三方的UI套件,未來如果要做各式各樣的APP,在pub.dev
上搜尋一下有沒有別人寫好的套件,說不定可以省下大量的時間,避免重複造輪子。
不過,使用時要先詳閱該套件的license,避免違法和侵權的行為喔!
明天一起來看看如何將寫好的資料存在手機裡吧!
想問一下 在 7. 加入長按功能:長按之後要導頁到對應的每日圖片頁
body: const AstroPicture(
title: '',
pictureUrl: '',
desc: '',
note: '',
isFavorite: false));
AstroPicture 不是應該傳入 apodData嗎?