├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ ├── feature-request.md │ └── usage-help.md └── workflows │ ├── check.yml │ ├── gradle-wrapper.yaml │ └── publish-release.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── build.gradle.kts ├── buildSrc ├── .gitignore ├── build.gradle.kts └── src │ └── main │ └── java │ └── com │ └── kizitonwose │ └── calendar │ └── buildsrc │ └── Build.kt ├── compose-multiplatform ├── library │ ├── .gitignore │ ├── api │ │ ├── android │ │ │ └── library.api │ │ ├── desktop │ │ │ └── library.api │ │ └── library.klib.api │ ├── build.gradle.kts │ ├── gradle.properties │ └── src │ │ ├── androidMain │ │ ├── AndroidManifest.xml │ │ └── kotlin │ │ │ └── com │ │ │ └── kizitonwose │ │ │ └── calendar │ │ │ └── data │ │ │ └── Utils.android.kt │ │ ├── commonMain │ │ └── kotlin │ │ │ └── com │ │ │ └── kizitonwose │ │ │ └── calendar │ │ │ ├── compose │ │ │ ├── Calendar.kt │ │ │ ├── CalendarDefaults.kt │ │ │ ├── CalendarInfo.kt │ │ │ ├── CalendarLayoutInfo.kt │ │ │ ├── CalendarMonths.kt │ │ │ ├── CalendarState.kt │ │ │ ├── ContentHeightMode.kt │ │ │ ├── ItemPlacementInfo.kt │ │ │ ├── heatmapcalendar │ │ │ │ ├── HeatMapCalendar.kt │ │ │ │ ├── HeatMapCalendarState.kt │ │ │ │ ├── HeatMapWeek.kt │ │ │ │ └── HeatMapWeekHeaderPosition.kt │ │ │ ├── weekcalendar │ │ │ │ ├── WeekCalendar.kt │ │ │ │ ├── WeekCalendarLayoutInfo.kt │ │ │ │ └── WeekCalendarState.kt │ │ │ └── yearcalendar │ │ │ │ ├── YearCalendarLayoutInfo.kt │ │ │ │ ├── YearCalendarMonths.kt │ │ │ │ ├── YearCalendarState.kt │ │ │ │ ├── YearContentHeightMode.kt │ │ │ │ └── YearItemPlacementInfo.kt │ │ │ ├── core │ │ │ ├── CalendarDay.kt │ │ │ ├── CalendarMonth.kt │ │ │ ├── CalendarYear.kt │ │ │ ├── DayPosition.kt │ │ │ ├── ExperimentalCalendarApi.kt │ │ │ ├── Extensions.kt │ │ │ ├── OutDateStyle.kt │ │ │ ├── Week.kt │ │ │ ├── WeekDay.kt │ │ │ ├── WeekDayPosition.kt │ │ │ ├── Year.kt │ │ │ ├── YearMonth.kt │ │ │ ├── format │ │ │ │ └── Format.kt │ │ │ └── serializers │ │ │ │ ├── YearMonthSerializers.kt │ │ │ │ └── YearSerializers.kt │ │ │ └── data │ │ │ ├── DataStore.kt │ │ │ ├── Extensions.kt │ │ │ ├── MonthData.kt │ │ │ ├── Utils.kt │ │ │ ├── VisibleItemState.kt │ │ │ ├── WeekData.kt │ │ │ └── YearData.kt │ │ ├── commonTest │ │ └── kotlin │ │ │ └── com │ │ │ └── kizitonwose │ │ │ └── calendar │ │ │ ├── compose │ │ │ ├── CalendarStateTest.kt │ │ │ ├── HeatMapCalendarStateTest.kt │ │ │ ├── StateSaverTest.kt │ │ │ ├── WeekCalendarStateTest.kt │ │ │ └── YearCalendarStateTest.kt │ │ │ ├── core │ │ │ ├── ExtensionTest.kt │ │ │ ├── YearMonthSerializationTest.kt │ │ │ ├── YearMonthTest.kt │ │ │ ├── YearSerializationTest.kt │ │ │ └── YearTest.kt │ │ │ ├── data │ │ │ ├── HeatMapDataTest.kt │ │ │ ├── MonthDataTest.kt │ │ │ ├── WeekDataTest.kt │ │ │ └── YearDataTest.kt │ │ │ └── utils │ │ │ └── Utils.kt │ │ ├── desktopMain │ │ └── kotlin │ │ │ └── com │ │ │ └── kizitonwose │ │ │ └── calendar │ │ │ └── data │ │ │ └── Utils.desktop.kt │ │ ├── iosMain │ │ └── kotlin │ │ │ └── com │ │ │ └── kizitonwose │ │ │ └── calendar │ │ │ ├── core │ │ │ └── Extensions.ios.kt │ │ │ └── data │ │ │ └── Utils.ios.kt │ │ ├── jvmMain │ │ └── kotlin │ │ │ └── com │ │ │ └── kizitonwose │ │ │ └── calendar │ │ │ └── core │ │ │ ├── Converters.kt │ │ │ └── Extensions.jvm.kt │ │ └── wasmJsMain │ │ └── kotlin │ │ └── com │ │ └── kizitonwose │ │ └── calendar │ │ ├── core │ │ ├── Extensions.wasmJs.kt │ │ └── FirstDayFromMap.kt │ │ └── data │ │ └── Utils.wasmJs.kt └── sample │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ ├── androidMain │ ├── AndroidManifest.xml │ ├── kotlin │ │ ├── Device.android.kt │ │ └── com │ │ │ └── kizitonwose │ │ │ └── calendar │ │ │ └── compose │ │ │ └── multiplatform │ │ │ └── MainActivity.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ └── strings.xml │ ├── commonMain │ └── kotlin │ │ ├── App.kt │ │ ├── AppIcons.kt │ │ ├── Colors.kt │ │ ├── ContinuousSelectionHelper.kt │ │ ├── Device.kt │ │ ├── Example10Page.kt │ │ ├── Example11Page.kt │ │ ├── Example1Page.kt │ │ ├── Example2Page.kt │ │ ├── Example2PageHighlight.kt │ │ ├── Example3Page.kt │ │ ├── Example4Page.kt │ │ ├── Example5Page.kt │ │ ├── Example6Page.kt │ │ ├── Example7Page.kt │ │ ├── Example8Page.kt │ │ ├── Example9Page.kt │ │ ├── Example9PageAnimatedVisibility.kt │ │ ├── Flight.kt │ │ ├── Format.kt │ │ ├── ListPage.kt │ │ ├── PageAnimation.kt │ │ ├── Scaffold.kt │ │ ├── SimpleCalendarTitle.kt │ │ ├── Theme.kt │ │ └── Utils.kt │ ├── desktopMain │ └── kotlin │ │ ├── Device.desktop.kt │ │ └── Main.kt │ ├── iosMain │ └── kotlin │ │ ├── Device.ios.kt │ │ ├── Format.ios.kt │ │ └── MainViewController.kt │ ├── jvmMain │ └── kotlin │ │ └── Format.jvm.kt │ └── wasmJsMain │ ├── kotlin │ ├── Device.wasmJs.kt │ ├── Format.wasmJs.kt │ └── Main.kt │ └── resources │ ├── index.html │ └── styles.css ├── compose ├── .gitignore ├── api │ └── compose.api ├── build.gradle.kts ├── gradle.properties └── src │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── com │ │ └── kizitonwose │ │ └── calendar │ │ └── compose │ │ ├── Calendar.kt │ │ ├── CalendarDefaults.kt │ │ ├── CalendarInfo.kt │ │ ├── CalendarLayoutInfo.kt │ │ ├── CalendarMonths.kt │ │ ├── CalendarState.kt │ │ ├── ContentHeightMode.kt │ │ ├── ItemPlacementInfo.kt │ │ ├── VisibleItemState.kt │ │ ├── heatmapcalendar │ │ ├── HeatMapCalendar.kt │ │ ├── HeatMapCalendarState.kt │ │ ├── HeatMapWeek.kt │ │ └── HeatMapWeekHeaderPosition.kt │ │ ├── weekcalendar │ │ ├── WeekCalendar.kt │ │ ├── WeekCalendarLayoutInfo.kt │ │ └── WeekCalendarState.kt │ │ └── yearcalendar │ │ ├── YearCalendarLayoutInfo.kt │ │ ├── YearCalendarMonths.kt │ │ ├── YearCalendarState.kt │ │ ├── YearContentHeightMode.kt │ │ └── YearItemPlacementInfo.kt │ └── test │ └── java │ └── com │ └── kizitonwose │ └── calendar │ └── compose │ ├── CalendarStateTest.kt │ ├── HeatMapCalendarStateTest.kt │ ├── StateSaverTest.kt │ ├── WeekCalendarStateTest.kt │ └── YearCalendarStateTest.kt ├── core ├── .gitignore ├── api │ └── core.api ├── build.gradle.kts ├── gradle.properties └── src │ └── main │ └── java │ └── com │ └── kizitonwose │ └── calendar │ └── core │ ├── CalendarDay.kt │ ├── CalendarMonth.kt │ ├── CalendarYear.kt │ ├── DayPosition.kt │ ├── ExperimentalCalendarApi.kt │ ├── Extensions.kt │ ├── OutDateStyle.kt │ ├── Week.kt │ ├── WeekDay.kt │ └── WeekDayPosition.kt ├── data ├── .gitignore ├── api │ └── data.api ├── build.gradle.kts ├── gradle.properties └── src │ ├── main │ └── java │ │ └── com │ │ └── kizitonwose │ │ └── calendar │ │ └── data │ │ ├── DataStore.kt │ │ ├── Extensions.kt │ │ ├── MonthData.kt │ │ ├── Utils.kt │ │ ├── WeekData.kt │ │ └── YearData.kt │ └── test │ └── java │ └── com │ └── kizitonwose │ └── calendar │ └── data │ ├── DayOfWeekTest.kt │ ├── HeatMapDataTest.kt │ ├── MonthDataTest.kt │ ├── Utils.kt │ ├── WeekDataTest.kt │ └── YearDataTest.kt ├── docs ├── Compose.md ├── MigrationGuide.md └── View.md ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── in_out_dates.png └── showcase.png ├── kotlin-js-store └── yarn.lock ├── sample ├── .gitignore ├── build.gradle.kts └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── kizitonwose │ │ └── calendar │ │ └── sample │ │ ├── CalendarComposeTest.kt │ │ ├── CalendarViewTest.kt │ │ ├── WeekCalendarViewTest.kt │ │ └── utils │ │ ├── TestDayViewContainer.kt │ │ └── TestUtils.kt │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── kizitonwose │ │ └── calendar │ │ └── sample │ │ ├── HomeActivity.kt │ │ ├── compose │ │ ├── CalendarComposeActivity.kt │ │ ├── Example10Page.kt │ │ ├── Example11Page.kt │ │ ├── Example1Page.kt │ │ ├── Example2Page.kt │ │ ├── Example2PageHighlight.kt │ │ ├── Example3Page.kt │ │ ├── Example4Page.kt │ │ ├── Example5Page.kt │ │ ├── Example6Page.kt │ │ ├── Example7Page.kt │ │ ├── Example8Page.kt │ │ ├── Example9Page.kt │ │ ├── Example9PageAnimatedVisibility.kt │ │ ├── ListPage.kt │ │ ├── PageAnimation.kt │ │ ├── Scaffold.kt │ │ ├── SimpleCalendarTitle.kt │ │ ├── Theme.kt │ │ └── Utils.kt │ │ ├── shared │ │ ├── ContinuousSelectionHelper.kt │ │ ├── Flight.kt │ │ ├── StatusBarColorLifecycleObserver.kt │ │ └── Utils.kt │ │ └── view │ │ ├── BaseFragment.kt │ │ ├── CalendarViewActivity.kt │ │ ├── CalendarViewOptionsAdapter.kt │ │ ├── Example10Fragment.kt │ │ ├── Example1Fragment.kt │ │ ├── Example2Fragment.kt │ │ ├── Example3Fragment.kt │ │ ├── Example4Fragment.kt │ │ ├── Example5Fragment.kt │ │ ├── Example6Fragment.kt │ │ ├── Example7Fragment.kt │ │ ├── Example8Fragment.kt │ │ ├── Example9Fragment.kt │ │ ├── Typeface.kt │ │ └── Utils.kt │ └── res │ ├── anim │ ├── fade_in.xml │ ├── fade_out.xml │ ├── slide_in_left.xml │ ├── slide_in_right.xml │ ├── slide_in_up.xml │ ├── slide_out_down.xml │ ├── slide_out_left.xml │ └── slide_out_right.xml │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ ├── example_1_selected_bg.xml │ ├── example_1_today_bg.xml │ ├── example_2_selected_bg.xml │ ├── example_3_selected_bg.xml │ ├── example_3_today_bg.xml │ ├── example_4_continuous_selected_bg_end.xml │ ├── example_4_continuous_selected_bg_middle.xml │ ├── example_4_continuous_selected_bg_start.xml │ ├── example_4_single_selected_bg.xml │ ├── example_4_today_bg.xml │ ├── example_5_selected_bg.xml │ ├── example_8_selected_bg.xml │ ├── example_8_today_bg.xml │ ├── ic_add.xml │ ├── ic_airplane_landing.xml │ ├── ic_airplane_takeoff.xml │ ├── ic_check.xml │ ├── ic_chevron_left.xml │ ├── ic_chevron_right.xml │ ├── ic_close.xml │ └── ic_launcher_background.xml │ ├── layout │ ├── calendar_day_legend_container.xml │ ├── calendar_day_legend_text.xml │ ├── calendar_view_activity.xml │ ├── calendar_view_options_item_view.xml │ ├── example_10_fragment.xml │ ├── example_1_calendar_day.xml │ ├── example_1_fragment.xml │ ├── example_2_calendar_day.xml │ ├── example_2_calendar_header.xml │ ├── example_2_fragment.xml │ ├── example_3_calendar_day.xml │ ├── example_3_calendar_header.xml │ ├── example_3_event_item_view.xml │ ├── example_3_fragment.xml │ ├── example_4_calendar_day.xml │ ├── example_4_calendar_header.xml │ ├── example_4_fragment.xml │ ├── example_5_calendar_day.xml │ ├── example_5_calendar_header.xml │ ├── example_5_event_item_view.xml │ ├── example_5_fragment.xml │ ├── example_6_calendar_day.xml │ ├── example_6_calendar_header.xml │ ├── example_6_fragment.xml │ ├── example_7_calendar_day.xml │ ├── example_7_fragment.xml │ ├── example_8_calendar_day.xml │ ├── example_8_calendar_footer.xml │ ├── example_8_calendar_header.xml │ ├── example_8_fragment.xml │ ├── example_9_calendar_day.xml │ ├── example_9_calendar_month_header.xml │ ├── example_9_calendar_year_header.xml │ ├── example_9_fragment.xml │ └── home_activity.xml │ ├── menu │ ├── example_10_menu.xml │ ├── example_2_menu.xml │ └── example_4_menu.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── settings.gradle.kts └── view ├── .gitignore ├── api └── view.api ├── build.gradle.kts ├── gradle.properties └── src └── main ├── AndroidManifest.xml ├── java └── com │ └── kizitonwose │ └── calendar │ └── view │ ├── Binder.kt │ ├── CalendarView.kt │ ├── DaySize.kt │ ├── LayoutHelper.kt │ ├── MarginValues.kt │ ├── MonthHeight.kt │ ├── WeekCalendarView.kt │ ├── YearCalendarView.kt │ └── internal │ ├── CalendarLayoutManager.kt │ ├── CalendarPageSnapHelper.kt │ ├── CalendarPageSnapHelperLegacy.kt │ ├── CustomViewClass.kt │ ├── DayHolder.kt │ ├── Extensions.kt │ ├── ItemRoot.kt │ ├── WeekHolder.kt │ ├── monthcalendar │ ├── MonthCalendarAdapter.kt │ ├── MonthCalendarLayoutManager.kt │ └── MonthViewHolder.kt │ ├── weekcalendar │ ├── WeekCalendarAdapter.kt │ ├── WeekCalendarLayoutManager.kt │ └── WeekViewHolder.kt │ └── yearcalendar │ ├── YearCalendarAdapter.kt │ ├── YearCalendarLayoutManager.kt │ ├── YearMonthHolder.kt │ ├── YearRoot.kt │ └── YearViewHolder.kt └── res └── values └── attrs.xml /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_style = space 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | 9 | [**/generated/**/*] 10 | ktlint = disabled 11 | 12 | [*.{kt,kts}] 13 | ij_kotlin_allow_trailing_comma = true 14 | ij_kotlin_allow_trailing_comma_on_call_site = true 15 | ktlint_standard_multiline-expression-wrapping = disabled 16 | ktlint_standard_function-expression-body = disabled 17 | ktlint_standard_chain-method-continuation = disabled 18 | ktlint_standard_class-signature = disabled 19 | # string-template-indent requires multiline-expression-wrapping to be enabled 20 | ktlint_standard_string-template-indent = disabled 21 | ktlint_standard_parameter-list-wrapping = disabled 22 | ktlint_standard_function-signature = disabled 23 | ktlint_standard_blank-line-before-declaration = disabled 24 | ktlint_standard_value-argument-comment = disabled 25 | ktlint_function_naming_ignore_when_annotated_with = Composable 26 | ktlint_standard_annotation = disabled 27 | max_line_length = 200 28 | 29 | [**/test/**/*.kt] 30 | ktlint_experimental = disabled 31 | 32 | [*.md] 33 | trim_trailing_whitespace = false 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a ticket for a bug. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Library information: 11 | - Version: [e.g. 2.0.0] 12 | - View or Compose module: [e.g. view] 13 | 14 | ### Describe the bug** 15 | A clear and concise description of what the bug is. 16 | 17 | ### To Reproduce (if applicable) 18 | Steps to reproduce the behavior: 19 | 1. Go to '....' 20 | 2. Click on '....' 21 | 4. See '....' 22 | 23 | ### Expected behavior (if applicable) 24 | A clear and concise description of what you expected to happen. 25 | 26 | ### Screenshots? (if applicable) 27 | If applicable, add screenshots or screen recordings to help explain the problem. 28 | 29 | ### Additional information 30 | Add any other information about the bug here. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Create a ticket to discuss a new feature. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Feature 11 | A clear and concise description of the feature. 12 | 13 | ### Tech (if applicable) 14 | Add any other information that could help the development of this feature. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/usage-help.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Usage help 3 | about: Ask a question regarding library usage. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Library information: 11 | - Version: [e.g. 2.0.0] 12 | - View or Compose module: [e.g. View] 13 | 14 | ### Question: 15 | [e.g. How do I do X or Y?] -------------------------------------------------------------------------------- /.github/workflows/gradle-wrapper.yaml: -------------------------------------------------------------------------------- 1 | name: Gradle Wrapper Validation 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - 'gradlew' 7 | - 'gradlew.bat' 8 | - 'gradle/wrapper/**' 9 | 10 | jobs: 11 | validate: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: gradle/actions/wrapper-validation@v3 16 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*.*.*' 7 | 8 | jobs: 9 | # Use macOS so iOS artifacts are published 10 | publish-release: 11 | name: Publish Release 12 | runs-on: macos-latest 13 | if: github.repository == 'kizitonwose/Calendar' 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-java@v4 17 | with: 18 | distribution: 'zulu' 19 | java-version: 17 20 | - uses: gradle/actions/setup-gradle@v4 21 | - name: Deploy to Maven 22 | run: ./gradlew publish 23 | env: 24 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} 25 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} 26 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SONATYPE_NEXUS_SIGNING_KEY }} 27 | ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.SONATYPE_NEXUS_SIGNING_KEY_ID }} 28 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SONATYPE_NEXUS_SIGNING_KEY_PASSWORD }} 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .DS_Store 5 | /build 6 | /captures 7 | .externalNativeBuild 8 | .idea 9 | /.kotlin 10 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Kizito Nwose 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /buildSrc/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | java { 6 | toolchain { 7 | languageVersion.set(JavaLanguageVersion.of(17)) 8 | } 9 | } 10 | 11 | kotlin { 12 | jvmToolchain { 13 | languageVersion.set(JavaLanguageVersion.of(17)) 14 | } 15 | } 16 | 17 | repositories { 18 | mavenCentral() 19 | } 20 | -------------------------------------------------------------------------------- /buildSrc/src/main/java/com/kizitonwose/calendar/buildsrc/Build.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused", "ConstPropertyName") 2 | 3 | package com.kizitonwose.calendar.buildsrc 4 | 5 | import org.gradle.api.JavaVersion 6 | import org.gradle.jvm.toolchain.JavaLanguageVersion 7 | 8 | object Config { 9 | val compatibleJavaVersion = JavaVersion.VERSION_17 10 | val compatibleJavaLanguageVersion = JavaLanguageVersion.of(compatibleJavaVersion.majorVersion) 11 | } 12 | 13 | object Version { 14 | val android = "2.7.1-SNAPSHOT" 15 | val multiplatfrom = "2.7.1-SNAPSHOT" 16 | 17 | fun String.isNoPublish() = this == VERSION_NO_PUBLISH 18 | } 19 | 20 | private val VERSION_NO_PUBLISH = "NO_PUBLISH" 21 | 22 | object Android { 23 | const val minSdk = 21 24 | const val targetSdk = 35 25 | const val compileSdk = 35 26 | } 27 | 28 | val multiplatformProjects = listOf("library") 29 | val androidProjects = listOf("core", "data", "view", "compose") 30 | -------------------------------------------------------------------------------- /compose-multiplatform/library/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /compose-multiplatform/library/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=compose-multiplatform 2 | POM_DESCRIPTION=A highly customizable calendar library for Compose Multiplatform, backed by LazyRow/LazyColumn. 3 | POM_PACKAGING=aar 4 | -------------------------------------------------------------------------------- /compose-multiplatform/library/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /compose-multiplatform/library/src/androidMain/kotlin/com/kizitonwose/calendar/data/Utils.android.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.data 2 | 3 | import android.util.Log 4 | 5 | internal actual fun log(tag: String, message: String) = Log.w(tag, message).asUnit() 6 | -------------------------------------------------------------------------------- /compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/CalendarDefaults.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.compose 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.foundation.gestures.FlingBehavior 5 | import androidx.compose.foundation.gestures.ScrollableDefaults 6 | import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider 7 | import androidx.compose.foundation.gestures.snapping.SnapPosition 8 | import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior 9 | import androidx.compose.foundation.lazy.LazyListState 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.remember 12 | 13 | internal object CalendarDefaults { 14 | /** 15 | * The default implementation in [rememberSnapFlingBehavior] snaps to the center of the layout 16 | * but we want to snap to the start. For example, in a vertical calendar, when the layout size 17 | * is larger than the item size(e.g two or more visible months), we don't want the item's 18 | * center to be at the center of the layout when it snaps, instead we want the item's top 19 | * to be at the top of the layout. 20 | */ 21 | @OptIn(ExperimentalFoundationApi::class) 22 | @Composable 23 | private fun pagedFlingBehavior(state: LazyListState): FlingBehavior { 24 | val snappingLayout = remember(state) { 25 | val provider = SnapLayoutInfoProvider(state, SnapPosition.Start) 26 | CalendarSnapLayoutInfoProvider(provider) 27 | } 28 | return rememberSnapFlingBehavior(snappingLayout) 29 | } 30 | 31 | @Composable 32 | private fun continuousFlingBehavior(): FlingBehavior = ScrollableDefaults.flingBehavior() 33 | 34 | @Composable 35 | fun flingBehavior(isPaged: Boolean, state: LazyListState): FlingBehavior { 36 | return if (isPaged) pagedFlingBehavior(state) else continuousFlingBehavior() 37 | } 38 | } 39 | 40 | @ExperimentalFoundationApi 41 | @Suppress("FunctionName") 42 | private fun CalendarSnapLayoutInfoProvider( 43 | snapLayoutInfoProvider: SnapLayoutInfoProvider, 44 | ): SnapLayoutInfoProvider = object : SnapLayoutInfoProvider by snapLayoutInfoProvider { 45 | /** 46 | * In compose 1.3, the default was single page snapping (zero), but this changed 47 | * in compose 1.4 to decayed page snapping which is not great for calendar usage. 48 | */ 49 | override fun calculateApproachOffset(velocity: Float, decayOffset: Float): Float = 0f 50 | } 51 | -------------------------------------------------------------------------------- /compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/CalendarInfo.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.compose 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.kizitonwose.calendar.core.OutDateStyle 5 | import kotlinx.datetime.DayOfWeek 6 | 7 | @Immutable 8 | internal data class CalendarInfo( 9 | val indexCount: Int, 10 | private val firstDayOfWeek: DayOfWeek? = null, 11 | private val outDateStyle: OutDateStyle? = null, 12 | ) 13 | -------------------------------------------------------------------------------- /compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/CalendarLayoutInfo.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.compose 2 | 3 | import androidx.compose.foundation.lazy.LazyListItemInfo 4 | import androidx.compose.foundation.lazy.LazyListLayoutInfo 5 | import com.kizitonwose.calendar.core.CalendarMonth 6 | 7 | /** 8 | * Contains useful information about the currently displayed layout state of the calendar. 9 | * For example you can get the list of currently displayed months. 10 | * 11 | * Use [CalendarState.layoutInfo] to retrieve this. 12 | * @see LazyListLayoutInfo 13 | */ 14 | public class CalendarLayoutInfo(info: LazyListLayoutInfo, private val month: (Int) -> CalendarMonth) : 15 | LazyListLayoutInfo by info { 16 | /** 17 | * The list of [CalendarItemInfo] representing all the currently visible months. 18 | */ 19 | public val visibleMonthsInfo: List 20 | get() = visibleItemsInfo.map { 21 | CalendarItemInfo(it, month(it.index)) 22 | } 23 | } 24 | 25 | /** 26 | * Contains useful information about an individual [CalendarMonth] on the calendar. 27 | * 28 | * @param month The month in the list. 29 | * 30 | * @see CalendarLayoutInfo 31 | * @see LazyListItemInfo 32 | */ 33 | public class CalendarItemInfo(info: LazyListItemInfo, public val month: CalendarMonth) : LazyListItemInfo by info 34 | -------------------------------------------------------------------------------- /compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/ContentHeightMode.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.compose 2 | 3 | import androidx.compose.foundation.layout.aspectRatio 4 | import androidx.compose.foundation.layout.fillMaxHeight 5 | import androidx.compose.foundation.layout.height 6 | import androidx.compose.ui.Modifier 7 | 8 | /** 9 | * Determines how the height of the day content is calculated. 10 | */ 11 | public enum class ContentHeightMode { 12 | /** 13 | * The day container will wrap its height. This allows you to 14 | * use [Modifier.aspectRatio] if you want square day content 15 | * or [Modifier.height] if you want a specific height value 16 | * for the day content. 17 | */ 18 | Wrap, 19 | 20 | /** 21 | * The days in each month will spread to fill the parent's height after 22 | * any available header and footer content height has been accounted for. 23 | * This allows you to use [Modifier.fillMaxHeight] for the day content 24 | * height. With this option, your Calendar composable should also 25 | * be created with [Modifier.fillMaxHeight] or [Modifier.height]. 26 | */ 27 | Fill, 28 | } 29 | -------------------------------------------------------------------------------- /compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/ItemPlacementInfo.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.compose 2 | 3 | import androidx.compose.foundation.gestures.Orientation 4 | import androidx.compose.runtime.Immutable 5 | import androidx.compose.runtime.Stable 6 | import androidx.compose.runtime.withFrameNanos 7 | import androidx.compose.ui.layout.LayoutCoordinates 8 | import androidx.compose.ui.unit.round 9 | import kotlinx.coroutines.isActive 10 | import kotlin.coroutines.coroutineContext 11 | 12 | @Immutable 13 | internal data class ItemCoordinates( 14 | val itemRootCoordinates: LayoutCoordinates, 15 | val firstDayCoordinates: LayoutCoordinates, 16 | ) 17 | 18 | @Stable 19 | internal class ItemPlacementInfo { 20 | private var itemCoordinates: ItemCoordinates? = null 21 | 22 | fun onItemPlaced(itemCoordinates: ItemCoordinates) { 23 | this.itemCoordinates = itemCoordinates 24 | } 25 | 26 | suspend fun awaitFistDayOffsetAndSize(orientation: Orientation): OffsetSize? { 27 | var itemCoordinates = this.itemCoordinates 28 | while (coroutineContext.isActive && itemCoordinates == null) { 29 | withFrameNanos {} 30 | itemCoordinates = this.itemCoordinates 31 | } 32 | if (itemCoordinates == null) { 33 | return null 34 | } 35 | val (itemRootCoordinates, firstDayCoordinates) = itemCoordinates 36 | val daySize = firstDayCoordinates.size 37 | val dayOffset = itemRootCoordinates.localPositionOf(firstDayCoordinates).round() 38 | return when (orientation) { 39 | Orientation.Vertical -> OffsetSize( 40 | offset = dayOffset.y, 41 | size = daySize.height, 42 | ) 43 | 44 | Orientation.Horizontal -> { 45 | OffsetSize( 46 | offset = dayOffset.x, 47 | size = daySize.width, 48 | ) 49 | } 50 | } 51 | } 52 | 53 | @Immutable 54 | internal data class OffsetSize( 55 | val offset: Int, 56 | val size: Int, 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/heatmapcalendar/HeatMapWeek.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.compose.heatmapcalendar 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.kizitonwose.calendar.compose.HeatMapCalendar 5 | import com.kizitonwose.calendar.core.CalendarDay 6 | 7 | /** 8 | * Represents a week on the heatmap calendar. 9 | * 10 | * This model exists only as a wrapper class with the [Immutable] annotation for compose. 11 | * The alternative would be to use the `kotlinx.ImmutableList` type for the `days` value 12 | * which is used ONLY in the dayContent parameter of the [HeatMapCalendar] but then we 13 | * would force that dependency on the library consumers. 14 | * 15 | * @param days the days in this week. 16 | */ 17 | @Immutable 18 | public data class HeatMapWeek(val days: List) 19 | -------------------------------------------------------------------------------- /compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/heatmapcalendar/HeatMapWeekHeaderPosition.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.compose.heatmapcalendar 2 | 3 | import com.kizitonwose.calendar.compose.HeatMapCalendar 4 | 5 | /** 6 | * Determines the position of the week header 7 | * composable (Mon, Tue, Wed...) in the [HeatMapCalendar] 8 | */ 9 | public enum class HeatMapWeekHeaderPosition { 10 | /** 11 | * The header is positioned at the start of the calendar. 12 | */ 13 | Start, 14 | 15 | /** 16 | * The header is positioned at the end of the calendar. 17 | */ 18 | End, 19 | } 20 | -------------------------------------------------------------------------------- /compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendarLayoutInfo.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.compose.weekcalendar 2 | 3 | import androidx.compose.foundation.lazy.LazyListItemInfo 4 | import androidx.compose.foundation.lazy.LazyListLayoutInfo 5 | import com.kizitonwose.calendar.core.Week 6 | 7 | /** 8 | * Contains useful information about the currently displayed layout state of the calendar. 9 | * For example you can get the list of currently displayed months. 10 | * 11 | * Use [WeekCalendarState.layoutInfo] to retrieve this. 12 | * 13 | * @see LazyListLayoutInfo 14 | */ 15 | public class WeekCalendarLayoutInfo( 16 | info: LazyListLayoutInfo, 17 | private val getIndexData: (Int) -> Week, 18 | ) : LazyListLayoutInfo by info { 19 | /** 20 | * The list of [WeekCalendarItemInfo] representing all the currently visible weeks. 21 | */ 22 | public val visibleWeeksInfo: List 23 | get() = visibleItemsInfo.map { info -> 24 | WeekCalendarItemInfo(info, getIndexData(info.index)) 25 | } 26 | } 27 | 28 | /** 29 | * Contains useful information about an individual week on the calendar. 30 | * 31 | * @param week The week in the list. 32 | 33 | * @see WeekCalendarLayoutInfo 34 | * @see LazyListItemInfo 35 | */ 36 | public class WeekCalendarItemInfo(info: LazyListItemInfo, public val week: Week) : 37 | LazyListItemInfo by info 38 | -------------------------------------------------------------------------------- /compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarLayoutInfo.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.compose.yearcalendar 2 | 3 | import androidx.compose.foundation.lazy.LazyListItemInfo 4 | import androidx.compose.foundation.lazy.LazyListLayoutInfo 5 | import com.kizitonwose.calendar.core.CalendarYear 6 | 7 | /** 8 | * Contains useful information about the currently displayed layout state of the calendar. 9 | * For example you can get the list of currently displayed years. 10 | * 11 | * Use [YearCalendarState.layoutInfo] to retrieve this. 12 | * 13 | * @see LazyListLayoutInfo 14 | */ 15 | public class YearCalendarLayoutInfo( 16 | info: LazyListLayoutInfo, 17 | private val getIndexData: (Int) -> CalendarYear, 18 | ) : LazyListLayoutInfo by info { 19 | /** 20 | * The list of [YearCalendarItemInfo] representing all the currently visible years. 21 | */ 22 | public val visibleYearsInfo: List 23 | get() = visibleItemsInfo.map { info -> 24 | YearCalendarItemInfo(info, getIndexData(info.index)) 25 | } 26 | } 27 | 28 | /** 29 | * Contains useful information about an individual year on the calendar. 30 | * 31 | * @param year The year in the list. 32 | 33 | * @see YearCalendarLayoutInfo 34 | * @see LazyListItemInfo 35 | */ 36 | public class YearCalendarItemInfo(info: LazyListItemInfo, public val year: CalendarYear) : 37 | LazyListItemInfo by info 38 | -------------------------------------------------------------------------------- /compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.compose.yearcalendar 2 | 3 | import androidx.compose.foundation.layout.aspectRatio 4 | import androidx.compose.foundation.layout.fillMaxHeight 5 | import androidx.compose.foundation.layout.height 6 | import androidx.compose.ui.Modifier 7 | 8 | /** 9 | * Determines how the height of the month content is calculated. 10 | */ 11 | public enum class YearContentHeightMode { 12 | /** 13 | * The calendar months and days will wrap content height. This allows 14 | * you to use [Modifier.aspectRatio] if you want square day content 15 | * or [Modifier.height] if you want a specific height value 16 | * for the day content. 17 | */ 18 | Wrap, 19 | 20 | /** 21 | * The calendar months will be distributed uniformly to fill the 22 | * parent's height. However, the days within the calendar months will 23 | * wrap content height. This allows you to spread the calendar months 24 | * evenly across the screen while using [Modifier.aspectRatio] if you 25 | * want square day content or [Modifier.height] if you want a specific 26 | * height value for the day content. 27 | */ 28 | Fill, 29 | 30 | /** 31 | * The calendar months and days will uniformly stretch to fill the 32 | * parent's height. This allows you to use [Modifier.fillMaxHeight] for 33 | * the day content height. With this option, your Calendar composable should 34 | * also be created with [Modifier.fillMaxHeight] or [Modifier.height]. 35 | */ 36 | Stretch, 37 | } 38 | -------------------------------------------------------------------------------- /compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/CalendarDay.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.core 2 | 3 | import androidx.compose.runtime.Immutable 4 | import kotlinx.datetime.LocalDate 5 | 6 | /** 7 | * Represents a day on the calendar. 8 | * 9 | * @param date the date for this day. 10 | * @param position the [DayPosition] for this day. 11 | */ 12 | @Immutable 13 | public data class CalendarDay(val date: LocalDate, val position: DayPosition) 14 | -------------------------------------------------------------------------------- /compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/CalendarMonth.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.core 2 | 3 | import androidx.compose.runtime.Immutable 4 | 5 | /** 6 | * Represents a month on the calendar. 7 | * 8 | * @param yearMonth the calendar month value. 9 | * @param weekDays the weeks in this month. 10 | */ 11 | @Immutable 12 | @ConsistentCopyVisibility 13 | public data class CalendarMonth internal constructor( 14 | val yearMonth: YearMonth, 15 | val weekDays: List>, 16 | ) { 17 | override fun equals(other: Any?): Boolean { 18 | if (this === other) return true 19 | if (other == null || this::class != other::class) return false 20 | 21 | other as CalendarMonth 22 | 23 | if (yearMonth != other.yearMonth) return false 24 | if (weekDays.first().first() != other.weekDays.first().first()) return false 25 | if (weekDays.last().last() != other.weekDays.last().last()) return false 26 | 27 | return true 28 | } 29 | 30 | override fun hashCode(): Int { 31 | var result = yearMonth.hashCode() 32 | result = 31 * result + weekDays.first().first().hashCode() 33 | result = 31 * result + weekDays.last().last().hashCode() 34 | return result 35 | } 36 | 37 | override fun toString(): String { 38 | return "CalendarMonth { " + 39 | "first = ${weekDays.first().first()}, " + 40 | "last = ${weekDays.last().last()} " + 41 | "} " 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/CalendarYear.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.core 2 | 3 | import androidx.compose.runtime.Immutable 4 | 5 | /** 6 | * Represents a year on the calendar. 7 | * 8 | * @param year the calendar year value. 9 | * @param months the months in this year. 10 | */ 11 | @Immutable 12 | public data class CalendarYear( 13 | val year: Year, 14 | val months: List, 15 | ) { 16 | override fun equals(other: Any?): Boolean { 17 | if (this === other) return true 18 | if (other == null || this::class != other::class) return false 19 | 20 | other as CalendarYear 21 | 22 | if (year != other.year) return false 23 | if (months.first() != other.months.first()) return false 24 | if (months.last() != other.months.last()) return false 25 | 26 | return true 27 | } 28 | 29 | override fun hashCode(): Int { 30 | var result = year.hashCode() 31 | result = 31 * result + months.first().hashCode() 32 | result = 31 * result + months.last().hashCode() 33 | return result 34 | } 35 | 36 | override fun toString(): String { 37 | return "CalendarYear { " + 38 | "year = $year, " + 39 | "firstMonth = ${months.first()}, " + 40 | "lastMonth = ${months.last()} " + 41 | "} " 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/DayPosition.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.core 2 | 3 | /** 4 | * Describes the position of a [CalendarDay] in the month. 5 | */ 6 | public enum class DayPosition { 7 | /** 8 | * The day is positioned at the start of the month to 9 | * ensure proper alignment of the first day of the week. 10 | * The day belongs to the previous month on the calendar. 11 | */ 12 | InDate, 13 | 14 | /** 15 | * The day belongs to the current month on the calendar. 16 | */ 17 | MonthDate, 18 | 19 | /** 20 | * The day is positioned at the end of the month to 21 | * to fill the remaining days after the days in the month. 22 | * The day belongs to the next month on the calendar. 23 | * @see [OutDateStyle] 24 | */ 25 | OutDate, 26 | } 27 | -------------------------------------------------------------------------------- /compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/ExperimentalCalendarApi.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.core 2 | 3 | @RequiresOptIn( 4 | message = "This calendar API is experimental and is " + 5 | "likely to change or to be removed in the future.", 6 | level = RequiresOptIn.Level.ERROR, 7 | ) 8 | @Retention(AnnotationRetention.BINARY) 9 | public annotation class ExperimentalCalendarApi 10 | -------------------------------------------------------------------------------- /compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/OutDateStyle.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.core 2 | 3 | /** 4 | * Determines how [DayPosition.OutDate] are 5 | * generated for each month on the calendar. 6 | */ 7 | public enum class OutDateStyle { 8 | /** 9 | * The calendar will generate outDates until it reaches 10 | * the end of the month row. This means that if a month 11 | * has 5 rows, it will display 5 rows and if a month 12 | * has 6 rows, it will display 6 rows. 13 | */ 14 | EndOfRow, 15 | 16 | /** 17 | * The calendar will generate outDates until it 18 | * reaches the end of a 6 x 7 grid on each month. 19 | * This means that all months will have 6 rows. 20 | */ 21 | EndOfGrid, 22 | } 23 | -------------------------------------------------------------------------------- /compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Week.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.core 2 | 3 | import androidx.compose.runtime.Immutable 4 | 5 | /** 6 | * Represents a week on the week-based calendar. 7 | * 8 | * @param days the days in this week. 9 | */ 10 | @Immutable 11 | @ConsistentCopyVisibility 12 | public data class Week internal constructor(val days: List) { 13 | override fun equals(other: Any?): Boolean { 14 | if (this === other) return true 15 | if (other == null || this::class != other::class) return false 16 | 17 | other as Week 18 | 19 | if (days.first() != other.days.first()) return false 20 | if (days.last() != other.days.last()) return false 21 | 22 | return true 23 | } 24 | 25 | override fun hashCode(): Int { 26 | var result = days.first().hashCode() 27 | result = 31 * result + days.last().hashCode() 28 | return result 29 | } 30 | 31 | override fun toString(): String { 32 | return "Week { " + 33 | "first = ${days.first()}, " + 34 | "last = ${days.last()} " + 35 | "} " 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/WeekDay.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.core 2 | 3 | import androidx.compose.runtime.Immutable 4 | import kotlinx.datetime.LocalDate 5 | 6 | /** 7 | * Represents a day on the week calendar. 8 | * 9 | * @param date the date for this day. 10 | * @param position the [WeekDayPosition] for this day. 11 | */ 12 | @Immutable 13 | public data class WeekDay(val date: LocalDate, val position: WeekDayPosition) 14 | -------------------------------------------------------------------------------- /compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/WeekDayPosition.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.core 2 | 3 | /** 4 | * Describes the position of a [WeekDay] on the calendar. 5 | */ 6 | public enum class WeekDayPosition { 7 | /** 8 | * The day is positioned at the start of the calendar to 9 | * ensure proper alignment of the first day of the week. 10 | * The day is before the provided start date. 11 | */ 12 | InDate, 13 | 14 | /** 15 | * The day is in the range of the provided start and end dates. 16 | */ 17 | RangeDate, 18 | 19 | /** 20 | * The day is positioned at the end of the calendar to to fill the 21 | * remaining days in the last week after the provided end date. 22 | * The day is after the provided end date. 23 | */ 24 | OutDate, 25 | } 26 | -------------------------------------------------------------------------------- /compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/format/Format.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.core.format 2 | 3 | import com.kizitonwose.calendar.core.Year 4 | import com.kizitonwose.calendar.core.YearMonth 5 | import com.kizitonwose.calendar.core.atMonth 6 | import com.kizitonwose.calendar.core.atStartOfMonth 7 | import com.kizitonwose.calendar.core.yearMonth 8 | import kotlinx.datetime.LocalDate 9 | import kotlinx.datetime.format.char 10 | 11 | private val ISO_YEAR_MONTH by lazy { 12 | LocalDate.Format { 13 | year() 14 | char('-') 15 | monthNumber() 16 | } 17 | } 18 | 19 | private val ISO_YEAR by lazy { 20 | LocalDate.Format { year() } 21 | } 22 | 23 | private val ISO_LOCAL_DATE by lazy { 24 | LocalDate.Formats.ISO 25 | } 26 | 27 | internal fun LocalDate.toIso8601String() = ISO_LOCAL_DATE.format(this) 28 | 29 | internal fun YearMonth.toIso8601String() = ISO_YEAR_MONTH.format(atStartOfMonth()) 30 | 31 | internal fun Year.toIso8601String() = ISO_YEAR.format(atMonth(1).atStartOfMonth()) 32 | 33 | internal fun String.fromIso8601LocalDate(): LocalDate = 34 | LocalDate.parse(this, ISO_LOCAL_DATE) 35 | 36 | internal fun String.fromIso8601YearMonth(): YearMonth = 37 | LocalDate.parse("$this-01", ISO_LOCAL_DATE).yearMonth 38 | 39 | internal fun String.fromIso8601Year(): Year = 40 | Year(LocalDate.parse("$this-01-01", ISO_LOCAL_DATE).year) 41 | -------------------------------------------------------------------------------- /compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/data/DataStore.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.data 2 | 3 | /** 4 | * Basically [MutableMap.getOrPut] but allows us read the map 5 | * in multiple places without calling `getOrPut` everywhere. 6 | */ 7 | internal class DataStore( 8 | private val store: MutableMap = HashMap(), 9 | private val create: (offset: Int) -> V, 10 | ) : MutableMap by store { 11 | override fun get(key: Int): V { 12 | val value = store[key] 13 | return if (value == null) { 14 | val data = create(key) 15 | put(key, data) 16 | data 17 | } else { 18 | value 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/data/Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.data 2 | 3 | import com.kizitonwose.calendar.core.CalendarDay 4 | import com.kizitonwose.calendar.core.DayPosition 5 | import com.kizitonwose.calendar.core.YearMonth 6 | import com.kizitonwose.calendar.core.minusMonths 7 | import com.kizitonwose.calendar.core.plusMonths 8 | import com.kizitonwose.calendar.core.yearMonth 9 | import kotlinx.datetime.DayOfWeek 10 | 11 | // E.g DayOfWeek.SATURDAY.daysUntil(DayOfWeek.TUESDAY) = 3 12 | internal fun DayOfWeek.daysUntil(other: DayOfWeek) = (7 + (other.ordinal - ordinal)) % 7 13 | 14 | // E.g DayOfWeek.SATURDAY.plusDays(3) = DayOfWeek.TUESDAY 15 | internal fun DayOfWeek.plusDays(days: Int): DayOfWeek { 16 | val amount = (days % 7) 17 | return DayOfWeek.entries[(ordinal + (amount + 7)) % 7] 18 | } 19 | 20 | // Find the actual month on the calendar where this date is shown. 21 | internal val CalendarDay.positionYearMonth: YearMonth 22 | get() = when (position) { 23 | DayPosition.InDate -> date.yearMonth.plusMonths(1) 24 | DayPosition.MonthDate -> date.yearMonth 25 | DayPosition.OutDate -> date.yearMonth.minusMonths(1) 26 | } 27 | 28 | internal inline fun Iterable.indexOfFirstOrNull(predicate: (T) -> Boolean): Int? { 29 | val result = indexOfFirst(predicate) 30 | return if (result == -1) null else result 31 | } 32 | -------------------------------------------------------------------------------- /compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/data/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.data 2 | 3 | internal fun > checkRange(start: T, end: T) { 4 | check(end >= start) { 5 | "start: $start is greater than end: $end" 6 | } 7 | } 8 | 9 | internal fun T.asUnit() = Unit 10 | 11 | internal expect fun log(tag: String, message: String) 12 | -------------------------------------------------------------------------------- /compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/data/VisibleItemState.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.data 2 | 3 | import androidx.compose.runtime.Immutable 4 | 5 | @Immutable 6 | internal class VisibleItemState( 7 | val firstVisibleItemIndex: Int = 0, 8 | val firstVisibleItemScrollOffset: Int = 0, 9 | ) 10 | -------------------------------------------------------------------------------- /compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/data/WeekData.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.data 2 | 3 | import com.kizitonwose.calendar.core.Week 4 | import com.kizitonwose.calendar.core.WeekDay 5 | import com.kizitonwose.calendar.core.WeekDayPosition 6 | import com.kizitonwose.calendar.core.minusDays 7 | import com.kizitonwose.calendar.core.plusDays 8 | import com.kizitonwose.calendar.core.plusWeeks 9 | import com.kizitonwose.calendar.core.weeksUntil 10 | import kotlinx.datetime.DayOfWeek 11 | import kotlinx.datetime.LocalDate 12 | 13 | internal data class WeekDateRange( 14 | val startDateAdjusted: LocalDate, 15 | val endDateAdjusted: LocalDate, 16 | ) 17 | 18 | internal fun getWeekCalendarAdjustedRange( 19 | startDate: LocalDate, 20 | endDate: LocalDate, 21 | firstDayOfWeek: DayOfWeek, 22 | ): WeekDateRange { 23 | val inDays = firstDayOfWeek.daysUntil(startDate.dayOfWeek) 24 | val startDateAdjusted = startDate.minusDays(inDays) 25 | val weeksBetween = startDateAdjusted.weeksUntil(endDate) 26 | val endDateAdjusted = startDateAdjusted.plusWeeks(weeksBetween).plusDays(6) 27 | return WeekDateRange(startDateAdjusted = startDateAdjusted, endDateAdjusted = endDateAdjusted) 28 | } 29 | 30 | internal fun getWeekCalendarData( 31 | startDateAdjusted: LocalDate, 32 | offset: Int, 33 | desiredStartDate: LocalDate, 34 | desiredEndDate: LocalDate, 35 | ): WeekData { 36 | val firstDayInWeek = startDateAdjusted.plusWeeks(offset) 37 | return WeekData(firstDayInWeek, desiredStartDate, desiredEndDate) 38 | } 39 | 40 | internal data class WeekData( 41 | private val firstDayInWeek: LocalDate, 42 | private val desiredStartDate: LocalDate, 43 | private val desiredEndDate: LocalDate, 44 | ) { 45 | val week: Week = Week((0 until 7).map { dayOffset -> getDay(dayOffset) }) 46 | 47 | private fun getDay(dayOffset: Int): WeekDay { 48 | val date = firstDayInWeek.plusDays(dayOffset) 49 | val position = when { 50 | date < desiredStartDate -> WeekDayPosition.InDate 51 | date > desiredEndDate -> WeekDayPosition.OutDate 52 | else -> WeekDayPosition.RangeDate 53 | } 54 | return WeekDay(date, position) 55 | } 56 | } 57 | 58 | internal fun getWeekIndex(startDateAdjusted: LocalDate, date: LocalDate): Int { 59 | return startDateAdjusted.weeksUntil(date) 60 | } 61 | 62 | internal fun getWeekIndicesCount(startDateAdjusted: LocalDate, endDateAdjusted: LocalDate): Int { 63 | // Add one to include the start week itself! 64 | return getWeekIndex(startDateAdjusted, endDateAdjusted) + 1 65 | } 66 | -------------------------------------------------------------------------------- /compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/data/YearData.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.data 2 | 3 | import com.kizitonwose.calendar.core.CalendarYear 4 | import com.kizitonwose.calendar.core.OutDateStyle 5 | import com.kizitonwose.calendar.core.Year 6 | import com.kizitonwose.calendar.core.atMonth 7 | import com.kizitonwose.calendar.core.plusYears 8 | import com.kizitonwose.calendar.core.yearsUntil 9 | import kotlinx.datetime.DayOfWeek 10 | import kotlinx.datetime.Month 11 | 12 | internal fun getCalendarYearData( 13 | startYear: Year, 14 | offset: Int, 15 | firstDayOfWeek: DayOfWeek, 16 | outDateStyle: OutDateStyle, 17 | ): CalendarYear { 18 | val year = startYear.plusYears(offset) 19 | val months = List(Month.entries.size) { index -> 20 | getCalendarMonthData( 21 | startMonth = year.atMonth(Month.JANUARY), 22 | offset = index, 23 | firstDayOfWeek = firstDayOfWeek, 24 | outDateStyle = outDateStyle, 25 | ).calendarMonth 26 | } 27 | return CalendarYear(year, months) 28 | } 29 | 30 | internal fun getYearIndex(startYear: Year, targetYear: Year): Int { 31 | return startYear.yearsUntil(targetYear) 32 | } 33 | 34 | internal fun getYearIndicesCount(startYear: Year, endYear: Year): Int { 35 | // Add one to include the start year itself! 36 | return getYearIndex(startYear, endYear) + 1 37 | } 38 | -------------------------------------------------------------------------------- /compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearSerializationTest.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.core 2 | 3 | import com.kizitonwose.calendar.core.serializers.YearComponentSerializer 4 | import com.kizitonwose.calendar.core.serializers.YearIso8601Serializer 5 | import kotlinx.serialization.KSerializer 6 | import kotlinx.serialization.SerializationException 7 | import kotlinx.serialization.json.Json 8 | import kotlinx.serialization.serializer 9 | import kotlin.test.Test 10 | import kotlin.test.assertEquals 11 | import kotlin.test.assertFailsWith 12 | 13 | class YearSerializationTest { 14 | private fun iso8601Serialization(serializer: KSerializer) { 15 | for ((localDate, json) in listOf( 16 | Pair(Year(2020), "\"2020\""), 17 | Pair(Year(-2020), "\"-2020\""), 18 | Pair(Year(2019), "\"2019\""), 19 | )) { 20 | assertEquals(json, Json.encodeToString(serializer, localDate)) 21 | assertEquals(localDate, Json.decodeFromString(serializer, json)) 22 | } 23 | } 24 | 25 | private fun componentSerialization(serializer: KSerializer) { 26 | for ((localDate, json) in listOf( 27 | Pair(Year(2020), "{\"year\":2020}"), 28 | Pair(Year(-2020), "{\"year\":-2020}"), 29 | Pair(Year(2019), "{\"year\":2019}"), 30 | )) { 31 | assertEquals(json, Json.encodeToString(serializer, localDate)) 32 | assertEquals(localDate, Json.decodeFromString(serializer, json)) 33 | } 34 | // all components must be present 35 | assertFailsWith { 36 | Json.decodeFromString(serializer, "{}") 37 | } 38 | // invalid values must fail to construct 39 | assertFailsWith { 40 | Json.decodeFromString(serializer, "{\"year\":1000000000000}") 41 | } 42 | } 43 | 44 | @Test 45 | fun testIso8601Serialization() { 46 | iso8601Serialization(YearIso8601Serializer) 47 | } 48 | 49 | @Test 50 | fun testComponentSerialization() { 51 | componentSerialization(YearComponentSerializer) 52 | } 53 | 54 | @Test 55 | fun testDefaultSerializers() { 56 | // should be the same as the ISO 8601 57 | iso8601Serialization(Json.serializersModule.serializer()) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/utils/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.utils 2 | 3 | import com.kizitonwose.calendar.core.OutDateStyle 4 | import com.kizitonwose.calendar.core.YearMonth 5 | import com.kizitonwose.calendar.core.minusMonths 6 | import com.kizitonwose.calendar.core.plusMonths 7 | import com.kizitonwose.calendar.data.getCalendarMonthData 8 | import kotlinx.datetime.DayOfWeek 9 | 10 | internal infix fun Pair.toTriple(that: C): Triple = Triple(first, second, that) 11 | 12 | internal fun YearMonth.weeksInMonth(firstDayOfWeek: DayOfWeek) = getCalendarMonthData( 13 | startMonth = this, 14 | offset = 0, 15 | firstDayOfWeek = firstDayOfWeek, 16 | outDateStyle = OutDateStyle.EndOfRow, 17 | ).calendarMonth.weekDays.count() 18 | 19 | internal val YearMonth.nextMonth: YearMonth 20 | get() = this.plusMonths(1) 21 | 22 | internal val YearMonth.previousMonth: YearMonth 23 | get() = this.minusMonths(1) 24 | -------------------------------------------------------------------------------- /compose-multiplatform/library/src/desktopMain/kotlin/com/kizitonwose/calendar/data/Utils.desktop.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.data 2 | 3 | import java.util.logging.Level 4 | import java.util.logging.Logger 5 | 6 | internal actual fun log(tag: String, message: String) = 7 | logger.warning("$tag : $message") 8 | 9 | private val logger = Logger.getLogger("Calendar").apply { 10 | level = Level.WARNING 11 | } 12 | -------------------------------------------------------------------------------- /compose-multiplatform/library/src/iosMain/kotlin/com/kizitonwose/calendar/core/Extensions.ios.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.core 2 | 3 | import androidx.compose.ui.text.intl.Locale 4 | import kotlinx.datetime.DayOfWeek 5 | import platform.Foundation.NSCalendar 6 | import platform.Foundation.NSLocale 7 | 8 | /** 9 | * Returns the first day of the week from the provided locale. 10 | */ 11 | public actual fun firstDayOfWeekFromLocale(locale: Locale): DayOfWeek { 12 | val firstWeekday = NSCalendar.currentCalendar.let { 13 | it.setLocale(NSLocale(locale.toLanguageTag())) 14 | // https://developer.apple.com/documentation/foundation/calendar/2293656-firstweekday 15 | // Value is one-based, starting from sunday 16 | it.firstWeekday.toInt() 17 | } 18 | // Get the index value from a sunday-based array. 19 | return daysOfWeek(firstDayOfWeek = DayOfWeek.SUNDAY)[firstWeekday - 1] 20 | } 21 | -------------------------------------------------------------------------------- /compose-multiplatform/library/src/iosMain/kotlin/com/kizitonwose/calendar/data/Utils.ios.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.data 2 | 3 | import platform.Foundation.NSLog 4 | 5 | internal actual fun log(tag: String, message: String) = 6 | NSLog("$tag : $message") 7 | -------------------------------------------------------------------------------- /compose-multiplatform/library/src/jvmMain/kotlin/com/kizitonwose/calendar/core/Converters.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.core 2 | 3 | import java.time.Year as jtYear 4 | import java.time.YearMonth as jtYearMonth 5 | 6 | public fun YearMonth.toJavaYearMonth(): jtYearMonth = jtYearMonth.of(year, month) 7 | 8 | public fun jtYearMonth.toKotlinYearMonth(): YearMonth = YearMonth(year, month) 9 | 10 | public fun Year.toJavaYear(): jtYear = jtYear.of(value) 11 | 12 | public fun jtYear.toKotlinYear(): Year = Year(value) 13 | -------------------------------------------------------------------------------- /compose-multiplatform/library/src/jvmMain/kotlin/com/kizitonwose/calendar/core/Extensions.jvm.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.core 2 | 3 | import androidx.compose.ui.text.intl.Locale 4 | import java.time.DayOfWeek 5 | import java.time.temporal.WeekFields 6 | import java.util.Locale as JavaLocale 7 | 8 | public actual fun firstDayOfWeekFromLocale(locale: Locale): DayOfWeek = 9 | WeekFields.of(JavaLocale.forLanguageTag(locale.toLanguageTag())).firstDayOfWeek 10 | -------------------------------------------------------------------------------- /compose-multiplatform/library/src/wasmJsMain/kotlin/com/kizitonwose/calendar/core/Extensions.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.core 2 | 3 | import androidx.compose.ui.text.intl.Locale 4 | import kotlinx.datetime.DayOfWeek 5 | 6 | /** 7 | * Returns the first day of the week from the provided locale. 8 | */ 9 | public actual fun firstDayOfWeekFromLocale(locale: Locale): DayOfWeek { 10 | return try { 11 | val firstDay = jsFirstDayFromTag(locale.toLanguageTag()) 12 | daysOfWeek(firstDayOfWeek = DayOfWeek.MONDAY)[firstDay - 1] 13 | // Unavailable on Firefox 14 | } catch (e: Exception) { 15 | firstDayFromMap(locale) 16 | } 17 | } 18 | 19 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getWeekInfo#firstday 20 | private fun jsFirstDayFromTag(languageTag: String): Int = js("new Intl.Locale(languageTag).weekInfo?.firstDay") 21 | -------------------------------------------------------------------------------- /compose-multiplatform/library/src/wasmJsMain/kotlin/com/kizitonwose/calendar/data/Utils.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.data 2 | 3 | internal actual fun log(tag: String, message: String) = 4 | consoleLog("$tag : $message") 5 | 6 | @JsFun("(output) => console.log(output)") 7 | private external fun consoleLog(vararg output: String?) 8 | -------------------------------------------------------------------------------- /compose-multiplatform/sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /compose-multiplatform/sample/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /compose-multiplatform/sample/src/androidMain/kotlin/Device.android.kt: -------------------------------------------------------------------------------- 1 | actual fun isMobile(): Boolean = true 2 | -------------------------------------------------------------------------------- /compose-multiplatform/sample/src/androidMain/kotlin/com/kizitonwose/calendar/compose/multiplatform/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.compose.multiplatform 2 | 3 | import App 4 | import android.os.Bundle 5 | import androidx.activity.ComponentActivity 6 | import androidx.activity.compose.setContent 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.tooling.preview.Preview 9 | 10 | class MainActivity : ComponentActivity() { 11 | override fun onCreate(savedInstanceState: Bundle?) { 12 | super.onCreate(savedInstanceState) 13 | setContent { 14 | App() 15 | } 16 | } 17 | } 18 | 19 | @Preview 20 | @Composable 21 | fun AppAndroidPreview() { 22 | App() 23 | } 24 | -------------------------------------------------------------------------------- /compose-multiplatform/sample/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /compose-multiplatform/sample/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /compose-multiplatform/sample/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /compose-multiplatform/sample/src/androidMain/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kizitonwose/Calendar/54a745ab5ad6cf0c6632e3917147bc5d208e33d5/compose-multiplatform/sample/src/androidMain/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /compose-multiplatform/sample/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kizitonwose/Calendar/54a745ab5ad6cf0c6632e3917147bc5d208e33d5/compose-multiplatform/sample/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /compose-multiplatform/sample/src/androidMain/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kizitonwose/Calendar/54a745ab5ad6cf0c6632e3917147bc5d208e33d5/compose-multiplatform/sample/src/androidMain/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /compose-multiplatform/sample/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kizitonwose/Calendar/54a745ab5ad6cf0c6632e3917147bc5d208e33d5/compose-multiplatform/sample/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /compose-multiplatform/sample/src/androidMain/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kizitonwose/Calendar/54a745ab5ad6cf0c6632e3917147bc5d208e33d5/compose-multiplatform/sample/src/androidMain/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /compose-multiplatform/sample/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kizitonwose/Calendar/54a745ab5ad6cf0c6632e3917147bc5d208e33d5/compose-multiplatform/sample/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /compose-multiplatform/sample/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kizitonwose/Calendar/54a745ab5ad6cf0c6632e3917147bc5d208e33d5/compose-multiplatform/sample/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /compose-multiplatform/sample/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kizitonwose/Calendar/54a745ab5ad6cf0c6632e3917147bc5d208e33d5/compose-multiplatform/sample/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /compose-multiplatform/sample/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kizitonwose/Calendar/54a745ab5ad6cf0c6632e3917147bc5d208e33d5/compose-multiplatform/sample/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /compose-multiplatform/sample/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kizitonwose/Calendar/54a745ab5ad6cf0c6632e3917147bc5d208e33d5/compose-multiplatform/sample/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /compose-multiplatform/sample/src/androidMain/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Calendar Multiplatform Sample 3 | 4 | -------------------------------------------------------------------------------- /compose-multiplatform/sample/src/commonMain/kotlin/Colors.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.graphics.Color 2 | 3 | object Colors { 4 | val example1Selection = Color(0xFFFCCA3E) 5 | val example1Bg = Color(0xFF3A284C) 6 | val example1BgLight = Color(0xFF433254) 7 | val example1BgSecondary = Color(0xFF51356E) 8 | val example1WhiteLight = Color(0x4DFFFFFF) 9 | val example4GrayPast = Color(0xFFBEBEBE) 10 | val example4Gray = Color(0xFF474747) 11 | val example5PageBgColor = Color(0xFF0E0E0E) 12 | val example5ItemViewBgColor = Color(0xFF1B1B1B) 13 | val example5ToolbarColor = Color(0xFF282828) 14 | val example5TextGrey = Color(0xFFDCDCDC) 15 | val example5TextGreyLight = Color(0xFF616161) 16 | val example6MonthBgColor = Color(0xFFB2EBF2) 17 | val example6MonthBgColor2 = Color(0xFFF2C4B2) 18 | val example6MonthBgColor3 = Color(0xFFB2B8F2) 19 | val example7Yellow = Color(0xFFFFEB3B) 20 | val primary = Color(0xFF3F51B5) 21 | val accent = Color(0xFFFF4081) 22 | } 23 | -------------------------------------------------------------------------------- /compose-multiplatform/sample/src/commonMain/kotlin/ContinuousSelectionHelper.kt: -------------------------------------------------------------------------------- 1 | import com.kizitonwose.calendar.core.atEndOfMonth 2 | import com.kizitonwose.calendar.core.atStartOfMonth 3 | import com.kizitonwose.calendar.core.yearMonth 4 | import kotlinx.datetime.LocalDate 5 | import kotlinx.datetime.daysUntil 6 | import kotlin.LazyThreadSafetyMode.NONE 7 | 8 | data class DateSelection(val startDate: LocalDate? = null, val endDate: LocalDate? = null) { 9 | val daysBetween by lazy(NONE) { 10 | if (startDate == null || endDate == null) { 11 | null 12 | } else { 13 | startDate.daysUntil(endDate) 14 | } 15 | } 16 | } 17 | 18 | private val rangeFormatter = LocalDate.Formats.ISO 19 | fun dateRangeDisplayText(startDate: LocalDate, endDate: LocalDate): String { 20 | return "Selected: ${rangeFormatter.format(startDate)} - ${rangeFormatter.format(endDate)}" 21 | } 22 | 23 | object ContinuousSelectionHelper { 24 | fun getSelection( 25 | clickedDate: LocalDate, 26 | dateSelection: DateSelection, 27 | ): DateSelection { 28 | val (selectionStartDate, selectionEndDate) = dateSelection 29 | return if (selectionStartDate != null) { 30 | if (clickedDate < selectionStartDate || selectionEndDate != null) { 31 | DateSelection(startDate = clickedDate, endDate = null) 32 | } else if (clickedDate != selectionStartDate) { 33 | DateSelection(startDate = selectionStartDate, endDate = clickedDate) 34 | } else { 35 | DateSelection(startDate = clickedDate, endDate = null) 36 | } 37 | } else { 38 | DateSelection(startDate = clickedDate, endDate = null) 39 | } 40 | } 41 | 42 | fun isInDateBetweenSelection( 43 | inDate: LocalDate, 44 | startDate: LocalDate, 45 | endDate: LocalDate, 46 | ): Boolean { 47 | if (startDate.yearMonth == endDate.yearMonth) return false 48 | if (inDate.yearMonth == startDate.yearMonth) return true 49 | val firstDateInThisMonth = inDate.yearMonth.next.atStartOfMonth() 50 | return firstDateInThisMonth in startDate..endDate && startDate != firstDateInThisMonth 51 | } 52 | 53 | fun isOutDateBetweenSelection( 54 | outDate: LocalDate, 55 | startDate: LocalDate, 56 | endDate: LocalDate, 57 | ): Boolean { 58 | if (startDate.yearMonth == endDate.yearMonth) return false 59 | if (outDate.yearMonth == endDate.yearMonth) return true 60 | val lastDateInThisMonth = outDate.yearMonth.previous.atEndOfMonth() 61 | return lastDateInThisMonth in startDate..endDate && endDate != lastDateInThisMonth 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /compose-multiplatform/sample/src/commonMain/kotlin/Device.kt: -------------------------------------------------------------------------------- 1 | expect fun isMobile(): Boolean 2 | -------------------------------------------------------------------------------- /compose-multiplatform/sample/src/commonMain/kotlin/Format.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.text.intl.Locale 2 | import androidx.compose.ui.text.toUpperCase 3 | import com.kizitonwose.calendar.core.Week 4 | import com.kizitonwose.calendar.core.YearMonth 5 | import com.kizitonwose.calendar.core.yearMonth 6 | import kotlinx.datetime.DayOfWeek 7 | import kotlinx.datetime.Month 8 | 9 | fun YearMonth.displayText(short: Boolean = false): String { 10 | return "${month.displayText(short = short)} $year" 11 | } 12 | 13 | fun Month.displayText(short: Boolean = true): String { 14 | return getDisplayName(short, enLocale) 15 | } 16 | 17 | fun DayOfWeek.displayText(uppercase: Boolean = false, narrow: Boolean = false): String { 18 | return getDisplayName(narrow, enLocale).let { value -> 19 | if (uppercase) value.toUpperCase(enLocale) else value 20 | } 21 | } 22 | 23 | expect fun Month.getDisplayName(short: Boolean, locale: Locale): String 24 | 25 | expect fun DayOfWeek.getDisplayName(narrow: Boolean = false, locale: Locale): String 26 | 27 | private val enLocale = Locale("en-US") 28 | 29 | fun getWeekPageTitle(week: Week): String { 30 | val firstDate = week.days.first().date 31 | val lastDate = week.days.last().date 32 | return when { 33 | firstDate.yearMonth == lastDate.yearMonth -> { 34 | firstDate.yearMonth.displayText() 35 | } 36 | 37 | firstDate.year == lastDate.year -> { 38 | "${firstDate.month.displayText(short = false)} - ${lastDate.yearMonth.displayText()}" 39 | } 40 | 41 | else -> { 42 | "${firstDate.yearMonth.displayText()} - ${lastDate.yearMonth.displayText()}" 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /compose-multiplatform/sample/src/commonMain/kotlin/Scaffold.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.foundation.layout.PaddingValues 2 | import androidx.compose.foundation.layout.calculateEndPadding 3 | import androidx.compose.foundation.layout.calculateStartPadding 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.compositionLocalOf 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.platform.LocalLayoutDirection 9 | 10 | val LocalScaffoldPaddingValues = compositionLocalOf { PaddingValues() } 11 | 12 | @Composable 13 | fun Modifier.applyScaffoldHorizontalPaddings() = 14 | padding( 15 | start = LocalScaffoldPaddingValues.current.calculateStartPadding(LocalLayoutDirection.current), 16 | end = LocalScaffoldPaddingValues.current.calculateEndPadding(LocalLayoutDirection.current), 17 | ) 18 | 19 | @Composable 20 | fun Modifier.applyScaffoldTopPadding() = 21 | padding(top = LocalScaffoldPaddingValues.current.calculateTopPadding()) 22 | 23 | @Composable 24 | fun Modifier.applyScaffoldBottomPadding() = 25 | padding(bottom = LocalScaffoldPaddingValues.current.calculateBottomPadding()) 26 | -------------------------------------------------------------------------------- /compose-multiplatform/sample/src/commonMain/kotlin/Theme.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.material3.lightColorScheme 2 | import androidx.compose.ui.graphics.Color 3 | 4 | val SampleColorScheme = lightColorScheme( 5 | primary = Color(0xFF3F51B5), 6 | onPrimary = Color.White, 7 | primaryContainer = Color(0xFF3F51B5), 8 | onPrimaryContainer = Color.White, 9 | inversePrimary = Color.Black, 10 | secondary = Color(0xFF3F51B5), 11 | onSecondary = Color.White, 12 | secondaryContainer = Color(0xFF3F51B5), 13 | onSecondaryContainer = Color.White, 14 | tertiary = Color(0xFF3F51B5), 15 | onTertiary = Color.White, 16 | tertiaryContainer = Color(0xFF3F51B5), 17 | onTertiaryContainer = Color.White, 18 | background = Color.White, 19 | onBackground = Color.Black, 20 | surface = Color.White, 21 | onSurface = Color.Black, 22 | surfaceVariant = Color.White, 23 | onSurfaceVariant = Color.Black, 24 | surfaceTint = Color.White, 25 | inverseSurface = Color(0xFF121212), 26 | inverseOnSurface = Color.White, 27 | error = Color(0xFFB00020), 28 | onError = Color.White, 29 | errorContainer = Color(0xFFB00020), 30 | onErrorContainer = Color.White, 31 | outline = Color(0xFFAFAFAF), 32 | outlineVariant = Color(0xFFCCCCCC), 33 | scrim = Color.Black, 34 | surfaceBright = Color.White, 35 | surfaceContainer = Color.White, 36 | surfaceContainerHigh = Color.White, 37 | surfaceContainerHighest = Color.White, 38 | surfaceContainerLow = Color.White, 39 | surfaceContainerLowest = Color.White, 40 | surfaceDim = Color.White, 41 | ) 42 | -------------------------------------------------------------------------------- /compose-multiplatform/sample/src/desktopMain/kotlin/Device.desktop.kt: -------------------------------------------------------------------------------- 1 | actual fun isMobile(): Boolean = false 2 | -------------------------------------------------------------------------------- /compose-multiplatform/sample/src/desktopMain/kotlin/Main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.window.Window 2 | import androidx.compose.ui.window.application 3 | 4 | fun main() = application { 5 | Window( 6 | onCloseRequest = ::exitApplication, 7 | title = "Calendar Sample", 8 | ) { 9 | App() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /compose-multiplatform/sample/src/iosMain/kotlin/Device.ios.kt: -------------------------------------------------------------------------------- 1 | actual fun isMobile(): Boolean = true 2 | -------------------------------------------------------------------------------- /compose-multiplatform/sample/src/iosMain/kotlin/Format.ios.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.text.intl.Locale 2 | import com.kizitonwose.calendar.core.daysOfWeek 3 | import kotlinx.datetime.DayOfWeek 4 | import kotlinx.datetime.Month 5 | import platform.Foundation.NSCalendar 6 | import platform.Foundation.NSLocale 7 | 8 | actual fun Month.getDisplayName(short: Boolean, locale: Locale): String = 9 | NSCalendar.currentCalendar.let { 10 | it.setLocale(NSLocale(locale.toLanguageTag())) 11 | it.monthSymbols[Month.entries.indexOf(this)] as String 12 | } 13 | 14 | actual fun DayOfWeek.getDisplayName(narrow: Boolean, locale: Locale): String = 15 | NSCalendar.currentCalendar.let { 16 | it.setLocale(NSLocale(locale.toLanguageTag())) 17 | val values = if (narrow) { 18 | it.veryShortWeekdaySymbols 19 | } else { 20 | it.shortWeekdaySymbols 21 | } 22 | values[sundayBasedWeek.indexOf(this)] as String 23 | } 24 | 25 | private val sundayBasedWeek = daysOfWeek(firstDayOfWeek = DayOfWeek.SUNDAY) 26 | -------------------------------------------------------------------------------- /compose-multiplatform/sample/src/iosMain/kotlin/MainViewController.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.window.ComposeUIViewController 2 | 3 | @Suppress("FunctionName") 4 | fun MainViewController() = ComposeUIViewController { App() } 5 | -------------------------------------------------------------------------------- /compose-multiplatform/sample/src/jvmMain/kotlin/Format.jvm.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.text.intl.Locale 2 | import kotlinx.datetime.DayOfWeek 3 | import kotlinx.datetime.Month 4 | import java.time.format.TextStyle 5 | import java.util.Locale as JavaLocale 6 | 7 | actual fun Month.getDisplayName(short: Boolean, locale: Locale): String { 8 | val style = if (short) TextStyle.SHORT else TextStyle.FULL 9 | return getDisplayName(style, JavaLocale.forLanguageTag(locale.toLanguageTag())) 10 | } 11 | 12 | actual fun DayOfWeek.getDisplayName(narrow: Boolean, locale: Locale): String { 13 | val style = if (narrow) TextStyle.NARROW else TextStyle.SHORT 14 | return getDisplayName(style, JavaLocale.forLanguageTag(locale.toLanguageTag())) 15 | } 16 | -------------------------------------------------------------------------------- /compose-multiplatform/sample/src/wasmJsMain/kotlin/Device.wasmJs.kt: -------------------------------------------------------------------------------- 1 | actual fun isMobile(): Boolean = false 2 | -------------------------------------------------------------------------------- /compose-multiplatform/sample/src/wasmJsMain/kotlin/Format.wasmJs.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.text.capitalize 2 | import androidx.compose.ui.text.intl.Locale 3 | import androidx.compose.ui.text.toLowerCase 4 | import kotlinx.datetime.DayOfWeek 5 | import kotlinx.datetime.Month 6 | 7 | actual fun Month.getDisplayName(short: Boolean, locale: Locale): String { 8 | val name = name.toLowerCase(enLocale).capitalize(enLocale) 9 | return if (short) name.take(3) else name 10 | } 11 | 12 | actual fun DayOfWeek.getDisplayName(narrow: Boolean, locale: Locale): String { 13 | return name.toLowerCase(enLocale).capitalize(enLocale).take(if (narrow) 1 else 3) 14 | } 15 | 16 | private val enLocale = Locale("en-US") 17 | -------------------------------------------------------------------------------- /compose-multiplatform/sample/src/wasmJsMain/kotlin/Main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.ExperimentalComposeUiApi 2 | import androidx.compose.ui.window.ComposeViewport 3 | import kotlinx.browser.document 4 | 5 | @OptIn(ExperimentalComposeUiApi::class) 6 | fun main() { 7 | ComposeViewport(document.body!!) { 8 | App() 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /compose-multiplatform/sample/src/wasmJsMain/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Calendar Library 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /compose-multiplatform/sample/src/wasmJsMain/resources/styles.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | width: 100%; 3 | height: 100%; 4 | margin: 0; 5 | padding: 0; 6 | overflow: hidden; 7 | } -------------------------------------------------------------------------------- /compose/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /compose/build.gradle.kts: -------------------------------------------------------------------------------- 1 | 2 | import com.kizitonwose.calendar.buildsrc.Android 3 | import com.kizitonwose.calendar.buildsrc.Config 4 | import com.kizitonwose.calendar.buildsrc.Version 5 | 6 | plugins { 7 | alias(libs.plugins.androidLibrary) 8 | alias(libs.plugins.kotlinAndroid) 9 | alias(libs.plugins.composeCompiler) 10 | alias(libs.plugins.mavenPublish) 11 | } 12 | 13 | android { 14 | compileSdk = Android.compileSdk 15 | namespace = "com.kizitonwose.calendar.compose" 16 | defaultConfig { 17 | minSdk = Android.minSdk 18 | } 19 | java { 20 | toolchain { 21 | languageVersion.set(Config.compatibleJavaLanguageVersion) 22 | } 23 | } 24 | kotlin { 25 | jvmToolchain { 26 | languageVersion.set(Config.compatibleJavaLanguageVersion) 27 | } 28 | } 29 | buildFeatures { 30 | compose = true 31 | } 32 | } 33 | 34 | dependencies { 35 | api(project(":core")) 36 | implementation(project(":data")) 37 | implementation(libs.kotlin.stdlib) 38 | 39 | implementation(libs.compose.ui.ui) 40 | implementation(libs.compose.ui.tooling) 41 | implementation(libs.compose.foundation) 42 | implementation(libs.compose.runtime) 43 | 44 | testImplementation(platform(libs.test.junit5.bom)) 45 | testImplementation(libs.test.junit5.api) 46 | testRuntimeOnly(libs.test.junit5.engine) 47 | testRuntimeOnly(libs.test.junit.platform.launcher) 48 | } 49 | 50 | mavenPublishing { 51 | coordinates(version = Version.android) 52 | } 53 | -------------------------------------------------------------------------------- /compose/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=compose 2 | POM_DESCRIPTION=A highly customizable calendar library for Android, backed by LazyRow/LazyColumn. 3 | POM_PACKAGING=aar 4 | -------------------------------------------------------------------------------- /compose/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /compose/src/main/java/com/kizitonwose/calendar/compose/CalendarDefaults.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.compose 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.foundation.gestures.FlingBehavior 5 | import androidx.compose.foundation.gestures.ScrollableDefaults 6 | import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider 7 | import androidx.compose.foundation.gestures.snapping.SnapPosition 8 | import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior 9 | import androidx.compose.foundation.lazy.LazyListState 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.remember 12 | 13 | internal object CalendarDefaults { 14 | /** 15 | * The default implementation in [rememberSnapFlingBehavior] snaps to the center of the layout 16 | * but we want to snap to the start. For example, in a vertical calendar, when the layout size 17 | * is larger than the item size(e.g two or more visible months), we don't want the item's 18 | * center to be at the center of the layout when it snaps, instead we want the item's top 19 | * to be at the top of the layout. 20 | */ 21 | @OptIn(ExperimentalFoundationApi::class) 22 | @Composable 23 | private fun pagedFlingBehavior(state: LazyListState): FlingBehavior { 24 | val snappingLayout = remember(state) { 25 | val provider = SnapLayoutInfoProvider(state, SnapPosition.Start) 26 | CalendarSnapLayoutInfoProvider(provider) 27 | } 28 | return rememberSnapFlingBehavior(snappingLayout) 29 | } 30 | 31 | @Composable 32 | private fun continuousFlingBehavior(): FlingBehavior = ScrollableDefaults.flingBehavior() 33 | 34 | @Composable 35 | fun flingBehavior(isPaged: Boolean, state: LazyListState): FlingBehavior { 36 | return if (isPaged) pagedFlingBehavior(state) else continuousFlingBehavior() 37 | } 38 | } 39 | 40 | @ExperimentalFoundationApi 41 | @Suppress("FunctionName") 42 | private fun CalendarSnapLayoutInfoProvider( 43 | snapLayoutInfoProvider: SnapLayoutInfoProvider, 44 | ): SnapLayoutInfoProvider = object : SnapLayoutInfoProvider by snapLayoutInfoProvider { 45 | /** 46 | * In compose 1.3, the default was single page snapping (zero), but this changed 47 | * in compose 1.4 to decayed page snapping which is not great for calendar usage. 48 | */ 49 | override fun calculateApproachOffset(velocity: Float, decayOffset: Float): Float = 0f 50 | } 51 | -------------------------------------------------------------------------------- /compose/src/main/java/com/kizitonwose/calendar/compose/CalendarInfo.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.compose 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.kizitonwose.calendar.core.OutDateStyle 5 | import java.time.DayOfWeek 6 | 7 | @Immutable 8 | internal data class CalendarInfo( 9 | val indexCount: Int, 10 | private val firstDayOfWeek: DayOfWeek? = null, 11 | private val outDateStyle: OutDateStyle? = null, 12 | ) 13 | -------------------------------------------------------------------------------- /compose/src/main/java/com/kizitonwose/calendar/compose/CalendarLayoutInfo.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.compose 2 | 3 | import androidx.compose.foundation.lazy.LazyListItemInfo 4 | import androidx.compose.foundation.lazy.LazyListLayoutInfo 5 | import com.kizitonwose.calendar.core.CalendarMonth 6 | 7 | /** 8 | * Contains useful information about the currently displayed layout state of the calendar. 9 | * For example you can get the list of currently displayed months. 10 | * 11 | * Use [CalendarState.layoutInfo] to retrieve this. 12 | * @see LazyListLayoutInfo 13 | */ 14 | public class CalendarLayoutInfo(info: LazyListLayoutInfo, private val month: (Int) -> CalendarMonth) : 15 | LazyListLayoutInfo by info { 16 | /** 17 | * The list of [CalendarItemInfo] representing all the currently visible months. 18 | */ 19 | public val visibleMonthsInfo: List 20 | get() = visibleItemsInfo.map { 21 | CalendarItemInfo(it, month(it.index)) 22 | } 23 | } 24 | 25 | /** 26 | * Contains useful information about an individual [CalendarMonth] on the calendar. 27 | * 28 | * @param month The month in the list. 29 | * 30 | * @see CalendarLayoutInfo 31 | * @see LazyListItemInfo 32 | */ 33 | public class CalendarItemInfo(info: LazyListItemInfo, public val month: CalendarMonth) : LazyListItemInfo by info 34 | -------------------------------------------------------------------------------- /compose/src/main/java/com/kizitonwose/calendar/compose/ContentHeightMode.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.compose 2 | 3 | import androidx.compose.foundation.layout.aspectRatio 4 | import androidx.compose.foundation.layout.fillMaxHeight 5 | import androidx.compose.foundation.layout.height 6 | import androidx.compose.ui.Modifier 7 | 8 | /** 9 | * Determines how the height of the day content is calculated. 10 | */ 11 | public enum class ContentHeightMode { 12 | /** 13 | * The day container will wrap its height. This allows you to 14 | * use [Modifier.aspectRatio] if you want square day content 15 | * or [Modifier.height] if you want a specific height value 16 | * for the day content. 17 | */ 18 | Wrap, 19 | 20 | /** 21 | * The days in each month will spread to fill the parent's height after 22 | * any available header and footer content height has been accounted for. 23 | * This allows you to use [Modifier.fillMaxHeight] for the day content 24 | * height. With this option, your Calendar composable should also 25 | * be created with [Modifier.fillMaxHeight] or [Modifier.height]. 26 | */ 27 | Fill, 28 | } 29 | -------------------------------------------------------------------------------- /compose/src/main/java/com/kizitonwose/calendar/compose/ItemPlacementInfo.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.compose 2 | 3 | import androidx.compose.foundation.gestures.Orientation 4 | import androidx.compose.runtime.Immutable 5 | import androidx.compose.runtime.Stable 6 | import androidx.compose.runtime.withFrameNanos 7 | import androidx.compose.ui.layout.LayoutCoordinates 8 | import androidx.compose.ui.unit.round 9 | import kotlinx.coroutines.isActive 10 | import kotlin.coroutines.coroutineContext 11 | 12 | @Immutable 13 | internal data class ItemCoordinates( 14 | val itemRootCoordinates: LayoutCoordinates, 15 | val firstDayCoordinates: LayoutCoordinates, 16 | ) 17 | 18 | @Stable 19 | internal class ItemPlacementInfo { 20 | private var itemCoordinates: ItemCoordinates? = null 21 | 22 | fun onItemPlaced(itemCoordinates: ItemCoordinates) { 23 | this.itemCoordinates = itemCoordinates 24 | } 25 | 26 | suspend fun awaitFistDayOffsetAndSize(orientation: Orientation): OffsetSize? { 27 | var itemCoordinates = this.itemCoordinates 28 | while (coroutineContext.isActive && itemCoordinates == null) { 29 | withFrameNanos {} 30 | itemCoordinates = this.itemCoordinates 31 | } 32 | if (itemCoordinates == null) { 33 | return null 34 | } 35 | val (itemRootCoordinates, firstDayCoordinates) = itemCoordinates 36 | val daySize = firstDayCoordinates.size 37 | val dayOffset = itemRootCoordinates.localPositionOf(firstDayCoordinates).round() 38 | return when (orientation) { 39 | Orientation.Vertical -> OffsetSize( 40 | offset = dayOffset.y, 41 | size = daySize.height, 42 | ) 43 | 44 | Orientation.Horizontal -> { 45 | OffsetSize( 46 | offset = dayOffset.x, 47 | size = daySize.width, 48 | ) 49 | } 50 | } 51 | } 52 | 53 | @Immutable 54 | internal data class OffsetSize( 55 | val offset: Int, 56 | val size: Int, 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /compose/src/main/java/com/kizitonwose/calendar/compose/VisibleItemState.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.compose 2 | 3 | import androidx.compose.runtime.Immutable 4 | import java.io.Serializable 5 | 6 | @Immutable 7 | internal class VisibleItemState( 8 | val firstVisibleItemIndex: Int = 0, 9 | val firstVisibleItemScrollOffset: Int = 0, 10 | ) : Serializable 11 | -------------------------------------------------------------------------------- /compose/src/main/java/com/kizitonwose/calendar/compose/heatmapcalendar/HeatMapWeek.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.compose.heatmapcalendar 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.kizitonwose.calendar.compose.HeatMapCalendar 5 | import com.kizitonwose.calendar.core.CalendarDay 6 | import java.io.Serializable 7 | 8 | /** 9 | * Represents a week on the heatmap calendar. 10 | * 11 | * This model exists only as a wrapper class with the [Immutable] annotation for compose. 12 | * The alternative would be to use the `kotlinx.ImmutableList` type for the `days` value 13 | * which is used ONLY in the dayContent parameter of the [HeatMapCalendar] but then we 14 | * would force that dependency on the library consumers. 15 | * 16 | * @param days the days in this week. 17 | */ 18 | @Immutable 19 | public data class HeatMapWeek(val days: List) : Serializable 20 | -------------------------------------------------------------------------------- /compose/src/main/java/com/kizitonwose/calendar/compose/heatmapcalendar/HeatMapWeekHeaderPosition.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.compose.heatmapcalendar 2 | 3 | import com.kizitonwose.calendar.compose.HeatMapCalendar 4 | 5 | /** 6 | * Determines the position of the week header 7 | * composable (Mon, Tue, Wed...) in the [HeatMapCalendar] 8 | */ 9 | public enum class HeatMapWeekHeaderPosition { 10 | /** 11 | * The header is positioned at the start of the calendar. 12 | */ 13 | Start, 14 | 15 | /** 16 | * The header is positioned at the end of the calendar. 17 | */ 18 | End, 19 | } 20 | -------------------------------------------------------------------------------- /compose/src/main/java/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendarLayoutInfo.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.compose.weekcalendar 2 | 3 | import androidx.compose.foundation.lazy.LazyListItemInfo 4 | import androidx.compose.foundation.lazy.LazyListLayoutInfo 5 | import com.kizitonwose.calendar.core.Week 6 | 7 | /** 8 | * Contains useful information about the currently displayed layout state of the calendar. 9 | * For example you can get the list of currently displayed months. 10 | * 11 | * Use [WeekCalendarState.layoutInfo] to retrieve this. 12 | * 13 | * @see LazyListLayoutInfo 14 | */ 15 | public class WeekCalendarLayoutInfo( 16 | info: LazyListLayoutInfo, 17 | private val getIndexData: (Int) -> Week, 18 | ) : LazyListLayoutInfo by info { 19 | /** 20 | * The list of [WeekCalendarItemInfo] representing all the currently visible weeks. 21 | */ 22 | public val visibleWeeksInfo: List 23 | get() = visibleItemsInfo.map { info -> 24 | WeekCalendarItemInfo(info, getIndexData(info.index)) 25 | } 26 | } 27 | 28 | /** 29 | * Contains useful information about an individual week on the calendar. 30 | * 31 | * @param week The week in the list. 32 | 33 | * @see WeekCalendarLayoutInfo 34 | * @see LazyListItemInfo 35 | */ 36 | public class WeekCalendarItemInfo(info: LazyListItemInfo, public val week: Week) : 37 | LazyListItemInfo by info 38 | -------------------------------------------------------------------------------- /compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarLayoutInfo.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.compose.yearcalendar 2 | 3 | import androidx.compose.foundation.lazy.LazyListItemInfo 4 | import androidx.compose.foundation.lazy.LazyListLayoutInfo 5 | import com.kizitonwose.calendar.core.CalendarYear 6 | 7 | /** 8 | * Contains useful information about the currently displayed layout state of the calendar. 9 | * For example you can get the list of currently displayed years. 10 | * 11 | * Use [YearCalendarState.layoutInfo] to retrieve this. 12 | * 13 | * @see LazyListLayoutInfo 14 | */ 15 | public class YearCalendarLayoutInfo( 16 | info: LazyListLayoutInfo, 17 | private val getIndexData: (Int) -> CalendarYear, 18 | ) : LazyListLayoutInfo by info { 19 | /** 20 | * The list of [YearCalendarItemInfo] representing all the currently visible years. 21 | */ 22 | public val visibleYearsInfo: List 23 | get() = visibleItemsInfo.map { info -> 24 | YearCalendarItemInfo(info, getIndexData(info.index)) 25 | } 26 | } 27 | 28 | /** 29 | * Contains useful information about an individual year on the calendar. 30 | * 31 | * @param year The year in the list. 32 | 33 | * @see YearCalendarLayoutInfo 34 | * @see LazyListItemInfo 35 | */ 36 | public class YearCalendarItemInfo(info: LazyListItemInfo, public val year: CalendarYear) : 37 | LazyListItemInfo by info 38 | -------------------------------------------------------------------------------- /compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.compose.yearcalendar 2 | 3 | import androidx.compose.foundation.layout.aspectRatio 4 | import androidx.compose.foundation.layout.fillMaxHeight 5 | import androidx.compose.foundation.layout.height 6 | import androidx.compose.ui.Modifier 7 | 8 | /** 9 | * Determines how the height of the month content is calculated. 10 | */ 11 | public enum class YearContentHeightMode { 12 | /** 13 | * The calendar months and days will wrap content height. This allows 14 | * you to use [Modifier.aspectRatio] if you want square day content 15 | * or [Modifier.height] if you want a specific height value 16 | * for the day content. 17 | */ 18 | Wrap, 19 | 20 | /** 21 | * The calendar months will be distributed uniformly to fill the 22 | * parent's height. However, the days within the calendar months will 23 | * wrap content height. This allows you to spread the calendar months 24 | * evenly across the screen while using [Modifier.aspectRatio] if you 25 | * want square day content or [Modifier.height] if you want a specific 26 | * height value for the day content. 27 | */ 28 | Fill, 29 | 30 | /** 31 | * The calendar months and days will uniformly stretch to fill the 32 | * parent's height. This allows you to use [Modifier.fillMaxHeight] for 33 | * the day content height. With this option, your Calendar composable should 34 | * also be created with [Modifier.fillMaxHeight] or [Modifier.height]. 35 | */ 36 | Stretch, 37 | } 38 | -------------------------------------------------------------------------------- /compose/src/test/java/com/kizitonwose/calendar/compose/HeatMapCalendarStateTest.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.compose 2 | 3 | import com.kizitonwose.calendar.compose.heatmapcalendar.HeatMapCalendarState 4 | import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale 5 | import org.junit.jupiter.api.Assertions.assertEquals 6 | import org.junit.jupiter.api.Test 7 | import java.time.DayOfWeek 8 | import java.time.LocalDate 9 | import java.time.YearMonth 10 | 11 | class HeatMapCalendarStateTest { 12 | @Test 13 | fun `start month update is reflected in the state`() { 14 | val now = YearMonth.now() 15 | val updatedStartMonth = now.minusMonths(4) 16 | val state = createState( 17 | startMonth = now, 18 | endMonth = now, 19 | ) 20 | 21 | assertEquals(state.store[0].yearMonth, now) 22 | 23 | state.startMonth = updatedStartMonth 24 | 25 | assertEquals(state.store[0].yearMonth, updatedStartMonth) 26 | } 27 | 28 | @Test 29 | fun `end month update is reflected in the state`() { 30 | val now = YearMonth.now() 31 | val updatedEndMonth = now.plusMonths(4) 32 | val state = createState( 33 | startMonth = now, 34 | endMonth = now, 35 | ) 36 | 37 | assertEquals(state.store[0].yearMonth, now) 38 | 39 | state.endMonth = updatedEndMonth 40 | 41 | assertEquals(state.store[4].yearMonth, updatedEndMonth) 42 | } 43 | 44 | @Test 45 | fun `first day of the week update is reflected in the state`() { 46 | val firstDayOfWeek = LocalDate.now().dayOfWeek 47 | 48 | val state = createState(firstDayOfWeek = firstDayOfWeek) 49 | 50 | state.store[0].weekDays.forEach { week -> 51 | assertEquals(week.first().date.dayOfWeek, firstDayOfWeek) 52 | } 53 | 54 | do { 55 | val updatedFirstDayOfWeek = state.firstDayOfWeek.plus(1) 56 | state.firstDayOfWeek = updatedFirstDayOfWeek 57 | 58 | state.store[0].weekDays.forEach { week -> 59 | assertEquals(week.first().date.dayOfWeek, updatedFirstDayOfWeek) 60 | } 61 | } while (firstDayOfWeek != state.firstDayOfWeek) 62 | } 63 | 64 | private fun createState( 65 | startMonth: YearMonth = YearMonth.now(), 66 | endMonth: YearMonth = startMonth, 67 | firstVisibleMonth: YearMonth = startMonth, 68 | firstDayOfWeek: DayOfWeek = firstDayOfWeekFromLocale(), 69 | visibleItemState: VisibleItemState = VisibleItemState(), 70 | ) = HeatMapCalendarState( 71 | startMonth = startMonth, 72 | endMonth = endMonth, 73 | firstVisibleMonth = firstVisibleMonth, 74 | firstDayOfWeek = firstDayOfWeek, 75 | visibleItemState = visibleItemState, 76 | ) 77 | } 78 | -------------------------------------------------------------------------------- /compose/src/test/java/com/kizitonwose/calendar/compose/WeekCalendarStateTest.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.compose 2 | 3 | import com.kizitonwose.calendar.compose.weekcalendar.WeekCalendarState 4 | import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale 5 | import org.junit.jupiter.api.Assertions.assertEquals 6 | import org.junit.jupiter.api.Assertions.assertTrue 7 | import org.junit.jupiter.api.Test 8 | import java.time.DayOfWeek 9 | import java.time.LocalDate 10 | 11 | class WeekCalendarStateTest { 12 | @Test 13 | fun `start date update is reflected in the state`() { 14 | val now = LocalDate.now() 15 | val updatedStartDate = now.minusDays(7) 16 | val state = createState( 17 | startDate = now, 18 | endDate = now, 19 | ) 20 | 21 | assertTrue(state.store[0].days.map { it.date }.contains(now)) 22 | 23 | state.startDate = updatedStartDate 24 | 25 | assertTrue(state.store[0].days.map { it.date }.contains(updatedStartDate)) 26 | } 27 | 28 | @Test 29 | fun `end date update is reflected in the state`() { 30 | val now = LocalDate.now() 31 | val updatedEndDate = now.plusDays(7) 32 | val state = createState( 33 | startDate = now, 34 | endDate = now, 35 | ) 36 | 37 | assertTrue(state.store[0].days.map { it.date }.contains(now)) 38 | 39 | state.endDate = updatedEndDate 40 | 41 | assertTrue(state.store[1].days.map { it.date }.contains(updatedEndDate)) 42 | } 43 | 44 | @Test 45 | fun `first day of the week update is reflected in the state`() { 46 | val firstDayOfWeek = LocalDate.now().dayOfWeek 47 | 48 | val state = createState(firstDayOfWeek = firstDayOfWeek) 49 | 50 | assertEquals(state.store[0].days.first().date.dayOfWeek, firstDayOfWeek) 51 | 52 | do { 53 | val updatedFirstDayOfWeek = state.firstDayOfWeek.plus(1) 54 | state.firstDayOfWeek = updatedFirstDayOfWeek 55 | 56 | assertEquals(state.store[0].days.first().date.dayOfWeek, updatedFirstDayOfWeek) 57 | } while (firstDayOfWeek != state.firstDayOfWeek) 58 | } 59 | 60 | private fun createState( 61 | startDate: LocalDate = LocalDate.now(), 62 | endDate: LocalDate = startDate, 63 | firstVisibleWeekDate: LocalDate = startDate, 64 | firstDayOfWeek: DayOfWeek = firstDayOfWeekFromLocale(), 65 | visibleItemState: VisibleItemState = VisibleItemState(), 66 | ) = WeekCalendarState( 67 | startDate = startDate, 68 | endDate = endDate, 69 | firstVisibleWeekDate = firstVisibleWeekDate, 70 | firstDayOfWeek = firstDayOfWeek, 71 | visibleItemState = visibleItemState, 72 | ) 73 | } 74 | -------------------------------------------------------------------------------- /core/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.kizitonwose.calendar.buildsrc.Config 2 | import com.kizitonwose.calendar.buildsrc.Version 3 | 4 | plugins { 5 | alias(libs.plugins.kotlinJvm) 6 | alias(libs.plugins.mavenPublish) 7 | } 8 | 9 | java { 10 | toolchain { 11 | languageVersion.set(Config.compatibleJavaLanguageVersion) 12 | } 13 | } 14 | 15 | kotlin { 16 | jvmToolchain { 17 | languageVersion.set(Config.compatibleJavaLanguageVersion) 18 | } 19 | } 20 | 21 | dependencies { 22 | compileOnly(libs.compose.runtime) // Only needed for @Immutable annotation. 23 | } 24 | 25 | mavenPublishing { 26 | coordinates(version = Version.android) 27 | } 28 | -------------------------------------------------------------------------------- /core/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=core 2 | POM_DESCRIPTION=Calendar core module for shared public classes and extensions. 3 | POM_PACKAGING=aar 4 | -------------------------------------------------------------------------------- /core/src/main/java/com/kizitonwose/calendar/core/CalendarDay.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.core 2 | 3 | import androidx.compose.runtime.Immutable 4 | import java.io.Serializable 5 | import java.time.LocalDate 6 | 7 | /** 8 | * Represents a day on the calendar. 9 | * 10 | * @param date the date for this day. 11 | * @param position the [DayPosition] for this day. 12 | */ 13 | @Immutable 14 | public data class CalendarDay(val date: LocalDate, val position: DayPosition) : Serializable 15 | -------------------------------------------------------------------------------- /core/src/main/java/com/kizitonwose/calendar/core/CalendarMonth.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.core 2 | 3 | import androidx.compose.runtime.Immutable 4 | import java.io.Serializable 5 | import java.time.YearMonth 6 | 7 | /** 8 | * Represents a month on the calendar. 9 | * 10 | * @param yearMonth the calendar month value. 11 | * @param weekDays the weeks in this month. 12 | */ 13 | @Immutable 14 | public data class CalendarMonth( 15 | val yearMonth: YearMonth, 16 | val weekDays: List>, 17 | ) : Serializable { 18 | override fun equals(other: Any?): Boolean { 19 | if (this === other) return true 20 | if (javaClass != other?.javaClass) return false 21 | 22 | other as CalendarMonth 23 | 24 | if (yearMonth != other.yearMonth) return false 25 | if (weekDays.first().first() != other.weekDays.first().first()) return false 26 | if (weekDays.last().last() != other.weekDays.last().last()) return false 27 | 28 | return true 29 | } 30 | 31 | override fun hashCode(): Int { 32 | var result = yearMonth.hashCode() 33 | result = 31 * result + weekDays.first().first().hashCode() 34 | result = 31 * result + weekDays.last().last().hashCode() 35 | return result 36 | } 37 | 38 | override fun toString(): String { 39 | return "CalendarMonth { " + 40 | "yearMonth = $yearMonth, " + 41 | "firstDay = ${weekDays.first().first()}, " + 42 | "lastDay = ${weekDays.last().last()} " + 43 | "} " 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /core/src/main/java/com/kizitonwose/calendar/core/CalendarYear.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.core 2 | 3 | import androidx.compose.runtime.Immutable 4 | import java.io.Serializable 5 | import java.time.Year 6 | 7 | /** 8 | * Represents a year on the calendar. 9 | * 10 | * @param year the calendar year value. 11 | * @param months the months in this year. 12 | */ 13 | @Immutable 14 | public data class CalendarYear( 15 | val year: Year, 16 | val months: List, 17 | ) : Serializable { 18 | override fun equals(other: Any?): Boolean { 19 | if (this === other) return true 20 | if (javaClass != other?.javaClass) return false 21 | 22 | other as CalendarYear 23 | 24 | if (year != other.year) return false 25 | if (months.first() != other.months.first()) return false 26 | if (months.last() != other.months.last()) return false 27 | 28 | return true 29 | } 30 | 31 | override fun hashCode(): Int { 32 | var result = year.hashCode() 33 | result = 31 * result + months.first().hashCode() 34 | result = 31 * result + months.last().hashCode() 35 | return result 36 | } 37 | 38 | override fun toString(): String { 39 | return "CalendarYear { " + 40 | "year = $year, " + 41 | "firstMonth = ${months.first()}, " + 42 | "lastMonth = ${months.last()} " + 43 | "} " 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /core/src/main/java/com/kizitonwose/calendar/core/DayPosition.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.core 2 | 3 | /** 4 | * Describes the position of a [CalendarDay] in the month. 5 | */ 6 | public enum class DayPosition { 7 | /** 8 | * The day is positioned at the start of the month to 9 | * ensure proper alignment of the first day of the week. 10 | * The day belongs to the previous month on the calendar. 11 | */ 12 | InDate, 13 | 14 | /** 15 | * The day belongs to the current month on the calendar. 16 | */ 17 | MonthDate, 18 | 19 | /** 20 | * The day is positioned at the end of the month to 21 | * to fill the remaining days after the days in the month. 22 | * The day belongs to the next month on the calendar. 23 | * @see [OutDateStyle] 24 | */ 25 | OutDate, 26 | } 27 | -------------------------------------------------------------------------------- /core/src/main/java/com/kizitonwose/calendar/core/ExperimentalCalendarApi.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.core 2 | 3 | @RequiresOptIn( 4 | message = "This calendar API is experimental and is " + 5 | "likely to change or to be removed in the future.", 6 | level = RequiresOptIn.Level.ERROR, 7 | ) 8 | @Retention(AnnotationRetention.BINARY) 9 | public annotation class ExperimentalCalendarApi 10 | -------------------------------------------------------------------------------- /core/src/main/java/com/kizitonwose/calendar/core/Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.core 2 | 3 | import java.time.DayOfWeek 4 | import java.time.LocalDate 5 | import java.time.YearMonth 6 | import java.time.temporal.WeekFields 7 | import java.util.Locale 8 | 9 | /** 10 | * Returns the days of week values such that the desired 11 | * [firstDayOfWeek] property is at the start position. 12 | * 13 | * @see [firstDayOfWeekFromLocale] 14 | */ 15 | @JvmOverloads 16 | public fun daysOfWeek(firstDayOfWeek: DayOfWeek = firstDayOfWeekFromLocale()): List { 17 | val pivot = 7 - firstDayOfWeek.ordinal 18 | val daysOfWeek = DayOfWeek.entries 19 | // Order `daysOfWeek` array so that firstDayOfWeek is at the start position. 20 | return daysOfWeek.takeLast(pivot) + daysOfWeek.dropLast(pivot) 21 | } 22 | 23 | /** 24 | * Returns the first day of the week from the provided locale. 25 | */ 26 | @JvmOverloads 27 | public fun firstDayOfWeekFromLocale(locale: Locale = Locale.getDefault()): DayOfWeek = WeekFields.of(locale).firstDayOfWeek 28 | 29 | /** 30 | * Returns a [LocalDate] at the start of the month. 31 | * 32 | * Complements [YearMonth.atEndOfMonth]. 33 | */ 34 | public fun YearMonth.atStartOfMonth(): LocalDate = this.atDay(1) 35 | 36 | public val LocalDate.yearMonth: YearMonth 37 | get() = YearMonth.of(year, month) 38 | 39 | public val YearMonth.nextMonth: YearMonth 40 | get() = this.plusMonths(1) 41 | 42 | public val YearMonth.previousMonth: YearMonth 43 | get() = this.minusMonths(1) 44 | -------------------------------------------------------------------------------- /core/src/main/java/com/kizitonwose/calendar/core/OutDateStyle.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.core 2 | 3 | /** 4 | * Determines how [DayPosition.OutDate] are 5 | * generated for each month on the calendar. 6 | */ 7 | public enum class OutDateStyle { 8 | /** 9 | * The calendar will generate outDates until it reaches 10 | * the end of the month row. This means that if a month 11 | * has 5 rows, it will display 5 rows and if a month 12 | * has 6 rows, it will display 6 rows. 13 | */ 14 | EndOfRow, 15 | 16 | /** 17 | * The calendar will generate outDates until it 18 | * reaches the end of a 6 x 7 grid on each month. 19 | * This means that all months will have 6 rows. 20 | */ 21 | EndOfGrid, 22 | } 23 | -------------------------------------------------------------------------------- /core/src/main/java/com/kizitonwose/calendar/core/Week.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.core 2 | 3 | import androidx.compose.runtime.Immutable 4 | import java.io.Serializable 5 | 6 | /** 7 | * Represents a week on the week-based calendar. 8 | * 9 | * @param days the days in this week. 10 | */ 11 | @Immutable 12 | public data class Week(val days: List) : Serializable { 13 | override fun equals(other: Any?): Boolean { 14 | if (this === other) return true 15 | if (javaClass != other?.javaClass) return false 16 | 17 | other as Week 18 | 19 | if (days.first() != other.days.first()) return false 20 | if (days.last() != other.days.last()) return false 21 | 22 | return true 23 | } 24 | 25 | override fun hashCode(): Int { 26 | var result = days.first().hashCode() 27 | result = 31 * result + days.last().hashCode() 28 | return result 29 | } 30 | 31 | override fun toString(): String { 32 | return "Week { " + 33 | "first = ${days.first()}, " + 34 | "last = ${days.last()} " + 35 | "} " 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /core/src/main/java/com/kizitonwose/calendar/core/WeekDay.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.core 2 | 3 | import androidx.compose.runtime.Immutable 4 | import java.io.Serializable 5 | import java.time.LocalDate 6 | 7 | /** 8 | * Represents a day on the week calendar. 9 | * 10 | * @param date the date for this day. 11 | * @param position the [WeekDayPosition] for this day. 12 | */ 13 | @Immutable 14 | public data class WeekDay(val date: LocalDate, val position: WeekDayPosition) : Serializable 15 | -------------------------------------------------------------------------------- /core/src/main/java/com/kizitonwose/calendar/core/WeekDayPosition.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.core 2 | 3 | /** 4 | * Describes the position of a [WeekDay] on the calendar. 5 | */ 6 | public enum class WeekDayPosition { 7 | /** 8 | * The day is positioned at the start of the calendar to 9 | * ensure proper alignment of the first day of the week. 10 | * The day is before the provided start date. 11 | */ 12 | InDate, 13 | 14 | /** 15 | * The day is in the range of the provided start and end dates. 16 | */ 17 | RangeDate, 18 | 19 | /** 20 | * The day is positioned at the end of the calendar to to fill the 21 | * remaining days in the last week after the provided end date. 22 | * The day is after the provided end date. 23 | */ 24 | OutDate, 25 | } 26 | -------------------------------------------------------------------------------- /data/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /data/build.gradle.kts: -------------------------------------------------------------------------------- 1 | 2 | import com.kizitonwose.calendar.buildsrc.Config 3 | import com.kizitonwose.calendar.buildsrc.Version 4 | 5 | plugins { 6 | alias(libs.plugins.kotlinJvm) 7 | alias(libs.plugins.mavenPublish) 8 | } 9 | 10 | java { 11 | toolchain { 12 | languageVersion.set(Config.compatibleJavaLanguageVersion) 13 | } 14 | } 15 | 16 | kotlin { 17 | jvmToolchain { 18 | languageVersion.set(Config.compatibleJavaLanguageVersion) 19 | } 20 | } 21 | 22 | dependencies { 23 | implementation(project(":core")) 24 | implementation(libs.kotlin.stdlib) 25 | 26 | testImplementation(platform(libs.test.junit5.bom)) 27 | testImplementation(libs.test.junit5.api) 28 | testRuntimeOnly(libs.test.junit5.engine) 29 | testRuntimeOnly(libs.test.junit.platform.launcher) 30 | } 31 | 32 | mavenPublishing { 33 | coordinates(version = Version.android) 34 | } 35 | -------------------------------------------------------------------------------- /data/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=data 2 | POM_DESCRIPTION=Calendar data module for shared internal logic. 3 | POM_PACKAGING=aar 4 | -------------------------------------------------------------------------------- /data/src/main/java/com/kizitonwose/calendar/data/DataStore.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.data 2 | 3 | /** 4 | * Basically [MutableMap.getOrPut] but allows us read the map 5 | * in multiple places without calling `getOrPut` everywhere. 6 | */ 7 | public class DataStore( 8 | private val store: MutableMap = HashMap(), 9 | private val create: (offset: Int) -> V, 10 | ) : MutableMap by store { 11 | override fun get(key: Int): V { 12 | val value = store[key] 13 | return if (value == null) { 14 | val data = create(key) 15 | put(key, data) 16 | data 17 | } else { 18 | value 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /data/src/main/java/com/kizitonwose/calendar/data/Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.data 2 | 3 | import com.kizitonwose.calendar.core.CalendarDay 4 | import com.kizitonwose.calendar.core.DayPosition 5 | import com.kizitonwose.calendar.core.nextMonth 6 | import com.kizitonwose.calendar.core.previousMonth 7 | import com.kizitonwose.calendar.core.yearMonth 8 | import java.time.DayOfWeek 9 | import java.time.YearMonth 10 | 11 | // E.g DayOfWeek.SATURDAY.daysUntil(DayOfWeek.TUESDAY) = 3 12 | public fun DayOfWeek.daysUntil(other: DayOfWeek): Int = (7 + (other.ordinal - ordinal)) % 7 13 | 14 | // Find the actual month on the calendar where this date is shown. 15 | public val CalendarDay.positionYearMonth: YearMonth 16 | get() = when (position) { 17 | DayPosition.InDate -> date.yearMonth.nextMonth 18 | DayPosition.MonthDate -> date.yearMonth 19 | DayPosition.OutDate -> date.yearMonth.previousMonth 20 | } 21 | 22 | public inline fun Iterable.indexOfFirstOrNull(predicate: (T) -> Boolean): Int? { 23 | val result = indexOfFirst(predicate) 24 | return if (result == -1) null else result 25 | } 26 | -------------------------------------------------------------------------------- /data/src/main/java/com/kizitonwose/calendar/data/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.data 2 | 3 | public fun > checkRange(start: T, end: T) { 4 | check(end >= start) { 5 | "start: $start is greater than end: $end" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /data/src/main/java/com/kizitonwose/calendar/data/WeekData.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.data 2 | 3 | import com.kizitonwose.calendar.core.Week 4 | import com.kizitonwose.calendar.core.WeekDay 5 | import com.kizitonwose.calendar.core.WeekDayPosition 6 | import java.time.DayOfWeek 7 | import java.time.LocalDate 8 | import java.time.temporal.ChronoUnit 9 | 10 | public data class WeekDateRange( 11 | val startDateAdjusted: LocalDate, 12 | val endDateAdjusted: LocalDate, 13 | ) 14 | 15 | public fun getWeekCalendarAdjustedRange( 16 | startDate: LocalDate, 17 | endDate: LocalDate, 18 | firstDayOfWeek: DayOfWeek, 19 | ): WeekDateRange { 20 | val inDays = firstDayOfWeek.daysUntil(startDate.dayOfWeek) 21 | val startDateAdjusted = startDate.minusDays(inDays.toLong()) 22 | val weeksBetween = 23 | ChronoUnit.WEEKS.between(startDateAdjusted, endDate).toInt() 24 | val endDateAdjusted = startDateAdjusted.plusWeeks(weeksBetween.toLong()).plusDays(6) 25 | return WeekDateRange(startDateAdjusted = startDateAdjusted, endDateAdjusted = endDateAdjusted) 26 | } 27 | 28 | public fun getWeekCalendarData( 29 | startDateAdjusted: LocalDate, 30 | offset: Int, 31 | desiredStartDate: LocalDate, 32 | desiredEndDate: LocalDate, 33 | ): WeekData { 34 | val firstDayInWeek = startDateAdjusted.plusWeeks(offset.toLong()) 35 | return WeekData(firstDayInWeek, desiredStartDate, desiredEndDate) 36 | } 37 | 38 | @ConsistentCopyVisibility 39 | public data class WeekData internal constructor( 40 | private val firstDayInWeek: LocalDate, 41 | private val desiredStartDate: LocalDate, 42 | private val desiredEndDate: LocalDate, 43 | ) { 44 | val week: Week = Week((0 until 7).map { dayOffset -> getDay(dayOffset) }) 45 | 46 | private fun getDay(dayOffset: Int): WeekDay { 47 | val date = firstDayInWeek.plusDays(dayOffset.toLong()) 48 | val position = when { 49 | date < desiredStartDate -> WeekDayPosition.InDate 50 | date > desiredEndDate -> WeekDayPosition.OutDate 51 | else -> WeekDayPosition.RangeDate 52 | } 53 | return WeekDay(date, position) 54 | } 55 | } 56 | 57 | public fun getWeekIndex(startDateAdjusted: LocalDate, date: LocalDate): Int { 58 | return ChronoUnit.WEEKS.between(startDateAdjusted, date).toInt() 59 | } 60 | 61 | public fun getWeekIndicesCount(startDateAdjusted: LocalDate, endDateAdjusted: LocalDate): Int { 62 | // Add one to include the start week itself! 63 | return getWeekIndex(startDateAdjusted, endDateAdjusted) + 1 64 | } 65 | -------------------------------------------------------------------------------- /data/src/main/java/com/kizitonwose/calendar/data/YearData.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.data 2 | 3 | import com.kizitonwose.calendar.core.CalendarYear 4 | import com.kizitonwose.calendar.core.OutDateStyle 5 | import java.time.DayOfWeek 6 | import java.time.Month 7 | import java.time.Year 8 | import java.time.temporal.ChronoUnit 9 | 10 | public fun getCalendarYearData( 11 | startYear: Year, 12 | offset: Int, 13 | firstDayOfWeek: DayOfWeek, 14 | outDateStyle: OutDateStyle, 15 | ): CalendarYear { 16 | val year = startYear.plusYears(offset.toLong()) 17 | val months = List(Month.entries.size) { index -> 18 | getCalendarMonthData( 19 | startMonth = year.atMonth(Month.JANUARY), 20 | offset = index, 21 | firstDayOfWeek = firstDayOfWeek, 22 | outDateStyle = outDateStyle, 23 | ).calendarMonth 24 | } 25 | return CalendarYear(year, months) 26 | } 27 | 28 | public fun getYearIndex(startYear: Year, targetYear: Year): Int { 29 | return ChronoUnit.YEARS.between(startYear, targetYear).toInt() 30 | } 31 | 32 | public fun getYearIndicesCount(startYear: Year, endYear: Year): Int { 33 | // Add one to include the start year itself! 34 | return getYearIndex(startYear, endYear) + 1 35 | } 36 | -------------------------------------------------------------------------------- /data/src/test/java/com/kizitonwose/calendar/data/DayOfWeekTest.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.data 2 | 3 | import com.kizitonwose.calendar.core.daysOfWeek 4 | import org.junit.jupiter.api.Assertions.assertEquals 5 | import org.junit.jupiter.api.Test 6 | import java.time.DayOfWeek 7 | 8 | class DayOfWeekTest { 9 | @Test 10 | fun `days until works as expected`() { 11 | assertEquals(5, DayOfWeek.FRIDAY.daysUntil(DayOfWeek.WEDNESDAY)) 12 | assertEquals(2, DayOfWeek.TUESDAY.daysUntil(DayOfWeek.THURSDAY)) 13 | assertEquals(0, DayOfWeek.SUNDAY.daysUntil(DayOfWeek.SUNDAY)) 14 | assertEquals(0, DayOfWeek.WEDNESDAY.daysUntil(DayOfWeek.WEDNESDAY)) 15 | assertEquals(3, DayOfWeek.SATURDAY.daysUntil(DayOfWeek.TUESDAY)) 16 | assertEquals(5, DayOfWeek.WEDNESDAY.daysUntil(DayOfWeek.MONDAY)) 17 | assertEquals(1, DayOfWeek.THURSDAY.daysUntil(DayOfWeek.FRIDAY)) 18 | assertEquals(6, DayOfWeek.MONDAY.daysUntil(DayOfWeek.SUNDAY)) 19 | assertEquals(6, DayOfWeek.SUNDAY.daysUntil(DayOfWeek.SATURDAY)) 20 | } 21 | 22 | @Test 23 | fun `first day of the week works as expected`() { 24 | DayOfWeek.entries.forEach { dayOfWeek -> 25 | assertEquals(dayOfWeek, daysOfWeek(firstDayOfWeek = dayOfWeek).first()) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /data/src/test/java/com/kizitonwose/calendar/data/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.data 2 | 3 | import java.time.DayOfWeek 4 | import java.time.YearMonth 5 | import java.time.temporal.WeekFields 6 | 7 | fun YearMonth.weeksInMonth(firstDayOfWeek: DayOfWeek) = atEndOfMonth().get(WeekFields.of(firstDayOfWeek, 1).weekOfMonth()) 8 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | android.defaults.buildfeatures.resvalues=false 2 | android.defaults.buildfeatures.shaders=false 3 | android.useAndroidX=true 4 | 5 | kotlin.code.style=official 6 | 7 | org.gradle.caching=true 8 | org.gradle.jvmargs=-Dfile.encoding=UTF-8 -Xmx4g 9 | org.gradle.parallel=true 10 | 11 | SONATYPE_HOST=S01 12 | RELEASE_SIGNING_ENABLED=true 13 | 14 | GROUP=com.kizitonwose.calendar 15 | 16 | POM_NAME=Calendar 17 | POM_INCEPTION_YEAR=2019 18 | POM_URL=https://github.com/kizitonwose/calendar 19 | POM_SCM_URL=https://github.com/kizitonwose/calendar 20 | POM_SCM_CONNECTION=scm:git:git://github.com/kizitonwose/calendar.git 21 | POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/kizitonwose/calendar.git 22 | 23 | POM_LICENCE_NAME=MIT License 24 | POM_LICENCE_URL=https://github.com/kizitonwose/calendar/blob/main/LICENSE.md 25 | POM_LICENCE_DIST=repo 26 | 27 | POM_DEVELOPER_ID=kizitonwose 28 | POM_DEVELOPER_NAME=Kizito Nwose 29 | POM_DEVELOPER_URL=https://github.com/kizitonwose 30 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kizitonwose/Calendar/54a745ab5ad6cf0c6632e3917147bc5d208e33d5/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Jun 18 20:39:23 CEST 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /images/in_out_dates.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kizitonwose/Calendar/54a745ab5ad6cf0c6632e3917147bc5d208e33d5/images/in_out_dates.png -------------------------------------------------------------------------------- /images/showcase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kizitonwose/Calendar/54a745ab5ad6cf0c6632e3917147bc5d208e33d5/images/showcase.png -------------------------------------------------------------------------------- /sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /sample/build.gradle.kts: -------------------------------------------------------------------------------- 1 | 2 | import com.kizitonwose.calendar.buildsrc.Android 3 | import com.kizitonwose.calendar.buildsrc.Config 4 | 5 | plugins { 6 | alias(libs.plugins.androidApplication) 7 | alias(libs.plugins.kotlinAndroid) 8 | alias(libs.plugins.composeCompiler) 9 | } 10 | 11 | android { 12 | compileSdk = Android.compileSdk 13 | namespace = "com.kizitonwose.calendar.sample" 14 | defaultConfig { 15 | applicationId = "com.kizitonwose.calendar.sample" 16 | minSdk = Android.minSdk 17 | targetSdk = Android.targetSdk 18 | versionCode = 1 19 | versionName = "1.0" 20 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 21 | } 22 | buildFeatures { 23 | viewBinding = true 24 | compose = true 25 | } 26 | buildTypes { 27 | named("release") { 28 | isMinifyEnabled = true 29 | } 30 | } 31 | java { 32 | toolchain { 33 | languageVersion.set(Config.compatibleJavaLanguageVersion) 34 | } 35 | } 36 | kotlin { 37 | jvmToolchain { 38 | languageVersion.set(Config.compatibleJavaLanguageVersion) 39 | } 40 | } 41 | compileOptions { 42 | isCoreLibraryDesugaringEnabled = true 43 | } 44 | } 45 | 46 | dependencies { 47 | implementation(project(":view")) 48 | implementation(project(":compose")) 49 | coreLibraryDesugaring(libs.desugar) 50 | implementation(libs.kotlin.stdlib) 51 | 52 | implementation(libs.androidx.appcompat) 53 | implementation(libs.androidx.core.ktx) 54 | implementation(libs.androidx.constraintlayout) 55 | implementation(libs.androidx.cardview) 56 | implementation(libs.material.view) 57 | 58 | implementation(libs.compose.ui.ui) 59 | implementation(libs.compose.ui.tooling) 60 | implementation(libs.compose.foundation) 61 | implementation(libs.compose.runtime) 62 | implementation(libs.compose.material3) 63 | implementation(libs.compose.activity) 64 | implementation(libs.compose.navigation) 65 | 66 | testImplementation(libs.test.junit4) 67 | 68 | androidTestImplementation(libs.androidx.test.espresso.core) 69 | androidTestImplementation(libs.androidx.test.espresso.contrib) // RecyclerView actions. 70 | androidTestImplementation(libs.androidx.test.runner) 71 | androidTestImplementation(libs.androidx.test.rules) 72 | androidTestImplementation(libs.androidx.test.junit) 73 | 74 | androidTestImplementation(libs.compose.ui.test.junit4) 75 | debugImplementation(libs.compose.ui.test.manifest) // Compose test runner activity 76 | } 77 | -------------------------------------------------------------------------------- /sample/src/androidTest/java/com/kizitonwose/calendar/sample/utils/TestDayViewContainer.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.sample.utils 2 | 3 | import android.view.View 4 | import com.kizitonwose.calendar.view.ViewContainer 5 | 6 | internal class TestDayViewContainer(view: View) : ViewContainer(view) 7 | -------------------------------------------------------------------------------- /sample/src/androidTest/java/com/kizitonwose/calendar/sample/utils/TestUtils.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.sample.utils 2 | 3 | import android.graphics.Point 4 | import android.graphics.Rect 5 | import android.view.View 6 | import androidx.annotation.IdRes 7 | import androidx.recyclerview.widget.RecyclerView 8 | import androidx.test.espresso.Espresso 9 | import androidx.test.espresso.action.ViewActions.click 10 | import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition 11 | import androidx.test.espresso.matcher.ViewMatchers 12 | import androidx.test.ext.junit.rules.ActivityScenarioRule 13 | import androidx.test.platform.app.InstrumentationRegistry 14 | import com.kizitonwose.calendar.sample.R 15 | import java.lang.Thread.sleep 16 | 17 | internal fun View.getRectInWindow(): Rect { 18 | val point = getLocationInWindow() 19 | return Rect(point.x, point.y, point.x + width, point.y + height) 20 | } 21 | 22 | internal fun View.getLocationInWindow(): Point { 23 | val location = IntArray(2).apply(::getLocationInWindow) 24 | return Point(location[0], location[1]) 25 | } 26 | 27 | internal fun runOnMain(action: () -> Unit) { 28 | InstrumentationRegistry.getInstrumentation().runOnMainSync(action) 29 | } 30 | 31 | internal fun openExampleAt(position: Int) { 32 | Espresso.onView(ViewMatchers.withId(R.id.examplesRecyclerview)) 33 | .perform(actionOnItemAtPosition(position, click())) 34 | } 35 | 36 | internal fun ActivityScenarioRule<*>.getView(@IdRes id: Int): T { 37 | lateinit var view: T 38 | this.scenario.onActivity { activity -> 39 | view = activity.findViewById(id) 40 | } 41 | sleep(1000) 42 | return view 43 | } 44 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 27 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /sample/src/main/java/com/kizitonwose/calendar/sample/HomeActivity.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.sample 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.core.view.ViewCompat 7 | import androidx.core.view.WindowInsetsCompat.Type.systemBars 8 | import androidx.core.view.updatePadding 9 | import com.kizitonwose.calendar.sample.compose.CalendarComposeActivity 10 | import com.kizitonwose.calendar.sample.databinding.HomeActivityBinding 11 | import com.kizitonwose.calendar.sample.view.CalendarViewActivity 12 | 13 | class HomeActivity : AppCompatActivity() { 14 | override fun onCreate(savedInstanceState: Bundle?) { 15 | super.onCreate(savedInstanceState) 16 | val binding = HomeActivityBinding.inflate(layoutInflater) 17 | setContentView(binding.root) 18 | setSupportActionBar(binding.activityToolbar) 19 | applyInsets(binding) 20 | handleClicks(binding) 21 | } 22 | 23 | private fun handleClicks(binding: HomeActivityBinding) { 24 | binding.calendarViewSample.setOnClickListener { 25 | startActivity(Intent(this, CalendarViewActivity::class.java)) 26 | } 27 | binding.calendarComposeSample.setOnClickListener { 28 | startActivity(Intent(this, CalendarComposeActivity::class.java)) 29 | } 30 | } 31 | 32 | private fun applyInsets(binding: HomeActivityBinding) { 33 | ViewCompat.setOnApplyWindowInsetsListener( 34 | binding.root, 35 | ) { _, windowInsets -> 36 | val insets = windowInsets.getInsets(systemBars()) 37 | binding.activityAppBar.updatePadding(top = insets.top) 38 | binding.root.updatePadding( 39 | left = insets.left, 40 | right = insets.right, 41 | bottom = insets.bottom, 42 | ) 43 | windowInsets 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /sample/src/main/java/com/kizitonwose/calendar/sample/compose/Scaffold.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.sample.compose 2 | 3 | import androidx.compose.foundation.layout.PaddingValues 4 | import androidx.compose.foundation.layout.calculateEndPadding 5 | import androidx.compose.foundation.layout.calculateStartPadding 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.compositionLocalOf 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.platform.LocalLayoutDirection 11 | 12 | val LocalScaffoldPaddingValues = compositionLocalOf { PaddingValues() } 13 | 14 | @Composable 15 | fun Modifier.applyScaffoldHorizontalPaddings() = 16 | padding( 17 | start = LocalScaffoldPaddingValues.current.calculateStartPadding(LocalLayoutDirection.current), 18 | end = LocalScaffoldPaddingValues.current.calculateEndPadding(LocalLayoutDirection.current), 19 | ) 20 | 21 | @Composable 22 | fun Modifier.applyScaffoldTopPadding() = 23 | padding(top = LocalScaffoldPaddingValues.current.calculateTopPadding()) 24 | 25 | @Composable 26 | fun Modifier.applyScaffoldBottomPadding() = 27 | padding(bottom = LocalScaffoldPaddingValues.current.calculateBottomPadding()) 28 | -------------------------------------------------------------------------------- /sample/src/main/java/com/kizitonwose/calendar/sample/compose/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.sample.compose 2 | 3 | import androidx.compose.material3.lightColorScheme 4 | import androidx.compose.ui.graphics.Color 5 | 6 | val SampleColorScheme = lightColorScheme( 7 | primary = Color(0xFF3F51B5), 8 | onPrimary = Color.White, 9 | primaryContainer = Color(0xFF3F51B5), 10 | onPrimaryContainer = Color.White, 11 | inversePrimary = Color.Black, 12 | secondary = Color(0xFF3F51B5), 13 | onSecondary = Color.White, 14 | secondaryContainer = Color(0xFF3F51B5), 15 | onSecondaryContainer = Color.White, 16 | tertiary = Color(0xFF3F51B5), 17 | onTertiary = Color.White, 18 | tertiaryContainer = Color(0xFF3F51B5), 19 | onTertiaryContainer = Color.White, 20 | background = Color.White, 21 | onBackground = Color.Black, 22 | surface = Color.White, 23 | onSurface = Color.Black, 24 | surfaceVariant = Color.White, 25 | onSurfaceVariant = Color.Black, 26 | surfaceTint = Color.White, 27 | inverseSurface = Color(0xFF121212), 28 | inverseOnSurface = Color.White, 29 | error = Color(0xFFB00020), 30 | onError = Color.White, 31 | errorContainer = Color(0xFFB00020), 32 | onErrorContainer = Color.White, 33 | outline = Color(0xFFAFAFAF), 34 | outlineVariant = Color(0xFFCCCCCC), 35 | scrim = Color.Black, 36 | surfaceBright = Color.White, 37 | surfaceContainer = Color.White, 38 | surfaceContainerHigh = Color.White, 39 | surfaceContainerHighest = Color.White, 40 | surfaceContainerLow = Color.White, 41 | surfaceContainerLowest = Color.White, 42 | surfaceDim = Color.White, 43 | ) 44 | -------------------------------------------------------------------------------- /sample/src/main/java/com/kizitonwose/calendar/sample/shared/StatusBarColorLifecycleObserver.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.sample.shared 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import android.view.View 6 | import androidx.annotation.ColorInt 7 | import androidx.core.graphics.ColorUtils 8 | import androidx.lifecycle.DefaultLifecycleObserver 9 | import androidx.lifecycle.LifecycleOwner 10 | import com.kizitonwose.calendar.sample.R 11 | import com.kizitonwose.calendar.sample.view.getColorCompat 12 | import java.lang.ref.WeakReference 13 | 14 | @Suppress("DEPRECATION") 15 | class StatusBarColorLifecycleObserver( 16 | activity: Activity, 17 | @ColorInt private val color: Int, 18 | ) : DefaultLifecycleObserver { 19 | private val isLightColor = ColorUtils.calculateLuminance(color) > 0.5 20 | private val defaultStatusBarColor = activity.getColorCompat(R.color.colorPrimary) 21 | private val activity = WeakReference(activity) 22 | 23 | override fun onStart(owner: LifecycleOwner) { 24 | activity.get()?.window?.apply { 25 | statusBarColor = color 26 | if (isLightColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 27 | decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR 28 | } 29 | } 30 | } 31 | 32 | override fun onStop(owner: LifecycleOwner) { 33 | activity.get()?.window?.apply { 34 | statusBarColor = defaultStatusBarColor 35 | if (isLightColor) decorView.systemUiVisibility = 0 36 | } 37 | } 38 | 39 | override fun onDestroy(owner: LifecycleOwner) = activity.clear() 40 | } 41 | -------------------------------------------------------------------------------- /sample/src/main/java/com/kizitonwose/calendar/sample/shared/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.sample.shared 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.ContextWrapper 6 | import com.kizitonwose.calendar.core.Week 7 | import com.kizitonwose.calendar.core.yearMonth 8 | import java.time.DayOfWeek 9 | import java.time.Month 10 | import java.time.Year 11 | import java.time.YearMonth 12 | import java.time.format.TextStyle 13 | import java.time.temporal.ChronoUnit 14 | import java.util.Locale 15 | 16 | fun YearMonth.displayText(short: Boolean = false): String { 17 | return "${this.month.displayText(short = short)} ${this.year}" 18 | } 19 | 20 | fun Month.displayText(short: Boolean = true): String { 21 | val style = if (short) TextStyle.SHORT else TextStyle.FULL 22 | return getDisplayName(style, Locale.ENGLISH) 23 | } 24 | 25 | fun DayOfWeek.displayText(uppercase: Boolean = false, narrow: Boolean = false): String { 26 | val style = if (narrow) TextStyle.NARROW else TextStyle.SHORT 27 | return getDisplayName(style, Locale.ENGLISH).let { value -> 28 | if (uppercase) value.uppercase(Locale.ENGLISH) else value 29 | } 30 | } 31 | 32 | fun Context.findActivity(): Activity { 33 | var context = this 34 | while (context is ContextWrapper) { 35 | if (context is Activity) return context 36 | context = context.baseContext 37 | } 38 | throw IllegalStateException("no activity") 39 | } 40 | 41 | fun getWeekPageTitle(week: Week): String { 42 | val firstDate = week.days.first().date 43 | val lastDate = week.days.last().date 44 | return when { 45 | firstDate.yearMonth == lastDate.yearMonth -> { 46 | firstDate.yearMonth.displayText() 47 | } 48 | firstDate.year == lastDate.year -> { 49 | "${firstDate.month.displayText(short = false)} - ${lastDate.yearMonth.displayText()}" 50 | } 51 | else -> { 52 | "${firstDate.yearMonth.displayText()} - ${lastDate.yearMonth.displayText()}" 53 | } 54 | } 55 | } 56 | 57 | fun Year.yearsUntil(other: Year) = ChronoUnit.YEARS.between(this, other) 58 | -------------------------------------------------------------------------------- /sample/src/main/java/com/kizitonwose/calendar/sample/view/Typeface.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.sample.view 2 | 3 | import android.content.Context 4 | import android.graphics.Typeface 5 | import androidx.core.graphics.TypefaceCompat 6 | 7 | /** 8 | * See the comment on the [TypefaceCompat.create] method 9 | * with the weight param for various weight definitions. 10 | */ 11 | object Typeface { 12 | fun normal(context: Context) = TypefaceCompat.create(context, Typeface.SANS_SERIF, 400, false) 13 | fun medium(context: Context) = TypefaceCompat.create(context, Typeface.SANS_SERIF, 500, false) 14 | fun semiBold(context: Context) = TypefaceCompat.create(context, Typeface.SANS_SERIF, 600, false) 15 | fun bold(context: Context) = TypefaceCompat.create(context, Typeface.SANS_SERIF, 700, false) 16 | } 17 | -------------------------------------------------------------------------------- /sample/src/main/java/com/kizitonwose/calendar/sample/view/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.kizitonwose.calendar.sample.view 2 | 3 | import android.content.Context 4 | import android.graphics.drawable.Drawable 5 | import android.util.TypedValue 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.inputmethod.InputMethodManager 9 | import android.widget.TextView 10 | import androidx.annotation.ColorRes 11 | import androidx.annotation.DrawableRes 12 | import androidx.core.content.ContextCompat 13 | import androidx.fragment.app.Fragment 14 | import androidx.lifecycle.findViewTreeLifecycleOwner 15 | import com.kizitonwose.calendar.sample.shared.StatusBarColorLifecycleObserver 16 | 17 | fun View.makeVisible() { 18 | visibility = View.VISIBLE 19 | } 20 | 21 | fun View.makeInVisible() { 22 | visibility = View.INVISIBLE 23 | } 24 | 25 | fun View.makeGone() { 26 | visibility = View.GONE 27 | } 28 | 29 | fun dpToPx(dp: Int, context: Context): Int = 30 | TypedValue.applyDimension( 31 | TypedValue.COMPLEX_UNIT_DIP, 32 | dp.toFloat(), 33 | context.resources.displayMetrics, 34 | ).toInt() 35 | 36 | internal val Context.layoutInflater: LayoutInflater 37 | get() = LayoutInflater.from(this) 38 | 39 | internal val Context.inputMethodManager 40 | get() = this.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager 41 | 42 | internal fun Context.getDrawableCompat(@DrawableRes drawable: Int): Drawable = 43 | requireNotNull(ContextCompat.getDrawable(this, drawable)) 44 | 45 | internal fun Context.getColorCompat(@ColorRes color: Int) = 46 | ContextCompat.getColor(this, color) 47 | 48 | internal fun TextView.setTextColorRes(@ColorRes color: Int) = 49 | setTextColor(context.getColorCompat(color)) 50 | 51 | fun Fragment.addStatusBarColorUpdate(@ColorRes colorRes: Int) { 52 | view?.findViewTreeLifecycleOwner()?.lifecycle?.addObserver( 53 | StatusBarColorLifecycleObserver( 54 | requireActivity(), 55 | requireContext().getColorCompat(colorRes), 56 | ), 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /sample/src/main/res/anim/fade_in.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /sample/src/main/res/anim/fade_out.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /sample/src/main/res/anim/slide_in_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /sample/src/main/res/anim/slide_in_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /sample/src/main/res/anim/slide_in_up.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sample/src/main/res/anim/slide_out_down.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sample/src/main/res/anim/slide_out_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /sample/src/main/res/anim/slide_out_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/example_1_selected_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/example_1_today_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/example_2_selected_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/example_3_selected_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/example_3_today_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/example_4_continuous_selected_bg_end.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/example_4_continuous_selected_bg_middle.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/example_4_continuous_selected_bg_start.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/example_4_single_selected_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/example_4_today_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/example_5_selected_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 9 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/example_8_selected_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/example_8_today_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_add.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_airplane_landing.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_airplane_takeoff.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_check.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_chevron_left.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_chevron_right.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_close.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/calendar_day_legend_container.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/calendar_day_legend_text.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/calendar_view_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 16 | 17 | 23 | 24 | 25 | 26 | 31 | 32 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/calendar_view_options_item_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 19 | 20 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/example_10_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 18 | 19 | 25 | 26 | 27 | 28 | 38 | 39 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/example_1_calendar_day.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/example_2_calendar_day.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/example_2_calendar_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 22 | 23 | 32 | 33 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/example_2_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 18 | 19 | 24 | 25 | 32 | 33 | 34 | 35 | 43 | 44 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/example_3_calendar_day.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 18 | 19 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/example_3_calendar_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/example_3_event_item_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 16 | 17 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/example_4_calendar_day.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 13 | 14 | 19 | 24 | 25 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/example_4_calendar_header.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/example_5_calendar_day.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 25 | 26 | 33 | 34 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/example_5_calendar_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/example_6_calendar_day.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/example_6_calendar_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 19 | 20 | 23 | 24 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/example_6_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/example_7_calendar_day.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 15 | 16 | 24 | 25 | 34 | 35 | 36 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/example_7_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 18 | 19 | 24 | 25 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/example_8_calendar_day.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/example_8_calendar_footer.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/example_8_calendar_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/example_8_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 19 | 20 | 25 | 26 | 35 | 36 | 45 | 46 | 47 | 48 | 49 | 50 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/example_9_calendar_day.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/example_9_calendar_month_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 21 | 22 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/example_9_calendar_year_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 24 | 25 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/example_9_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 18 | 19 | 25 | 26 | 27 | 28 | 37 | 38 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/home_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 17 | 18 | 24 | 25 | 26 | 27 | 32 | 33 |