From a1a61dd96c40c24dc40fee5de5ebf91e49da9440 Mon Sep 17 00:00:00 2001 From: Matteo Miceli Date: Fri, 14 Oct 2022 17:41:53 +0200 Subject: [PATCH 1/2] chore: fix edge case when starDate = maxDate --- .../composable/CalendarContent.kt | 87 +++++++++++-------- .../composable/CalendarMonthYearSelector.kt | 45 ++-------- 2 files changed, 57 insertions(+), 75 deletions(-) diff --git a/composecalendar/src/main/java/com/squaredem/composecalendar/composable/CalendarContent.kt b/composecalendar/src/main/java/com/squaredem/composecalendar/composable/CalendarContent.kt index afec053..45515af 100644 --- a/composecalendar/src/main/java/com/squaredem/composecalendar/composable/CalendarContent.kt +++ b/composecalendar/src/main/java/com/squaredem/composecalendar/composable/CalendarContent.kt @@ -16,12 +16,11 @@ package com.squaredem.composecalendar.composable +import android.util.Log import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -30,7 +29,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.snapshotFlow -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.text.font.FontWeight @@ -50,7 +48,6 @@ import java.time.LocalDate import java.time.format.TextStyle import java.util.* import kotlin.math.abs -import kotlin.math.absoluteValue import kotlin.math.ceil import kotlin.math.floor @@ -70,7 +67,10 @@ internal fun CalendarContent( val initialPage = getStartPage(startDate, dateRange, totalPageCount) val isPickingYear = remember { mutableStateOf(false) } + + // for display only, used in CalendarMonthYearSelector val currentPagerDate = remember { mutableStateOf(startDate.withDayOfMonth(1)) } + val selectedDate = remember { mutableStateOf(startDate) } val pagerState = rememberPagerState(initialPage) @@ -87,17 +87,8 @@ internal fun CalendarContent( if (!LocalInspectionMode.current) { LaunchedEffect(pagerState) { snapshotFlow { pagerState.targetPage }.collect { page -> - val pageDiff = page.minus(initialPage).absoluteValue.toLong() - - val date = if (page > initialPage) { - startDate.plusMonths(pageDiff) - } else if (page < initialPage) { - startDate.minusMonths(pageDiff) - } else { - startDate - } - - currentPagerDate.value = date + val currentDate = getDateFromCurrentPage(page, dateRange) + currentPagerDate.value = currentDate } } } @@ -109,12 +100,27 @@ internal fun CalendarContent( CalendarTopBar(selectedDate.value) CalendarMonthYearSelector( - coroutineScope, - pagerState, - currentPagerDate.value - ) { - isPickingYear.value = !isPickingYear.value - } + currentPagerDate.value, + onChipClicked = { isPickingYear.value = !isPickingYear.value }, + onNextMonth = { + coroutineScope.launch { + try { + pagerState.animateScrollToPage(pagerState.currentPage + 1) + } catch (e: Exception) { + // avoid IndexOutOfBounds and animation crashes + } + } + }, + onPreviousMonth = { + coroutineScope.launch { + try { + pagerState.animateScrollToPage(pagerState.currentPage - 1) + } catch (e: Exception) { + // avoid IndexOutOfBounds and animation crashes + } + } + } + ) if (!isPickingYear.value) { @@ -139,24 +145,18 @@ internal fun CalendarContent( count = totalPageCount, state = pagerState ) { page -> - val pageDiff = page.minus(initialPage).absoluteValue.toLong() - - val date = if (page > initialPage) { - startDate.plusMonths(pageDiff) - } else if (page < initialPage) { - startDate.minusMonths(pageDiff) - } else { - startDate + val currentDate = getDateFromCurrentPage(page, dateRange) + + currentDate?.let { + // grid + CalendarGrid( + it.withDayOfMonth(1), + dateRange, + selectedDate.value, + setSelectedDate, + true + ) } - - // grid - CalendarGrid( - date.withDayOfMonth(1), - dateRange, - selectedDate.value, - setSelectedDate, - true - ) } } else { @@ -217,6 +217,17 @@ private fun getDateRange(min: LocalDate, max: LocalDate): DateRange { return lowerBound.rangeTo(upperBound) step DateRangeStep.Month() } +private fun getDateFromCurrentPage( + currentPage: Int, + dateRange: DateRange, +): LocalDate? { + return try { + dateRange.elementAt(currentPage) + } catch (e: Exception) { + null + } +} + @Preview(showBackground = true) @Composable private fun Preview() { diff --git a/composecalendar/src/main/java/com/squaredem/composecalendar/composable/CalendarMonthYearSelector.kt b/composecalendar/src/main/java/com/squaredem/composecalendar/composable/CalendarMonthYearSelector.kt index fdd21c4..0c61971 100644 --- a/composecalendar/src/main/java/com/squaredem/composecalendar/composable/CalendarMonthYearSelector.kt +++ b/composecalendar/src/main/java/com/squaredem/composecalendar/composable/CalendarMonthYearSelector.kt @@ -35,19 +35,18 @@ import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.PagerState import com.squaredem.composecalendar.utils.LogCompositions import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch import java.text.SimpleDateFormat import java.time.LocalDate import java.time.OffsetTime import java.util.* -@OptIn(ExperimentalPagerApi::class, ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class) @Composable internal fun CalendarMonthYearSelector( - coroutineScope: CoroutineScope, - pagerState: PagerState, pagerDate: LocalDate, - onChipClicked: () -> Unit + onChipClicked: () -> Unit, + onNextMonth: () -> Unit, + onPreviousMonth: () -> Unit ) { LogCompositions("CalendarMonthYearSelector") @@ -73,39 +72,11 @@ internal fun CalendarMonthYearSelector( onClick = onChipClicked, ) Spacer(modifier = Modifier.weight(1F)) - IconButton( - onClick = { - coroutineScope.launch { - with(pagerState) { - try { - animateScrollToPage(currentPage - 1) - } catch (e: Exception) { - // avoid IOOB and animation crashes - } - } - } - } - ) { - Icon( - Icons.Default.ChevronLeft, "ChevronLeft" - ) + IconButton(onClick = onPreviousMonth) { + Icon(Icons.Default.ChevronLeft, "ChevronLeft") } - IconButton( - onClick = { - coroutineScope.launch { - with(pagerState) { - try { - animateScrollToPage(currentPage + 1) - } catch (e: Exception) { - // avoid IOOB and animation crashes - } - } - } - } - ) { - Icon( - Icons.Default.ChevronRight, "ChevronRight", - ) + IconButton(onClick = onNextMonth) { + Icon(Icons.Default.ChevronRight, "ChevronRight") } } } From c2222dcad3e1804f065acc4b9636ea49cd662c22 Mon Sep 17 00:00:00 2001 From: Matteo Miceli Date: Fri, 19 May 2023 15:00:41 +0200 Subject: [PATCH 2/2] chore: remove accompanist, update AGP, update dependencies --- .idea/androidTestResultsUserPreferences.xml | 51 ++++++++ .idea/compiler.xml | 2 +- .idea/inspectionProfiles/Project_Default.xml | 41 ++++++ .idea/kotlinc.xml | 6 + .idea/misc.xml | 3 +- README.md | 15 ++- app/build.gradle | 15 ++- app/src/main/AndroidManifest.xml | 4 +- .../composecalendardemo/MainActivity.kt | 34 +++-- build.gradle | 14 +- composecalendar/build.gradle | 17 +-- .../composecalendar/CalendarDayTest.kt | 101 +++++++++++++++ .../CalendarMonthYearSelectorTest.kt | 100 +++++++++++++++ .../ExampleInstrumentedTest.kt | 24 ---- composecalendar/src/main/AndroidManifest.xml | 21 --- .../composecalendar/ComposeCalendar.kt | 12 +- .../composable/CalendarContent.kt | 121 ++++++++++-------- .../composecalendar/composable/CalendarDay.kt | 16 +-- .../composable/CalendarGrid.kt | 2 - .../composable/CalendarMonthYearSelector.kt | 4 +- .../composable/CalendarTopBar.kt | 8 +- .../composable/CalendarYear.kt | 24 +++- .../composecalendar/model/DateWrapper.kt | 1 - gradle.properties | 4 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 25 files changed, 474 insertions(+), 168 deletions(-) create mode 100644 .idea/androidTestResultsUserPreferences.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/kotlinc.xml create mode 100644 composecalendar/src/androidTest/java/com/squaredem/composecalendar/CalendarDayTest.kt create mode 100644 composecalendar/src/androidTest/java/com/squaredem/composecalendar/CalendarMonthYearSelectorTest.kt delete mode 100644 composecalendar/src/androidTest/java/com/squaredem/composecalendar/ExampleInstrumentedTest.kt delete mode 100644 composecalendar/src/main/AndroidManifest.xml diff --git a/.idea/androidTestResultsUserPreferences.xml b/.idea/androidTestResultsUserPreferences.xml new file mode 100644 index 0000000..111e030 --- /dev/null +++ b/.idea/androidTestResultsUserPreferences.xml @@ -0,0 +1,51 @@ + + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml index fb7f4a8..b589d56 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..44ca2d9 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,41 @@ + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..0fc3113 --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 2a4d5b5..43ae24e 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,5 @@ - - + diff --git a/README.md b/README.md index e91b5b1..0a88d49 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ then add the latest ComposeCalendar version to your `app/build.gradle` file depe ```groovy dependencies { - implementation 'com.squaredem:composecalendar:1.0.4' + implementation 'com.squaredem:composecalendar:1.1.0' } ``` @@ -62,6 +62,19 @@ ComposeCalendar( > Note: currently min and max dates are being coerced respectively to year **1900** and **2100**. +### Top bar customization + +You can choose whether to show or hide the top bar with the selected date. +You can also pass a custom `DateFormat` for the string. + +```kotlin +ComposeCalendar( + ... + showSelectedDate = true, + selectedDateFormat = DateFormat.getDateInstance(DateFormat.DEFAULT) +) +``` + ## What's next - Date range selection (from date A to date B) diff --git a/app/build.gradle b/app/build.gradle index 684b719..c4cede1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,12 +4,12 @@ plugins { } android { - compileSdk 32 + compileSdk 33 defaultConfig { applicationId "com.squaredem.composecalendardemo" minSdk 26 - targetSdk 32 + targetSdk 33 versionCode 1 versionName "1.0" @@ -43,23 +43,24 @@ android { excludes += '/META-INF/{AL2.0,LGPL2.1}' } } + namespace 'composecalendardemo' } dependencies { - implementation "androidx.core:core-ktx:$corektx_version" + implementation "androidx.core:core-ktx:1.10.1" implementation "androidx.compose.ui:ui:$compose_version" implementation "androidx.compose.material3:material3:$material3_version" implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.0' - implementation 'androidx.activity:activity-compose:1.5.0' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' + implementation 'androidx.activity:activity-compose:1.7.1' implementation project(path: ':composecalendar') debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.3' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3a8c786..ddb091c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,6 @@ + xmlns:tools="http://schemas.android.com/tools"> diff --git a/app/src/main/java/com/squaredem/composecalendardemo/MainActivity.kt b/app/src/main/java/com/squaredem/composecalendardemo/MainActivity.kt index 2bc27f1..67f750a 100644 --- a/app/src/main/java/com/squaredem/composecalendardemo/MainActivity.kt +++ b/app/src/main/java/com/squaredem/composecalendardemo/MainActivity.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2022 Matteo Miceli + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.squaredem.composecalendardemo import android.os.Bundle @@ -7,6 +23,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -44,16 +61,16 @@ private fun MainActivityContent() { ) { val showDialog = rememberSaveable { mutableStateOf(false) } - val selectedDateMillis = rememberSaveable { mutableStateOf(null) } + val selectedDate = rememberSaveable { mutableStateOf(null) } Column( + modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - selectedDateMillis.value?.let { + selectedDate.value?.let { Text(text = it.toString()) } - Button(onClick = { showDialog.value = true }) { Text("Show dialog") } @@ -61,14 +78,13 @@ private fun MainActivityContent() { if (showDialog.value) { ComposeCalendar( - startDate = LocalDate.now(), - minDate = LocalDate.now(), - maxDate = LocalDate.MAX, - onDone = { it: LocalDate -> - selectedDateMillis.value = it + onDone = { + selectedDate.value = it showDialog.value = false }, - onDismiss = { showDialog.value = false } + onDismiss = { + showDialog.value = false + } ) } diff --git a/build.gradle b/build.gradle index 2e8cae6..f42dec3 100644 --- a/build.gradle +++ b/build.gradle @@ -1,17 +1,15 @@ buildscript { ext { - compose_compiler_version = '1.2.0' - compose_version = '1.3.0-alpha01' - material3_version = '1.0.0-alpha14' - corektx_version = '1.8.0' - accompanist_version = '0.25.1' + compose_compiler_version = '1.4.3' + compose_version = '1.4.3' + material3_version = '1.1.0' } } plugins { - id 'com.android.application' version '7.2.1' apply false - id 'com.android.library' version '7.2.1' apply false - id 'org.jetbrains.kotlin.android' version '1.7.0' apply false + id 'com.android.application' version '8.0.1' apply false + id 'com.android.library' version '8.0.1' apply false + id 'org.jetbrains.kotlin.android' version '1.8.10' apply false id 'io.github.gradle-nexus.publish-plugin' version "1.1.0" } diff --git a/composecalendar/build.gradle b/composecalendar/build.gradle index e490743..1c07aa1 100644 --- a/composecalendar/build.gradle +++ b/composecalendar/build.gradle @@ -20,11 +20,11 @@ plugins { } android { - compileSdk 32 + compileSdk 33 defaultConfig { minSdk 26 - targetSdk 32 + targetSdk 33 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" @@ -49,6 +49,7 @@ android { composeOptions { kotlinCompilerExtensionVersion compose_compiler_version } + namespace 'com.squaredem.composecalendar' } dependencies { @@ -64,18 +65,18 @@ dependencies { // compose-material implementation "androidx.compose.material:material-icons-extended:$compose_version" - // pager - implementation "com.google.accompanist:accompanist-pager:$accompanist_version" - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.3' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" + debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" + debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" } ext { PUBLISH_GROUP_ID = 'com.squaredem' PUBLISH_ARTIFACT_ID = 'composecalendar' - PUBLISH_VERSION = '1.0.4' + PUBLISH_VERSION = '1.1.0' } apply from: "${rootProject.projectDir}/scripts/publish-module.gradle" diff --git a/composecalendar/src/androidTest/java/com/squaredem/composecalendar/CalendarDayTest.kt b/composecalendar/src/androidTest/java/com/squaredem/composecalendar/CalendarDayTest.kt new file mode 100644 index 0000000..3954605 --- /dev/null +++ b/composecalendar/src/androidTest/java/com/squaredem/composecalendar/CalendarDayTest.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2022 Matteo Miceli + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.squaredem.composecalendar + +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.squaredem.composecalendar.composable.CalendarDay +import com.squaredem.composecalendar.model.DateWrapper + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* +import org.junit.Rule +import java.time.LocalDate +import java.time.Month + +@RunWith(AndroidJUnit4::class) +class CalendarDayTest { + + @get:Rule + val composeRule = createComposeRule() + + @Test + fun onTap_CalendarDay_whenCurrentMonthAndDateInRange() { + var selectedDate: LocalDate? = null + val targetDate = LocalDate.of(2023, Month.FEBRUARY, 1) + + val date = DateWrapper( + localDate = targetDate, + isSelectedDay = false, + isCurrentDay = false, + isCurrentMonth = true, + isInDateRange = true + ) + + val onSelected: (LocalDate) -> Unit = { + selectedDate = it + } + + composeRule.setContent { + CalendarDay( + date = date, + onSelected = onSelected + ) + } + + composeRule.onNodeWithText("1").performClick() + + assertNotNull(selectedDate) + assertEquals(2023, selectedDate!!.year) + assertEquals(Month.FEBRUARY, selectedDate!!.month) + assertEquals(1, selectedDate!!.dayOfMonth) + } + + @Test + fun onTap_CalendarDay_whenNotCurrentMonthAndNotDateInRange() { + var selectedDate: LocalDate? = null + val targetDate = LocalDate.of(2023, Month.FEBRUARY, 1) + + val date = DateWrapper( + localDate = targetDate, + isSelectedDay = false, + isCurrentDay = false, + isCurrentMonth = false, + isInDateRange = false + ) + + val onSelected: (LocalDate) -> Unit = { + selectedDate = it + } + + composeRule.setContent { + CalendarDay( + date = date, + onSelected = onSelected + ) + } + + composeRule.onNodeWithText("1").performClick() + + assertNull(selectedDate) + } + +} \ No newline at end of file diff --git a/composecalendar/src/androidTest/java/com/squaredem/composecalendar/CalendarMonthYearSelectorTest.kt b/composecalendar/src/androidTest/java/com/squaredem/composecalendar/CalendarMonthYearSelectorTest.kt new file mode 100644 index 0000000..4b6ea6b --- /dev/null +++ b/composecalendar/src/androidTest/java/com/squaredem/composecalendar/CalendarMonthYearSelectorTest.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2022 Matteo Miceli + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.squaredem.composecalendar + +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import com.squaredem.composecalendar.composable.CalendarMonthYearSelector +import org.junit.Rule +import org.junit.Test +import java.text.SimpleDateFormat +import java.time.LocalDate +import java.time.Month +import java.util.Locale + +class CalendarMonthYearSelectorTest { + + @get:Rule + val composeRule = createComposeRule() + + @Test + fun onTap_MonthYearChip_callLambda() { + var isChipClicked = false + + val pagerDate = LocalDate.of(2023, Month.FEBRUARY, 1) + val format = SimpleDateFormat("MMMM yyyy", Locale.ENGLISH) + + val onChipClicked = { isChipClicked = true } + + composeRule.setContent { + CalendarMonthYearSelector( + pagerDate = pagerDate, + onChipClicked = onChipClicked, + onNextMonth = {}, + onPreviousMonth = {}, + pagerMonthFormat = format + ) + } + + composeRule.onNodeWithText("february 2023", ignoreCase = true).performClick() + + assert(isChipClicked) + } + + @Test + fun onTap_previousMonthChevron_callLambda() { + var previousMonthTapped = false + + val onPreviousMonth = { previousMonthTapped = true } + + composeRule.setContent { + CalendarMonthYearSelector( + pagerDate = LocalDate.now(), + onChipClicked = {}, + onNextMonth = {}, + onPreviousMonth = onPreviousMonth + ) + } + + composeRule.onNodeWithContentDescription("ChevronLeft").performClick() + + assert(previousMonthTapped) + } + + @Test + fun onTap_nextMonthChevron_callLambda() { + var nextMonthTapped = false + + val onNextMonth = { nextMonthTapped = true } + + composeRule.setContent { + CalendarMonthYearSelector( + pagerDate = LocalDate.now(), + onChipClicked = {}, + onNextMonth = onNextMonth, + onPreviousMonth = {} + ) + } + + composeRule.onNodeWithContentDescription("ChevronRight").performClick() + + assert(nextMonthTapped) + } + +} diff --git a/composecalendar/src/androidTest/java/com/squaredem/composecalendar/ExampleInstrumentedTest.kt b/composecalendar/src/androidTest/java/com/squaredem/composecalendar/ExampleInstrumentedTest.kt deleted file mode 100644 index b7575f3..0000000 --- a/composecalendar/src/androidTest/java/com/squaredem/composecalendar/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.squaredem.composecalendar - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.squaredem.composecalendar.test", appContext.packageName) - } -} \ No newline at end of file diff --git a/composecalendar/src/main/AndroidManifest.xml b/composecalendar/src/main/AndroidManifest.xml deleted file mode 100644 index c78708d..0000000 --- a/composecalendar/src/main/AndroidManifest.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/composecalendar/src/main/java/com/squaredem/composecalendar/ComposeCalendar.kt b/composecalendar/src/main/java/com/squaredem/composecalendar/ComposeCalendar.kt index e5305fe..5b83dbe 100644 --- a/composecalendar/src/main/java/com/squaredem/composecalendar/ComposeCalendar.kt +++ b/composecalendar/src/main/java/com/squaredem/composecalendar/ComposeCalendar.kt @@ -24,18 +24,20 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.res.stringResource import com.squaredem.composecalendar.composable.CalendarContent +import java.text.DateFormat import java.time.LocalDate -import java.time.OffsetTime @Composable fun ComposeCalendar( startDate: LocalDate = LocalDate.now(), minDate: LocalDate = LocalDate.MIN, maxDate: LocalDate = LocalDate.MAX, + showSelectedDate: Boolean = true, + selectedDateFormat: DateFormat = DateFormat.getDateInstance(DateFormat.DEFAULT), onDone: (millis: LocalDate) -> Unit, onDismiss: () -> Unit ) { - val selectedDate = remember { mutableStateOf(startDate) } + val selectedDate = remember { mutableStateOf(LocalDate.now()) } AlertDialog( onDismissRequest = onDismiss, @@ -56,9 +58,9 @@ fun ComposeCalendar( startDate = startDate, minDate = minDate, maxDate = maxDate, - onSelected = { - selectedDate.value = it - } + onDateSelected = { selectedDate.value = it }, + showSelectedDate = showSelectedDate, + selectedDateFormat = selectedDateFormat ) } ) diff --git a/composecalendar/src/main/java/com/squaredem/composecalendar/composable/CalendarContent.kt b/composecalendar/src/main/java/com/squaredem/composecalendar/composable/CalendarContent.kt index afffcd2..9ee4a38 100644 --- a/composecalendar/src/main/java/com/squaredem/composecalendar/composable/CalendarContent.kt +++ b/composecalendar/src/main/java/com/squaredem/composecalendar/composable/CalendarContent.kt @@ -16,11 +16,16 @@ package com.squaredem.composecalendar.composable +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -34,14 +39,12 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.google.accompanist.pager.ExperimentalPagerApi -import com.google.accompanist.pager.HorizontalPager -import com.google.accompanist.pager.rememberPagerState import com.squaredem.composecalendar.daterange.DateRange import com.squaredem.composecalendar.daterange.DateRangeStep import com.squaredem.composecalendar.daterange.rangeTo import com.squaredem.composecalendar.utils.LogCompositions import kotlinx.coroutines.launch +import java.text.DateFormat import java.time.DayOfWeek import java.time.LocalDate import java.time.format.TextStyle @@ -50,13 +53,15 @@ import kotlin.math.abs import kotlin.math.ceil import kotlin.math.floor -@OptIn(ExperimentalPagerApi::class) +@OptIn(ExperimentalFoundationApi::class) @Composable internal fun CalendarContent( startDate: LocalDate, minDate: LocalDate, maxDate: LocalDate, - onSelected: (LocalDate) -> Unit, + onDateSelected: (LocalDate) -> Unit, + showSelectedDate: Boolean, + selectedDateFormat: DateFormat ) { LogCompositions("CalendarContent") @@ -79,7 +84,7 @@ internal fun CalendarContent( } val setSelectedDate: (LocalDate) -> Unit = { - onSelected(it) + onDateSelected(it) selectedDate.value = it } @@ -96,10 +101,15 @@ internal fun CalendarContent( modifier = Modifier.wrapContentHeight(), verticalArrangement = Arrangement.spacedBy(16.dp) ) { - CalendarTopBar(selectedDate.value) + if (showSelectedDate) { + CalendarTopBar( + selectedDate = selectedDate.value, + dateFormat = selectedDateFormat + ) + } CalendarMonthYearSelector( - currentPagerDate.value, + pagerDate = currentPagerDate.value, onChipClicked = { isPickingYear.value = !isPickingYear.value }, onNextMonth = { coroutineScope.launch { @@ -123,58 +133,63 @@ internal fun CalendarContent( } ) - if (!isPickingYear.value) { + Box { - Row( - horizontalArrangement = Arrangement.spacedBy(4.dp) + Column( + verticalArrangement = Arrangement.spacedBy(16.dp) ) { - DayOfWeek.values().forEach { - Text( - modifier = Modifier.weight(1f), - text = it.getDisplayName(TextStyle.NARROW, Locale.getDefault()), - textAlign = TextAlign.Center, - fontWeight = FontWeight.SemiBold - ) + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + DayOfWeek.values().forEach { + Text( + modifier = Modifier.weight(1f), + text = it.getDisplayName(TextStyle.NARROW, Locale.getDefault()), + textAlign = TextAlign.Center, + fontWeight = FontWeight.SemiBold + ) + } } - } - - HorizontalPager( - count = totalPageCount, - state = pagerState - ) { page -> - val currentDate = getDateFromCurrentPage(page, dateRange) - currentDate?.let { - // grid - CalendarGrid( - it.withDayOfMonth(1), - dateRange, - selectedDate.value, - setSelectedDate, - true - ) + HorizontalPager( + pageCount = totalPageCount, + state = pagerState + ) { page -> + val currentDate = getDateFromCurrentPage(page, dateRange) + currentDate?.let { + // grid + CalendarGrid( + it.withDayOfMonth(1), + dateRange, + selectedDate.value, + setSelectedDate + ) + } } + } - } else { - - CalendarYearGrid( - gridState = gridState, - dateRangeByYear = dateRangeByYear, - selectedYear = selectedDate.value.year, - currentYear = startDate.year, - onYearSelected = { year -> - coroutineScope.launch { - val newPage = dateRange.indexOfFirst { - it.year == year && it.month == selectedDate.value.month + if (isPickingYear.value) { + Surface { + CalendarYearGrid( + gridState = gridState, + dateRangeByYear = dateRangeByYear, + selectedYear = selectedDate.value.year, + currentYear = startDate.year, + onYearSelected = { year -> + coroutineScope.launch { + val newPage = dateRange.indexOfFirst { + it.year == year && it.month == selectedDate.value.month + } + pagerState.scrollToPage(newPage) + } + currentPagerDate.value = currentPagerDate.value.withYear(year) + isPickingYear.value = false } - pagerState.scrollToPage(newPage) - } - currentPagerDate.value = currentPagerDate.value.withYear(year) - isPickingYear.value = false + ) } - ) - + } } + } } @@ -233,6 +248,8 @@ private fun Preview() { startDate = LocalDate.now(), minDate = LocalDate.now(), maxDate = LocalDate.MAX, - onSelected = {}, + onDateSelected = {}, + showSelectedDate = true, + selectedDateFormat = DateFormat.getDateInstance(DateFormat.DEFAULT) ) } diff --git a/composecalendar/src/main/java/com/squaredem/composecalendar/composable/CalendarDay.kt b/composecalendar/src/main/java/com/squaredem/composecalendar/composable/CalendarDay.kt index b40232a..23bd68b 100644 --- a/composecalendar/src/main/java/com/squaredem/composecalendar/composable/CalendarDay.kt +++ b/composecalendar/src/main/java/com/squaredem/composecalendar/composable/CalendarDay.kt @@ -47,11 +47,6 @@ internal fun CalendarDay( .aspectRatio(1F) .clip(CircleShape) - if (!date.isCurrentMonth && date.showCurrentMonthOnly) { - Box(modifier = currentModifier) - return - } - currentModifier = when { date.isSelectedDay -> { currentModifier @@ -60,6 +55,7 @@ internal fun CalendarDay( shape = CircleShape ) } + date.isCurrentDay -> { currentModifier .border( @@ -71,25 +67,25 @@ internal fun CalendarDay( else -> currentModifier } - if (date.isInDateRange || (date.isCurrentMonth && date.showCurrentMonthOnly)) { + if (date.isInDateRange && date.isCurrentMonth) { currentModifier = currentModifier.clickable { onSelected(date.localDate) } } val textColor = when { - !date.isInDateRange -> { + !date.isInDateRange || !date.isCurrentMonth -> { MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38F) } + date.isSelectedDay -> { MaterialTheme.colorScheme.onPrimary } + date.isCurrentDay -> { MaterialTheme.colorScheme.primary } - !date.isCurrentMonth -> { - MaterialTheme.colorScheme.primary.copy(alpha = 0.6F) - } + else -> Color.Unspecified } diff --git a/composecalendar/src/main/java/com/squaredem/composecalendar/composable/CalendarGrid.kt b/composecalendar/src/main/java/com/squaredem/composecalendar/composable/CalendarGrid.kt index 42d847e..eeff241 100644 --- a/composecalendar/src/main/java/com/squaredem/composecalendar/composable/CalendarGrid.kt +++ b/composecalendar/src/main/java/com/squaredem/composecalendar/composable/CalendarGrid.kt @@ -35,7 +35,6 @@ internal fun CalendarGrid( dateRange: DateRange, selectedDate: LocalDate, onSelected: (LocalDate) -> Unit, - showCurrentMonthOnly: Boolean ) { LogCompositions("CalendarGrid") @@ -61,7 +60,6 @@ internal fun CalendarGrid( isCurrentDay, isCurrentMonth, isInDateRange, - showCurrentMonthOnly ) } diff --git a/composecalendar/src/main/java/com/squaredem/composecalendar/composable/CalendarMonthYearSelector.kt b/composecalendar/src/main/java/com/squaredem/composecalendar/composable/CalendarMonthYearSelector.kt index e894a4f..fda7242 100644 --- a/composecalendar/src/main/java/com/squaredem/composecalendar/composable/CalendarMonthYearSelector.kt +++ b/composecalendar/src/main/java/com/squaredem/composecalendar/composable/CalendarMonthYearSelector.kt @@ -32,6 +32,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.squaredem.composecalendar.utils.LogCompositions +import java.text.DateFormat import java.text.SimpleDateFormat import java.time.LocalDate import java.time.OffsetTime @@ -44,11 +45,10 @@ internal fun CalendarMonthYearSelector( onChipClicked: () -> Unit, onNextMonth: () -> Unit, onPreviousMonth: () -> Unit, + pagerMonthFormat: DateFormat = SimpleDateFormat("MMMM yyyy", Locale.getDefault()) ) { LogCompositions("CalendarMonthYearSelector") - val pagerMonthFormat = SimpleDateFormat("MMMM yyyy", Locale.getDefault()) - Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically diff --git a/composecalendar/src/main/java/com/squaredem/composecalendar/composable/CalendarTopBar.kt b/composecalendar/src/main/java/com/squaredem/composecalendar/composable/CalendarTopBar.kt index 0d2fa2e..0c40d96 100644 --- a/composecalendar/src/main/java/com/squaredem/composecalendar/composable/CalendarTopBar.kt +++ b/composecalendar/src/main/java/com/squaredem/composecalendar/composable/CalendarTopBar.kt @@ -31,7 +31,10 @@ import java.time.OffsetTime import java.util.* @Composable -internal fun CalendarTopBar(selectedDate: LocalDate) { +internal fun CalendarTopBar( + selectedDate: LocalDate, + dateFormat: DateFormat +) { Column( modifier = Modifier .fillMaxWidth() @@ -39,9 +42,8 @@ internal fun CalendarTopBar(selectedDate: LocalDate) { ) { LogCompositions("CalendarTopBar") - val dateFormatter = DateFormat.getDateInstance(DateFormat.DEFAULT) Text( - text = dateFormatter.format( + text = dateFormat.format( Date.from(selectedDate.atTime(OffsetTime.now()).toInstant()) ), style = MaterialTheme.typography.headlineLarge, diff --git a/composecalendar/src/main/java/com/squaredem/composecalendar/composable/CalendarYear.kt b/composecalendar/src/main/java/com/squaredem/composecalendar/composable/CalendarYear.kt index 035ec7e..ac658e5 100644 --- a/composecalendar/src/main/java/com/squaredem/composecalendar/composable/CalendarYear.kt +++ b/composecalendar/src/main/java/com/squaredem/composecalendar/composable/CalendarYear.kt @@ -16,11 +16,13 @@ package com.squaredem.composecalendar.composable +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.Button import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import com.squaredem.composecalendar.utils.LogCompositions @Composable @@ -32,20 +34,28 @@ internal fun CalendarYear( ) { LogCompositions("CalendarYear") + val modifier = Modifier.fillMaxWidth() + if (isSelectedYear) { - Button(onClick = { - setSelectedYear(year) - }) { + Button( + modifier = modifier, + onClick = { + setSelectedYear(year) + }) { Text("$year", maxLines = 1) } } else if (isCurrentYear) { - OutlinedButton(onClick = { - setSelectedYear(year) - }) { + OutlinedButton( + modifier = modifier, + onClick = { + setSelectedYear(year) + }) { Text("$year", maxLines = 1) } } else { - TextButton(onClick = { setSelectedYear(year) }) { + TextButton( + modifier = modifier, + onClick = { setSelectedYear(year) }) { Text("$year", maxLines = 1) } } diff --git a/composecalendar/src/main/java/com/squaredem/composecalendar/model/DateWrapper.kt b/composecalendar/src/main/java/com/squaredem/composecalendar/model/DateWrapper.kt index ff0e5e5..110a253 100644 --- a/composecalendar/src/main/java/com/squaredem/composecalendar/model/DateWrapper.kt +++ b/composecalendar/src/main/java/com/squaredem/composecalendar/model/DateWrapper.kt @@ -24,5 +24,4 @@ internal data class DateWrapper( val isCurrentDay: Boolean, val isCurrentMonth: Boolean, val isInDateRange: Boolean, - val showCurrentMonthOnly: Boolean ) diff --git a/gradle.properties b/gradle.properties index cd0519b..022338b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,4 +20,6 @@ kotlin.code.style=official # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true \ No newline at end of file +android.nonTransitiveRClass=true +android.defaults.buildfeatures.buildconfig=true +android.nonFinalResIds=false \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2de896a..385a865 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Sat Jul 16 10:03:18 CEST 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME