("release") {
55 | from(components["release"])
56 | groupId = "com.github.OrlanDroyd"
57 | artifactId = "ComposeCalendar"
58 | version = "1.1.0"
59 | }
60 | }
61 | }
62 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.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 |
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.application")
3 | id("org.jetbrains.kotlin.android")
4 | }
5 |
6 | android {
7 | namespace = "com.gmail.orlandroyd.calendar_example"
8 | compileSdk = 34
9 |
10 | defaultConfig {
11 | applicationId = "com.gmail.orlandroyd.calendar_example"
12 | minSdk = 21
13 | targetSdk = 34
14 | versionCode = 1
15 | versionName = "1.0"
16 |
17 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
18 | vectorDrawables {
19 | useSupportLibrary = true
20 | }
21 | }
22 |
23 | buildTypes {
24 | release {
25 | isMinifyEnabled = false
26 | proguardFiles(
27 | getDefaultProguardFile("proguard-android-optimize.txt"),
28 | "proguard-rules.pro"
29 | )
30 | }
31 | }
32 | compileOptions {
33 | sourceCompatibility = JavaVersion.VERSION_1_8
34 | targetCompatibility = JavaVersion.VERSION_1_8
35 | }
36 | kotlinOptions {
37 | jvmTarget = "1.8"
38 | }
39 | buildFeatures {
40 | compose = true
41 | }
42 | composeOptions {
43 | kotlinCompilerExtensionVersion = "1.4.3"
44 | }
45 | packaging {
46 | resources {
47 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
48 | }
49 | }
50 | }
51 |
52 | dependencies {
53 |
54 | implementation("androidx.core:core-ktx:1.9.0")
55 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
56 | implementation("androidx.activity:activity-compose:1.8.0")
57 | implementation(platform("androidx.compose:compose-bom:2023.03.00"))
58 | implementation("androidx.compose.ui:ui")
59 | implementation("androidx.compose.ui:ui-graphics")
60 | implementation("androidx.compose.ui:ui-tooling-preview")
61 | implementation("androidx.compose.material3:material3")
62 | testImplementation("junit:junit:4.13.2")
63 | androidTestImplementation("androidx.test.ext:junit:1.1.5")
64 | androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
65 | androidTestImplementation(platform("androidx.compose:compose-bom:2023.03.00"))
66 | androidTestImplementation("androidx.compose.ui:ui-test-junit4")
67 | debugImplementation("androidx.compose.ui:ui-tooling")
68 | debugImplementation("androidx.compose.ui:ui-test-manifest")
69 |
70 | // Compose Calendar dependency
71 | implementation("com.github.OrlanDroyd:ComposeCalendar:1.1.0")
72 | }
--------------------------------------------------------------------------------
/README.md.backup:
--------------------------------------------------------------------------------
1 | Compose Calendar
2 |
3 |
4 |
5 |
6 |
7 | ## Compose Calendar is a series of 4 UI elements that allow you to select:
8 |
9 | ### Specific date
10 | 
11 |
12 | ### Date and time
13 | 
14 |
15 | ### Date range
16 | 
17 |
18 | ### Only the month and year
19 | 
20 |
21 | ## Download
22 |
23 |
24 | ### Gradle
25 |
26 | Add the dependency below to your module's `build.gradle` file:
27 | ```gradle
28 | dependencies {
29 | implementation 'com.github.orlandroyd:ComposeCalendar:1.0.0'
30 | }
31 | ```
32 | Add a repository in your `settings.gradle` file:
33 | ```
34 | dependencyResolutionManagement {
35 | repositories {
36 | ...
37 | maven { url 'https://jitpack.io' }
38 | }
39 | }
40 | ```
41 | ## Usage
42 |
43 | There are only one required parameter: `visible`.
44 |
45 | ```kotlin
46 | val context = LocalContext.current
47 |
48 | var isVisibleDatePickerDialog by remember {
49 | mutableStateOf(false)
50 | }
51 |
52 | DatePickerDialog(
53 | visible = isVisibleDatePickerDialog,
54 | onClose = { isVisibleDatePickerDialog = false },
55 | onDateSelected = { isVisibleDatePickerDialog = false}
56 | )
57 | ```
58 |
59 | You can also modify other parameters, such as colors, shading and surface
60 |
61 | ## Like what you see? :yellow_heart:
62 | ⭐ Give a star to this repository.
63 | ☕ Buy me a coffee: https://ko-fi.com/orlandroyd
64 |
65 | # License
66 | ```xml
67 | Designed and developed by 2022 stevdza-san (Stefan Jovanović)
68 |
69 | Licensed under the Apache License, Version 2.0 (the "License");
70 | you may not use this file except in compliance with the License.
71 | You may obtain a copy of the License at
72 |
73 | http://www.apache.org/licenses/LICENSE-2.0
74 |
75 | Unless required by applicable law or agreed to in writing, software
76 | distributed under the License is distributed on an "AS IS" BASIS,
77 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
78 | See the License for the specific language governing permissions and
79 | limitations under the License.
80 | ```
--------------------------------------------------------------------------------
/app/src/main/java/com/gmail/orlandroyd/calendar_example/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.gmail.orlandroyd.calendar_example.ui.theme
2 |
3 | import android.app.Activity
4 | import android.os.Build
5 | import androidx.compose.foundation.isSystemInDarkTheme
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.darkColorScheme
8 | import androidx.compose.material3.dynamicDarkColorScheme
9 | import androidx.compose.material3.dynamicLightColorScheme
10 | import androidx.compose.material3.lightColorScheme
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.SideEffect
13 | import androidx.compose.ui.graphics.toArgb
14 | import androidx.compose.ui.platform.LocalContext
15 | import androidx.compose.ui.platform.LocalView
16 | import androidx.core.view.WindowCompat
17 |
18 | private val DarkColorScheme = darkColorScheme(
19 | primary = Purple80,
20 | secondary = PurpleGrey80,
21 | tertiary = Pink80
22 | )
23 |
24 | private val LightColorScheme = lightColorScheme(
25 | primary = Purple40,
26 | secondary = PurpleGrey40,
27 | tertiary = Pink40
28 |
29 | /* Other default colors to override
30 | background = Color(0xFFFFFBFE),
31 | surface = Color(0xFFFFFBFE),
32 | onPrimary = Color.White,
33 | onSecondary = Color.White,
34 | onTertiary = Color.White,
35 | onBackground = Color(0xFF1C1B1F),
36 | onSurface = Color(0xFF1C1B1F),
37 | */
38 | )
39 |
40 | @Composable
41 | fun ComposeCalendarTheme(
42 | darkTheme: Boolean = isSystemInDarkTheme(),
43 | // Dynamic color is available on Android 12+
44 | dynamicColor: Boolean = true,
45 | content: @Composable () -> Unit
46 | ) {
47 | val colorScheme = when {
48 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
49 | val context = LocalContext.current
50 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
51 | }
52 |
53 | darkTheme -> DarkColorScheme
54 | else -> LightColorScheme
55 | }
56 | val view = LocalView.current
57 | if (!view.isInEditMode) {
58 | SideEffect {
59 | val window = (view.context as Activity).window
60 | window.statusBarColor = colorScheme.primary.toArgb()
61 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
62 | }
63 | }
64 |
65 | MaterialTheme(
66 | colorScheme = colorScheme,
67 | typography = Typography,
68 | content = content
69 | )
70 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
Compose Calendar
2 |
3 |
4 |
5 |
6 |
7 | ## Compose Calendar is a series of 4 UI elements that allow you to select:
8 |
9 | ### Specific date
10 | ```kotlin
11 | var isVisible by remember { mutableStateOf(true) }
12 | DatePickerDlg(
13 | visible = isVisible,
14 | onClose = { isVisible = false },
15 | onDateSelected = { isVisible = false}
16 | )
17 | ```
18 |
19 |
20 | ### Date and time
21 | ```kotlin
22 | var isVisible by remember { mutableStateOf(true) }
23 | DatePickerDlg(
24 | visible = isVisible,
25 | showSetHours = true,
26 | onClose = { isVisible = false },
27 | onDateSelected = { isVisible = false}
28 | )
29 | ```
30 |
31 |
32 | ### Date range
33 | ```kotlin
34 | var isVisible by remember { mutableStateOf(true) }
35 | DateRangePickerDlg(
36 | visible = isVisible,
37 | onClose = { isVisible = false },
38 | onDatesSelected = { isVisible = false}
39 | )
40 | ```
41 |
42 |
43 | ### Only the month and year
44 | ```kotlin
45 | var isVisible by remember { mutableStateOf(true) }
46 | MonthYearPickerDlg(
47 | visible = isVisible,
48 | onClose = { isVisible = false },
49 | onDateSelected = { isVisible = false}
50 | )
51 | ```
52 |
53 |
54 | ## Download
55 |
56 |
57 | ### Gradle
58 |
59 | Add the dependency below to your module's `build.gradle` file:
60 | ```gradle
61 | dependencies {
62 | implementation 'com.github.OrlanDroyd:ComposeCalendar:1.1.0'
63 | }
64 | ```
65 | Add a repository in your `settings.gradle` file:
66 | ```
67 | dependencyResolutionManagement {
68 | repositories {
69 | ...
70 | maven { url 'https://jitpack.io' }
71 | }
72 | }
73 | ```
74 | ## Usage
75 |
76 | There are only one required parameter: `visible`.
77 |
78 | ```kotlin
79 | var isVisible by remember { mutableStateOf(true) }
80 | DatePickerDlg(
81 | visible = isVisible,
82 | onClose = { isVisible = false },
83 | onDateSelected = { isVisible = false}
84 | )
85 | ```
86 |
87 | You can also modify other parameters, such as colors, shading and surface
88 |
89 | ## Like what you see? :yellow_heart:
90 | ⭐ Give a star to this repository.
91 |
92 | [](https://ko-fi.com/C0C3Q54JR)
93 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/ComposeCalendar/src/main/java/com/gmail/orlandroyd/composecalendar/util/DateUtil.kt:
--------------------------------------------------------------------------------
1 | package com.gmail.orlandroyd.composecalendar.util
2 |
3 | import android.app.TimePickerDialog
4 | import android.content.Context
5 | import android.widget.TextView
6 | import androidx.core.content.ContextCompat
7 | import java.text.SimpleDateFormat
8 | import java.util.Calendar
9 | import java.util.Date
10 | import java.util.Locale
11 |
12 | fun Date.getCalendar(): Calendar {
13 | val calendar = Calendar.getInstance()
14 | calendar.time = this
15 | return calendar
16 | }
17 |
18 | fun Context.showTimePickerDialog(
19 | currentDate: Date? = null,
20 | timeSelected: (Calendar) -> Unit,
21 | textColor: Int
22 | ) {
23 | val calendar = Calendar.getInstance()
24 | calendar.time = currentDate ?: Date()
25 | val hour = calendar.get(Calendar.HOUR_OF_DAY)
26 | val minute = calendar.get(Calendar.MINUTE)
27 | val dialog = TimePickerDialog(this, { _, hourSelected, minuteSelected ->
28 | val calendarSelected = Calendar.getInstance()
29 | calendarSelected.time = calendar.time
30 | calendarSelected.set(Calendar.HOUR_OF_DAY, hourSelected)
31 | calendarSelected.set(Calendar.MINUTE, minuteSelected)
32 | timeSelected(calendarSelected)
33 | }, hour, minute, true)
34 | dialog.show()
35 | dialog.getButton(TimePickerDialog.BUTTON_POSITIVE).setResourceTextColor(textColor)
36 | dialog.getButton(TimePickerDialog.BUTTON_NEGATIVE).setResourceTextColor(textColor)
37 | }
38 |
39 | fun Date.getParsedDate(
40 | format: String = "yyyy-MM-dd HH:mm:ss",
41 | locale: Locale = Locale.getDefault()
42 | ): String {
43 | val df = SimpleDateFormat(format, locale)
44 | return df.format(this)
45 | }
46 |
47 | fun Calendar.sameDay(otherDay: Calendar): Boolean {
48 | val day1 = get(Calendar.DAY_OF_YEAR)
49 | val day2 = otherDay.get(Calendar.DAY_OF_YEAR)
50 | val year1 = get(Calendar.YEAR)
51 | val year2 = otherDay.get(Calendar.YEAR)
52 | return day1 == day2 && year1 == year2
53 | }
54 |
55 | fun Date.sameDay(otherDay: Date): Boolean {
56 | val calendar1 = getCalendar()
57 | val calendar2 = otherDay.getCalendar()
58 | val day1 = calendar1.get(Calendar.DAY_OF_YEAR)
59 | val day2 = calendar2.get(Calendar.DAY_OF_YEAR)
60 | val year1 = calendar1.get(Calendar.YEAR)
61 | val year2 = calendar2.get(Calendar.YEAR)
62 | return day1 == day2 && year1 == year2
63 | }
64 |
65 | fun Date.getFullMonthTextDate(locale: Locale = Locale.getDefault()): String {
66 | val day = getParsedDate("dd")
67 | val month = getParsedDate("MMMM").replaceFirstChar { it.uppercase() }
68 | val year = getParsedDate("yyyy")
69 | return "$day $month $year"
70 | }
71 |
72 | fun Calendar.between(range: Pair): Boolean {
73 | val day = get(Calendar.DAY_OF_YEAR)
74 | val dayStart = range.first.getCalendar().get(Calendar.DAY_OF_YEAR)
75 | val dayEnd = range.second.getCalendar().get(Calendar.DAY_OF_YEAR)
76 | val year = get(Calendar.YEAR)
77 | val yearStart = range.first.getCalendar().get(Calendar.YEAR)
78 | val yearEnd = range.second.getCalendar().get(Calendar.YEAR)
79 | val wrong =
80 | year < yearStart || year > yearEnd || year == yearStart && day < dayStart || year == yearEnd && day > dayEnd
81 | return !wrong
82 | }
83 |
84 | fun Date?.getMonthText(locale: Locale = Locale.getDefault()): String {
85 | val text = this?.getParsedDate("MMM, yyyy")
86 | return text?.replaceFirstChar { it.uppercase() } ?: ""
87 | }
88 |
89 | fun Date.getCalendarMonthDays(): List {
90 | val calendar = Calendar.getInstance()
91 | calendar.time = this
92 | val cells = mutableListOf()
93 |
94 | // Date moved to the first day of the month
95 | calendar[Calendar.DAY_OF_MONTH] = 1
96 | var monthBeginningCell = calendar[Calendar.DAY_OF_WEEK] - 2
97 | if (monthBeginningCell == -1) {
98 | monthBeginningCell = 6
99 | }
100 |
101 | // Date moved to the monday before or equal to the first day of the month
102 | calendar.add(Calendar.DAY_OF_MONTH, -monthBeginningCell)
103 |
104 | // Filling the calendar
105 | while (cells.size < monthBeginningCell) {
106 | cells.add(calendar.time)
107 | calendar.add(Calendar.DAY_OF_MONTH, 1)
108 | }
109 | val initialMonth = calendar[Calendar.MONTH]
110 | while (initialMonth == calendar[Calendar.MONTH]) {
111 | cells.add(calendar.time)
112 | calendar.add(Calendar.DAY_OF_MONTH, 1)
113 | }
114 | while (Calendar.MONDAY != calendar[Calendar.DAY_OF_WEEK]) {
115 | cells.add(calendar.time)
116 | calendar.add(Calendar.DAY_OF_MONTH, 1)
117 | }
118 | return cells
119 | }
120 |
121 | fun TextView.setResourceTextColor(colorResource: Int) {
122 | setTextColor(ContextCompat.getColor(context, colorResource))
123 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/gmail/orlandroyd/calendar_example/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.gmail.orlandroyd.calendar_example
2 |
3 | import android.os.Bundle
4 | import android.widget.Toast
5 | import androidx.activity.ComponentActivity
6 | import androidx.activity.compose.setContent
7 | import androidx.compose.foundation.layout.Arrangement
8 | import androidx.compose.foundation.layout.Column
9 | import androidx.compose.foundation.layout.Spacer
10 | import androidx.compose.foundation.layout.fillMaxSize
11 | import androidx.compose.foundation.layout.height
12 | import androidx.compose.material3.Button
13 | import androidx.compose.material3.MaterialTheme
14 | import androidx.compose.material3.Surface
15 | import androidx.compose.material3.Text
16 | import androidx.compose.runtime.getValue
17 | import androidx.compose.runtime.mutableStateOf
18 | import androidx.compose.runtime.remember
19 | import androidx.compose.runtime.setValue
20 | import androidx.compose.ui.Alignment
21 | import androidx.compose.ui.Modifier
22 | import androidx.compose.ui.platform.LocalContext
23 | import androidx.compose.ui.unit.dp
24 | import com.gmail.orlandroyd.calendar_example.ui.theme.ComposeCalendarTheme
25 | import com.gmail.orlandroyd.composecalendar.DatePickerDlg
26 | import com.gmail.orlandroyd.composecalendar.DateRangePickerDlg
27 | import com.gmail.orlandroyd.composecalendar.MonthYearPickerDlg
28 | import com.gmail.orlandroyd.composecalendar.util.getFullMonthTextDate
29 |
30 | class MainActivity : ComponentActivity() {
31 | override fun onCreate(savedInstanceState: Bundle?) {
32 | super.onCreate(savedInstanceState)
33 | setContent {
34 | ComposeCalendarTheme {
35 | Surface(
36 | modifier = Modifier
37 | .fillMaxSize(),
38 | color = MaterialTheme.colorScheme.background
39 | ) {
40 |
41 | val context = LocalContext.current
42 |
43 | var isVisibleDatePickerDialog by remember {
44 | mutableStateOf(false)
45 | }
46 |
47 | var isVisibleDatePickerDialogHours by remember {
48 | mutableStateOf(false)
49 | }
50 |
51 | var isVisibleDateRangePickerDialog by remember {
52 | mutableStateOf(false)
53 | }
54 |
55 | var isVisibleMonthYearPickerDialog by remember {
56 | mutableStateOf(false)
57 | }
58 |
59 | Column(
60 | modifier = Modifier.fillMaxSize(),
61 | verticalArrangement = Arrangement.Center,
62 | horizontalAlignment = Alignment.CenterHorizontally
63 | ) {
64 | Button(onClick = { isVisibleDatePickerDialog = true }) {
65 | Text("DatePickerDialog")
66 | }
67 | Spacer(modifier = Modifier.height(16.dp))
68 | Button(onClick = { isVisibleDatePickerDialogHours = true }) {
69 | Text("DatePickerDialog + Hours")
70 | }
71 | Spacer(modifier = Modifier.height(16.dp))
72 | Button(onClick = { isVisibleDateRangePickerDialog = true }) {
73 | Text("DateRangePickerDialog")
74 | }
75 | Spacer(modifier = Modifier.height(16.dp))
76 | Button(onClick = { isVisibleMonthYearPickerDialog = true }) {
77 | Text("MonthYearPickerDialog")
78 | }
79 | }
80 |
81 | DatePickerDlg(
82 | visible = isVisibleDatePickerDialog,
83 | onClose = { isVisibleDatePickerDialog = false },
84 | onDateSelected = {
85 | isVisibleDatePickerDialog = false
86 | Toast.makeText(context, it.getFullMonthTextDate(), Toast.LENGTH_SHORT)
87 | .show()
88 | }
89 | )
90 |
91 | DatePickerDlg(
92 | visible = isVisibleDatePickerDialogHours,
93 | onClose = { isVisibleDatePickerDialogHours = false },
94 | showSetHours = true,
95 | onDateSelected = {
96 | isVisibleDatePickerDialogHours = false
97 | Toast.makeText(context, it.getFullMonthTextDate(), Toast.LENGTH_SHORT)
98 | .show()
99 | }
100 | )
101 |
102 | DateRangePickerDlg(
103 | visible = isVisibleDateRangePickerDialog,
104 | onClose = { isVisibleDateRangePickerDialog = false },
105 | onDatesSelected = {
106 | isVisibleDateRangePickerDialog = false
107 | Toast.makeText(
108 | context,
109 | "${it.first.getFullMonthTextDate()} - ${it.second.getFullMonthTextDate()}",
110 | Toast.LENGTH_SHORT
111 | ).show()
112 | }
113 | )
114 |
115 | MonthYearPickerDlg(
116 | visible = isVisibleMonthYearPickerDialog,
117 | onClose = { isVisibleMonthYearPickerDialog = false },
118 | onDateSelected = {
119 | isVisibleMonthYearPickerDialog = false
120 | Toast.makeText(context, it.getFullMonthTextDate(), Toast.LENGTH_SHORT)
121 | .show()
122 | }
123 | )
124 |
125 | }
126 | }
127 | }
128 | }
129 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/ComposeCalendar/src/main/java/com/gmail/orlandroyd/composecalendar/DateRangePickerDlg.kt:
--------------------------------------------------------------------------------
1 | package com.gmail.orlandroyd.composecalendar
2 |
3 | import androidx.activity.compose.BackHandler
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.clickable
6 | import androidx.compose.foundation.layout.Arrangement
7 | import androidx.compose.foundation.layout.Column
8 | import androidx.compose.foundation.layout.Row
9 | import androidx.compose.foundation.layout.fillMaxWidth
10 | import androidx.compose.foundation.layout.height
11 | import androidx.compose.foundation.layout.padding
12 | import androidx.compose.foundation.layout.size
13 | import androidx.compose.foundation.layout.wrapContentHeight
14 | import androidx.compose.foundation.shape.RoundedCornerShape
15 | import androidx.compose.material3.Divider
16 | import androidx.compose.material3.Icon
17 | import androidx.compose.material3.Text
18 | import androidx.compose.runtime.Composable
19 | import androidx.compose.runtime.getValue
20 | import androidx.compose.runtime.mutableStateOf
21 | import androidx.compose.runtime.remember
22 | import androidx.compose.runtime.setValue
23 | import androidx.compose.ui.Alignment
24 | import androidx.compose.ui.Modifier
25 | import androidx.compose.ui.graphics.Color
26 | import androidx.compose.ui.graphics.graphicsLayer
27 | import androidx.compose.ui.res.painterResource
28 | import androidx.compose.ui.text.font.FontWeight
29 | import androidx.compose.ui.text.style.TextAlign
30 | import androidx.compose.ui.unit.dp
31 | import androidx.compose.ui.unit.sp
32 | import com.gmail.orlandroyd.composecalendar.util.getFullMonthTextDate
33 | import com.gmail.orlandroyd.composecalendar.util.sameDay
34 | import java.util.Date
35 |
36 | @Composable
37 | fun DateRangePickerDlg(
38 | visible: Boolean,
39 | title: String = "",
40 | txtSelectHour: String = "Seleccionar hora",
41 | acceptText: String = "Aceptar",
42 | primaryColor: Color = Color(0xFF4395D6),
43 | surfaceColor: Color = Color(0xFFFFFFFF),
44 | dialogColor: Color = Color(0xB1000000),
45 | secondaryColor: Color = Color(0xFF9F9E9E),
46 | dividerColor: Color = Color(0xFFE2E2E2),
47 | secondaryTextColor: Color = Color(0xFF9F9E9E),
48 | accentColor: Color = Color(0xFF4395D6),
49 | acceptTextColor: Color = Color(0xFFFFFFFF),
50 | primaryTextColor: Color = Color(0xFF1A1A1A),
51 | currentSelection: Pair? = null,
52 | onDatesSelected: (Pair) -> Unit = {},
53 | onClearFilter: () -> Unit = {},
54 | onClose: () -> Unit = {},
55 | ) {
56 | if (visible) {
57 | BackHandler {
58 | onClose()
59 | }
60 | }
61 | BaseCenteredDialog(
62 | visible = visible,
63 | dialogColor = dialogColor,
64 | onOutsideTouch = {
65 | onClose()
66 | }
67 | ) {
68 | var selectedDates by remember {
69 | mutableStateOf(currentSelection)
70 | }
71 | var selectedDate by remember {
72 | mutableStateOf(null)
73 | }
74 | Column(
75 | modifier = Modifier
76 | .graphicsLayer {
77 | shape = RoundedCornerShape(15.dp)
78 | clip = true
79 | }
80 | .background(surfaceColor)
81 | ) {
82 | Row(
83 | verticalAlignment = Alignment.CenterVertically,
84 | horizontalArrangement = Arrangement.Center,
85 | modifier = Modifier
86 | .padding(10.dp)
87 | .height(48.dp),
88 |
89 | ) {
90 | Column(
91 | Modifier.weight(1f)
92 | ) {
93 | Icon(
94 | painter = painterResource(id = R.drawable.ic_close),
95 | contentDescription = null,
96 | modifier = Modifier
97 | .size(24.dp)
98 | .clickableNoRipple { onClose() }
99 | .padding(start = 4.dp),
100 | tint = primaryColor
101 | )
102 | }
103 |
104 | Column(
105 | modifier = Modifier.weight(2f),
106 | verticalArrangement = Arrangement.Center,
107 | horizontalAlignment = Alignment.CenterHorizontally
108 | ) {
109 | Text(
110 | text = title,
111 | fontSize = 16.sp,
112 | color = primaryTextColor,
113 | fontWeight = FontWeight.Bold,
114 | textAlign = TextAlign.Center,
115 | )
116 | }
117 |
118 | // val clearTextColor = if (currentSelection != null) {
119 | // primaryColor
120 | // } else {
121 | // secondaryColor
122 | // }
123 | Column(
124 | Modifier.weight(1f)
125 | ) {
126 | // ClickableText(
127 | // text = AnnotatedString(txtSelectHour),
128 | // onClick = { onClearFilter() },
129 | // modifier = Modifier
130 | // .align(Alignment.End),
131 | // style = TextStyle(
132 | // color = clearTextColor,
133 | // )
134 | // )
135 | }
136 | }
137 |
138 | Divider(thickness = 1.dp, color = dividerColor)
139 |
140 | DatePicker(
141 | modifier = Modifier
142 | .fillMaxWidth()
143 | .wrapContentHeight()
144 | .padding(10.dp),
145 | selectedDate = selectedDate,
146 | selectedDates = selectedDates,
147 | onDateClicked = { newDate ->
148 | if (selectedDates == null && selectedDate == null) {
149 | selectedDate = newDate
150 | } else if (selectedDate != null) {
151 | val current = selectedDate ?: Date()
152 | if (current.sameDay(newDate)) {
153 | selectedDate = null
154 | } else {
155 | selectedDates = if (current.time < newDate.time) {
156 | Pair(selectedDate ?: Date(), newDate)
157 | } else {
158 | Pair(newDate, selectedDate ?: Date())
159 | }
160 | selectedDate = null
161 | }
162 | } else {
163 | val startDate = selectedDates?.first ?: Date()
164 | val endDate = selectedDates?.second ?: Date()
165 | when {
166 | newDate.sameDay(startDate) -> {
167 | selectedDate = endDate
168 | selectedDates = null
169 | }
170 |
171 | newDate.sameDay(endDate) -> {
172 | selectedDate = startDate
173 | selectedDates = null
174 | }
175 |
176 | newDate.time < startDate.time -> {
177 | selectedDates = Pair(newDate, endDate)
178 | }
179 |
180 | else -> {
181 | selectedDates = Pair(startDate, newDate)
182 | }
183 | }
184 | }
185 | },
186 | isRange = true,
187 | )
188 |
189 | Divider(thickness = 1.dp, color = dividerColor)
190 |
191 | var dateText = ""
192 | (selectedDates?.first ?: selectedDate)?.let {
193 | dateText = it.getFullMonthTextDate()
194 | }
195 | selectedDates?.second?.let {
196 | dateText += " - "
197 | dateText += it.getFullMonthTextDate()
198 | }
199 |
200 | Text(
201 | text = dateText,
202 | fontSize = 16.sp,
203 | color = secondaryTextColor,
204 | modifier = Modifier
205 | .fillMaxWidth()
206 | .padding(15.dp),
207 | textAlign = TextAlign.Center,
208 | )
209 |
210 | Text(
211 | text = acceptText,
212 | color = acceptTextColor,
213 | fontSize = 16.sp,
214 | fontWeight = FontWeight.Bold,
215 | textAlign = TextAlign.Center,
216 | modifier = Modifier
217 | .fillMaxWidth()
218 | .padding(10.dp, 0.dp, 10.dp, 10.dp)
219 | .graphicsLayer {
220 | shape = RoundedCornerShape(4.dp)
221 | clip = true
222 | }
223 | .background(accentColor)
224 | .clickable {
225 | selectedDates?.let { dates ->
226 | onDatesSelected(dates)
227 | }
228 | }
229 | .padding(10.dp)
230 | )
231 | }
232 | }
233 | }
234 |
--------------------------------------------------------------------------------
/ComposeCalendar/src/main/java/com/gmail/orlandroyd/composecalendar/DatePickerDlg.kt:
--------------------------------------------------------------------------------
1 | package com.gmail.orlandroyd.composecalendar
2 |
3 | import android.widget.Toast
4 | import androidx.activity.compose.BackHandler
5 | import androidx.compose.foundation.background
6 | import androidx.compose.foundation.border
7 | import androidx.compose.foundation.clickable
8 | import androidx.compose.foundation.layout.Column
9 | import androidx.compose.foundation.layout.Row
10 | import androidx.compose.foundation.layout.Spacer
11 | import androidx.compose.foundation.layout.fillMaxWidth
12 | import androidx.compose.foundation.layout.padding
13 | import androidx.compose.foundation.layout.size
14 | import androidx.compose.foundation.layout.width
15 | import androidx.compose.foundation.layout.wrapContentHeight
16 | import androidx.compose.foundation.shape.RoundedCornerShape
17 | import androidx.compose.material3.Divider
18 | import androidx.compose.material3.Icon
19 | import androidx.compose.material3.Text
20 | import androidx.compose.runtime.Composable
21 | import androidx.compose.runtime.getValue
22 | import androidx.compose.runtime.mutableStateOf
23 | import androidx.compose.runtime.remember
24 | import androidx.compose.runtime.setValue
25 | import androidx.compose.ui.Alignment
26 | import androidx.compose.ui.Modifier
27 | import androidx.compose.ui.graphics.Color
28 | import androidx.compose.ui.graphics.graphicsLayer
29 | import androidx.compose.ui.platform.LocalContext
30 | import androidx.compose.ui.res.painterResource
31 | import androidx.compose.ui.text.font.FontWeight
32 | import androidx.compose.ui.text.style.TextAlign
33 | import androidx.compose.ui.unit.dp
34 | import androidx.compose.ui.unit.sp
35 | import com.gmail.orlandroyd.composecalendar.util.getCalendar
36 | import com.gmail.orlandroyd.composecalendar.util.getParsedDate
37 | import com.gmail.orlandroyd.composecalendar.util.showTimePickerDialog
38 | import java.util.Calendar
39 | import java.util.Date
40 |
41 | @Composable
42 | fun DatePickerDlg(
43 | visible: Boolean,
44 | title: String = "",
45 | showSetHours: Boolean = false,
46 | titleColor: Color = Color(0xFF4395D6),
47 | dialogColor: Color = Color(0xB1000000),
48 | primaryColor: Color = Color(0xFF4395D6),
49 | primaryTextColor: Color = Color(0xFF1A1A1A),
50 | secondaryTextColor: Color = Color(0xFF9F9E9E),
51 | surfaceColor: Color = Color(0xFFFFFFFF),
52 | dividerColor: Color = Color(0xFFE2E2E2),
53 | iconsColor: Color = Color(0xFF9F9E9E),
54 | acceptTextColor: Color = Color(0xFFFFFFFF),
55 | accentColor: Color = Color(0xFF4395D6),
56 | acceptText: String = "Aceptar",
57 | messageFutureHours: String = "La hora debe estar en el futuro",
58 | messageSelectedHours: String = "Seleccionar hora",
59 | currentSelection: Date? = null,
60 | onDateSelected: (Date) -> Unit = {},
61 | onClose: () -> Unit = {},
62 | ) {
63 | if (visible) {
64 | BackHandler {
65 | onClose()
66 | }
67 | }
68 | BaseCenteredDialog(
69 | visible = visible,
70 | dialogColor = dialogColor,
71 | onOutsideTouch = {
72 | onClose()
73 | },
74 | ) {
75 | var selectedDate by remember {
76 | mutableStateOf(currentSelection)
77 | }
78 | var selectedTime by remember {
79 | mutableStateOf(currentSelection)
80 | }
81 | Column(
82 | modifier = Modifier
83 | .graphicsLayer {
84 | shape = RoundedCornerShape(15.dp)
85 | clip = true
86 | }
87 | .background(surfaceColor)
88 | ) {
89 | Row(
90 | verticalAlignment = Alignment.CenterVertically,
91 | modifier = Modifier.padding(10.dp)
92 | ) {
93 | Icon(
94 | painter = painterResource(id = R.drawable.ic_close),
95 | contentDescription = null,
96 | modifier = Modifier
97 | .size(24.dp)
98 | .clickableNoRipple { onClose() },
99 | tint = primaryColor
100 | )
101 |
102 | Text(
103 | text = title,
104 | fontWeight = FontWeight.Bold,
105 | textAlign = TextAlign.Center,
106 | fontSize = 17.sp,
107 | modifier = Modifier.weight(1f),
108 | color = titleColor
109 | )
110 |
111 | Spacer(modifier = Modifier.width(24.dp))
112 | }
113 |
114 | Divider(thickness = 1.dp, color = dividerColor)
115 |
116 | DatePicker(
117 | modifier = Modifier
118 | .fillMaxWidth()
119 | .wrapContentHeight()
120 | .padding(10.dp),
121 | selectedDate = selectedDate,
122 | onDateClicked = {
123 | selectedDate = it
124 | selectedTime = null
125 | }
126 | )
127 |
128 | Divider(thickness = 1.dp, color = dividerColor)
129 |
130 | if (showSetHours) {
131 | val context = LocalContext.current
132 | Row(
133 | verticalAlignment = Alignment.CenterVertically,
134 | modifier = Modifier
135 | .padding(10.dp)
136 | .graphicsLayer {
137 | shape = RoundedCornerShape(4.dp)
138 | clip = true
139 | }
140 | .border(
141 | width = 1.dp,
142 | color = dividerColor,
143 | shape = RoundedCornerShape(4.dp)
144 | )
145 | .clickableNoRipple {
146 | selectedDate?.let { date ->
147 | val dateCalendar = date.getCalendar()
148 | val currentTime =
149 | selectedTime?.getCalendar() ?: Calendar.getInstance()
150 | currentTime[Calendar.YEAR] = dateCalendar[Calendar.YEAR]
151 | currentTime[Calendar.DAY_OF_YEAR] =
152 | dateCalendar[Calendar.DAY_OF_YEAR]
153 | context.showTimePickerDialog(
154 | currentDate = currentTime.time,
155 | timeSelected = { timeCalendar ->
156 | selectedDate?.let {
157 | val today = Calendar.getInstance()
158 | val selected = Calendar.getInstance()
159 | selected.time = today.time
160 |
161 | selected[Calendar.YEAR] =
162 | timeCalendar[Calendar.YEAR]
163 | selected[Calendar.DAY_OF_YEAR] =
164 | timeCalendar[Calendar.DAY_OF_YEAR]
165 | selected[Calendar.HOUR_OF_DAY] =
166 | timeCalendar[Calendar.HOUR_OF_DAY]
167 | selected[Calendar.MINUTE] =
168 | timeCalendar[Calendar.MINUTE]
169 | if (selected.timeInMillis > today.timeInMillis) {
170 | selectedTime = timeCalendar.time
171 | } else {
172 | Toast
173 | .makeText(
174 | context,
175 | messageFutureHours,
176 | Toast.LENGTH_SHORT
177 | )
178 | .show()
179 | }
180 | }
181 | },
182 | textColor = R.color.grey
183 | )
184 | }
185 | }
186 | .padding(10.dp)
187 | ) {
188 | Icon(
189 | painter = painterResource(id = R.drawable.ic_clock),
190 | contentDescription = null,
191 | Modifier.size(24.dp),
192 | tint = iconsColor
193 | )
194 | val textColor =
195 | if (selectedTime == null) secondaryTextColor else primaryTextColor
196 | val text =
197 | selectedTime?.getParsedDate("HH:mm") ?: messageSelectedHours
198 | Text(
199 | text = text,
200 | fontSize = 16.sp,
201 | fontWeight = FontWeight.Bold,
202 | color = textColor,
203 | modifier = Modifier
204 | .weight(1f)
205 | .padding(start = 10.dp),
206 | )
207 | Icon(
208 | painter = painterResource(id = R.drawable.ic_dropdown),
209 | contentDescription = null,
210 | modifier = Modifier.size(24.dp),
211 | tint = primaryColor
212 | )
213 | }
214 | }
215 |
216 | Text(
217 | text = acceptText,
218 | color = acceptTextColor,
219 | fontWeight = FontWeight.Bold,
220 | textAlign = TextAlign.Center,
221 | fontSize = 16.sp,
222 | modifier = Modifier
223 | .fillMaxWidth()
224 | .padding(10.dp, 0.dp, 10.dp, 10.dp)
225 | .graphicsLayer {
226 | shape = RoundedCornerShape(4.dp)
227 | clip = true
228 | }
229 | .background(accentColor)
230 | .clickable {
231 | selectedDate?.let { time ->
232 | onDateSelected(time)
233 | }
234 | }
235 | .padding(10.dp)
236 | )
237 | }
238 | }
239 | }
240 |
--------------------------------------------------------------------------------
/ComposeCalendar/src/main/java/com/gmail/orlandroyd/composecalendar/Composables.kt:
--------------------------------------------------------------------------------
1 | package com.gmail.orlandroyd.composecalendar
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.animation.ExperimentalAnimationApi
5 | import androidx.compose.animation.fadeIn
6 | import androidx.compose.animation.fadeOut
7 | import androidx.compose.animation.slideInVertically
8 | import androidx.compose.animation.slideOutVertically
9 | import androidx.compose.foundation.background
10 | import androidx.compose.foundation.layout.*
11 | import androidx.compose.foundation.shape.CircleShape
12 | import androidx.compose.foundation.shape.RoundedCornerShape
13 | import androidx.compose.material3.Divider
14 | import androidx.compose.material3.Icon
15 | import androidx.compose.material3.Text
16 | import androidx.compose.runtime.*
17 | import androidx.compose.ui.Alignment.Companion.Center
18 | import androidx.compose.ui.Alignment.Companion.CenterVertically
19 | import androidx.compose.ui.Modifier
20 | import androidx.compose.ui.graphics.Color
21 | import androidx.compose.ui.graphics.graphicsLayer
22 | import androidx.compose.ui.res.painterResource
23 | import androidx.compose.ui.text.font.FontWeight
24 | import androidx.compose.ui.text.style.TextAlign
25 | import androidx.compose.ui.unit.TextUnit
26 | import androidx.compose.ui.unit.dp
27 | import androidx.compose.ui.unit.sp
28 | import com.gmail.orlandroyd.composecalendar.util.between
29 | import com.gmail.orlandroyd.composecalendar.util.getCalendar
30 | import com.gmail.orlandroyd.composecalendar.util.getCalendarMonthDays
31 | import com.gmail.orlandroyd.composecalendar.util.getMonthText
32 | import com.gmail.orlandroyd.composecalendar.util.sameDay
33 | import java.util.*
34 |
35 |
36 | @Composable
37 | fun DatePicker(
38 | modifier: Modifier = Modifier,
39 | selectedDate: Date? = null,
40 | selectedDates: Pair? = null,
41 | monthTextSize: TextUnit = 14.sp,
42 | cellTextSize: TextUnit = 14.sp,
43 | onDateClicked: (Date) -> Unit = {},
44 | isRange: Boolean = false
45 | ) {
46 | val nonNullMonth = selectedDate ?: Date()
47 | var month by remember {
48 | mutableStateOf(nonNullMonth)
49 | }
50 | val weeks = month.getCalendarMonthDays().chunked(7)
51 | Column(
52 | modifier = modifier,
53 | ) {
54 | DatePickerHeader(
55 | selectedMonth = month,
56 | monthTextSize = monthTextSize,
57 | weekDaysTextSize = cellTextSize,
58 | onMonthChange = {
59 | month = it
60 | },
61 | )
62 | weeks.forEach { days ->
63 | if (!isRange || selectedDates == null) {
64 | DatePickerWeek(
65 | days = days,
66 | selectedDate = selectedDate,
67 | textSize = cellTextSize,
68 | onSelectionChange = onDateClicked,
69 | currentMonth = month.getCalendar().get(Calendar.MONTH)
70 | )
71 | } else {
72 | DateRangePickerWeek(
73 | days = days,
74 | selectedDates = selectedDates,
75 | textSize = cellTextSize,
76 | onSelectionChange = onDateClicked,
77 | currentMonth = month.getCalendar().get(Calendar.MONTH)
78 | )
79 | }
80 | }
81 | }
82 | }
83 |
84 | @Composable
85 | private fun DatePickerHeader(
86 | selectedMonth: Date,
87 | monthTextSize: TextUnit = 14.sp,
88 | weekDaysTextSize: TextUnit = 14.sp,
89 | onMonthChange: (Date) -> Unit,
90 | primaryTextColor: Color = Color(0xFF1A1A1A),
91 | secondaryTextColor: Color = Color(0xFF9F9E9E),
92 | dividerColor: Color = Color(0xFFE2E2E2),
93 | ) {
94 | Row(
95 | Modifier
96 | .fillMaxWidth()
97 | ) {
98 | val calendar = Calendar.getInstance()
99 | calendar.time = selectedMonth
100 | Icon(
101 | painter = painterResource(id = R.drawable.ic_calendar_previous),
102 | contentDescription = null,
103 | tint = Color.Unspecified,
104 | modifier = Modifier
105 | .align(CenterVertically)
106 | .padding(12.dp, 5.dp, 12.dp, 5.dp)
107 | .size(24.dp)
108 | .clickableNoRipple {
109 | calendar.add(Calendar.MONTH, -1)
110 | onMonthChange(calendar.time)
111 | }
112 | )
113 | Spacer(modifier = Modifier.weight(1f))
114 | Icon(
115 | painter = painterResource(id = R.drawable.ic_calendar),
116 | contentDescription = null,
117 | tint = Color.Unspecified,
118 | modifier = Modifier
119 | .align(CenterVertically)
120 | .padding(15.dp, 0.dp, 0.dp, 0.dp)
121 | )
122 | Text(
123 | text = selectedMonth.getMonthText(),
124 | fontSize = monthTextSize,
125 | fontWeight = FontWeight.Bold,
126 | modifier = Modifier
127 | .align(CenterVertically)
128 | .padding(10.dp, 0.dp, 15.dp, 0.dp),
129 | color = primaryTextColor
130 | )
131 | Spacer(modifier = Modifier.weight(1f))
132 | Icon(
133 | painter = painterResource(id = R.drawable.ic_calendar_next),
134 | contentDescription = null,
135 | tint = Color.Unspecified,
136 | modifier = Modifier
137 | .align(CenterVertically)
138 | .padding(12.dp, 5.dp, 12.dp, 5.dp)
139 | .size(24.dp)
140 | .clickableNoRipple {
141 | val myCalendar = Calendar.getInstance()
142 | myCalendar.time = selectedMonth
143 | myCalendar.add(Calendar.MONTH, 1)
144 | onMonthChange(myCalendar.time)
145 | }
146 | )
147 | }
148 | Spacer(modifier = Modifier.height(5.dp))
149 | Row(Modifier.fillMaxWidth()) {
150 | Text(
151 | text = "L",
152 | fontSize = weekDaysTextSize,
153 | fontWeight = FontWeight.Bold,
154 | textAlign = TextAlign.Center,
155 | color = secondaryTextColor,
156 | modifier = Modifier
157 | .weight(1f)
158 | .aspectRatio(1f)
159 | )
160 | Text(
161 | text = "M",
162 | fontSize = weekDaysTextSize,
163 | fontWeight = FontWeight.Bold,
164 | textAlign = TextAlign.Center,
165 | color = secondaryTextColor,
166 | modifier = Modifier
167 | .weight(1f)
168 | .aspectRatio(1f)
169 | )
170 | Text(
171 | text = "X",
172 | fontSize = weekDaysTextSize,
173 | fontWeight = FontWeight.Bold,
174 | textAlign = TextAlign.Center,
175 | color = secondaryTextColor,
176 | modifier = Modifier
177 | .weight(1f)
178 | .aspectRatio(1f),
179 | )
180 | Text(
181 | text = "J",
182 | fontSize = weekDaysTextSize,
183 | fontWeight = FontWeight.Bold,
184 | textAlign = TextAlign.Center,
185 | color = secondaryTextColor,
186 | modifier = Modifier
187 | .weight(1f)
188 | .aspectRatio(1f)
189 | )
190 | Text(
191 | text = "V",
192 | fontSize = weekDaysTextSize,
193 | fontWeight = FontWeight.Bold,
194 | textAlign = TextAlign.Center,
195 | color = secondaryTextColor,
196 | modifier = Modifier
197 | .weight(1f)
198 | .aspectRatio(1f)
199 | )
200 | Text(
201 | text = "S",
202 | fontSize = weekDaysTextSize,
203 | fontWeight = FontWeight.Bold,
204 | textAlign = TextAlign.Center,
205 | color = secondaryTextColor,
206 | modifier = Modifier
207 | .weight(1f)
208 | .aspectRatio(1f)
209 | )
210 | Text(
211 | text = "D",
212 | fontSize = weekDaysTextSize,
213 | fontWeight = FontWeight.Bold,
214 | textAlign = TextAlign.Center,
215 | color = secondaryTextColor,
216 | modifier = Modifier
217 | .weight(1f)
218 | .aspectRatio(1f),
219 | )
220 | }
221 | Divider(thickness = 1.dp, color = dividerColor)
222 | }
223 |
224 | @Composable
225 | private fun DatePickerWeek(
226 | days: List,
227 | selectedDate: Date?,
228 | currentMonth: Int,
229 | primaryTextColor: Color = Color(0xFF1A1A1A),
230 | backgroundColor: Color = Color(0xFFF7F9FA),
231 | primaryColor: Color = Color(0xFF4395D6),
232 | textSize: TextUnit = 14.sp,
233 | onSelectionChange: (Date) -> Unit = {},
234 | ) {
235 | val calendarSelected = selectedDate?.getCalendar()
236 | val daysCalendar = days.map {
237 | val calendar = Calendar.getInstance()
238 | calendar.time = it
239 | calendar
240 | }
241 | Row(
242 | Modifier.fillMaxWidth(),
243 | horizontalArrangement = Arrangement.Center,
244 | verticalAlignment = CenterVertically
245 | ) {
246 | daysCalendar.forEach { day ->
247 | val text = if (day[Calendar.MONTH] == currentMonth) {
248 | day[Calendar.DAY_OF_MONTH].toString()
249 | } else {
250 | ""
251 | }
252 | val textColor: Color
253 | val modifier: Modifier
254 | when {
255 | calendarSelected == null || !day.sameDay(calendarSelected) -> {
256 | textColor = primaryTextColor
257 | modifier = Modifier
258 | .weight(1f)
259 | .aspectRatio(1f)
260 | }
261 |
262 | else -> {
263 | textColor = backgroundColor
264 | modifier = Modifier
265 | .weight(1f)
266 | .aspectRatio(1f)
267 | .graphicsLayer {
268 | shape = CircleShape
269 | clip = true
270 | }
271 | .background(primaryColor)
272 | }
273 | }
274 | Box(modifier = modifier.clickableNoRipple {
275 | if (text.isNotEmpty()) {
276 | onSelectionChange(day.time)
277 | }
278 | }, contentAlignment = Center) {
279 | Text(
280 | text = text,
281 | fontSize = textSize,
282 | fontWeight = FontWeight.Bold,
283 | textAlign = TextAlign.Center,
284 | color = textColor,
285 | )
286 | }
287 | }
288 | }
289 | }
290 |
291 | @Composable
292 | fun DateRangePickerWeek(
293 | days: List,
294 | currentMonth: Int,
295 | selectedDates: Pair,
296 | textSize: TextUnit = 14.sp,
297 | primaryColor: Color = Color(0xFF4395D6),
298 | backgroundColor: Color = Color(0xFFF7F9FA),
299 | blueTransparentColor: Color = Color(0xCCE9F5FC),
300 | primaryTextColor: Color = Color(0xFF1A1A1A),
301 | onSelectionChange: (Date) -> Unit = {},
302 | ) {
303 | val startSelected = selectedDates.first.getCalendar()
304 | val endSelected = selectedDates.second.getCalendar()
305 | val daysCalendar = days.map {
306 | val calendar = Calendar.getInstance()
307 | calendar.time = it
308 | calendar
309 | }
310 | Row(Modifier.fillMaxWidth()) {
311 | daysCalendar.forEach { day ->
312 | val text = if (day[Calendar.MONTH] == currentMonth) {
313 | day[Calendar.DAY_OF_MONTH].toString()
314 | } else {
315 | ""
316 | }
317 | val textColor: Color
318 | val modifier: Modifier
319 | when {
320 | day.sameDay(startSelected) -> {
321 | textColor = backgroundColor
322 | modifier = Modifier
323 | .weight(1f)
324 | .aspectRatio(1f)
325 | .graphicsLayer {
326 | shape = RoundedCornerShape(
327 | topStartPercent = 50,
328 | topEndPercent = 0,
329 | bottomEndPercent = 0,
330 | bottomStartPercent = 50
331 | )
332 | clip = true
333 | }
334 | .background(blueTransparentColor)
335 | .graphicsLayer {
336 | shape = CircleShape
337 | clip = true
338 | }
339 | .background(primaryColor)
340 | }
341 |
342 | day.sameDay(endSelected) -> {
343 | textColor = backgroundColor
344 | modifier = Modifier
345 | .weight(1f)
346 | .aspectRatio(1f)
347 | .graphicsLayer {
348 | shape = RoundedCornerShape(
349 | topStartPercent = 0,
350 | topEndPercent = 50,
351 | bottomEndPercent = 50,
352 | bottomStartPercent = 0
353 | )
354 | clip = true
355 | }
356 | .background(blueTransparentColor)
357 | .graphicsLayer {
358 | shape = CircleShape
359 | clip = true
360 | }
361 | .background(primaryColor)
362 | }
363 |
364 | day.between(selectedDates) -> {
365 | textColor = primaryTextColor
366 | modifier = Modifier
367 | .weight(1f)
368 | .aspectRatio(1f)
369 | .background(blueTransparentColor)
370 | }
371 |
372 | else -> {
373 | textColor = primaryTextColor
374 | modifier = Modifier
375 | .weight(1f)
376 | .aspectRatio(1f)
377 | }
378 | }
379 | Box(modifier = modifier.clickableNoRipple {
380 | if (text.isNotEmpty()) {
381 | onSelectionChange(day.time)
382 | }
383 | }, contentAlignment = Center) {
384 | Text(
385 | text = text,
386 | fontSize = textSize,
387 | fontWeight = FontWeight.Bold,
388 | textAlign = TextAlign.Center,
389 | color = textColor,
390 | )
391 | }
392 |
393 | }
394 | }
395 | }
396 |
397 | @OptIn(ExperimentalAnimationApi::class)
398 | @Composable
399 | fun BaseCenteredDialog(
400 | visible: Boolean,
401 | dialogColor: Color,
402 | onOutsideTouch: () -> Unit = {},
403 | content: @Composable () -> Unit,
404 | ) {
405 | AnimatedVisibility(
406 | visible = visible,
407 | modifier = Modifier
408 | .fillMaxSize(),
409 | enter = fadeIn(),
410 | exit = fadeOut()
411 | ) {
412 | Box(
413 | contentAlignment = Center,
414 | modifier = Modifier
415 | .fillMaxSize()
416 | .clickableNoRipple { onOutsideTouch() }
417 | .background(dialogColor)
418 | ) {
419 | Box(
420 | modifier = Modifier
421 | .wrapContentHeight()
422 | .statusBarsPadding()
423 | .padding(horizontal = 15.dp, vertical = 15.dp)
424 | .animateEnterExit(
425 | enter = slideInVertically(),
426 | exit = slideOutVertically()
427 | )
428 | .clickableNoRipple { }
429 | ) {
430 | content()
431 | }
432 | }
433 | }
434 | }
--------------------------------------------------------------------------------
/ComposeCalendar/src/main/java/com/gmail/orlandroyd/composecalendar/MonthPicker.kt:
--------------------------------------------------------------------------------
1 | package com.gmail.orlandroyd.composecalendar
2 |
3 | import android.util.DisplayMetrics
4 | import androidx.compose.animation.Crossfade
5 | import androidx.compose.foundation.background
6 | import androidx.compose.foundation.clickable
7 | import androidx.compose.foundation.interaction.MutableInteractionSource
8 | import androidx.compose.foundation.layout.Arrangement
9 | import androidx.compose.foundation.layout.Box
10 | import androidx.compose.foundation.layout.Column
11 | import androidx.compose.foundation.layout.Row
12 | import androidx.compose.foundation.layout.aspectRatio
13 | import androidx.compose.foundation.layout.fillMaxWidth
14 | import androidx.compose.foundation.layout.padding
15 | import androidx.compose.foundation.layout.requiredHeight
16 | import androidx.compose.foundation.lazy.LazyColumn
17 | import androidx.compose.foundation.lazy.items
18 | import androidx.compose.foundation.lazy.rememberLazyListState
19 | import androidx.compose.foundation.shape.RoundedCornerShape
20 | import androidx.compose.material3.Card
21 | import androidx.compose.material3.Text
22 | import androidx.compose.runtime.Composable
23 | import androidx.compose.runtime.LaunchedEffect
24 | import androidx.compose.runtime.getValue
25 | import androidx.compose.runtime.mutableStateOf
26 | import androidx.compose.runtime.remember
27 | import androidx.compose.runtime.rememberCoroutineScope
28 | import androidx.compose.runtime.setValue
29 | import androidx.compose.ui.Alignment
30 | import androidx.compose.ui.Modifier
31 | import androidx.compose.ui.composed
32 | import androidx.compose.ui.graphics.Color
33 | import androidx.compose.ui.layout.onGloballyPositioned
34 | import androidx.compose.ui.platform.LocalContext
35 | import androidx.compose.ui.unit.dp
36 | import androidx.compose.ui.unit.sp
37 | import kotlinx.coroutines.launch
38 | import java.text.DateFormatSymbols
39 | import java.util.Calendar
40 | import java.util.Date
41 | import java.util.Locale
42 |
43 | @Composable
44 | fun ComposeCalendar(
45 | minDate: Date? = null,
46 | maxDate: Date? = null,
47 | currentDate: Date? = null,
48 | locale: Locale = Locale.getDefault(),
49 | title: String = "",
50 | onDateSelected: (Date) -> Unit = {},
51 | onClose: () -> Unit = {},
52 | showOnlyMonth: Boolean = false,
53 | showOnlyYear: Boolean = false,
54 | themeColor: Color = Color(0xFF4395D6),
55 | negativeButtonTitle: String = "CANCEL",
56 | positiveButtonTitle: String = "OK",
57 | monthSelectedColor: Color = Color(0x80FFFFFF),
58 | ) {
59 | if (showOnlyMonth && showOnlyYear) {
60 | throw IllegalStateException("'showOnlyMonth' and 'showOnlyYear' states cannot be true at the same time")
61 | } else {
62 |
63 | var minYear = 1970
64 | var minMonth = 0
65 | var maxYear = 2100
66 | var maxMonth = 11
67 | minDate?.let {
68 | val calendarMin = Calendar.getInstance()
69 | calendarMin.time = minDate
70 | minMonth = calendarMin.get(Calendar.MONTH)
71 | minYear = calendarMin.get(Calendar.YEAR)
72 | }
73 | maxDate?.let {
74 | val calendarMax = Calendar.getInstance()
75 | calendarMax.time = maxDate
76 | maxMonth = calendarMax.get(Calendar.MONTH)
77 | maxYear = calendarMax.get(Calendar.YEAR)
78 | }
79 |
80 | val (height, setHeight) = remember {
81 | mutableStateOf(0)
82 | }
83 |
84 | val calendar = Calendar.getInstance(locale)
85 | currentDate?.let {
86 | calendar.time = currentDate
87 | }
88 | val currentMonth = calendar.get(Calendar.MONTH)
89 | var currentYear = calendar.get(Calendar.YEAR)
90 |
91 | if (minYear > currentYear) {
92 | currentYear = minYear
93 | }
94 | if (maxYear < currentYear) {
95 | currentYear = maxYear
96 | }
97 |
98 | val months = (DateFormatSymbols(locale).shortMonths).toList()
99 | val monthList = months.mapIndexed { index, name ->
100 | MonthData(name = name, index = index)
101 | }
102 | val (selectedMonth, setMonth) = remember {
103 | mutableStateOf(
104 | MonthData(
105 | name = DateFormatSymbols(locale).shortMonths[currentMonth],
106 | index = currentMonth
107 | )
108 | )
109 | }
110 | val (selectedYear, setYear) = remember {
111 | mutableStateOf(currentYear)
112 | }
113 | val (showMonths, setShowMonths) = remember {
114 | mutableStateOf(!showOnlyYear)
115 | }
116 |
117 | val calendarDate = Calendar.getInstance()
118 | var selectedDate by remember {
119 | mutableStateOf(calendarDate.time)
120 | }
121 |
122 | LaunchedEffect(key1 = selectedYear, key2 = selectedMonth) {
123 | calendarDate.set(Calendar.YEAR, selectedYear)
124 | calendarDate.set(Calendar.MONTH, selectedMonth.index)
125 | selectedDate = calendarDate.time
126 | }
127 | LaunchedEffect(key1 = selectedYear) {
128 | if (selectedYear == minYear) {
129 | if (selectedMonth.index < minMonth) {
130 | setMonth(monthList[minMonth])
131 | }
132 | }
133 | if (selectedYear == maxYear) {
134 | if (selectedMonth.index > maxMonth) {
135 | setMonth(monthList[maxMonth])
136 | }
137 | }
138 | }
139 |
140 | Card(
141 | modifier = Modifier
142 | .fillMaxWidth(0.9f)
143 | ) {
144 | Column(modifier = Modifier.fillMaxWidth()) {
145 | CalendarHeader(
146 | selectedMonth = selectedMonth.name,
147 | selectedYear = selectedYear,
148 | showMonths = showMonths,
149 | setShowMonths = setShowMonths,
150 | title = title,
151 | showOnlyMonth = showOnlyMonth,
152 | showOnlyYear = showOnlyYear,
153 | themeColor = themeColor,
154 | monthSelectedColor = monthSelectedColor
155 | )
156 | Crossfade(targetState = showMonths, label = "") {
157 | when (it) {
158 | true -> CalendarMonthView(
159 | selectedMonth = selectedMonth,
160 | setMonth = setMonth,
161 | minMonth = minMonth,
162 | maxMonth = maxMonth,
163 | setShowMonths = setShowMonths,
164 | minYear = minYear,
165 | maxYear = maxYear,
166 | selectedYear = selectedYear,
167 | monthList = monthList,
168 | setHeight = setHeight,
169 | showOnlyMonth = showOnlyMonth,
170 | themeColor = themeColor
171 | )
172 |
173 | false -> CalendarYearView(
174 | selectedYear = selectedYear,
175 | setYear = setYear,
176 | minYear = minYear,
177 | maxYear = maxYear,
178 | height = height,
179 | themeColor = themeColor
180 | )
181 | }
182 | }
183 | CalendarBottom(
184 | onPositiveClick = { onDateSelected(selectedDate) },
185 | onCancelClick = onClose,
186 | themeColor = themeColor,
187 | negativeButtonTitle = negativeButtonTitle,
188 | positiveButtonTitle = positiveButtonTitle
189 | )
190 | }
191 | }
192 | }
193 | }
194 |
195 |
196 | data class MonthData(
197 | val name: String,
198 | val index: Int
199 | )
200 |
201 | interface SelectDateListener {
202 | fun onDateSelected(date: Date)
203 | fun onCanceled()
204 | }
205 |
206 | @Composable
207 | fun CalendarYearView(
208 | selectedYear: Int,
209 | setYear: (Int) -> Unit,
210 | minYear: Int,
211 | maxYear: Int,
212 | height: Int,
213 | themeColor: Color
214 | ) {
215 | val years = IntRange(minYear, maxYear).toList()
216 | val listState = rememberLazyListState()
217 | val scope = rememberCoroutineScope()
218 | val selectedIndex = years.indexOf(selectedYear)
219 | val metrics = LocalContext.current.resources.displayMetrics
220 |
221 | val mHeight =
222 | (if (height == 0) 5 * (metrics.heightPixels) / 10 else height) / (metrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)
223 | LazyColumn(
224 | state = listState, modifier = Modifier
225 | .fillMaxWidth()
226 | .requiredHeight((mHeight + 40).dp) //40dp for padding in MonthView
227 | , horizontalAlignment = Alignment.CenterHorizontally
228 | ) {
229 | items(years) { year ->
230 | Text(text = year.toString(),
231 | fontSize = if (year == selectedYear) 35.sp else 30.sp,
232 | color = if (year == selectedYear) themeColor else Color.Black,
233 | modifier = Modifier
234 | .padding(vertical = 10.dp)
235 | .clickable {
236 | setYear(year)
237 | })
238 | }
239 | scope.launch {
240 | listState.animateScrollToItem(selectedIndex)
241 | }
242 | }
243 | }
244 |
245 | @Composable
246 | fun CalendarMonthView(
247 | monthList: List,
248 | selectedMonth: MonthData,
249 | setMonth: (MonthData) -> Unit,
250 | minMonth: Int,
251 | maxMonth: Int,
252 | minYear: Int,
253 | maxYear: Int,
254 | selectedYear: Int,
255 | setShowMonths: (Boolean) -> Unit,
256 | setHeight: (Int) -> Unit,
257 | showOnlyMonth: Boolean,
258 | themeColor: Color
259 | ) {
260 |
261 | val NUMBER_OF_ROW_ITEMS = 3
262 | var numberOfElement = 0
263 |
264 | LazyColumn(modifier = Modifier
265 | .padding(horizontal = 20.dp)
266 | .padding(vertical = 20.dp)
267 | .onGloballyPositioned { setHeight(it.size.height) }) {
268 | items(items = monthList.chunked(NUMBER_OF_ROW_ITEMS)) { rowItems ->
269 | Row(
270 | modifier = Modifier.fillMaxWidth(),
271 | horizontalArrangement = Arrangement.SpaceBetween
272 | ) {
273 | for ((index, item) in rowItems.withIndex()) {
274 | MonthItem(
275 | month = item,
276 | index = index,
277 | numberOfElement = numberOfElement,
278 | rowSize = NUMBER_OF_ROW_ITEMS,
279 | selectedMonth = selectedMonth.name,
280 | setMonth = setMonth,
281 | minMonth = minMonth,
282 | maxMonth = maxMonth,
283 | minYear = minYear,
284 | maxYear = maxYear,
285 | selectedYear = selectedYear,
286 | setShowMonths = setShowMonths,
287 | showOnlyMonth = showOnlyMonth,
288 | themeColor = themeColor
289 | )
290 | numberOfElement += 1
291 | }
292 | }
293 | }
294 | }
295 | }
296 |
297 | @Composable
298 | fun MonthItem(
299 | month: MonthData,
300 | selectedMonth: String,
301 | setMonth: (MonthData) -> Unit,
302 | index: Int,
303 | numberOfElement: Int,
304 | rowSize: Int,
305 | minMonth: Int,
306 | maxMonth: Int,
307 | minYear: Int,
308 | maxYear: Int,
309 | selectedYear: Int,
310 | setShowMonths: (Boolean) -> Unit,
311 | showOnlyMonth: Boolean,
312 | themeColor: Color
313 | ) {
314 | val enabled = checkDate(
315 | minYear = minYear,
316 | maxYear = maxYear,
317 | selectedYear = selectedYear,
318 | maxMonth = maxMonth,
319 | minMonth = minMonth,
320 | numberOfElement = numberOfElement
321 | )
322 | Box(
323 | modifier = Modifier
324 | .background(
325 | color = if (month.name == selectedMonth) themeColor else Color.Transparent,
326 | shape = RoundedCornerShape(100)
327 | )
328 | .fillMaxWidth(1f / (rowSize - index + 1f))
329 | .aspectRatio(1f)
330 | .clickable(
331 | indication = null,
332 | interactionSource = remember { MutableInteractionSource() },
333 | enabled = enabled
334 | ) {
335 | setMonth(month)
336 | if (!showOnlyMonth) {
337 | setShowMonths(false)
338 | }
339 | },
340 | contentAlignment = Alignment.Center
341 | ) {
342 | Text(
343 | text = month.name.uppercase(),
344 | color = if (enabled && month.name == selectedMonth) Color.White
345 | else if (enabled) Color.Black
346 | else Color.Gray
347 | )
348 | }
349 | }
350 |
351 | private fun checkDate(
352 | minYear: Int,
353 | maxYear: Int,
354 | selectedYear: Int,
355 | minMonth: Int,
356 | maxMonth: Int,
357 | numberOfElement: Int
358 | ): Boolean {
359 | if (minMonth == 0) return true
360 | if (minYear == maxYear) return numberOfElement in minMonth..maxMonth
361 | if (selectedYear == minYear) {
362 | return numberOfElement >= minMonth
363 | } else if (selectedYear == maxYear) {
364 | if (numberOfElement > maxMonth) return false
365 | }
366 | return true
367 | }
368 |
369 | @Composable
370 | fun CalendarHeader(
371 | selectedMonth: String,
372 | selectedYear: Int,
373 | showMonths: Boolean,
374 | setShowMonths: (Boolean) -> Unit,
375 | title: String,
376 | showOnlyMonth: Boolean,
377 | showOnlyYear: Boolean,
378 | themeColor: Color,
379 | monthSelectedColor: Color
380 | ) {
381 | Column(
382 | modifier = Modifier
383 | .fillMaxWidth()
384 | .background(themeColor),
385 | horizontalAlignment = Alignment.CenterHorizontally
386 | ) {
387 | Text(
388 | text = title,
389 | fontSize = 16.sp,
390 | modifier = Modifier.padding(top = 16.dp),
391 | color = Color.White
392 | )
393 | Row {
394 | if (!showOnlyYear) {
395 | Text(
396 | text = selectedMonth.uppercase(),
397 | fontSize = 35.sp,
398 | modifier = Modifier
399 | .padding(
400 | bottom = 20.dp,
401 | start = if (showOnlyMonth) 0.dp else 30.dp,
402 | end = if (showOnlyMonth) 0.dp else 10.dp
403 | )
404 | .clickableNoRipple { setShowMonths(true) },
405 | color = if (showMonths) Color.White else monthSelectedColor
406 | )
407 | }
408 | if (!showOnlyMonth) {
409 | Text(
410 | text = selectedYear.toString(),
411 | fontSize = 35.sp,
412 | modifier = Modifier
413 | .padding(
414 | bottom = 20.dp,
415 | start = if (showOnlyYear) 0.dp else 10.dp,
416 | end = if (showOnlyYear) 0.dp else 30.dp
417 | )
418 | .clickableNoRipple { setShowMonths(false) },
419 | color = if (showMonths) monthSelectedColor else Color.White
420 | )
421 | }
422 | }
423 | }
424 | }
425 |
426 | @Composable
427 | fun CalendarBottom(
428 | onPositiveClick: () -> Unit,
429 | onCancelClick: () -> Unit,
430 | themeColor: Color,
431 | negativeButtonTitle: String,
432 | positiveButtonTitle: String
433 | ) {
434 | Row(
435 | modifier = Modifier
436 | .fillMaxWidth()
437 | .padding(horizontal = 30.dp)
438 | .padding(vertical = 10.dp),
439 | horizontalArrangement = Arrangement.End
440 | ) {
441 | Text(text = negativeButtonTitle,
442 | color = themeColor,
443 | modifier = Modifier
444 | .clickableNoRipple { onCancelClick() }
445 | .padding(10.dp))
446 | Text(text = positiveButtonTitle,
447 | color = themeColor,
448 | modifier = Modifier
449 | .clickableNoRipple { onPositiveClick() }
450 | .padding(10.dp))
451 | }
452 | }
453 |
454 | fun Modifier.clickableNoRipple(onClick: () -> Unit) = composed {
455 | clickable(
456 | indication = null,
457 | interactionSource = remember { MutableInteractionSource() },
458 | ) {
459 | onClick()
460 | }
461 | }
--------------------------------------------------------------------------------