├── jitpack.yml ├── tasticalendar ├── .gitignore ├── consumer-rules.pro ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── res │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── dimens.xml │ │ │ │ └── attrs.xml │ │ │ ├── drawable │ │ │ │ ├── tasticalendar_circle.xml │ │ │ │ └── tasticalendar_ring.xml │ │ │ └── layout │ │ │ │ ├── tasticalendar_year.xml │ │ │ │ └── tasticalendar_month.xml │ │ └── java │ │ │ └── com │ │ │ └── minar │ │ │ └── tasticalendar │ │ │ ├── model │ │ │ ├── TcSundayHighlight.kt │ │ │ └── TastiCalendarEvent.kt │ │ │ ├── utilities │ │ │ ├── UiUtils.kt │ │ │ ├── TextUtils.kt │ │ │ └── ColorUtils.kt │ │ │ └── core │ │ │ ├── TastiCalendarYear.kt │ │ │ └── TastiCalendarMonth.kt │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── minar │ │ │ └── tasticalendar │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── com │ │ └── minar │ │ └── tasticalendar │ │ └── ExampleInstrumentedTest.kt ├── proguard-rules.pro └── build.gradle ├── .idea ├── .gitignore ├── compiler.xml ├── vcs.xml ├── kotlinc.xml ├── migrations.xml ├── misc.xml ├── gradle.xml └── other.xml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── settings.gradle ├── gradle.properties ├── .github └── workflows │ └── main.yml ├── README.md ├── gradlew.bat └── gradlew /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk17 -------------------------------------------------------------------------------- /tasticalendar/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /tasticalendar/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m-i-n-a-r/tasticalendar/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /tasticalendar/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tasticalendar/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | \?? 4 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /tasticalendar/src/main/java/com/minar/tasticalendar/model/TcSundayHighlight.kt: -------------------------------------------------------------------------------- 1 | package com.minar.tasticalendar.model 2 | 3 | @Suppress("unused") 4 | enum class TcSundayHighlight { 5 | NONE, 6 | BOLD, 7 | COLORED, 8 | BOLDCOLORED, 9 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Sep 18 17:25:42 CEST 2025 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | app/ 17 | -------------------------------------------------------------------------------- /.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /tasticalendar/src/main/res/drawable/tasticalendar_circle.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /tasticalendar/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 2dp 4 | 1dp 5 | 8dp 6 | 3dp 7 | 16dp 8 | -------------------------------------------------------------------------------- /tasticalendar/src/main/res/drawable/tasticalendar_ring.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | gradlePluginPortal() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | } 15 | rootProject.name = "TastiCalendar" 16 | include ':tasticalendar' 17 | -------------------------------------------------------------------------------- /tasticalendar/src/test/java/com/minar/tasticalendar/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.minar.tasticalendar 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | -------------------------------------------------------------------------------- /tasticalendar/src/main/java/com/minar/tasticalendar/utilities/UiUtils.kt: -------------------------------------------------------------------------------- 1 | package com.minar.tasticalendar.utilities 2 | 3 | import android.view.View 4 | import com.google.android.material.snackbar.Snackbar 5 | 6 | // Show a snackbar containing a given text, anchored to a given view 7 | fun showSnackbar( 8 | content: String, 9 | baseView: View, 10 | duration: Int = 5000 11 | ) { 12 | val snackbar = Snackbar.make(baseView, content, duration) 13 | snackbar.isGestureInsetBottomIgnored = true 14 | // Anchor the snackbar to the desired view 15 | snackbar.anchorView = baseView 16 | snackbar.show() 17 | } -------------------------------------------------------------------------------- /tasticalendar/src/main/java/com/minar/tasticalendar/model/TastiCalendarEvent.kt: -------------------------------------------------------------------------------- 1 | package com.minar.tasticalendar.model 2 | 3 | import java.time.LocalDate 4 | 5 | /** 6 | * A simple class to wrap a date and a label/description for the date. 7 | *

