iT邦幫忙

2024 iThome 鐵人賽

DAY 13
0
Mobile Development

github裡永遠有一個還沒做的SideProject :用Kotlin來開發點沒用的酷東西系列 第 13

Day13:使用Accompanist建構行事曆功能,並同步Google行事曆上的資料

  • 分享至 

  • xImage
  •  

前言

今天我們來創建一個月曆元件,然後將其利用我們昨天做的GoogleCalendarAuth.kt功能進行抓取自己的Google行事曆上的資料,並將其顯示在月曆元件上,今天的目的主要是確認我們昨天的事前準備是否能成功運行和為之後的雙向同步做準備。

月曆元件的建構

1.使用Accompanist

在建構月曆元件的時候我們會使用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
}

2.建立CalendarPager.kt

程式碼先附上

  • kotlin
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提供了一個方便的方式來實作水平滑動的月曆效果,並且透過 HorizontalPagerrememberPagerState()管理每個月的頁面切換與狀態記錄。
然後在我們DAY10建立的UI設計中日曆的部分加入以下程式碼
-kotlin

 // 右側:日曆,使用 CalendarPager 替換日曆顯示
            Box(
                modifier = Modifier
                    .weight(1f)
                    .fillMaxHeight()
                    .padding(4.dp)
                    .background(Color.White),
                contentAlignment = Alignment.Center
            ) {
                CalendarPager() // 調用獨立的 CalendarPager 組件
            }
        }
    }

這樣你應該能看到成果如下圖所示
https://ithelp.ithome.com.tw/upload/images/20240927/20162649AI9aVCFvho.png

修改GoogleCalendarAuth.kt

為了同步行事曆,我們需要獲取我們GOOGLE行事曆上的資料,因此我們要對昨天的GoogleCalendarAuth.kt進行一些修改,修改後的程式碼如下

  • kotlin
 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 的事件。這個函式使用CoroutineScopeDispatchers.IO在背景執行,確保事件抓取過程不會阻塞主執行緒,並在完成時透過withContext(Dispatchers.Main)回到主執行緒,通知呼叫者結果,同時改使用 OffsetDateTimeZoneOffset.UTC來處理時間,這比傳統的java.util.Calendar更具現代性並且對時區處理更為簡潔。

修改CalendarPager.kt

在修改GoogleCalendarAuth.kt新增fetchEvents函式來獲得Google行事曆的事件後,我們要回到CalendarPager.kt來進行相對應的修改,
修改後的程式碼如下

  • kotlin
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建立selectedEventsselectedDateshowDialog狀態變量,這些狀態用於跟蹤選擇的日期、顯示相關事件,以及控制是否顯示彈出視窗,並使用SimpleDateFormat來解析行程的開始日期,確保顯示的行程活動時間準確無誤。

修改MainActivity.kt

最後我們修改一下我們的主程式,程式碼如下

  • kotlin
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日曆上新增測試用的行程
https://ithelp.ithome.com.tw/upload/images/20240927/20162649SBVlHK8kcm.png
呈現的結果應該如下圖所示
https://ithelp.ithome.com.tw/upload/images/20240927/20162649tjEPjNFTo6.png
(其他行程就先請你們當沒看到)
點擊27號後會跳出視窗顯示今日得事件,如下圖所示
https://ithelp.ithome.com.tw/upload/images/20240927/201626490aPfsL26VR.png

後話

今天的內容就先到這邊,今天試著從GOOGLE行事曆終將事件抓下來同步到APP的日曆元件上,只能說中途繞了不少彎路,導致拖得比較晚一點(看看程式碼裡面的一堆LOG),感謝你能看到這邊,讓我們明天再見。


上一篇
Day12:連結GOOGLE行事曆(2)
下一篇
Day14:建構單日行事曆頁面和使用夜間模式來設定UI
系列文
github裡永遠有一個還沒做的SideProject :用Kotlin來開發點沒用的酷東西30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言