今天我們來創建一個月曆元件,然後將其利用我們昨天做的GoogleCalendarAuth.kt功能進行抓取自己的Google行事曆上的資料,並將其顯示在月曆元件上,今天的目的主要是確認我們昨天的事前準備是否能成功運行和為之後的雙向同步做準備。
在建構月曆元件的時候我們會使用Accompanist
來幫助我們建構,Accompanist是一組為Jetpack Compose提供擴展功能的 Android開源庫。由於Jetpack Compose是一個相對新的框架,某些功能尚未在官方Compose中實現或完善,而 Accompanist 提供了這些額外的功能以彌補不足,例如 Pager、Insets、Glide 影像載入等。
為了使用Accompanist,我們需要在build.gradle(APP)
添加添加 Accompanist 的依賴項
dependencies {
implementation("com.google.accompanist:accompanist-pager:0.32.0") // 提供 Jetpack Compose 的 Pager 組件
implementation("com.google.accompanist:accompanist-pager-indicators:0.32.0") // 提供 Pager 組件的指示器
implementation("com.google.accompanist:accompanist-insets:0.30.1") // 處理視窗插入(如狀態欄、導航欄)的組件
implementation("com.google.accompanist:accompanist-coil:0.15.0") // 提供與 Coil 圖片加載庫的整合,用於 Jetpack Compose
implementation("com.google.accompanist:accompanist-swiperefresh:0.32.0") // 提供下拉刷新組件
implementation("com.google.accompanist:accompanist-navigation-animation:0.32.0") // 提供 Jetpack Compose 中的導航動畫效果
implementation("com.google.accompanist:accompanist-flowlayout:0.32.0") // 提供 FlowLayout 排版方式,讓組件能夠自動換行
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4") //使用 Coroutine 在背景線程中執行 fetchEvents
}
程式碼先附上
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.rememberPagerState
import java.util.Calendar
import java.util.Locale
@OptIn(ExperimentalPagerApi::class)
@Composable
fun CalendarPager() {
val pagerState = rememberPagerState()
HorizontalPager(
count = 12, // 一年有12個月
state = pagerState,
modifier = Modifier.fillMaxSize()
) { page ->
// 使用 Calendar 來取得月份與年份
val calendar = Calendar.getInstance()
calendar.add(Calendar.MONTH, page)
val currentYear = calendar.get(Calendar.YEAR)
val currentMonth = calendar.getDisplayName(Calendar.MONTH, Calendar.LONG, Locale.getDefault()) ?: ""
// 取得今天的日期
val today = Calendar.getInstance()
val todayDay = today.get(Calendar.DAY_OF_MONTH)
val isCurrentMonth = (today.get(Calendar.MONTH) == calendar.get(Calendar.MONTH) && today.get(Calendar.YEAR) == calendar.get(Calendar.YEAR))
// 顯示日曆內容
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
.background(Color.LightGray),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceBetween // 確保元素之間均勻分配
) {
// 顯示月份標題
Text(
text = "$currentMonth $currentYear",
style = TextStyle(fontSize = 20.sp, fontWeight = FontWeight.Bold, color = Color.Black),
modifier = Modifier.padding(8.dp)
)
// 顯示星期標題
val daysOfWeek = listOf("日", "一", "二", "三", "四", "五", "六")
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
daysOfWeek.forEach { day ->
Text(
text = day,
style = TextStyle(fontSize = 16.sp, color = Color.Black),
modifier = Modifier.weight(1f),
textAlign = TextAlign.Center
)
}
}
// 計算當月天數和第一天的位置
val daysInMonth = calendar.getActualMaximum(Calendar.DAY_OF_MONTH)
calendar.set(Calendar.DAY_OF_MONTH, 1)
val firstDayOfMonth = (calendar.get(Calendar.DAY_OF_WEEK) + 5) % 7 // 調整為以日為第一天
val totalCells = daysInMonth + firstDayOfMonth
val rows = (totalCells / 7) + if (totalCells % 7 > 0) 1 else 0
// 顯示日期,將每一列的高度設定為 fillMaxHeight / rows
Column(
modifier = Modifier.fillMaxHeight(), // 讓日曆表格填滿可用垂直空間
verticalArrangement = Arrangement.SpaceEvenly // 使各列均勻分配垂直空間
) {
for (row in 0 until rows) {
Row(
modifier = Modifier
.fillMaxWidth()
.weight(1f), // 確保每一列均勻分配垂直空間
horizontalArrangement = Arrangement.SpaceBetween
) {
for (col in 0..6) {
val dayOfMonth = row * 7 + col - firstDayOfMonth + 1
Box(
modifier = Modifier
.weight(1f) // 使每個 Box 均勻分配水平空間
.aspectRatio(1f) // 確保每個 Box 是正方形
.padding(2.dp) // 為 Box 提供一點間距
.background(
if (isCurrentMonth && dayOfMonth == todayDay) Color.Blue else Color.Transparent,
shape = CircleShape
),
contentAlignment = Alignment.Center
) {
if (dayOfMonth in 1..daysInMonth) {
Text(
text = dayOfMonth.toString(),
style = TextStyle(
fontSize = 16.sp,
color = if (isCurrentMonth && dayOfMonth == todayDay) Color.White else Color.Black,
fontWeight = if (isCurrentMonth && dayOfMonth == todayDay) FontWeight.Bold else FontWeight.Normal
),
textAlign = TextAlign.Center
)
}
}
}
}
}
}
}
}
}
在這段程式碼中,Accompanist的Pager
提供了一個方便的方式來實作水平滑動的月曆效果,並且透過 HorizontalPager
和rememberPagerState()
管理每個月的頁面切換與狀態記錄。
然後在我們DAY10建立的UI設計中日曆的部分加入以下程式碼
-kotlin
// 右側:日曆,使用 CalendarPager 替換日曆顯示
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.padding(4.dp)
.background(Color.White),
contentAlignment = Alignment.Center
) {
CalendarPager() // 調用獨立的 CalendarPager 組件
}
}
}
這樣你應該能看到成果如下圖所示
為了同步行事曆,我們需要獲取我們GOOGLE行事曆上的資料,因此我們要對昨天的GoogleCalendarAuth.kt進行一些修改,修改後的程式碼如下
package com.example.a2024ironman
import android.app.Activity
import android.content.Intent
import android.util.Log
import androidx.activity.result.ActivityResultLauncher
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
import com.google.android.gms.common.api.ApiException
import com.google.api.client.extensions.android.http.AndroidHttp
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential
import com.google.api.services.calendar.Calendar
import com.google.api.services.calendar.CalendarScopes
import com.google.api.client.json.gson.GsonFactory
import com.google.api.services.calendar.model.Event
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import java.time.LocalDateTime
import java.time.OffsetDateTime
import java.time.format.DateTimeFormatter
import java.time.ZoneOffset
class GoogleCalendarAuth(
private val activity: Activity,
private val onSignInSuccess: (Boolean) -> Unit
) {
companion object {
const val RC_SIGN_IN = 1001
}
private var googleSignInClient = GoogleSignIn.getClient(
activity,
GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestEmail()
.requestScopes(com.google.android.gms.common.api.Scope(CalendarScopes.CALENDAR))
.build()
)
private var calendarService: Calendar? = null
// 開始 Google 登入流程
fun signIn(launcher: ActivityResultLauncher<Intent>) {
val signInIntent = googleSignInClient.signInIntent
launcher.launch(signInIntent)
}
// 在 onActivityResult 中處理登入結果
fun handleSignInResult(requestCode: Int, data: Intent?) {
if (requestCode == RC_SIGN_IN) {
val task = GoogleSignIn.getSignedInAccountFromIntent(data)
try {
val account = task.getResult(ApiException::class.java)
setupCalendarService(account)
Log.d("GoogleCalendarAuth", "Sign-in successful")
onSignInSuccess(true) // 成功時回調 true
} catch (e: ApiException) {
Log.e("GoogleCalendarAuth", "Sign-in failed, code=${e.statusCode}")
onSignInSuccess(false) // 失敗時回調 false
}
} else {
Log.d("GoogleCalendarAuth", "handleSignIAnResult not called")
}
}
// 設定 Google Calendar 服務
private fun setupCalendarService(account: GoogleSignInAccount?) {
val credential = GoogleAccountCredential.usingOAuth2(
activity, listOf(CalendarScopes.CALENDAR)
)
credential.selectedAccount = account?.account
calendarService = Calendar.Builder(
AndroidHttp.newCompatibleTransport(),
GsonFactory.getDefaultInstance(),
credential
).setApplicationName("YourAppName").build()
}
fun fetchEvents(
calendarId: String = "primary",
onEventsFetched: (List<Event>) -> Unit,
onError: (Exception) -> Unit
) {
CoroutineScope(Dispatchers.IO).launch {
try {
// 使用 OffsetDateTime 來獲取當前的 UTC 時間(包括時區偏移)
val now = OffsetDateTime.now(ZoneOffset.UTC)
// 設定 timeMin 為當前日期的 50 天前
val timeMin = now.minusDays(50).toInstant().toString()
// 設定 timeMax 為當前日期的 50 天後
val timeMax = now.plusDays(50).toInstant().toString()
val events = calendarService?.events()?.list(calendarId)
?.setMaxResults(100) // 限制最多讀取 100 個事件
?.setOrderBy("startTime")
?.setSingleEvents(true)
?.setTimeMin(com.google.api.client.util.DateTime(timeMin)) // 設置 timeMin 篩選
?.setTimeMax(com.google.api.client.util.DateTime(timeMax)) // 設置 timeMax 篩選
?.execute()
Log.d("GoogleCalendarAuth", "Fetched events: ${events?.items?.size}")
events?.items?.forEach {
Log.d("GoogleCalendarAuth", "Event: ${it.summary} at ${it.start.dateTime ?: it.start.date}")
}
// 將事件傳遞回主線程
withContext(Dispatchers.Main) {
onEventsFetched(events?.items ?: emptyList())
}
} catch (e: Exception) {
Log.e("GoogleCalendarAuth", "Error fetching events", e)
withContext(Dispatchers.Main) {
onError(e)
}
}
}
}
// 取得 Google Calendar 服務
fun getCalendarService(): Calendar? {
return calendarService
}
}
介於長度和時間的問題,這邊就簡單講一下GoogleCalendarAuth.kt新增的內容和修改的部分,舊程式使用activity.startActivityForResult
來處理 Google 登入,這是一種較舊的方式。
新程式則採用了 ActivityResultLauncher<Intent>
,讓signIn
方法可以透過 launcher.launch(signInIntent)
來啟動登入活動,並新增了fetchEvents
函式,用來擷取 Google Calendar 的事件。這個函式使用CoroutineScope
和Dispatchers.IO
在背景執行,確保事件抓取過程不會阻塞主執行緒,並在完成時透過withContext(Dispatchers.Main)
回到主執行緒,通知呼叫者結果,同時改使用 OffsetDateTime
與ZoneOffset.UTC
來處理時間,這比傳統的java.util.Calendar
更具現代性並且對時區處理更為簡潔。
在修改GoogleCalendarAuth.kt新增fetchEvents
函式來獲得Google行事曆的事件後,我們要回到CalendarPager.kt來進行相對應的修改,
修改後的程式碼如下
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import com.google.api.services.calendar.model.Event
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.rememberPagerState
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
import java.util.TimeZone
@OptIn(ExperimentalPagerApi::class)
@Composable
fun CalendarPager(events: List<Event>) {
val pagerState = rememberPagerState()
// 狀態變量來跟蹤選擇的日期與事件,以及是否顯示對話框
var selectedEvents by remember { mutableStateOf<List<Event>>(emptyList()) }
var selectedDate by remember { mutableStateOf<String?>(null) }
var showDialog by remember { mutableStateOf(false) } // 是否顯示對話框
HorizontalPager(
count = 12, // 一年有12個月
state = pagerState,
modifier = Modifier.fillMaxSize()
) { page ->
val calendar = Calendar.getInstance()
calendar.add(Calendar.MONTH, page)
val currentYear = calendar.get(Calendar.YEAR)
val currentMonth = calendar.getDisplayName(Calendar.MONTH, Calendar.LONG, Locale.getDefault()) ?: ""
val today = Calendar.getInstance()
val todayDay = today.get(Calendar.DAY_OF_MONTH)
val isCurrentMonth = (today.get(Calendar.MONTH) == calendar.get(Calendar.MONTH) && today.get(Calendar.YEAR) == calendar.get(Calendar.YEAR))
// 計算當月天數和第一天的位置
val daysInMonth = calendar.getActualMaximum(Calendar.DAY_OF_MONTH)
calendar.set(Calendar.DAY_OF_MONTH, 1)
val firstDayOfMonth = (calendar.get(Calendar.DAY_OF_WEEK) + 5) % 7
val totalCells = daysInMonth + firstDayOfMonth
val rows = (totalCells / 7) + if (totalCells % 7 > 0) 1 else 0
// 篩選出當月的事件,正確處理事件時間並考慮時區
val monthEvents = events.filter { event ->
val startDate = event.start.dateTime ?: event.start.date
val eventCalendar = Calendar.getInstance()
eventCalendar.timeZone = TimeZone.getTimeZone("UTC")
// 使用 SimpleDateFormat 解析事件日期
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
startDate.toString()?.let {
val parsedDate = dateFormat.parse(it)
if (parsedDate != null) {
eventCalendar.time = parsedDate
}
}
eventCalendar.get(Calendar.MONTH) == calendar.get(Calendar.MONTH) &&
eventCalendar.get(Calendar.YEAR) == calendar.get(Calendar.YEAR)
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
.background(Color.LightGray),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceBetween
) {
// 顯示月份標題
Text(
text = "$currentMonth $currentYear",
style = TextStyle(fontSize = 20.sp, fontWeight = FontWeight.Bold, color = Color.Black),
modifier = Modifier.padding(8.dp)
)
val daysOfWeek = listOf("日", "一", "二", "三", "四", "五", "六")
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
daysOfWeek.forEach { day ->
Text(
text = day,
style = TextStyle(fontSize = 16.sp, color = Color.Black),
modifier = Modifier.weight(1f),
textAlign = TextAlign.Center
)
}
}
Column(
modifier = Modifier.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceEvenly
) {
for (row in 0 until rows) {
Row(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
horizontalArrangement = Arrangement.SpaceBetween
) {
for (col in 0..6) {
val dayOfMonth = row * 7 + col - firstDayOfMonth + 1
val eventsForDay = monthEvents.filter { event ->
val startDate = event.start.dateTime ?: event.start.date
val eventCalendar = Calendar.getInstance()
eventCalendar.timeZone = TimeZone.getTimeZone("UTC")
// 使用 startDate.value 轉換為 Date
val parsedDate = Date(startDate.value)
eventCalendar.time = parsedDate
eventCalendar.get(Calendar.DAY_OF_MONTH) == dayOfMonth
}
Box(
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.padding(2.dp)
.background(
if (isCurrentMonth && dayOfMonth == todayDay) Color.Blue else Color.Transparent,
shape = CircleShape
)
.clickable {
// 點擊日期時更新選取的事件與日期,並顯示對話框
selectedDate = "$currentMonth $dayOfMonth, $currentYear"
selectedEvents = eventsForDay
showDialog = true
},
contentAlignment = Alignment.Center
) {
if (dayOfMonth in 1..daysInMonth) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = dayOfMonth.toString(),
style = TextStyle(
fontSize = 16.sp,
color = if (isCurrentMonth && dayOfMonth == todayDay) Color.White else Color.Black,
fontWeight = if (isCurrentMonth && dayOfMonth == todayDay) FontWeight.Bold else FontWeight.Normal
),
textAlign = TextAlign.Center
)
if (eventsForDay.isNotEmpty()) {
Text(
text = eventsForDay.first().summary,
style = TextStyle(fontSize = 10.sp, color = Color.Red),
maxLines = 1,
textAlign = TextAlign.Center
)
}
}
}
}
}
}
}
}
}
}
// 顯示選擇的日期事件彈出視窗
if (showDialog) {
Dialog(onDismissRequest = { showDialog = false }) {
Surface(
shape = MaterialTheme.shapes.medium,
color = Color.White,
modifier = Modifier.padding(16.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.Start
) {
Text(
text = "行程安排: $selectedDate",
style = TextStyle(fontSize = 20.sp, fontWeight = FontWeight.Bold),
modifier = Modifier.padding(bottom = 8.dp)
)
if (selectedEvents.isEmpty()) {
Text("當天沒有行程", style = TextStyle(fontSize = 16.sp, color = Color.Gray))
} else {
selectedEvents.forEach { event ->
Text("• ${event.summary}", style = TextStyle(fontSize = 16.sp, color = Color.Black))
}
}
Spacer(modifier = Modifier.height(16.dp))
// 關閉按鈕
Button(
onClick = { showDialog = false },
modifier = Modifier.align(Alignment.End)
) {
Text("關閉")
}
}
}
}
}
}
新的程式碼主要是在畫面上新增了從GOOGLE行事曆上獲取的事件,透過使用List<Event>
作為參數,並篩選當月的行程活動。點擊日期後,彈出視窗將顯示該日的行程摘要,讓使用者可以查看每個日期的詳細活動內容。
同時為了狀態管理,使用remember
建立selectedEvents
、selectedDate
和showDialog
狀態變量,這些狀態用於跟蹤選擇的日期、顯示相關事件,以及控制是否顯示彈出視窗,並使用SimpleDateFormat
來解析行程的開始日期,確保顯示的行程活動時間準確無誤。
最後我們修改一下我們的主程式,程式碼如下
class MainActivity : ComponentActivity() {
// 初始化 GoogleCalendarAuth
private var googleCalendarAuth: GoogleCalendarAuth? = null
// 用來處理登入結果的 launcher
private val signInLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
googleCalendarAuth?.handleSignInResult(GoogleCalendarAuth.RC_SIGN_IN, result.data)
} else {
Log.e("MainActivity", "Sign-in failed or canceled")
}
}
// 狀態變數以保存從 Google Calendar 獲取的事件
private var calendarEvents by mutableStateOf<List<Event>>(emptyList())
override fun onCreate(savedInstanceState: Bundle?) {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
super.onCreate(savedInstanceState)
// 初始化 GoogleCalendarAuth,並設定成功/失敗的回調
googleCalendarAuth = GoogleCalendarAuth(this) { success ->
if (success) {
Log.d("MainActivity", "Google Calendar Auth successful")
// 如果登入成功,開始同步 Google Calendar 事件
googleCalendarAuth?.fetchEvents(
onEventsFetched = { events ->
calendarEvents = events
Log.d("MainActivity", "Fetched ${events.size} events from Google Calendar")
},
onError = { error ->
Log.e("MainActivity", "Error fetching calendar events: ${error.message}")
}
)
} else {
Log.e("MainActivity", "Google Calendar Auth failed")
}
}
// 啟動 Google 登入流程
googleCalendarAuth?.signIn(signInLauncher)
setContent {
CalendarLayout()
}
}
@Composable
fun CalendarLayout() {
Row(modifier = Modifier.fillMaxSize().padding(8.dp)) {
// 左側:時間與代辦事項
Column(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.padding(4.dp)
) {
// 顯示時間區塊
Box(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.padding(4.dp)
.background(Color.LightGray),
contentAlignment = Alignment.Center
) {
Text(
text = "時間",
style = TextStyle(
fontSize = 20.sp,
color = Color.Black
)
)
}
Spacer(modifier = Modifier.height(8.dp))
// 顯示代辦事項區塊
Box(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.padding(4.dp)
.background(Color.LightGray),
contentAlignment = Alignment.Center
) {
Text(
text = "代辦",
style = TextStyle(
fontSize = 20.sp,
color = Color.Black
)
)
}
}
Spacer(modifier = Modifier.width(8.dp))
// 右側:日曆,將 calendarEvents 傳遞給 CalendarPager
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.padding(4.dp)
.background(Color.White),
contentAlignment = Alignment.Center
) {
// 傳入 calendarEvents 給 CalendarPager
CalendarPager(events = calendarEvents)
}
}
}
@Preview(
showBackground = true,
device = "spec:orientation=landscape,width=411dp,height=891dp"
)
@Composable
fun PreviewCalendarLayout() {
Surface {
CalendarLayout()
}
}
}
新程式加入了GoogleCalendarAuth
的初始化和Google Sign-In
授權登入流程,並使用 ActivityResultContracts.StartActivityForResult
來替代舊的startActivityForResult
,這是一種更現代的方式來處理登入結果。
定義了signInLauncher
,用來處理Google Sign-In
的結果,當登入成功後呼叫handleSignInResult
來處理登入結果。並使用fetchEvents
方法來擷取 Google Calendar 事件,將結果儲存在calendarEvents
狀態變數中,這樣 Compose UI 可以自動更新以顯示最新的事件。
為了驗證效果,我們在GOOGLE日曆上新增測試用的行程
呈現的結果應該如下圖所示
(其他行程就先請你們當沒看到)
點擊27號後會跳出視窗顯示今日得事件,如下圖所示
今天的內容就先到這邊,今天試著從GOOGLE行事曆終將事件抓下來同步到APP的日曆元件上,只能說中途繞了不少彎路,導致拖得比較晚一點(看看程式碼裡面的一堆LOG),感謝你能看到這邊,讓我們明天再見。