今天要來講述的是Android 中 Calendar View (日曆元件)
日曆元件這個東西,也不知道該算常出現呢...還是算不常XD
沒辦法...因為平常在使用其他的APP時也很少看到麻(´・д・`)
那我為何會講?因為這也是工作上不小心用到了麻(・∀・)
Calendar類其實Google是有內建給你的
但是我當時在寫開發的時候,也不曉得是沒研究透徹還是怎樣,一直都找不到自己想要的功能
而我要的功能簡單來說就是在相應的日期上面標註個Icon而已,很常見麻
...黑啦,有這麼簡單就好了(茶🍵...)
所以我在這邊是使用了一個Calencar的第三方開源庫完成的
雖然啦,現在回頭再來做事覺得阿不就這樣而已
不過在當下還是會覺得OOXX
好滴,接著就要進入主題了
首先是今天的功能
以及GitHub
->https://github.com/thumbb13555/CalenderViewDemo
1.載入需要的開源庫
前面本次的內容我並非使用原生的Calendar完成
那麼就來提供庫的地址吧
https://github.com/Applandeo/Material-Calendar-View
Google搜尋的話搜尋Material-Calendar-View就可以找到摟
那請在目錄下的Gradle Scripts->build.gradle(Module: app)->dependencies加入這行
implementation 'com.applandeo:material-calendar-view:1.7.0'
2.寫介面
這個開源庫的Readme中其實有蠻詳盡的說明,甚至連範例都幫你寫好了( ゚Д゚)b
關於在xml檔案中的設定在這邊,我後面會說,但也可以先自己參考
->https://github.com/Applandeo/Material-Calendar-View#colors-customization
PO我今天的範例xml擋案吧
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <Button android:id="@+id/button_GetTheDay" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="24dp" android:text="取得日期" app:layout_constraintEnd_toStartOf="@+id/button_Today" app:layout_constraintTop_toBottomOf="@+id/calendarView" /> <com.applandeo.materialcalendarview.CalendarView android:id="@+id/calendarView" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginTop="16dp" android:layout_marginEnd="16dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:type="one_day_picker" app:headerColor="#FF9800" app:todayLabelColor="#FF9800" app:selectionLabelColor="#FFFFFF" app:selectionColor="#FF9800" app:eventsEnabled="true" /> <Button android:id="@+id/button_SetTarget" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:text="設置標記" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@+id/button_GetTheDay" /> <Button android:id="@+id/button_CancelTarget" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="解除標記" app:layout_constraintStart_toEndOf="@+id/button_SetTarget" app:layout_constraintTop_toTopOf="@+id/button_GetTheDay" /> <Button android:id="@+id/button_Today" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="16dp" android:text="回到今日" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@+id/button_GetTheDay" /> </androidx.constraintlayout.widget.ConstraintLayout>
在xml內可以先行設定calendar顯示設定
app:type="one_day_picker" app:headerColor="#FF9800" app:todayLabelColor="#FF9800" app:selectionLabelColor="#FFFFFF" app:selectionColor="#FF9800" app:eventsEnabled="true"
...簡單英文還看得懂吧?我就不詳細講囉XD
注意的是,type的部分共有
app:type="one_day_picker"
app:type="many_days_picker"
app:type="range_picker"
三種,其他的設定上方給的連結也都有喔,去翻閱一下吧XD
這時候按下執行~~
啊,閃退惹(╯=▃=)╯︵┻━┻
淦(・∀・)凸
原因下面會講,先進到下一章吧
3.撰寫主要內容
這邊開始講述我每個按鈕背後所做的事情
我會把每個程式拆開來講
那如果不想看我廢話的朋友
請直接拉到最下面複製吧!我會貼完整程式
3-1 修正閃退
到這邊大家就要翻一下Log日誌了
在日誌中應該有這個關鍵字
java.lang.RuntimeException: Unable to start activity ComponentInfo
這個原因是在於,這個開源庫的Calendar View本身呈現上就是個耗時的工作執行緒
如果將之放在主執行緒的onCreate中,就會造成閃退。
那要怎麼處理?一個比較直覺的作法就是不要放在主執行緒就好啦XDDDDDDD
所以我就乾脆這樣寫了(白眼
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ProgressDialog dialog = ProgressDialog.show(this, "", "請稍候"); new Thread(() -> { /**由於此開源庫的Calender為耗時工作,故加入背景執行使載入介面時不會閃退*/ runOnUiThread(() -> { setContentView(R.layout.activity_main); dialog.dismiss(); }); }).start(); }
再來就是按鈕的四個功能了,簡單描述一下
由左至右分別是"設置標記"、"刪除標記"、"取得(當下所選的)日期"、"回到今日"
這四項功能
設置標記就是在選到的日期上加入Saved的icon
刪除標記就是上面那個的反向操作
取得日期是指點下後會在Toast上顯示該選擇到的日期
回到今日就是跳回今天的日期
大致就是這樣...一項一項講吧!從取得日期開始
3-2 取得日期
先來看看這功能怎麼寫的
/**取得選中之日期*/ btGetDay.setOnClickListener(v -> { /**利用forEach迴圈找出指定元素*/ for (Calendar calendar : calendarView.getSelectedDates()) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd"); Toast.makeText(this, sdf.format(calendar.getTime()), Toast.LENGTH_SHORT).show(); } });
Calendar View 的呈現原理實際上就是陣列+RecyclerView+ViewPager的應用
故要找出相應的日期的話,就是用從Calendar中撈出所需要的那個“它”
於是我們便使用Foreach迴圈,就可以找出該值
然後再加上SimpleDateFormat就可以顯示出希望的格式了
3-3 回到(跳回)今日
一樣先看程式
/**跳至今日(指定)日期*/ btToday.setOnClickListener(v -> { /**取得今日之Date*/ Date date = new Date(); Calendar calendar = Calendar.getInstance(); SimpleDateFormat sdfY = new SimpleDateFormat("yyyy"); SimpleDateFormat sdfM = new SimpleDateFormat("MM"); SimpleDateFormat sdfD = new SimpleDateFormat("dd"); /**calender設置為今日*/ calendar.set(Integer.parseInt(sdfY.format(date)) , Integer.parseInt(sdfM.format(date)) - 1 , Integer.parseInt(sdfD.format(date))); try { /**刷新介面*/ calendarView.setDate(calendar); } catch (OutOfDateRangeException e) { e.printStackTrace(); } });
關鍵是
calendar.set(Integer.parseInt(sdfY.format(date)) , Integer.parseInt(sdfM.format(date)) - 1 , Integer.parseInt(sdfD.format(date)));
這一行
這行他的功能就是將整個介面跳回指定日期(包還指定跳回今日)
那要如何跳到指定的時間呢?答案就在Date()的部分
只要改變Date()設定的時間,就可以跳到不一樣的時過囉
再來是設置Icon的部分
3-4 設置標記
在開始前,先去找出存擋的這個Icon吧
這個Icon是Android studio 送你的
新增:
drawable->New->Vector Asset
點擊Clip Art
最後按回OK就可以囉(´∀`)
來看程式吧
List<EventDay> event = new ArrayList<>(); /**設置標記*/ btSetTarget.setOnClickListener(v -> { new Thread(() -> { /**利用forEach迴圈找出指定元素*/ for (Calendar calendar : calendarView.getSelectedDates()) { /**取得選定日之Date*/ calendar.setTime(calendar.getTime()); /**在event陣列中新增一個元素*/ event.add(new EventDay(calendar, R.drawable.ic_baseline_save_24)); runOnUiThread(() -> { /**刷新介面*/ calendarView.setEvents(event); }); } }).start(); });
其實也是一樣的套路
1. 用Foreach找出日期
2. 選定指定日期的時間
3. Event陣列中新增一個元素
就是這樣而已
那重點是這行
event.add(new EventDay(calendar, R.drawable.ic_baseline_save_24));
EventDay是一個實體類,把他加入event陣列而已,就是這樣(・ωー)~☆
3-5 刪除標記
看程式
/**解除標記*/ btClearTarget.setOnClickListener(v -> { new Thread(() -> { /**利用forEach迴圈找出指定元素*/ for (Calendar calendar : calendarView.getSelectedDates()) { /**取得選定日之Date*/ calendar.setTime(calendar.getTime()); /**利用for迴圈找出指定元素之index*/ for (int i = 0; i < event.size(); i++) { Long select = calendar.getTimeInMillis(); Long target = event.get(i).getCalendar().getTimeInMillis(); if (select.equals(target)){ /**刪除指定元素*/ event.remove(i); runOnUiThread(() -> { /**刷新介面*/ calendarView.setEvents(event); }); } } } }).start(); });
這裡的重點除了跟上面的套路一樣之外,重點是刪除指定元素
所以在第三個註解中,我寫了個for迴圈找出相應的index
然後套入event的陣列,並刪除之
好啦最後就是完整程式啦
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ProgressDialog dialog = ProgressDialog.show(this, "", "請稍候"); new Thread(() -> { /**由於此開源庫的Calender為耗時工作,故加入背景執行使載入介面時不會閃退*/ runOnUiThread(() -> { setContentView(R.layout.activity_main); dialog.dismiss(); setView(); }); }).start(); } private void setView() { Button btSetTarget, btClearTarget, btToday, btGetDay; CalendarView calendarView = findViewById(R.id.calendarView); btSetTarget = findViewById(R.id.button_SetTarget); btClearTarget = findViewById(R.id.button_CancelTarget); btToday = findViewById(R.id.button_Today); btGetDay = findViewById(R.id.button_GetTheDay); List<EventDay> event = new ArrayList<>(); /**設置標記*/ btSetTarget.setOnClickListener(v -> { new Thread(() -> { /**利用forEach迴圈找出指定元素*/ for (Calendar calendar : calendarView.getSelectedDates()) { /**取得選定日之Date*/ calendar.setTime(calendar.getTime()); /**在event陣列中新增一個元素*/ event.add(new EventDay(calendar, R.drawable.ic_baseline_save_24)); runOnUiThread(() -> { /**刷新介面*/ calendarView.setEvents(event); }); } }).start(); }); /**解除標記*/ btClearTarget.setOnClickListener(v -> { new Thread(() -> { /**利用forEach迴圈找出指定元素*/ for (Calendar calendar : calendarView.getSelectedDates()) { /**取得選定日之Date*/ calendar.setTime(calendar.getTime()); /**利用for迴圈找出指定元素之index*/ for (int i = 0; i < event.size(); i++) { Long select = calendar.getTimeInMillis(); Long target = event.get(i).getCalendar().getTimeInMillis(); if (select.equals(target)){ /**刪除指定元素*/ event.remove(i); runOnUiThread(() -> { /**刷新介面*/ calendarView.setEvents(event); }); } } } }).start(); }); /**取得選中之日期*/ btGetDay.setOnClickListener(v -> { /**利用forEach迴圈找出指定元素*/ for (Calendar calendar : calendarView.getSelectedDates()) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd"); Toast.makeText(this, sdf.format(calendar.getTime()), Toast.LENGTH_SHORT).show(); } }); /**跳至今日(指定)日期*/ btToday.setOnClickListener(v -> { /**取得今日之Date*/ Date date = new Date(); Calendar calendar = Calendar.getInstance(); SimpleDateFormat sdfY = new SimpleDateFormat("yyyy"); SimpleDateFormat sdfM = new SimpleDateFormat("MM"); SimpleDateFormat sdfD = new SimpleDateFormat("dd"); /**calender設置為今日*/ calendar.set(Integer.parseInt(sdfY.format(date)) , Integer.parseInt(sdfM.format(date)) - 1 , Integer.parseInt(sdfD.format(date))); try { /**刷新介面*/ calendarView.setDate(calendar); } catch (OutOfDateRangeException e) { e.printStackTrace(); } }); } }
結語
本質上,用了第三方庫感覺就輸了
也沒有啦,第三方庫就是給人類帶來方便的東西,是人類偉大的發明(笑)
如果有做出來的朋友,在運行的時候應該多少會發現
每次在進行操作的時候,多多少少都會畫面頓一下
這姑且算是這個庫的問題,因為上面也說過了
Calendar View的本質就是用陣列+RecyclerView+ViewPager組成的
在遍歷的過程中會拖到速度也算是正常現象
了不起就加個Therad就可以解決啦XDDDDD
好啦,那文章到這邊