到底
@@ -0,0 +1,2 @@
|
||||
/build
|
||||
google-services.json
|
||||
@@ -0,0 +1,60 @@
|
||||
import org.ajoberstar.grgit.Grgit
|
||||
|
||||
plugins {
|
||||
id(Android.ApplicationPluginId)
|
||||
kotlin(Kotlin.AndroidPluginId)
|
||||
id("common-android-plugin")
|
||||
}
|
||||
|
||||
val commitsCount = Grgit.open(mapOf("dir" to rootDir)).log().size
|
||||
|
||||
android {
|
||||
namespace = "io.github.boguszpawlowski.composecalendar.sample"
|
||||
defaultConfig {
|
||||
versionCode = commitsCount
|
||||
versionName = "0.0.1"
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources {
|
||||
excludes.addAll(listOf("META-INF/AL2.0", "META-INF/LGPL2.1"))
|
||||
}
|
||||
}
|
||||
|
||||
// compileOptions {
|
||||
// isCoreLibraryDesugaringEnabled = true
|
||||
// }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// coreLibraryDesugaring(Kotlin.DesugarJdkLibs)
|
||||
|
||||
// implementation(project(":library"))
|
||||
implementation(project(":kotlinx-datetime"))
|
||||
|
||||
implementation("io.github.boguszpawlowski.composecalendar:composecalendar:1.3.0")
|
||||
|
||||
implementation(Material.Core)
|
||||
|
||||
implementation(AndroidX.AppCompat)
|
||||
implementation(AndroidX.ComposeActivity)
|
||||
|
||||
implementation(Compose.Runtime)
|
||||
implementation(Compose.Navigation)
|
||||
implementation(Compose.Ui)
|
||||
implementation(Compose.UiTooling)
|
||||
implementation(Compose.Foundation)
|
||||
implementation(Compose.FoundationLayout)
|
||||
implementation(Compose.Material)
|
||||
implementation(Timber.Core)
|
||||
|
||||
debugImplementation(Debug.LeakCanary)
|
||||
debugImplementation(Hyperion.Core)
|
||||
debugImplementation(Hyperion.Crash)
|
||||
debugImplementation(Hyperion.GeigerCounter)
|
||||
debugImplementation(Hyperion.Measurement)
|
||||
debugImplementation(ComposeTest.Manifest)
|
||||
|
||||
androidTestImplementation(ComposeTest.Core)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.kts.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,50 @@
|
||||
package io.github.boguszpawlowski.composecalendar.sample
|
||||
|
||||
import androidx.compose.ui.test.assertTextEquals
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performTouchInput
|
||||
import androidx.compose.ui.test.swipeLeft
|
||||
import androidx.compose.ui.test.swipeRight
|
||||
import io.github.boguszpawlowski.composecalendar.StaticCalendar
|
||||
import io.github.boguszpawlowski.composecalendar.rememberCalendarState
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.time.YearMonth
|
||||
|
||||
internal class ScrollBehaviorTest {
|
||||
|
||||
private val initialMonth = YearMonth.of(2012, 5)
|
||||
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
@Test
|
||||
fun monthChangeTest() {
|
||||
composeTestRule.setContent {
|
||||
StaticCalendar(
|
||||
calendarState = rememberCalendarState(initialMonth = initialMonth)
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag("Decrement").performClick()
|
||||
composeTestRule.onNodeWithTag("MonthLabel", true).assertTextEquals("April")
|
||||
composeTestRule.onNodeWithTag("Increment").performClick()
|
||||
composeTestRule.onNodeWithTag("MonthLabel", true).assertTextEquals("May")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun scrollMonthChangeTest() {
|
||||
composeTestRule.setContent {
|
||||
StaticCalendar(
|
||||
calendarState = rememberCalendarState(initialMonth = initialMonth)
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag("MonthPager").performTouchInput { swipeLeft() }
|
||||
composeTestRule.onNodeWithTag("MonthLabel", true).assertTextEquals("June")
|
||||
composeTestRule.onNodeWithTag("MonthPager").performTouchInput { swipeRight() }
|
||||
composeTestRule.onNodeWithTag("MonthLabel", true).assertTextEquals("May")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.ComposeCalendar">
|
||||
<activity
|
||||
android:name="io.github.boguszpawlowski.composecalendar.sample.MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,104 @@
|
||||
package io.github.boguszpawlowski.composecalendar.sample
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.github.boguszpawlowski.composecalendar.StaticCalendar
|
||||
import io.github.boguszpawlowski.composecalendar.day.NonSelectableDayState
|
||||
import io.github.boguszpawlowski.composecalendar.header.MonthState
|
||||
import java.time.DayOfWeek
|
||||
import java.time.DayOfWeek.SUNDAY
|
||||
import java.time.format.TextStyle.NARROW
|
||||
import java.util.Locale
|
||||
|
||||
@Composable
|
||||
fun CustomComponentsSample() {
|
||||
StaticCalendar(
|
||||
modifier = Modifier.animateContentSize(),
|
||||
showAdjacentMonths = false,
|
||||
firstDayOfWeek = SUNDAY,
|
||||
monthContainer = { MonthContainer(it) },
|
||||
dayContent = { DayContent(dayState = it) },
|
||||
daysOfWeekHeader = { WeekHeader(daysOfWeek = it) },
|
||||
monthHeader = { MonthHeader(monthState = it) },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DayContent(dayState: NonSelectableDayState) {
|
||||
Text(
|
||||
text = dayState.date.dayOfMonth.toString(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.h6,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WeekHeader(daysOfWeek: List<DayOfWeek>) {
|
||||
Row {
|
||||
daysOfWeek.forEach { dayOfWeek ->
|
||||
Text(
|
||||
textAlign = TextAlign.Center,
|
||||
text = dayOfWeek.getDisplayName(NARROW, Locale.ROOT),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.wrapContentHeight()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MonthHeader(monthState: MonthState) {
|
||||
Row {
|
||||
Text(monthState.currentMonth.year.toString(), style = MaterialTheme.typography.h3)
|
||||
Text(monthState.currentMonth.month.name, style = MaterialTheme.typography.h3)
|
||||
IconButton(onClick = { monthState.currentMonth = monthState.currentMonth.plusMonths(1) }) {
|
||||
Image(
|
||||
imageVector = Icons.Default.Star,
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colors.onSurface),
|
||||
contentDescription = "Next",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MonthContainer(content: @Composable (PaddingValues) -> Unit) {
|
||||
Card(
|
||||
elevation = 0.dp,
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
border = BorderStroke(1.dp, Color.LightGray),
|
||||
content = { content(PaddingValues(4.dp)) },
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun CustomComponentsPreview() {
|
||||
MaterialTheme {
|
||||
Surface {
|
||||
CustomComponentsSample()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package io.github.boguszpawlowski.composecalendar.sample
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import io.github.boguszpawlowski.composecalendar.Calendar
|
||||
import io.github.boguszpawlowski.composecalendar.CalendarState
|
||||
import io.github.boguszpawlowski.composecalendar.header.MonthState
|
||||
import io.github.boguszpawlowski.composecalendar.selection.SelectionState
|
||||
import java.time.LocalDate
|
||||
import java.time.YearMonth
|
||||
|
||||
@Composable
|
||||
fun CustomSelectionSample() {
|
||||
Calendar(calendarState = rememberMonthSelectionState())
|
||||
}
|
||||
|
||||
private class MonthSelectionState(
|
||||
initialSelection: YearMonth? = null,
|
||||
) : SelectionState {
|
||||
|
||||
private var selection by mutableStateOf(initialSelection)
|
||||
|
||||
override fun isDateSelected(date: LocalDate): Boolean =
|
||||
date.yearMonth == selection
|
||||
|
||||
override fun onDateSelected(date: LocalDate) {
|
||||
selection = if (date.yearMonth == selection) null else date.yearMonth
|
||||
}
|
||||
|
||||
companion object {
|
||||
@Suppress("FunctionName") // Factory function
|
||||
fun Saver(): Saver<MonthSelectionState, Any> = Saver(
|
||||
save = { it.selection?.toString() },
|
||||
restore = { restored ->
|
||||
val selection = (restored as? String)?.let { YearMonth.parse(it) }
|
||||
MonthSelectionState(selection)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberMonthSelectionState(
|
||||
initialMonth: YearMonth = YearMonth.now(),
|
||||
minMonth: YearMonth = initialMonth.minusMonths(10000),
|
||||
maxMonth: YearMonth = initialMonth.plusMonths(10000),
|
||||
initialSelection: YearMonth? = null,
|
||||
monthState: MonthState = rememberSaveable(saver = MonthState.Saver()) {
|
||||
MonthState(
|
||||
initialMonth = initialMonth,
|
||||
minMonth = minMonth,
|
||||
maxMonth = maxMonth
|
||||
)
|
||||
},
|
||||
selectionState: MonthSelectionState = rememberSaveable(saver = MonthSelectionState.Saver()) {
|
||||
MonthSelectionState(initialSelection = initialSelection)
|
||||
}
|
||||
): CalendarState<MonthSelectionState> = remember { CalendarState(monthState, selectionState) }
|
||||
|
||||
private val LocalDate.yearMonth: YearMonth
|
||||
get() = YearMonth.of(year, month)
|
||||
@@ -0,0 +1,103 @@
|
||||
package io.github.boguszpawlowski.composecalendar.sample
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.github.boguszpawlowski.composecalendar.SelectableCalendar
|
||||
import io.github.boguszpawlowski.composecalendar.kotlinxDateTime.now
|
||||
import io.github.boguszpawlowski.composecalendar.rememberSelectableCalendarState
|
||||
import io.github.boguszpawlowski.composecalendar.selection.DynamicSelectionState
|
||||
import io.github.boguszpawlowski.composecalendar.selection.SelectionMode.Multiple
|
||||
import io.github.boguszpawlowski.composecalendar.selection.SelectionState
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.toJavaLocalDate
|
||||
import kotlinx.datetime.toKotlinLocalDate
|
||||
|
||||
@Composable
|
||||
fun KotlinXDateTimeSample() {
|
||||
var selection by remember { mutableStateOf(emptyList<LocalDate>()) }
|
||||
|
||||
Column {
|
||||
DateTimeCalendar(
|
||||
today = LocalDate.now(),
|
||||
onSelectionChanged = { selection = it },
|
||||
dayContent = { DayContent(dayState = it) },
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = "Selection: ${selection.joinToString { it.toString() }}",
|
||||
style = MaterialTheme.typography.h6,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BoxScope.DayContent(
|
||||
dayState: KotlinDayState<DynamicSelectionState>,
|
||||
) {
|
||||
val isSelected = dayState.selectionState.isDateSelected(dayState.date)
|
||||
|
||||
Text(
|
||||
text = dayState.date.dayOfMonth.toString(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.Center)
|
||||
.aspectRatio(1f)
|
||||
.clickable {
|
||||
dayState.selectionState.onDateSelected(dayState.date)
|
||||
},
|
||||
color = if (isSelected) Color.Red else Color.Unspecified,
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.h6,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DateTimeCalendar(
|
||||
today: LocalDate,
|
||||
onSelectionChanged: (List<LocalDate>) -> Unit,
|
||||
dayContent: @Composable BoxScope.(KotlinDayState<DynamicSelectionState>) -> Unit,
|
||||
) {
|
||||
SelectableCalendar(
|
||||
calendarState = rememberSelectableCalendarState(
|
||||
confirmSelectionChange = { selection -> onSelectionChanged(selection.map { it.toKotlinLocalDate() }); true },
|
||||
initialSelectionMode = Multiple,
|
||||
),
|
||||
today = today.toJavaLocalDate(),
|
||||
showAdjacentMonths = false,
|
||||
dayContent = { dayState ->
|
||||
dayContent(
|
||||
KotlinDayState(
|
||||
date = dayState.date.toKotlinLocalDate(),
|
||||
isCurrentDay = dayState.isCurrentDay,
|
||||
selectionState = dayState.selectionState,
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
data class KotlinDayState<T : SelectionState>(
|
||||
val date: LocalDate,
|
||||
val isCurrentDay: Boolean,
|
||||
val selectionState: T,
|
||||
)
|
||||
|
||||
private fun SelectionState.isDateSelected(date: LocalDate) = isDateSelected(date.toJavaLocalDate())
|
||||
private fun SelectionState.onDateSelected(date: LocalDate) = onDateSelected(date.toJavaLocalDate())
|
||||
@@ -0,0 +1,113 @@
|
||||
package io.github.boguszpawlowski.composecalendar.sample
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.darkColors
|
||||
import androidx.compose.material.lightColors
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import io.github.boguszpawlowski.composecalendar.BuildConfig
|
||||
import timber.log.Timber
|
||||
import timber.log.Timber.DebugTree
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.plant(DebugTree())
|
||||
}
|
||||
setContent {
|
||||
MainScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MainScreen() {
|
||||
MaterialTheme(
|
||||
colors = if (isSystemInDarkTheme()) darkColors() else lightColors()
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
val navController = rememberNavController()
|
||||
|
||||
NavHost(navController = navController, startDestination = "main") {
|
||||
composable("main") { MainMenu(navController = navController) }
|
||||
composable("static") { StaticCalendarSample() }
|
||||
composable("week") { WeekCalendarSample() }
|
||||
composable("selection") { SelectableCalendarSample() }
|
||||
composable("components") { CustomComponentsSample() }
|
||||
composable("custom_selection") { CustomSelectionSample() }
|
||||
composable("viewmodel") { ViewModelSample() }
|
||||
composable("kotlinx_datetime") { KotlinXDateTimeSample() }
|
||||
composable("min_max_month") { MinMaxCalendarSample() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MainMenu(navController: NavController) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Button(onClick = { navController.navigate("static") }) {
|
||||
Text(text = "Static Calendar")
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Button(onClick = { navController.navigate("selection") }) {
|
||||
Text(text = "Selectable Calendar")
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Button(onClick = { navController.navigate("week") }) {
|
||||
Text(text = "Week Calendar")
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Button(onClick = { navController.navigate("components") }) {
|
||||
Text(text = "Custom Components")
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Button(onClick = { navController.navigate("custom_selection") }) {
|
||||
Text(text = "Custom Selection")
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Button(onClick = { navController.navigate("viewmodel") }) {
|
||||
Text(text = "ViewModel")
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Button(onClick = { navController.navigate("kotlinx_datetime") }) {
|
||||
Text(text = "Kotlinx DateTime")
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Button(onClick = { navController.navigate("min_max_month") }) {
|
||||
Text(text = "Min Max Month")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
package io.github.boguszpawlowski.composecalendar.sample
|
||||
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.github.boguszpawlowski.composecalendar.CalendarState
|
||||
import io.github.boguszpawlowski.composecalendar.StaticCalendar
|
||||
import io.github.boguszpawlowski.composecalendar.StaticWeekCalendar
|
||||
import io.github.boguszpawlowski.composecalendar.WeekCalendarState
|
||||
import io.github.boguszpawlowski.composecalendar.rememberCalendarState
|
||||
import io.github.boguszpawlowski.composecalendar.rememberWeekCalendarState
|
||||
import io.github.boguszpawlowski.composecalendar.selection.EmptySelectionState
|
||||
import io.github.boguszpawlowski.composecalendar.week.Week
|
||||
import java.time.YearMonth
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@Composable
|
||||
fun MinMaxCalendarSample() {
|
||||
val calendarState = rememberCalendarState(
|
||||
initialMonth = YearMonth.now(),
|
||||
minMonth = YearMonth.now(),
|
||||
maxMonth = YearMonth.now()
|
||||
)
|
||||
|
||||
val weekCalendarState = rememberWeekCalendarState(
|
||||
initialWeek = Week.now(),
|
||||
minWeek = Week.now(),
|
||||
maxWeek = Week.now(),
|
||||
)
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
StaticCalendar(calendarState = calendarState)
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
MinMaxControls(calendarState = calendarState)
|
||||
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
|
||||
StaticWeekCalendar(calendarState = weekCalendarState)
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
WeekMinMaxControls(calendarState = weekCalendarState)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MinMaxControls(
|
||||
calendarState: CalendarState<EmptySelectionState>,
|
||||
) {
|
||||
val dateFormatter = remember { DateTimeFormatter.ofPattern("yyyy-MM") }
|
||||
Text(
|
||||
text = "Calendar Min Month",
|
||||
style = MaterialTheme.typography.h5,
|
||||
)
|
||||
MinControls(
|
||||
calendarState = calendarState,
|
||||
dateFormatter = dateFormatter
|
||||
)
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
Text(
|
||||
text = "Calendar Max Month",
|
||||
style = MaterialTheme.typography.h5,
|
||||
)
|
||||
MaxControls(
|
||||
calendarState = calendarState,
|
||||
dateFormatter = dateFormatter
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MinControls(
|
||||
calendarState: CalendarState<EmptySelectionState>,
|
||||
dateFormatter: DateTimeFormatter,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
calendarState.monthState.minMonth = calendarState.monthState.minMonth.minusMonths(1L)
|
||||
}
|
||||
.size(25.dp)
|
||||
.border(1.dp, Color.Black),
|
||||
text = "-",
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
val minYearMonthText = remember(calendarState.monthState.minMonth) {
|
||||
dateFormatter.format(calendarState.monthState.minMonth)
|
||||
}
|
||||
Text(text = minYearMonthText)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
calendarState.monthState.minMonth = calendarState.monthState.minMonth.plusMonths(1L)
|
||||
}
|
||||
.size(25.dp)
|
||||
.border(1.dp, Color.Black),
|
||||
text = "+",
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MaxControls(
|
||||
calendarState: CalendarState<EmptySelectionState>,
|
||||
dateFormatter: DateTimeFormatter,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
calendarState.monthState.maxMonth = calendarState.monthState.maxMonth.minusMonths(1L)
|
||||
}
|
||||
.size(25.dp)
|
||||
.border(1.dp, Color.Black),
|
||||
text = "-",
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
val maxYearMonthText = remember(calendarState.monthState.maxMonth) {
|
||||
dateFormatter.format(calendarState.monthState.maxMonth)
|
||||
}
|
||||
Text(text = maxYearMonthText)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
calendarState.monthState.maxMonth = calendarState.monthState.maxMonth.plusMonths(1L)
|
||||
}
|
||||
.size(25.dp)
|
||||
.border(1.dp, Color.Black),
|
||||
text = "+",
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WeekMinMaxControls(
|
||||
calendarState: WeekCalendarState<EmptySelectionState>,
|
||||
) {
|
||||
val dateFormatter = remember { DateTimeFormatter.ofPattern("yyyy-MM-dd") }
|
||||
Text(
|
||||
text = "Calendar Min Week",
|
||||
style = MaterialTheme.typography.h5,
|
||||
)
|
||||
WeekMinControls(
|
||||
calendarState = calendarState,
|
||||
dateFormatter = dateFormatter
|
||||
)
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
Text(
|
||||
text = "Calendar Max Week",
|
||||
style = MaterialTheme.typography.h5,
|
||||
)
|
||||
WeekMaxControls(
|
||||
calendarState = calendarState,
|
||||
dateFormatter = dateFormatter
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WeekMinControls(
|
||||
calendarState: WeekCalendarState<EmptySelectionState>,
|
||||
dateFormatter: DateTimeFormatter,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
calendarState.weekState.minWeek = --calendarState.weekState.minWeek
|
||||
}
|
||||
.size(25.dp)
|
||||
.border(1.dp, Color.Black),
|
||||
text = "-",
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
val minYearMonthText = remember(calendarState.weekState.minWeek) {
|
||||
val start = dateFormatter.format(calendarState.weekState.minWeek.start)
|
||||
val end = dateFormatter.format(calendarState.weekState.minWeek.end)
|
||||
"$start - $end"
|
||||
}
|
||||
Text(text = minYearMonthText)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
calendarState.weekState.minWeek = ++calendarState.weekState.minWeek
|
||||
}
|
||||
.size(25.dp)
|
||||
.border(1.dp, Color.Black),
|
||||
text = "+",
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WeekMaxControls(
|
||||
calendarState: WeekCalendarState<EmptySelectionState>,
|
||||
dateFormatter: DateTimeFormatter,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
calendarState.weekState.maxWeek = --calendarState.weekState.maxWeek
|
||||
}
|
||||
.size(25.dp)
|
||||
.border(1.dp, Color.Black),
|
||||
text = "-",
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
val maxYearMonthText = remember(calendarState.weekState.maxWeek) {
|
||||
val start = dateFormatter.format(calendarState.weekState.maxWeek.start)
|
||||
val end = dateFormatter.format(calendarState.weekState.maxWeek.end)
|
||||
"$start - $end"
|
||||
}
|
||||
Text(text = maxYearMonthText)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
calendarState.weekState.maxWeek = ++calendarState.weekState.maxWeek
|
||||
}
|
||||
.size(25.dp)
|
||||
.border(1.dp, Color.Black),
|
||||
text = "+",
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package io.github.boguszpawlowski.composecalendar.sample
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.RadioButton
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.github.boguszpawlowski.composecalendar.SelectableCalendar
|
||||
import io.github.boguszpawlowski.composecalendar.rememberSelectableCalendarState
|
||||
import io.github.boguszpawlowski.composecalendar.selection.DynamicSelectionState
|
||||
import io.github.boguszpawlowski.composecalendar.selection.SelectionMode
|
||||
|
||||
@Composable
|
||||
fun SelectableCalendarSample() {
|
||||
val calendarState = rememberSelectableCalendarState()
|
||||
|
||||
Column(
|
||||
Modifier.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
SelectableCalendar(calendarState = calendarState)
|
||||
|
||||
SelectionControls(selectionState = calendarState.selectionState)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SelectionControls(
|
||||
selectionState: DynamicSelectionState,
|
||||
) {
|
||||
Text(
|
||||
text = "Calendar Selection Mode",
|
||||
style = MaterialTheme.typography.h5,
|
||||
)
|
||||
SelectionMode.values().forEach { selectionMode ->
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
RadioButton(
|
||||
selected = selectionState.selectionMode == selectionMode,
|
||||
onClick = { selectionState.selectionMode = selectionMode }
|
||||
)
|
||||
Text(text = selectionMode.name)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "Selection: ${selectionState.selection.joinToString { it.toString() }}",
|
||||
style = MaterialTheme.typography.h6,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package io.github.boguszpawlowski.composecalendar.sample
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.github.boguszpawlowski.composecalendar.StaticCalendar
|
||||
|
||||
@Composable
|
||||
fun StaticCalendarSample() {
|
||||
StaticCalendar(
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.animateContentSize(),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package io.github.boguszpawlowski.composecalendar.sample
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.contentColorFor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.github.boguszpawlowski.composecalendar.SelectableCalendar
|
||||
import io.github.boguszpawlowski.composecalendar.day.DayState
|
||||
import io.github.boguszpawlowski.composecalendar.header.MonthState
|
||||
import io.github.boguszpawlowski.composecalendar.rememberSelectableCalendarState
|
||||
import io.github.boguszpawlowski.composecalendar.selection.DynamicSelectionState
|
||||
import io.github.boguszpawlowski.composecalendar.selection.SelectionMode.Period
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.time.LocalDate
|
||||
import java.time.YearMonth
|
||||
import kotlin.random.Random
|
||||
import kotlin.random.nextLong
|
||||
|
||||
/**
|
||||
* In this sample, calendar composable is wired with an ViewModel. It's purpose is to show how to use
|
||||
* the composable in real world use-case, by an example implementation of a calendar
|
||||
* which can display planned recipes along with their prices. The recipes will also be re-fetched from
|
||||
* fake API every time month changes.
|
||||
*/
|
||||
@Composable
|
||||
fun ViewModelSample() {
|
||||
val viewModel = remember { RecipeViewModel() }
|
||||
val recipes by viewModel.recipesFlow.collectAsState()
|
||||
val selectedPrice by viewModel.selectedRecipesPriceFlow.collectAsState(0)
|
||||
val monthState = rememberMonthStateDefault()
|
||||
LaunchedEffect(monthState) {
|
||||
snapshotFlow { monthState.currentMonth }
|
||||
.onEach { viewModel.onMonthChanged(it) }
|
||||
.launchIn(this)
|
||||
}
|
||||
val state = rememberSelectableCalendarState(
|
||||
confirmSelectionChange = { viewModel.onSelectionChanged(it); true },
|
||||
monthState = monthState,
|
||||
initialSelectionMode = Period,
|
||||
)
|
||||
Column(
|
||||
Modifier.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
SelectableCalendar(
|
||||
calendarState = state,
|
||||
dayContent = { dayState ->
|
||||
RecipeDay(
|
||||
state = dayState,
|
||||
plannedRecipe = recipes.firstOrNull { it.date == dayState.date },
|
||||
)
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
Text(
|
||||
text = "Selected recipes price: $selectedPrice",
|
||||
style = MaterialTheme.typography.h6,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberMonthStateDefault() = rememberSaveable(saver = MonthState.Saver()) {
|
||||
MonthState(
|
||||
initialMonth = YearMonth.now(),
|
||||
minMonth = YearMonth.now().minusMonths(10000),
|
||||
maxMonth = YearMonth.now().plusMonths(10000),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom implementation of DayContent, which shows a dot if there is an recipe planned for this day.
|
||||
*/
|
||||
@Composable
|
||||
fun RecipeDay(
|
||||
state: DayState<DynamicSelectionState>,
|
||||
plannedRecipe: PlannedRecipe?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val date = state.date
|
||||
val selectionState = state.selectionState
|
||||
|
||||
val isSelected = selectionState.isDateSelected(date)
|
||||
|
||||
Card(
|
||||
modifier = modifier
|
||||
.aspectRatio(1f)
|
||||
.padding(2.dp),
|
||||
elevation = if (state.isFromCurrentMonth) 4.dp else 0.dp,
|
||||
border = if (state.isCurrentDay) BorderStroke(1.dp, MaterialTheme.colors.primary) else null,
|
||||
contentColor = if (isSelected) MaterialTheme.colors.secondary else contentColorFor(
|
||||
backgroundColor = MaterialTheme.colors.surface
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.clickable {
|
||||
selectionState.onDateSelected(date)
|
||||
},
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(text = date.dayOfMonth.toString())
|
||||
if (plannedRecipe != null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(10.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colors.primary)
|
||||
)
|
||||
Text(
|
||||
text = plannedRecipe.price.toString(),
|
||||
fontSize = 8.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class PlannedRecipe(
|
||||
val date: LocalDate,
|
||||
val price: Double,
|
||||
)
|
||||
|
||||
/**
|
||||
* ViewModel exposing list of our recipes
|
||||
*/
|
||||
class RecipeViewModel : ViewModel() {
|
||||
|
||||
private val selectionFlow = MutableStateFlow(emptyList<LocalDate>())
|
||||
val recipesFlow = MutableStateFlow(getRecipes())
|
||||
val selectedRecipesPriceFlow = recipesFlow.combine(selectionFlow) { recipes, selection ->
|
||||
recipes.filter { it.date in selection }.sumOf { it.price }
|
||||
}
|
||||
|
||||
fun onSelectionChanged(selection: List<LocalDate>) {
|
||||
selectionFlow.value = selection
|
||||
}
|
||||
|
||||
fun onMonthChanged(newMonth: YearMonth) = viewModelScope.launch {
|
||||
recipesFlow.value = getRecipesFromApi(startingDay = newMonth.atDay(15))
|
||||
}
|
||||
|
||||
private fun getRecipes(startingDay: LocalDate = LocalDate.now()) = listOf(
|
||||
PlannedRecipe(startingDay.plusDays(Random.nextLong(1L..3L)), 20.0),
|
||||
PlannedRecipe(startingDay.plusDays(Random.nextLong(4L..7L)), 20.0),
|
||||
PlannedRecipe(startingDay.plusDays(Random.nextLong(8L..11L)), 10.0),
|
||||
PlannedRecipe(startingDay.plusDays(Random.nextLong(-5L..-1L)), 25.0),
|
||||
)
|
||||
|
||||
/**
|
||||
* Simulated API call for new recipes
|
||||
*/
|
||||
private suspend fun getRecipesFromApi(startingDay: LocalDate) = withContext(Dispatchers.Default) {
|
||||
delay(100)
|
||||
getRecipes(startingDay)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ViewModelSamplePreview() {
|
||||
MaterialTheme {
|
||||
ViewModelSample()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package io.github.boguszpawlowski.composecalendar.sample
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.RadioButton
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.github.boguszpawlowski.composecalendar.SelectableWeekCalendar
|
||||
import io.github.boguszpawlowski.composecalendar.rememberSelectableWeekCalendarState
|
||||
import io.github.boguszpawlowski.composecalendar.selection.DynamicSelectionState
|
||||
import io.github.boguszpawlowski.composecalendar.selection.SelectionMode
|
||||
|
||||
@Composable
|
||||
fun WeekCalendarSample() {
|
||||
val calendarState = rememberSelectableWeekCalendarState()
|
||||
|
||||
Column(
|
||||
Modifier.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
SelectableWeekCalendar(calendarState = calendarState)
|
||||
|
||||
SelectionControls(selectionState = calendarState.selectionState)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SelectionControls(
|
||||
selectionState: DynamicSelectionState,
|
||||
) {
|
||||
Text(
|
||||
text = "Calendar Selection Mode",
|
||||
style = MaterialTheme.typography.h5,
|
||||
)
|
||||
SelectionMode.values().forEach { selectionMode ->
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
RadioButton(
|
||||
selected = selectionState.selectionMode == selectionMode,
|
||||
onClick = { selectionState.selectionMode = selectionMode }
|
||||
)
|
||||
Text(text = selectionMode.name)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "Selection: ${selectionState.selection.joinToString { it.toString() }}",
|
||||
style = MaterialTheme.typography.h6,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
@@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?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="com.boguszpawlowski.composecalendar.MainActivity">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Hello World!"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,16 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.ComposeCalendar" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_200</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/black</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_200</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">Compose Calendar</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,16 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.ComposeCalendar" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_500</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_700</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
||||