8 | * It is used to populate the yearly or monthly calendar, and to display an optional label 9 | * when the day is pressed. 10 | * @param date LocalDate, the complete date of the event, it can't be null. 11 | * @param displayText the text associated with the date. If null, it will be 12 | * initialized to an empty string. 13 | */ 14 | data class TastiCalendarEvent( 15 | val date: LocalDate, 16 | val displayText: String? = "" 17 | ) 18 | -------------------------------------------------------------------------------- /tasticalendar/src/main/java/com/minar/tasticalendar/utilities/TextUtils.kt: -------------------------------------------------------------------------------- 1 | package com.minar.tasticalendar.utilities 2 | 3 | import com.minar.tasticalendar.model.TastiCalendarEvent 4 | 5 | // Given a list of events, cumulate their texts in a single string 6 | fun formatEventList(events: List, separator: String? = ","): String { 7 | val sb = StringBuilder() 8 | for (event in events) { 9 | if (!event.displayText.isNullOrBlank() && events.lastIndex != events.indexOf(event)) 10 | sb.append("${event.displayText}${separator} ") 11 | if (!event.displayText.isNullOrBlank() && events.lastIndex == events.indexOf(event)) 12 | sb.append(event.displayText) 13 | } 14 | return sb.toString().trim() 15 | } -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | -------------------------------------------------------------------------------- /tasticalendar/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /tasticalendar/src/androidTest/java/com/minar/tasticalendar/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.minar.tasticalendar 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.minar.tasticalendar.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # GitHub Pages deploy action structure based on https://github.com/simonesestito/wasaphoto/blob/master/.github/workflows/public-openapi-docs.yml 2 | 3 | name: Deploy Javadoc 4 | 5 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 6 | permissions: 7 | contents: read 8 | pages: write 9 | id-token: write 10 | 11 | # Allow one concurrent deployment 12 | concurrency: 13 | group: "pages" 14 | cancel-in-progress: true 15 | 16 | on: 17 | push: 18 | branches: 19 | - master 20 | 21 | jobs: 22 | javadoc: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout project sources 26 | uses: actions/checkout@v4 27 | - name: Setup Gradle 28 | uses: gradle/gradle-build-action@v3 29 | - name: Generate javadoc 30 | run: gradle dokkaHtmlMultiModule 31 | - name: Redirect to the submodule directly 32 | run: echo "PCFET0NUWVBFIGh0bWw+PGh0bWw+PGhlYWQ+PG1ldGEgaHR0cC1lcXVpdj0iUmVmcmVzaCIgY29udGVudD0iMDsgdXJsPSd0YXN0aWNhbGVuZGFyL2luZGV4Lmh0bWwnIiAvPjwvaGVhZD48Ym9keT48L2JvZHk+PC9odG1sPgo=" | base64 -d > ./build/dokka/htmlMultiModule/index.html 33 | - name: Setup Pages 34 | uses: actions/configure-pages@v5 35 | - name: Upload artifact 36 | uses: actions/upload-pages-artifact@v3 37 | with: 38 | path: 'build/dokka/htmlMultiModule/' 39 | - name: Deploy to GitHub Pages 40 | id: deployment 41 | uses: actions/deploy-pages@v4 42 | 43 | -------------------------------------------------------------------------------- /tasticalendar/build.gradle: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 2 | 3 | plugins { 4 | id 'com.android.library' 5 | id 'org.jetbrains.kotlin.android' 6 | id 'maven-publish' 7 | id 'org.jetbrains.dokka' 8 | } 9 | 10 | android { 11 | namespace 'com.minar.tasticalendar' 12 | compileSdk = 36 13 | 14 | defaultConfig { 15 | minSdk 26 16 | targetSdk 36 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | consumerProguardFiles "consumer-rules.pro" 19 | } 20 | 21 | buildTypes { 22 | release { 23 | minifyEnabled false 24 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 25 | } 26 | } 27 | compileOptions { 28 | sourceCompatibility JavaVersion.VERSION_17 29 | targetCompatibility JavaVersion.VERSION_17 30 | } 31 | kotlin { 32 | compilerOptions { 33 | jvmTarget.set(JvmTarget.JVM_17) 34 | } 35 | } 36 | publishing { 37 | singleVariant("release") { 38 | withSourcesJar() 39 | withJavadocJar() 40 | } 41 | } 42 | buildFeatures { 43 | viewBinding true 44 | } 45 | } 46 | 47 | dependencies { 48 | 49 | implementation 'androidx.core:core-ktx:1.17.0' 50 | implementation 'androidx.appcompat:appcompat:1.7.1' 51 | 52 | // Material components 53 | implementation 'com.google.android.material:material:1.14.0-alpha04' 54 | implementation 'androidx.core:core-ktx:1.17.0' 55 | 56 | // Test dependencies 57 | testImplementation 'junit:junit:4.13.2' 58 | androidTestImplementation 'androidx.test.ext:junit:1.3.0' 59 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0' 60 | } 61 | 62 | publishing { 63 | publications { 64 | release(MavenPublication) { 65 | groupId = 'com.github.m-i-n-a-r' 66 | artifactId = 'tasticalendar' 67 | version = '1.4.0' 68 | 69 | afterEvaluate { 70 | from components.release 71 | } 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /tasticalendar/src/main/java/com/minar/tasticalendar/utilities/ColorUtils.kt: -------------------------------------------------------------------------------- 1 | package com.minar.tasticalendar.utilities 2 | 3 | import android.content.Context 4 | import android.graphics.Color 5 | import android.util.TypedValue 6 | import androidx.annotation.AttrRes 7 | import androidx.annotation.ColorInt 8 | import androidx.core.content.ContextCompat 9 | import com.google.android.material.color.MaterialColors 10 | 11 | 12 | // Get the color for an attribute with MaterialColors, or with resolveAttribute. If it doesn't exist, fallback 13 | fun getThemeColor(@AttrRes attrRes: Int, context: Context, @ColorInt fallback: Int = Color.BLACK): Int { 14 | return try { 15 | // Get the color using MaterialColors 16 | MaterialColors.getColor(context, attrRes, fallback) 17 | } catch (_: Exception) { 18 | val tv = TypedValue() 19 | val resolved = context.theme.resolveAttribute(attrRes, tv, true) 20 | if (!resolved) return fallback 21 | return if (tv.resourceId != 0) { 22 | ContextCompat.getColor(context, tv.resourceId) 23 | } else { 24 | tv.data 25 | } 26 | } 27 | } 28 | 29 | 30 | // Return a color to maximize the visibility on another color 31 | fun getBestContrast(color: Int, context: Context, alpha: Int = 255, isDark: Boolean? = null): Int { 32 | // Calculate the perceptive luminance 33 | val luma = 34 | (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) / 255 35 | // Return black for bright colors, white for dark colors 36 | return if (alpha < 80) { 37 | getThemeColor(com.google.android.material.R.attr.colorOnSurface, context) 38 | } else 39 | if (luma > 0.5) { 40 | // Brighter color, darker text 41 | if (isDark != null && isDark) getThemeColor(com.google.android.material.R.attr.colorOnSurfaceInverse, context) 42 | if (isDark != null && !isDark) getThemeColor(com.google.android.material.R.attr.colorOnSurface, context) 43 | Color.BLACK 44 | } else { 45 | // Darker color, brighter text 46 | if (isDark != null && isDark) getThemeColor(com.google.android.material.R.attr.colorOnSurface, context) 47 | if (isDark != null && !isDark) getThemeColor(com.google.android.material.R.attr.colorOnSurfaceInverse, context) 48 | Color.WHITE 49 | } 50 | } -------------------------------------------------------------------------------- /tasticalendar/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TastiCalendar 2 | 3 | A simple library, based on Material You, to implement a monthly or yearly calendar containing a collection of dates 4 | 5 |

6 | 7 | 8 | 9 | Javadoc 10 | 11 |

12 | 13 | ## Introduction 14 | I wrote this library starting from a piece of [Birday](https://www.github.com/m-i-n-a-r/birday), since I noticed that there isn't a similar library (at least, not a recent one). I kept it super simple and light, but I'm open to any pull request. 15 | **Important**: this library is provided as is, no updates are guaranteed since I have other projects to focus on. It works (I use it personally in 2 projects) and is quite complete in my opinion, but I'm open to any criticism. This library doesn't need any translation since it doesn't use any strings itself. 16 | 17 | ## How to use 18 | 1. Open the ```build.gradle (Project level)``` and, under repositories, make sure to have:\ 19 | ```maven { url "https://jitpack.io" }``` 20 | 21 | 2. Open the ```build.gradle (Module:app)``` file of your app, and under dependencies, add:\ 22 | ``` implementation 'com.github.m-i-n-a-r:tasticalendar:1.4.0' ``` 23 | 24 | 3. Sync Gradle, and you're good to go! 25 | 26 | 4. A simple example of use can be found in my Birday app, in [this file](https://github.com/m-i-n-a-r/birday/blob/master/app/src/main/java/com/minar/birday/fragments/OverviewFragment.kt) and in the corresponding [layout](https://github.com/m-i-n-a-r/birday/blob/master/app/src/main/res/layout/fragment_overview.xml). Or, you can [read the docs!](https://m-i-n-a-r.github.io/tasticalendar/) 27 | 28 | ## Features 29 |

30 |
31 | Monet support examples


32 |
33 | 3 of the four available scale factors


34 |

35 | 36 | - Super lightweight 37 | - The color scheme automatically adapts to the app (Material Expressive) 38 | - Month layout (with different scales) 39 | - Year layout (adaptive depending on the month scale factor) 40 | - Clickable days, clickable month titles 41 | - Disable weekdays, different sunday highlight strategies 42 | - Automatic or manual "Sunday as first day of the week" 43 | - Easily set any property or render a different year/month 44 | - Easily pass a collection of dates or TastiCalendarEvent objects to highlight a set of dates on a month 45 | - The library chooses the best contrast for the text color when a day is highlighted 46 | - Different highlighting strength based on the number of events in each day 47 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /tasticalendar/src/main/res/layout/tasticalendar_year.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 20 | 21 | 28 | 29 | 36 | 37 | 44 | 45 | 52 | 53 | 60 | 61 | 68 | 69 | 76 | 77 | 84 | 85 | 92 | 93 | 100 | 101 | 108 | 109 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /.idea/other.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 262 | 263 | -------------------------------------------------------------------------------- /tasticalendar/src/main/java/com/minar/tasticalendar/core/TastiCalendarYear.kt: -------------------------------------------------------------------------------- 1 | package com.minar.tasticalendar.core 2 | 3 | import android.content.Context 4 | import android.graphics.drawable.Drawable 5 | import android.util.AttributeSet 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.widget.LinearLayout 9 | import androidx.appcompat.content.res.AppCompatResources 10 | import com.minar.tasticalendar.R 11 | import com.minar.tasticalendar.databinding.TasticalendarYearBinding 12 | import com.minar.tasticalendar.model.TastiCalendarEvent 13 | import com.minar.tasticalendar.model.TcSundayHighlight 14 | import com.minar.tasticalendar.utilities.formatEventList 15 | import com.minar.tasticalendar.utilities.getThemeColor 16 | import java.time.LocalDate 17 | import java.time.temporal.WeekFields 18 | import java.util.* 19 | 20 | /** 21 | * A class representing a yearly calendar. 22 | *

23 | * This uses the month class to generate a grid of 12 months. 24 | * @see TastiCalendarMonth 25 | * @param context the context of the view. 26 | * @param attrs the set of attributes specified in the layout. 27 | */ 28 | @Suppress("MemberVisibilityCanBePrivate", "unused") 29 | class TastiCalendarYear(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) { 30 | // Custom attributes 31 | private var hideWeekDays: Boolean 32 | private var sundayHighlight = 0 33 | private var sundayFirst: Boolean 34 | private var showSnackBars: Boolean 35 | private var appearance: Int 36 | 37 | // Other useful variables 38 | private var year: Int = LocalDate.now().year 39 | private lateinit var monthList: MutableList 40 | private var binding: TasticalendarYearBinding 41 | private var events = mutableListOf() 42 | 43 | init { 44 | context.theme.obtainStyledAttributes( 45 | attrs, R.styleable.TastiCalendarYear, 0, 0 46 | ).apply { 47 | try { 48 | hideWeekDays = getBoolean(R.styleable.TastiCalendarYear_tcHideWeekDays, false) 49 | sundayHighlight = getInteger(R.styleable.TastiCalendarMonth_tcSundayHighlight, 0) 50 | sundayFirst = getBoolean(R.styleable.TastiCalendarYear_tcSundayFirst, false) 51 | showSnackBars = getBoolean(R.styleable.TastiCalendarYear_tcShowInfoSnackBars, true) 52 | appearance = getInteger(R.styleable.TastiCalendarYear_tcAppearance, 0) 53 | } finally { 54 | recycle() 55 | } 56 | } 57 | binding = TasticalendarYearBinding.inflate(LayoutInflater.from(context), this, true) 58 | initYear() 59 | } 60 | 61 | /** 62 | * Initializes the layout for the year, assigning the bindings and the visibilities. 63 | *

64 | * This is used once when the layout is first created. 65 | */ 66 | private fun initYear() { 67 | // Months 68 | val january = binding.tastiCalendarYearJan 69 | val february = binding.tastiCalendarYearFeb 70 | val march = binding.tastiCalendarYearMar 71 | val april = binding.tastiCalendarYearApr 72 | val may = binding.tastiCalendarYearMay 73 | val june = binding.tastiCalendarYearJun 74 | val july = binding.tastiCalendarYearJul 75 | val august = binding.tastiCalendarYearAug 76 | val september = binding.tastiCalendarYearSep 77 | val october = binding.tastiCalendarYearOct 78 | val november = binding.tastiCalendarYearNov 79 | val december = binding.tastiCalendarYearDec 80 | 81 | // Create a list of every month 82 | monthList = mutableListOf( 83 | january, 84 | february, 85 | march, 86 | april, 87 | may, 88 | june, 89 | july, 90 | august, 91 | september, 92 | october, 93 | november, 94 | december 95 | ) 96 | 97 | // This has to be done nevertheless, since it affects the numbers 98 | if (WeekFields.of(Locale.getDefault()).firstDayOfWeek.name == "SUNDAY") { 99 | for (month in monthList) { 100 | month.setSundayFirst(true) 101 | } 102 | } 103 | // Sunday highlighting 104 | if (hideWeekDays) setHideWeekDays(true) 105 | else { 106 | when (sundayHighlight) { 107 | 0 -> for (month in monthList) month.setSundayHighlight(TcSundayHighlight.NONE) 108 | 1 -> for (month in monthList) month.setSundayHighlight(TcSundayHighlight.BOLD) 109 | 2 -> for (month in monthList) month.setSundayHighlight(TcSundayHighlight.COLORED) 110 | 3 -> for (month in monthList) month.setSundayHighlight(TcSundayHighlight.BOLDCOLORED) 111 | } 112 | } 113 | 114 | // Set the appearance (0 small default, 1 medium, 2 large, 3 xlarge) 115 | when (appearance) { 116 | 0 -> return 117 | 1 -> setAppearance(1) 118 | 2 -> setAppearance(2) 119 | 3 -> setAppearance(3) 120 | } 121 | } 122 | 123 | /** 124 | * Highlights the current date with a ring. 125 | *

126 | * This is only used internally, to circle the current day, if the displayed year 127 | * is the current year. 128 | * @param drawable a drawable to replace the default ring, it can be null. 129 | * @param color a color to replace the default color (colorTertiary), it can be null. 130 | */ 131 | private fun highlightCurrentDate(drawable: Drawable? = null, color: Int? = null) { 132 | val date = LocalDate.now() 133 | if (date.year != year) return 134 | val chosenColor = 135 | color ?: getThemeColor(R.attr.colorTertiary, context) 136 | val chosenDrawable = 137 | drawable ?: AppCompatResources.getDrawable(context, R.drawable.tasticalendar_ring) 138 | highlightDate(date, chosenColor, chosenDrawable, asForeground = true) 139 | } 140 | 141 | /** 142 | * Renders a given year. 143 | *

144 | * This reloads the entire layout and apply the current settings, 145 | * it's the core method of the class. If both events and dates are not null, the 146 | * latter will be ignored. 147 | * @see TastiCalendarEvent 148 | * @param year the year to render, it can't be null, but it can also be negative. 149 | * @param events a list of TastiCalendarEvents, used to highlight a set of dates also adding 150 | * labels to it. It can be null. 151 | * @param dates a simple list of dates, used to highlight a set of dates. It can be null. 152 | */ 153 | fun renderYear( 154 | year: Int, 155 | events: List? = null, 156 | dates: List? = null 157 | ) { 158 | this.year = year 159 | for (month in monthList) { 160 | month.setYear(year) 161 | month.resetHighlighting() 162 | } 163 | 164 | // Highlight the dates (only if they exist in the current year) 165 | if (year == LocalDate.now().year) highlightCurrentDate() 166 | 167 | // Unify the lists to be the same list 168 | val finalList: MutableList? = 169 | if (events.isNullOrEmpty()) { 170 | if (!dates.isNullOrEmpty()) { 171 | dates.map { TastiCalendarEvent(it, "") }.toMutableList() 172 | } else null 173 | } else events.toMutableList() 174 | if (finalList.isNullOrEmpty()) return 175 | finalList.sortBy { it.date.withYear(1970) } 176 | 177 | // Compute temporary lists for each day 178 | var currentDate = finalList[0].date 179 | var dayEvents = mutableListOf() 180 | for (event in finalList) { 181 | // Compute the snackbar text 182 | if (event.date.isEqual(currentDate.withYear(event.date.year))) { 183 | dayEvents.add(event) 184 | } else { 185 | dayEvents = mutableListOf() 186 | dayEvents.add(event) 187 | currentDate = event.date 188 | } 189 | // Highlight the dates 190 | highlightDate( 191 | event.date, 192 | getThemeColor(R.attr.colorPrimary, context), 193 | AppCompatResources.getDrawable(context, R.drawable.tasticalendar_circle), 194 | makeBold = false, 195 | autoOpacity = true, 196 | autoTextColor = true, 197 | snackbarText = if (showSnackBars) formatEventList(dayEvents) else "" 198 | ) 199 | } 200 | } 201 | 202 | /** 203 | * A wrapper around the highlight function of the month. 204 | *

205 | * This highlights a day in a variety of ways depending on the 206 | * parameters. Some parameters may not work properly in certain cases. 207 | * 208 | * @param date the date to highlight. The year is not considered, and the date is used to 209 | * find the correct month in the year and call its function. 210 | * @param color the color used to highlight the month, if no drawable is specified, 211 | * by default of the library, it's taken from the system. 212 | * @param drawable a drawable used as background, replacing the default colored circle. 213 | * It can be null. 214 | * @param makeBold Boolean, false by default, if true the day text will be in bold style. It has some problems, 215 | * since when the font is bold, it loses the monospace feature. 216 | * @param autoOpacity Boolean, false by default, if true allow different opacity levels of the background, 217 | * depending on how many times the day has been highlighted before. 218 | * @param autoTextColor Boolean, false by default, if true the text color will be computed 219 | * automatically to grant the best contrast available. 220 | * @param asForeground Boolean, false by default, if true the drawable or color will be used as 221 | * foreground, thus covering the text, totally or partially. 222 | * @param snackbarText String, a text to display if the day cell is clicked, empty by default. If empty 223 | * or null, the cell won't react to clicks. 224 | * @see TastiCalendarMonth.highlightDay 225 | */ 226 | fun highlightDate( 227 | date: LocalDate?, 228 | color: Int, 229 | drawable: Drawable?, 230 | makeBold: Boolean = false, 231 | autoOpacity: Boolean = false, 232 | autoTextColor: Boolean = false, 233 | asForeground: Boolean = false, 234 | snackbarText: String = "" 235 | ) { 236 | if (date == null) return 237 | // Since we have the full date, check if the event should be considered in the current year 238 | if (date.year > year) return 239 | // Actually highlight the date 240 | monthList[date.month.value - 1].highlightDay( 241 | date.dayOfMonth, 242 | color, 243 | drawable, 244 | makeBold = makeBold, 245 | autoOpacity = autoOpacity, 246 | autoTextColor = autoTextColor, 247 | asForeground = asForeground, 248 | snackbarText = snackbarText 249 | ) 250 | } 251 | 252 | /** 253 | * Add the given prefix to the message shown when tapping the month header. 254 | *

255 | * This is used to display something before the number of events for each month, 256 | * in the year, in the form " " (eg: "Events: 12"). 257 | * @param prefix Int, can't be null, the id of the prefix to add to the snackbar message. 258 | * @param plural Boolean, false by default, if true the passed id is a plural String, 259 | * which will be formatted with the number of events accordingly. 260 | * @param refresh Boolean, true by default, if false the layout won't be refreshed. 261 | */ 262 | fun setSnackBarsPrefix(prefix: Int, plural: Boolean = false, refresh: Boolean = true) { 263 | for (month in monthList) 264 | month.setSnackBarsPrefix(prefix, plural, refresh) 265 | } 266 | 267 | /** 268 | * Sets the duration for the snackbar. 269 | *

270 | * This is used to change the default duration, set to 3000 milliseconds. Wrapper 271 | * for the month function. 272 | * @param duration Int, can't be null, defines the duration in milliseconds. 273 | * @param refresh Boolean, true by default, if false the layout won't be refreshed. 274 | */ 275 | fun setSnackBarsDuration(duration: Int, refresh: Boolean = true) { 276 | for (month in monthList) 277 | month.setSnackBarsDuration(duration, refresh) 278 | } 279 | 280 | /** 281 | * Sets the property to display the snack bars on tap or not. 282 | *

283 | * If true, it is used to display advanced information when a day or a month header is pressed. 284 | * @param enabled Boolean, can't be null, if true enables the advanced info parameter. 285 | * @param refresh Boolean, true by default, if false the layout won't be refreshed. 286 | */ 287 | fun setShowSnackBarsEnabled(enabled: Boolean, refresh: Boolean = true) { 288 | showSnackBars = enabled 289 | if (refresh) renderYear(year, events) 290 | } 291 | 292 | /** 293 | * Sets the base view for the snackbar. 294 | *

295 | * It can be used to avoid unwanted behaviors when a snackbar appears. For example, the snackbar 296 | * will spawn below the action button by default. Wrapper for the month function. 297 | * @param view View, not null, it should be the base view. If the view is invalid, the binding 298 | * root will be used instead. 299 | * @param refresh Boolean, true by default, if false the layout won't be refreshed. 300 | */ 301 | fun setSnackBarBaseView(view: View, refresh: Boolean = true) { 302 | for (month in monthList) 303 | month.setSnackBarBaseView(view, refresh) 304 | } 305 | 306 | /** 307 | * Forces sunday to be displayed as the first day of the week. 308 | *

309 | * This is used to force sunday as the first day of the week. If this method isn't called, the 310 | * first day of the week is automatically taken from the default locale. 311 | * @param appearance Enum of type TcAppearance, can't be null, specifies the selected 312 | * highlighting type for each month in the year. 313 | * @param refresh Boolean, true by default, if false the layout won't be refreshed. 314 | * @see TcSundayHighlight 315 | */ 316 | fun setSundayHighlight(appearance: TcSundayHighlight, refresh: Boolean = true) { 317 | for (month in monthList) 318 | month.setSundayHighlight(appearance, refresh) 319 | } 320 | 321 | /** 322 | * Selects an highlighting strategy for sunday, if needed. 323 | *

324 | * This is used to change the appearance of the sunday "S" in each month 325 | * @param enable Boolean, can't be null, if true sets sunday as the first day of the week 326 | * for each month in the year. 327 | * @param refresh Boolean, true by default, if false the layout won't be refreshed. 328 | */ 329 | fun setSundayFirst(enable: Boolean, refresh: Boolean = true) { 330 | if (enable != sundayFirst) 331 | for (month in monthList) 332 | month.setSundayFirst(enable, refresh) 333 | } 334 | 335 | /** 336 | * Sets the weekdays row visibility. 337 | *

338 | * This is used to show or hide the weekdays row. Useful to obtain compact layouts. 339 | * @param enable Boolean, can't be null, if true hides the week days row 340 | * for each month in the year. 341 | * @param refresh Boolean, true by default, if false the layout won't be refreshed. 342 | */ 343 | fun setHideWeekDays(enable: Boolean, refresh: Boolean = true) { 344 | if (enable != hideWeekDays) 345 | for (month in monthList) 346 | month.setHideWeekDays(enable, refresh) 347 | } 348 | 349 | /** 350 | * Changes the visual density of the year. 351 | *

352 | * It is used to cycle between appearances and it's basically a wrapper around 353 | * the function of TastiCalendarMonth. 354 | * @param appearance Int, can't be null, 0 means small, 1 medium, 2 large, 3 extra large. 355 | * Every other value is ignored. 356 | * @return the appearance set, useful in case of cycling 357 | */ 358 | fun setAppearance(appearance: Int): Int { 359 | if (appearance > 3 || appearance < 0) { 360 | this.appearance += 1 361 | if (this.appearance == 4) this.appearance = 0 362 | } else 363 | this.appearance = appearance 364 | for (month in monthList) { 365 | month.setAppearance(this.appearance) 366 | } 367 | return this.appearance 368 | } 369 | } -------------------------------------------------------------------------------- /tasticalendar/src/main/res/layout/tasticalendar_month.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 17 | 18 | 23 | 24 | 34 | 35 | 45 | 46 | 56 | 57 | 67 | 68 | 78 | 79 | 89 | 90 | 100 | 101 | 111 | 112 | 122 | 123 | 133 | 134 | 144 | 145 | 155 | 156 | 166 | 167 | 177 | 178 | 188 | 189 | 199 | 200 | 210 | 211 | 221 | 222 | 232 | 233 | 243 | 244 | 254 | 255 | 265 | 266 | 276 | 277 | 287 | 288 | 298 | 299 | 309 | 310 | 320 | 321 | 331 | 332 | 342 | 343 | 353 | 354 | 364 | 365 | 375 | 376 | 386 | 387 | 397 | 398 | 408 | 409 | 419 | 420 | 430 | 431 | 441 | 442 | 452 | 453 | 463 | 464 | 474 | 475 | 485 | 486 | 496 | 497 | 507 | 508 | -------------------------------------------------------------------------------- /tasticalendar/src/main/java/com/minar/tasticalendar/core/TastiCalendarMonth.kt: -------------------------------------------------------------------------------- 1 | package com.minar.tasticalendar.core 2 | 3 | import android.content.Context 4 | import android.content.res.ColorStateList 5 | import android.graphics.Typeface 6 | import android.graphics.drawable.Drawable 7 | import android.os.Build 8 | import android.util.AttributeSet 9 | import android.util.Range 10 | import android.view.LayoutInflater 11 | import android.view.View 12 | import android.widget.LinearLayout 13 | import android.widget.TextView 14 | import androidx.appcompat.content.res.AppCompatResources 15 | import com.minar.tasticalendar.R 16 | import com.minar.tasticalendar.databinding.TasticalendarMonthBinding 17 | import com.minar.tasticalendar.model.TastiCalendarEvent 18 | import com.minar.tasticalendar.model.TcSundayHighlight 19 | import com.minar.tasticalendar.utilities.formatEventList 20 | import com.minar.tasticalendar.utilities.getBestContrast 21 | import com.minar.tasticalendar.utilities.getThemeColor 22 | import com.minar.tasticalendar.utilities.showSnackbar 23 | import java.time.DayOfWeek 24 | import java.time.LocalDate 25 | import java.time.Month 26 | import java.time.format.DateTimeFormatter 27 | import java.time.format.FormatStyle 28 | import java.time.format.TextStyle 29 | import java.util.* 30 | import androidx.core.view.isVisible 31 | 32 | /** 33 | * A class representing a month. 34 | *

35 | * It can be used alone, or in combination with TastiCalendarYear to 36 | * render a yearly calendar. Many properties are customizable. 37 | * @see TastiCalendarYear 38 | * @param context the context of the view. 39 | * @param attrs the set of attributes specified in the layout. 40 | */ 41 | @Suppress("MemberVisibilityCanBePrivate", "unused") 42 | class TastiCalendarMonth(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) { 43 | // Custom attributes 44 | private var month = 0 45 | private var hideWeekDays: Boolean 46 | private var sundayAppearance = 0 47 | private var sundayFirst: Boolean 48 | private var showSnackBars: Boolean 49 | private var appearance = 0 50 | 51 | // Other useful variables 52 | private var dateWithChosenMonth: LocalDate 53 | private lateinit var cellsList: MutableList 54 | private lateinit var weekDaysList: MutableList 55 | private lateinit var monthTitle: TextView 56 | private var binding: TasticalendarMonthBinding 57 | private var eventCount = 0 58 | private var snackBarsPluralFormatting = false 59 | private var snackBarsPrefix: Int? = null 60 | private var snackBarsBaseView: View? = null 61 | private var snackBarsDuration: Int = 3000 62 | private var sundayIndex = 6 63 | 64 | init { 65 | context.theme.obtainStyledAttributes( 66 | attrs, R.styleable.TastiCalendarMonth, 0, 0 67 | ).apply { 68 | try { 69 | month = getInteger(R.styleable.TastiCalendarMonth_tcMonth, 0) 70 | hideWeekDays = getBoolean(R.styleable.TastiCalendarMonth_tcHideWeekDays, false) 71 | sundayAppearance = getInteger(R.styleable.TastiCalendarMonth_tcSundayHighlight, 0) 72 | sundayFirst = getBoolean(R.styleable.TastiCalendarMonth_tcSundayFirst, false) 73 | showSnackBars = getBoolean(R.styleable.TastiCalendarMonth_tcShowInfoSnackBars, true) 74 | appearance = getInteger(R.styleable.TastiCalendarMonth_tcAppearance, 0) 75 | } finally { 76 | recycle() 77 | } 78 | } 79 | binding = TasticalendarMonthBinding.inflate(LayoutInflater.from(context), this, true) 80 | dateWithChosenMonth = LocalDate.now().withMonth(month + 1).withDayOfMonth(1) 81 | initMonth() 82 | renderMonth(dateWithChosenMonth) 83 | } 84 | 85 | /** 86 | * Initializes the layout for the month, assigning the bindings and the visibilities. 87 | *

88 | * This is used once when the layout is first created. 89 | */ 90 | private fun initMonth() { 91 | // Week days 92 | val weekDayOne = binding.tastiCalendarWeekDayOne 93 | val weekDayTwo = binding.tastiCalendarWeekDayTwo 94 | val weekDayThree = binding.tastiCalendarWeekDayThree 95 | val weekDayFour = binding.tastiCalendarWeekDayFour 96 | val weekDayFive = binding.tastiCalendarWeekDayFive 97 | val weekDaySix = binding.tastiCalendarWeekDaySix 98 | val weekDaySeven = binding.tastiCalendarWeekDaySeven 99 | weekDaysList = mutableListOf( 100 | weekDayOne, weekDayTwo, weekDayThree, weekDayFour, weekDayFive, weekDaySix, weekDaySeven 101 | ) 102 | 103 | // Month cells 104 | val cell1 = binding.tastiCalendarCell1 105 | val cell2 = binding.tastiCalendarCell2 106 | val cell3 = binding.tastiCalendarCell3 107 | val cell4 = binding.tastiCalendarCell4 108 | val cell5 = binding.tastiCalendarCell5 109 | val cell6 = binding.tastiCalendarCell6 110 | val cell7 = binding.tastiCalendarCell7 111 | val cell8 = binding.tastiCalendarCell8 112 | val cell9 = binding.tastiCalendarCell9 113 | val cell10 = binding.tastiCalendarCell10 114 | val cell11 = binding.tastiCalendarCell11 115 | val cell12 = binding.tastiCalendarCell12 116 | val cell13 = binding.tastiCalendarCell13 117 | val cell14 = binding.tastiCalendarCell14 118 | val cell15 = binding.tastiCalendarCell15 119 | val cell16 = binding.tastiCalendarCell16 120 | val cell17 = binding.tastiCalendarCell17 121 | val cell18 = binding.tastiCalendarCell18 122 | val cell19 = binding.tastiCalendarCell19 123 | val cell20 = binding.tastiCalendarCell20 124 | val cell21 = binding.tastiCalendarCell21 125 | val cell22 = binding.tastiCalendarCell22 126 | val cell23 = binding.tastiCalendarCell23 127 | val cell24 = binding.tastiCalendarCell24 128 | val cell25 = binding.tastiCalendarCell25 129 | val cell26 = binding.tastiCalendarCell26 130 | val cell27 = binding.tastiCalendarCell27 131 | val cell28 = binding.tastiCalendarCell28 132 | val cell29 = binding.tastiCalendarCell29 133 | val cell30 = binding.tastiCalendarCell30 134 | val cell31 = binding.tastiCalendarCell31 135 | val cell32 = binding.tastiCalendarCell32 136 | val cell33 = binding.tastiCalendarCell33 137 | val cell34 = binding.tastiCalendarCell34 138 | val cell35 = binding.tastiCalendarCell35 139 | val cell36 = binding.tastiCalendarCell36 140 | val cell37 = binding.tastiCalendarCell37 141 | // Create a list of every cell 142 | cellsList = mutableListOf( 143 | cell1, 144 | cell2, 145 | cell3, 146 | cell4, 147 | cell5, 148 | cell6, 149 | cell7, 150 | cell8, 151 | cell9, 152 | cell10, 153 | cell11, 154 | cell12, 155 | cell13, 156 | cell14, 157 | cell15, 158 | cell16, 159 | cell17, 160 | cell18, 161 | cell19, 162 | cell20, 163 | cell21, 164 | cell22, 165 | cell23, 166 | cell24, 167 | cell25, 168 | cell26, 169 | cell27, 170 | cell28, 171 | cell29, 172 | cell30, 173 | cell31, 174 | cell32, 175 | cell33, 176 | cell34, 177 | cell35, 178 | cell36, 179 | cell37 180 | ) 181 | } 182 | 183 | /** 184 | * Renders a given range of days. 185 | *

186 | * This is used to hide unnecessary cells. 187 | * @param monthRange the range of days to render. 188 | */ 189 | private fun renderDays(monthRange: Range) { 190 | val min = monthRange.lower 191 | val max = monthRange.upper 192 | val emptyString = "" 193 | 194 | // Render the month numbers with a leading space for single digit numbers 195 | for (i in min..max) { 196 | val dayValue = i - min + 1 197 | // Manage single digit dates differently 198 | val dayNumber = if (dayValue <= 9) " $dayValue" else dayValue.toString() 199 | cellsList[i].text = dayNumber 200 | cellsList[i].visibility = VISIBLE 201 | // Accessibility related info 202 | try { 203 | val correspondingDate = 204 | LocalDate.of(dateWithChosenMonth.year, dateWithChosenMonth.month - 1, dayValue) 205 | val formatter: DateTimeFormatter = 206 | DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL) 207 | cellsList[i].contentDescription = correspondingDate.format(formatter) 208 | } catch (_: Exception) { 209 | } 210 | } 211 | // Hide unnecessary cells, also resetting the text to avoid false positive when highlighting 212 | if (min != 0) for (i in 0 until min) { 213 | cellsList[i].visibility = INVISIBLE 214 | cellsList[i].text = emptyString 215 | } 216 | when (dateWithChosenMonth.month) { 217 | Month.NOVEMBER, Month.APRIL, Month.JUNE, Month.SEPTEMBER -> { 218 | for (i in (30 + min) until cellsList.size) { 219 | cellsList[i].visibility = INVISIBLE 220 | cellsList[i].text = emptyString 221 | } 222 | } 223 | Month.FEBRUARY -> { 224 | val leapIndex = if (dateWithChosenMonth.isLeapYear) 29 else 28 225 | for (i in (leapIndex + min) until cellsList.size) { 226 | cellsList[i].visibility = INVISIBLE 227 | cellsList[i].text = emptyString 228 | } 229 | } 230 | else -> { 231 | for (i in (31 + min) until cellsList.size) { 232 | cellsList[i].visibility = INVISIBLE 233 | cellsList[i].text = emptyString 234 | } 235 | } 236 | } 237 | } 238 | 239 | /** 240 | * Highlights a given date in the month. 241 | *

242 | * This highlights a day in a variety of ways depending on the 243 | * parameters. Some parameters may not work properly in certain cases. 244 | * 245 | * @param day Int and not null, represents the day to highlight. If the month 246 | * doesn't have the day, nothing will be highlighted. 247 | * @param color the color used to highlight the month, used on the text itself 248 | * if no drawable is specified. 249 | * By default of the library, it's taken from the system. 250 | * @param drawable a drawable used as background, replacing the default colored circle. 251 | * It can be null. 252 | * @param makeBold Boolean, false by default, if true the day text will be in bold style. It has some problems, 253 | * since when the font is bold, it loses the monospace feature. 254 | * @param autoOpacity Boolean, false by default, if true allow different opacity levels of the background, 255 | * depending on how many times the day has been highlighted before. 256 | * @param autoTextColor Boolean, false by default, if true the text color will be computed 257 | * automatically to grant the best contrast available. 258 | * @param asForeground Boolean, false by default, if true the drawable or color will be used as 259 | * foreground, thus covering the text, totally or partially. 260 | * @param snackbarText String, a text to display if the day cell is clicked, empty by default. If empty 261 | * or null, the cell won't react to clicks. 262 | */ 263 | fun highlightDay( 264 | day: Int, 265 | color: Int, 266 | drawable: Drawable? = null, 267 | makeBold: Boolean = false, 268 | autoOpacity: Boolean = false, 269 | autoTextColor: Boolean = false, 270 | asForeground: Boolean = false, 271 | snackbarText: String = "" 272 | ) { 273 | // Update the global event count 274 | eventCount += 1 275 | var currentAlpha = 0 276 | // The textview will be hidden if the day doesn't exist in the current month 277 | for (cell in cellsList) { 278 | 279 | // Check, for each cell, if it's the wanted day and it's visible 280 | if (cell.text.trim() == day.toString() && cell.isVisible) { 281 | // Day found, now highlight it accordingly 282 | if (drawable == null) { 283 | // No drawable, color the text 284 | cell.setTextColor(color) 285 | } else { 286 | // Use the drawable as background or foreground 287 | if (asForeground) { 288 | cell.foreground = drawable 289 | cell.foregroundTintList = ColorStateList.valueOf(color) 290 | } 291 | // In case of background, compute the opacity 292 | else { 293 | if (autoOpacity) { 294 | if (cell.background != null) currentAlpha = cell.background.alpha 295 | } 296 | cell.background = drawable 297 | cell.backgroundTintList = ColorStateList.valueOf(color) 298 | 299 | if (autoOpacity) { 300 | if (currentAlpha > 185) cell.background.alpha = 255 301 | else cell.background.alpha = currentAlpha + 70 302 | } else cell.background.alpha = 255 303 | 304 | // Apply the automatic text color if requested 305 | if (autoTextColor) { 306 | cell.setTextColor( 307 | getBestContrast( 308 | color, 309 | context, 310 | cell.background.alpha, 311 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) 312 | resources.configuration.isNightModeActive 313 | else null 314 | ) 315 | ) 316 | } 317 | } 318 | } 319 | // The font will change since monospace doesn't have a bold style 320 | if (makeBold) { 321 | // This will result in a uglier layout and unaligned texts 322 | cell.setTypeface(null, Typeface.BOLD) 323 | } 324 | 325 | // Display a snackbar on tap if the text exists 326 | if (snackbarText.isNotBlank()) { 327 | cell.setOnClickListener { 328 | // Simply uppercase the first letter of the message, if it isn't already 329 | showSnackbar( 330 | snackbarText.replaceFirstChar { 331 | if (it.isLowerCase()) it.titlecase( 332 | Locale.ROOT 333 | ) else it.toString() 334 | }, 335 | snackBarsBaseView ?: binding.root, 336 | snackBarsDuration 337 | ) 338 | } 339 | } 340 | 341 | // Break the for cycle, since the correct day has been found 342 | break 343 | } 344 | } 345 | } 346 | 347 | /** 348 | * Renders a given month in a given year. 349 | *

350 | * This reloads the entire layout and apply the current settings, 351 | * it's the core method of the class. 352 | * 353 | * @param monthDate the date with the given month or year, if null the initial date is used. 354 | * @param events a list of TastiCalendarEvents, used to highlight a set of dates also adding 355 | * labels to it. It can be null. 356 | * @param dates a simple list of dates, used to highlight a set of dates. It can be null. 357 | */ 358 | fun renderMonth( 359 | monthDate: LocalDate = dateWithChosenMonth, 360 | events: List? = null, 361 | dates: List? = null 362 | ) { 363 | // Set the letters for the week days 364 | val monday = DayOfWeek.MONDAY 365 | val tuesday = DayOfWeek.TUESDAY 366 | val thursday = DayOfWeek.THURSDAY 367 | val wednesday = DayOfWeek.WEDNESDAY 368 | val friday = DayOfWeek.FRIDAY 369 | val saturday = DayOfWeek.SATURDAY 370 | val sunday = DayOfWeek.SUNDAY 371 | val locale = Locale.getDefault() 372 | if (!hideWeekDays) { 373 | if (!sundayFirst) { 374 | weekDaysList[0].text = monday.getDisplayName(TextStyle.NARROW, locale) 375 | weekDaysList[1].text = tuesday.getDisplayName(TextStyle.NARROW, locale) 376 | weekDaysList[2].text = wednesday.getDisplayName(TextStyle.NARROW, locale) 377 | weekDaysList[3].text = thursday.getDisplayName(TextStyle.NARROW, locale) 378 | weekDaysList[4].text = friday.getDisplayName(TextStyle.NARROW, locale) 379 | weekDaysList[5].text = saturday.getDisplayName(TextStyle.NARROW, locale) 380 | weekDaysList[6].text = sunday.getDisplayName(TextStyle.NARROW, locale) 381 | } else { 382 | sundayIndex = 0 383 | weekDaysList[0].text = sunday.getDisplayName(TextStyle.NARROW, locale) 384 | weekDaysList[1].text = monday.getDisplayName(TextStyle.NARROW, locale) 385 | weekDaysList[2].text = tuesday.getDisplayName(TextStyle.NARROW, locale) 386 | weekDaysList[3].text = wednesday.getDisplayName(TextStyle.NARROW, locale) 387 | weekDaysList[4].text = thursday.getDisplayName(TextStyle.NARROW, locale) 388 | weekDaysList[5].text = friday.getDisplayName(TextStyle.NARROW, locale) 389 | weekDaysList[6].text = saturday.getDisplayName(TextStyle.NARROW, locale) 390 | } 391 | } else { 392 | weekDaysList[0].visibility = GONE 393 | weekDaysList[1].visibility = GONE 394 | weekDaysList[2].visibility = GONE 395 | weekDaysList[3].visibility = GONE 396 | weekDaysList[4].visibility = GONE 397 | weekDaysList[5].visibility = GONE 398 | weekDaysList[6].visibility = GONE 399 | } 400 | // Some resetting logic 401 | if (monthDate.year != dateWithChosenMonth.year) { 402 | eventCount = 0 403 | binding.tastiCalendarMonthName.setTextColor( 404 | getThemeColor( 405 | R.attr.colorSecondary, 406 | context 407 | ) 408 | ) 409 | } 410 | val firstDayDate = monthDate.withDayOfMonth(1) 411 | dateWithChosenMonth = firstDayDate 412 | 413 | // Set the number and name (capitalized) for the month (from range 0-11 to 1-12) 414 | val firstDayOfWeekForChosenMonth = firstDayDate.dayOfWeek 415 | monthTitle = binding.tastiCalendarMonthName 416 | monthTitle.text = 417 | firstDayDate.month.getDisplayName(TextStyle.FULL_STANDALONE, Locale.getDefault()) 418 | .replaceFirstChar { 419 | if (it.isLowerCase()) it.titlecase(Locale.getDefault()) 420 | else it.toString() 421 | } 422 | 423 | if (!sundayFirst) 424 | // Case 1: monday is the first day of the week 425 | when (firstDayOfWeekForChosenMonth) { 426 | DayOfWeek.MONDAY -> renderDays(Range(0, 30)) 427 | DayOfWeek.TUESDAY -> renderDays(Range(1, 31)) 428 | DayOfWeek.WEDNESDAY -> renderDays(Range(2, 32)) 429 | DayOfWeek.THURSDAY -> renderDays(Range(3, 33)) 430 | DayOfWeek.FRIDAY -> renderDays(Range(4, 34)) 431 | DayOfWeek.SATURDAY -> renderDays(Range(5, 35)) 432 | DayOfWeek.SUNDAY -> renderDays(Range(6, 36)) 433 | else -> {} 434 | } 435 | else 436 | // Case 2: sunday is the first day of the week 437 | when (firstDayOfWeekForChosenMonth) { 438 | DayOfWeek.SUNDAY -> renderDays(Range(0, 30)) 439 | DayOfWeek.MONDAY -> renderDays(Range(1, 31)) 440 | DayOfWeek.TUESDAY -> renderDays(Range(2, 32)) 441 | DayOfWeek.WEDNESDAY -> renderDays(Range(3, 33)) 442 | DayOfWeek.THURSDAY -> renderDays(Range(4, 34)) 443 | DayOfWeek.FRIDAY -> renderDays(Range(5, 35)) 444 | DayOfWeek.SATURDAY -> renderDays(Range(6, 36)) 445 | else -> {} 446 | } 447 | 448 | // Show snack bars on month header press 449 | if (showSnackBars) { 450 | // Set the appropriate header 451 | monthTitle.setOnClickListener { 452 | showSnackbar( 453 | getSnackMessage(), 454 | snackBarsBaseView ?: binding.root, 455 | snackBarsDuration 456 | ) 457 | } 458 | } else { 459 | // Else remove any click listener 460 | monthTitle.setOnClickListener(null) 461 | } 462 | 463 | // Unify the lists to be the same list 464 | val finalList: MutableList? = 465 | if (events.isNullOrEmpty()) { 466 | if (!dates.isNullOrEmpty()) { 467 | dates.map { TastiCalendarEvent(it, "") }.toMutableList() 468 | } else null 469 | } else events.toMutableList() 470 | if (finalList.isNullOrEmpty()) return 471 | finalList.sortBy { it.date.withYear(1970) } 472 | 473 | // Highlight the events 474 | var currentDate = finalList[0].date 475 | var dayEvents = mutableListOf() 476 | for (event in finalList) { 477 | // Compute the snackbar text 478 | if (event.date.withYear(1970).isEqual(currentDate.withYear(1970))) { 479 | dayEvents.add(event) 480 | } else { 481 | dayEvents = mutableListOf() 482 | dayEvents.add(event) 483 | currentDate = event.date.withYear(1970) 484 | } 485 | // Highlight the dates 486 | highlightDay( 487 | event.date.dayOfMonth, 488 | getThemeColor(R.attr.colorPrimary, context), 489 | AppCompatResources.getDrawable(context, R.drawable.tasticalendar_circle), 490 | makeBold = false, 491 | autoOpacity = true, 492 | autoTextColor = true, 493 | snackbarText = if (showSnackBars) formatEventList(dayEvents) else "" 494 | ) 495 | } 496 | 497 | // Set the appearance (0 small default, 1 medium, 2 large, 3 xlarge) 498 | when (appearance) { 499 | 0 -> colorize() 500 | 1 -> setAppearance(1) 501 | 2 -> setAppearance(2) 502 | 3 -> setAppearance(3) 503 | } 504 | } 505 | 506 | /** 507 | * Obtains the message to display in the snackbar, dynamically 508 | */ 509 | private fun getSnackMessage(): String { 510 | var prefix = "" 511 | try { 512 | prefix = if (snackBarsPluralFormatting && snackBarsPrefix != null) { 513 | String.format( 514 | context.resources.getQuantityString(snackBarsPrefix!!, eventCount), 515 | eventCount 516 | ) 517 | } else context.getString(snackBarsPrefix!!) 518 | } catch (_: Exception) { 519 | snackBarsPrefix = null 520 | snackBarsPluralFormatting = false 521 | } 522 | 523 | // Build the final, formatted message for the month header 524 | val snackMessage = if (prefix.isNotEmpty()) 525 | if (snackBarsPluralFormatting) prefix 526 | else "$prefix $eventCount" 527 | else "-> $eventCount" 528 | // Capitalize the first character 529 | val capitalizedMessage = snackMessage.replaceFirstChar { 530 | if (it.isLowerCase()) it.titlecase(Locale.ROOT) 531 | else it.toString() 532 | } 533 | return capitalizedMessage 534 | } 535 | 536 | /** 537 | * Changes the visual density of the month. 538 | *

539 | * It is used to obtain different visualization styles. 540 | * @param appearance Int, can't be null, 0 means small, 1 medium, 2 large, 3 extra large. 541 | * Every other value is ignored. 542 | */ 543 | fun setAppearance(appearance: Int) { 544 | when (appearance) { 545 | 0 -> { 546 | for (cell in cellsList) { 547 | val textColor = cell.currentTextColor 548 | cell.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_LabelMedium) 549 | cell.setPadding(3, 3, 3, 3) 550 | cell.typeface = Typeface.MONOSPACE 551 | cell.setTextColor(textColor) 552 | } 553 | for (day in weekDaysList) { 554 | val textColor = day.currentTextColor 555 | day.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_LabelMedium) 556 | day.setTextColor(textColor) 557 | } 558 | monthTitle.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodyMedium) 559 | } 560 | 1 -> { 561 | for (cell in cellsList) { 562 | val textColor = cell.currentTextColor 563 | cell.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodyMedium) 564 | cell.setPadding(8, 8, 8, 8) 565 | cell.typeface = Typeface.MONOSPACE 566 | cell.setTextColor(textColor) 567 | } 568 | for (day in weekDaysList) { 569 | val textColor = day.currentTextColor 570 | day.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodyMedium) 571 | day.setTextColor(textColor) 572 | } 573 | monthTitle.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodyLarge) 574 | } 575 | 2 -> { 576 | for (cell in cellsList) { 577 | val textColor = cell.currentTextColor 578 | cell.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodyLarge) 579 | cell.setPadding(12, 12, 12, 12) 580 | cell.typeface = Typeface.MONOSPACE 581 | cell.setTextColor(textColor) 582 | } 583 | for (day in weekDaysList) { 584 | val textColor = day.currentTextColor 585 | day.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodyLarge) 586 | day.setTextColor(textColor) 587 | } 588 | monthTitle.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_HeadlineSmall) 589 | } 590 | 3 -> { 591 | for (cell in cellsList) { 592 | val textColor = cell.currentTextColor 593 | cell.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_HeadlineMedium) 594 | cell.setPadding(16, 16, 16, 16) 595 | cell.typeface = Typeface.MONOSPACE 596 | cell.setTextColor(textColor) 597 | } 598 | for (day in weekDaysList) { 599 | val textColor = day.currentTextColor 600 | day.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_HeadlineMedium) 601 | day.setTextColor(textColor) 602 | } 603 | monthTitle.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_HeadlineLarge) 604 | } 605 | else -> return 606 | } 607 | colorize() 608 | } 609 | 610 | /** 611 | * Forces sunday to be displayed as the first day of the week. 612 | *

613 | * This is used to force sunday as the first day of the week. If this method isn't called, the 614 | * first day of the week is automatically taken from the default locale. 615 | * @param appearance Enum of type TcAppearance, can't be null, specifies the selected 616 | * highlighting type 617 | * @param refresh Boolean, true by default, if false the layout won't be refreshed. 618 | * @see TcSundayHighlight 619 | */ 620 | fun setSundayHighlight(appearance: TcSundayHighlight, refresh: Boolean = true) { 621 | sundayAppearance = appearance.ordinal 622 | if (refresh) renderMonth() 623 | } 624 | 625 | /** 626 | * Forces sunday to be displayed as the first day of the week. 627 | *

628 | * This is used to force sunday as the first day of the week. If this method isn't called, the 629 | * first day of the week is automatically taken from the default locale. 630 | * @param enable Boolean, can't be null, if true sets sunday as the first day of the week 631 | * for the current month. 632 | * @param refresh Boolean, true by default, if false the layout won't be refreshed. 633 | */ 634 | fun setSundayFirst(enable: Boolean, refresh: Boolean = true) { 635 | if (enable != sundayFirst) { 636 | sundayFirst = enable 637 | if (refresh) renderMonth() 638 | } 639 | } 640 | 641 | /** 642 | * Sets the weekdays row visibility. 643 | *

644 | * This is used to show or hide the weekdays row. Useful to obtain compact layouts. 645 | * @param enable Boolean, can't be null, if true hides the week days row 646 | * for the current month. 647 | * @param refresh Boolean, true by default, if false the layout won't be refreshed. 648 | */ 649 | fun setHideWeekDays(enable: Boolean, refresh: Boolean = true) { 650 | if (enable != hideWeekDays) hideWeekDays = enable 651 | if (refresh) renderMonth() 652 | } 653 | 654 | /** 655 | * Add the given prefix to the message shown when tapping the month header. 656 | *

657 | * This is used to display something before the number of events in the month, 658 | * in the form " " (eg: "Events: 12"). 659 | * @param prefix Int, can't be null, the id of the prefix to add to the snackbar message. 660 | * @param plural Boolean, false by default, if true the passed id is a plural String, 661 | * which will be formatted with the number of events accordingly. 662 | */ 663 | fun setSnackBarsPrefix(prefix: Int, plural: Boolean = false, refresh: Boolean = true) { 664 | snackBarsPrefix = prefix 665 | snackBarsPluralFormatting = plural 666 | if (refresh) renderMonth() 667 | } 668 | 669 | /** 670 | * Sets the base view for the snackbar. 671 | *

672 | * It can be used to avoid unwanted behaviors when a snackbar appears. For example, the snackbar 673 | * will spawn below the action button by default 674 | * @param view View, not null, it should be the base view. If the view is invalid, the binding 675 | * root will be used instead 676 | * @param refresh Boolean, true by default, if false the layout won't be refreshed. 677 | */ 678 | fun setSnackBarBaseView(view: View, refresh: Boolean = true) { 679 | snackBarsBaseView = view 680 | if (refresh) renderMonth() 681 | } 682 | 683 | /** 684 | * Sets the duration for the snackbar. 685 | *

686 | * This is used to change the default duration, set to 3000 milliseconds. 687 | * @param duration Int, can't be null, defines the duration in milliseconds. 688 | * @param refresh Boolean, true by default, if false the layout won't be refreshed. 689 | */ 690 | fun setSnackBarsDuration(duration: Int, refresh: Boolean = true) { 691 | snackBarsDuration = duration 692 | if (refresh) renderMonth() 693 | } 694 | 695 | /** 696 | * Renders the same month for a different year. 697 | *

698 | * Basically a wrapper around renderMonth, used to quickly switch only the year. 699 | * @param year Int, can't be null, represents the year to display this month into. 700 | */ 701 | fun setYear(year: Int) = renderMonth(dateWithChosenMonth.withYear(year)) 702 | 703 | /** 704 | * Resets the highlighting for the current month, thus removing every event from it. 705 | *

706 | * It can be used to display a new set of events, or a different month. 707 | */ 708 | fun resetHighlighting() { 709 | for (cell in cellsList) { 710 | if (cell.background != null) { 711 | cell.background.alpha = 0 712 | cell.background = null 713 | cell.setOnClickListener(null) 714 | } 715 | cell.setTextColor( 716 | getThemeColor( 717 | R.attr.colorOnBackground, 718 | context 719 | ) 720 | ) 721 | cell.foreground = null 722 | } 723 | } 724 | 725 | /** 726 | * Mainly used when the appearance changes, it colorizes some elements in the layout. 727 | *

728 | * This is used to colorize the current month, the other months, and to slightly change 729 | * the weekdays text opacity (.85). 730 | */ 731 | private fun colorize() { 732 | if (dateWithChosenMonth.month == LocalDate.now().month && dateWithChosenMonth.year == LocalDate.now().year) { 733 | monthTitle.setTextColor( 734 | getThemeColor( 735 | R.attr.colorTertiary, 736 | context 737 | ) 738 | ) 739 | } else monthTitle.setTextColor( 740 | getThemeColor( 741 | R.attr.colorSecondary, 742 | context 743 | ) 744 | ) 745 | for (weekDay in weekDaysList) weekDay.alpha = .85f 746 | 747 | // Set the highlighting style for sunday 748 | when (sundayAppearance) { 749 | 0 -> { 750 | weekDaysList[sundayIndex].setTypeface(null, Typeface.NORMAL) 751 | weekDaysList[sundayIndex].setTextColor( 752 | getThemeColor( 753 | R.attr.colorOnSurfaceVariant, 754 | context 755 | ) 756 | ) 757 | } 758 | 1 -> { 759 | weekDaysList[sundayIndex].setTypeface(null, Typeface.BOLD) 760 | weekDaysList[sundayIndex].setTextColor( 761 | getThemeColor( 762 | R.attr.colorOnSurfaceVariant, 763 | context 764 | ) 765 | ) 766 | } 767 | 2 -> { 768 | weekDaysList[sundayIndex].setTypeface(null, Typeface.NORMAL) 769 | weekDaysList[sundayIndex].setTextColor( 770 | getThemeColor( 771 | R.attr.colorTertiary, 772 | context 773 | ) 774 | ) 775 | } 776 | 3 -> { 777 | weekDaysList[sundayIndex].setTypeface(null, Typeface.BOLD) 778 | weekDaysList[sundayIndex].setTextColor( 779 | getThemeColor( 780 | R.attr.colorTertiary, 781 | context 782 | ) 783 | ) 784 | } 785 | } 786 | } 787 | } --------------------------------------------------------------------------------