Implement swipeable month view with HorizontalPager and assemble app

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
xfy 2026-05-14 13:29:18 +08:00
parent edf881d1cc
commit eb8b16047a
4 changed files with 129 additions and 47 deletions

View File

@ -1,49 +1,15 @@
package plus.rua.project
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.safeContentPadding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import org.jetbrains.compose.resources.painterResource
import yaya.shared.generated.resources.Res
import yaya.shared.generated.resources.compose_multiplatform
import plus.rua.project.ui.CalendarMonthView
@Composable
@Preview
fun App() {
MaterialTheme {
var showContent by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.background(MaterialTheme.colorScheme.primaryContainer)
.safeContentPadding()
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Button(onClick = { showContent = !showContent }) {
Text("Click me!")
}
AnimatedVisibility(showContent) {
val greeting = remember { Greeting().greet() }
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(painterResource(Res.drawable.compose_multiplatform), null)
Text("Compose: $greeting")
}
}
}
CalendarMonthView(modifier = Modifier)
}
}
}

View File

@ -1,9 +0,0 @@
package plus.rua.project
class Greeting {
private val platform = getPlatform()
fun greet(): String {
return "Hello, ${platform.name}!"
}
}

View File

@ -0,0 +1,43 @@
package plus.rua.project.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.todayIn
import kotlin.time.Clock
import plus.rua.project.CalendarViewModel
@Composable
fun CalendarMonthView(
viewModel: CalendarViewModel = remember { CalendarViewModel() },
modifier: Modifier = Modifier
) {
val today = remember { Clock.System.todayIn(TimeZone.currentSystemDefault()) }
var currentYear by remember { mutableIntStateOf(viewModel.currentYear) }
var currentMonth by remember { mutableIntStateOf(viewModel.currentMonth) }
Column(modifier = modifier.fillMaxSize()) {
MonthHeader(
year = currentYear,
month = currentMonth,
weekNumber = viewModel.getIsoWeekNumber(viewModel.selectedDate)
)
CalendarPager(
selectedDate = viewModel.selectedDate,
today = today,
onDateClick = { date -> viewModel.selectDate(date) },
onMonthChanged = { year, month ->
currentYear = year
currentMonth = month
},
modifier = Modifier.weight(1f)
)
}
}

View File

@ -0,0 +1,82 @@
package plus.rua.project.ui
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerDefaults
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import kotlinx.coroutines.launch
import kotlinx.datetime.LocalDate
private const val START_PAGE = Int.MAX_VALUE / 2
@Composable
fun CalendarPager(
selectedDate: LocalDate,
today: LocalDate,
onDateClick: (LocalDate) -> Unit,
onMonthChanged: (year: Int, month: Int) -> Unit,
modifier: Modifier = Modifier
) {
val initialYearMonth = remember { today.toYearMonth() }
val pagerState = rememberPagerState(
initialPage = START_PAGE,
pageCount = { Int.MAX_VALUE }
)
val coroutineScope = rememberCoroutineScope()
// Sync settled page to onMonthChanged
LaunchedEffect(pagerState) {
snapshotFlow { pagerState.settledPage }.collect { page ->
val yearMonth = pageToYearMonth(page, initialYearMonth)
onMonthChanged(yearMonth.first, yearMonth.second)
}
}
HorizontalPager(
state = pagerState,
beyondViewportPageCount = 1,
flingBehavior = PagerDefaults.flingBehavior(state = pagerState),
modifier = modifier
) { page ->
val (year, month) = pageToYearMonth(page, initialYearMonth)
CalendarMonthPage(
year = year,
month = month,
selectedDate = selectedDate,
today = today,
onDateClick = { date ->
onDateClick(date)
// If clicking a date in a different month, scroll to that page
val clickedYearMonth = date.toYearMonth()
if (clickedYearMonth != pageToYearMonth(page, initialYearMonth)) {
val targetPage = yearMonthToPage(clickedYearMonth, initialYearMonth)
if (targetPage != pagerState.currentPage) {
coroutineScope.launch {
pagerState.animateScrollToPage(targetPage)
}
}
}
}
)
}
}
@Suppress("DEPRECATION")
private fun LocalDate.toYearMonth(): Pair<Int, Int> = Pair(year, monthNumber)
private fun pageToYearMonth(page: Int, initial: Pair<Int, Int>): Pair<Int, Int> {
val offset = page - START_PAGE
val totalMonths = initial.first * 12 + (initial.second - 1) + offset
return Pair(totalMonths / 12, totalMonths % 12 + 1)
}
private fun yearMonthToPage(yearMonth: Pair<Int, Int>, initial: Pair<Int, Int>): Int {
val targetTotal = yearMonth.first * 12 + (yearMonth.second - 1)
val initialTotal = initial.first * 12 + (initial.second - 1)
return START_PAGE + (targetTotal - initialTotal)
}