diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..a08bd72 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,50 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace 'org.fnives.android.compose.learning' + compileSdk defaultCompileSdkVersion + + defaultConfig { + applicationId "org.fnives.android.compose.learning" + minSdk defaultMinSdkVersion + targetSdk defaultTargetSdkVersion + versionCode defaultVersionCode + versionName defaultVersionName + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + buildFeatures { + compose true + } + composeOptions { + kotlinCompilerExtensionVersion composeKotlinCompilerExtensionVersion + } + compileOptions { + sourceCompatibility compileCompatibility + targetCompatibility compileCompatibility + } + kotlinOptions { + jvmTarget = kotlinJvmTarget + } +} + +dependencies { + implementation "androidx.appcompat:appcompat:$appcompat_version" + implementation "androidx.activity:activity-compose:$appcompat_version" + implementation "androidx.compose.ui:ui-tooling:$compose_version" + implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" + + debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" + + implementation project(":picker") +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..0b2deb8 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/org/fknives/android/compose/learning/MainActivity.kt b/app/src/main/java/org/fknives/android/compose/learning/MainActivity.kt new file mode 100644 index 0000000..bff6c45 --- /dev/null +++ b/app/src/main/java/org/fknives/android/compose/learning/MainActivity.kt @@ -0,0 +1,21 @@ +package org.fknives.android.compose.learning + +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import org.fknives.android.compose.learning.navigation.Navigation +import org.fknives.android.compose.learning.theme.AppTheme + +class MainActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + AppTheme { + Navigation { onNavigationClick -> + MainContent(onNavigationClick) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/fknives/android/compose/learning/MainScreen.kt b/app/src/main/java/org/fknives/android/compose/learning/MainScreen.kt new file mode 100644 index 0000000..b802b01 --- /dev/null +++ b/app/src/main/java/org/fknives/android/compose/learning/MainScreen.kt @@ -0,0 +1,65 @@ +package org.fknives.android.compose.learning + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import org.fknives.android.compose.learning.navigation.Screens + +val Screens.text: String + get() = when (this) { + Screens.NumberPicker -> "Number Picker" + Screens.TimePicker -> "Time Picker" + Screens.TextPicker -> "Text Picker" + } + +@Composable +fun MainContent( + onNavigation: (Screens) -> Unit +) { + Surface( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + ) { + LazyVerticalGrid( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(), + columns = GridCells.Adaptive(240.dp), + verticalArrangement = Arrangement.Bottom, + contentPadding = PaddingValues(16.dp) + ) { + Screens.values().forEach { + item { + MainNavigationItem(it, onClick = onNavigation) + } + } + } + } +} + +@Composable +fun MainNavigationItem( + screens: Screens, + onClick: (Screens) -> Unit, + text: String = screens.text +) { + Text( + text = text, + fontSize = MaterialTheme.typography.h5.fontSize, + textAlign = TextAlign.Center, + modifier = Modifier + .padding(8.dp) + .clickable { + onClick(screens) + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/org/fknives/android/compose/learning/navigation/Navigation.kt b/app/src/main/java/org/fknives/android/compose/learning/navigation/Navigation.kt new file mode 100644 index 0000000..4204ca8 --- /dev/null +++ b/app/src/main/java/org/fknives/android/compose/learning/navigation/Navigation.kt @@ -0,0 +1,68 @@ +package org.fknives.android.compose.learning.navigation + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.Crossfade +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import org.fknives.android.compose.learning.pickers.NumberPickerScreen +import org.fknives.android.compose.learning.pickers.TextPickerScreen +import org.fknives.android.compose.learning.pickers.TimePickerScreen +import org.fknives.android.compose.learning.text + +@Composable +fun Navigation( + mainScreen: @Composable (onNavigation: (Screens) -> Unit) -> Unit +) { + var screen by remember { mutableStateOf(null) } + + Crossfade(targetState = screen) { currentScreen -> + if (currentScreen == null) { + mainScreen { selected -> + screen = selected + } + } else { + DetailScreen(title = currentScreen.text, onBack = { screen = null }) { + when (currentScreen) { + Screens.NumberPicker -> NumberPickerScreen() + Screens.TimePicker -> TimePickerScreen() + Screens.TextPicker -> TextPickerScreen() + } + } + } + } +} + +@Composable +fun DetailScreen( + title: String, + onBack: () -> Unit, + content: @Composable () -> Unit +) { + BackHandler(onBack = onBack) + Column( + Modifier + .fillMaxWidth() + .fillMaxHeight() + ) { + TopAppBar { + IconButton(onClick = onBack) { + Icon(imageVector = Icons.Default.ArrowBack, contentDescription = "Back navigation") + } + Text(title) + } + Surface( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) { + content() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/fknives/android/compose/learning/navigation/Screens.kt b/app/src/main/java/org/fknives/android/compose/learning/navigation/Screens.kt new file mode 100644 index 0000000..92c2cf2 --- /dev/null +++ b/app/src/main/java/org/fknives/android/compose/learning/navigation/Screens.kt @@ -0,0 +1,7 @@ +package org.fknives.android.compose.learning.navigation + +enum class Screens { + TextPicker, + NumberPicker, + TimePicker +} \ No newline at end of file diff --git a/app/src/main/java/org/fknives/android/compose/learning/pickers/LabeledPickers.kt b/app/src/main/java/org/fknives/android/compose/learning/pickers/LabeledPickers.kt new file mode 100644 index 0000000..99ddc59 --- /dev/null +++ b/app/src/main/java/org/fknives/android/compose/learning/pickers/LabeledPickers.kt @@ -0,0 +1,27 @@ +package org.fknives.android.compose.learning.pickers + +import androidx.compose.foundation.layout.Column +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign + +@Composable +fun Labeled( + text: String, + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) { + Surface(modifier = modifier) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = text, + textAlign = TextAlign.Center + ) + + content() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/fknives/android/compose/learning/pickers/NumberPickerScreen.kt b/app/src/main/java/org/fknives/android/compose/learning/pickers/NumberPickerScreen.kt new file mode 100644 index 0000000..c311968 --- /dev/null +++ b/app/src/main/java/org/fknives/android/compose/learning/pickers/NumberPickerScreen.kt @@ -0,0 +1,131 @@ +package org.fknives.android.compose.learning.pickers + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.MaterialTheme +import androidx.compose.material.ProvideTextStyle +import androidx.compose.material.Surface +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.fknives.android.compose.picker.number.LinedInnerTextPicker +import org.fknives.android.compose.picker.number.NumberPicker +import org.fknives.android.compose.picker.number.NumberPickerConfig +import org.fknives.android.compose.picker.text.TextPicker + + +@Composable +fun NumberPickerScreen() { + Surface( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceEvenly, + ) { + Labeled(text = "Number Picker", modifier = Modifier.padding(16.dp)) { + var selected by remember { mutableStateOf(0) } + NumberPicker( + modifier = Modifier.defaultMinSize(minWidth = 200.dp), + onSelectedChange = { + selected = it + }, + textStyle = MaterialTheme.typography.h5, + config = NumberPickerConfig(maximum = 10), + selectedValue = selected + ) + } + Labeled(text = "Limited Minute Picker", modifier = Modifier.padding(16.dp)) { + var selected by remember { mutableStateOf(0) } + NumberPicker( + config = NumberPickerConfig.configMinutePicker, + selectedValue = selected, + onSelectedChange = { selected = it }, + roundAround = false, + textStyle = MaterialTheme.typography.h5 + ) + } + Labeled(text = "Lined Minute Picker", modifier = Modifier.padding(16.dp)) { + var selected by remember { mutableStateOf(0) } + ProvideTextStyle(MaterialTheme.typography.h5) { + NumberPicker( + config = NumberPickerConfig.configMinutePicker, + selectedValue = selected, + onSelectedChange = { selected = it }, + timePicker = { LinedInnerTextPicker(modifier = Modifier.defaultMinSize(minWidth = 200.dp)) } + ) + } + } + + Labeled(text = "Custom Hour Picker", modifier = Modifier.padding(16.dp)) { + var selected by remember { mutableStateOf(0) } + NumberPicker( + config = NumberPickerConfig.configHourPicker24, + selectedValue = selected, + onSelectedChange = { selected = it } + ) { + TextPicker( + modifier = Modifier.defaultMinSize(minWidth = 200.dp), + textStyle = MaterialTheme.typography.h5, + roundAround = false, + textForIndex = textForIndex, + itemCount = itemCount, + selectedIndex = selectedIndex, + onSelectedIndexChange = onSelectedIndexChange, + onIndexDifferenceChanging = onIndexDifferenceChanging, + state = state + ) + } + } + + Labeled(text = "Skipping 1, 1-5 Picker(1,3,5)", modifier = Modifier.padding(16.dp)) { + var selected by remember { mutableStateOf(1) } + ProvideTextStyle(MaterialTheme.typography.h5) { + NumberPicker( + config = NumberPickerConfig(maximum = 5, minimum = 1, skipInBetween = 1), + selectedValue = selected, + onSelectedChange = { selected = it } + ) + } + } + + Labeled(text = "Skipping 3, 1-5 Picker(1,4)", modifier = Modifier.padding(16.dp)) { + var selected by remember { mutableStateOf(1) } + ProvideTextStyle(MaterialTheme.typography.h5) { + NumberPicker( + config = NumberPickerConfig(maximum = 5, minimum = 1, skipInBetween = 2), + selectedValue = selected, + onSelectedChange = { selected = it } + ) + } + } + + Labeled(text = "Skipping 1, 1-5 Picker(5,3,1) reverse", modifier = Modifier.padding(16.dp)) { + var selected by remember { mutableStateOf(1) } + ProvideTextStyle(MaterialTheme.typography.h5) { + NumberPicker( + config = NumberPickerConfig(maximum = 5, minimum = 1, skipInBetween = 1, reversedOrder = true), + selectedValue = selected, + onSelectedChange = { selected = it } + ) + } + } + + Labeled(text = "Skipping 2, 1-5 Picker(5,2) reverse", modifier = Modifier.padding(16.dp)) { + var selected by remember { mutableStateOf(1) } + ProvideTextStyle(MaterialTheme.typography.h5) { + NumberPicker( + config = NumberPickerConfig(maximum = 5, minimum = 1, skipInBetween = 2, reversedOrder = true), + selectedValue = selected, + roundAround = false, + onSelectedChange = { selected = it }, + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/fknives/android/compose/learning/pickers/TextPickerScreen.kt b/app/src/main/java/org/fknives/android/compose/learning/pickers/TextPickerScreen.kt new file mode 100644 index 0000000..8db189a --- /dev/null +++ b/app/src/main/java/org/fknives/android/compose/learning/pickers/TextPickerScreen.kt @@ -0,0 +1,73 @@ +package org.fknives.android.compose.learning.pickers + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.fknives.android.compose.picker.text.LinedTextPicker +import org.fknives.android.compose.picker.text.TextPicker + +@Composable +fun TextPickerScreen() { + Surface( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + ) { + val text = listOf("Alma", "Banan", "Citrom","Dinnye","Eper","Fuge","Goji","Karfiol") + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceEvenly + ) { + Labeled(text = "Text Picker") { + var selected by remember { mutableStateOf(0) } + TextPicker( + modifier = Modifier.defaultMinSize(minWidth = 200.dp), + selectedIndex = selected, + textForIndex = text::get, + onSelectedIndexChange = { + selected = it + }, + textStyle = MaterialTheme.typography.h5, + itemCount = text.size + ) + } + Labeled(text = "Limited Text Picker") { + var selected by remember { mutableStateOf(0) } + TextPicker( + modifier = Modifier.defaultMinSize(minWidth = 200.dp), + selectedIndex = selected, + textForIndex = text::get, + onSelectedIndexChange = { + selected = it + }, + roundAround = false, + textStyle = MaterialTheme.typography.h5, + itemCount = text.size + ) + } + Labeled(text = "Lined Text Picker") { + var selected by remember { mutableStateOf(0) } + LinedTextPicker( + modifier = Modifier.defaultMinSize(minWidth = 200.dp), + selected = selected, + textForIndex = text::get, + onSelectedChange = { + selected = it + }, + textStyle = MaterialTheme.typography.h5, + itemCount = text.size + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/fknives/android/compose/learning/pickers/TimePickerScreen.kt b/app/src/main/java/org/fknives/android/compose/learning/pickers/TimePickerScreen.kt new file mode 100644 index 0000000..d845bd1 --- /dev/null +++ b/app/src/main/java/org/fknives/android/compose/learning/pickers/TimePickerScreen.kt @@ -0,0 +1,50 @@ +package org.fknives.android.compose.learning.pickers + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import org.fknives.android.compose.picker.time.clock.ClockTimePicker +import org.fknives.android.compose.picker.time.SelectedTime +import org.fknives.android.compose.picker.time.TimePicker + +@Composable +fun TimePickerScreen() { + Surface( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceEvenly, + ) { + Labeled(text = "Clock Time Picker") { + var selectedTime by remember { mutableStateOf(SelectedTime.get()) } + + ClockTimePicker( + selectedTime = selectedTime, + onSelectedTimeChanged = { selectedTime = it } + ) + } + + Labeled(text = "Standard Time Picker") { + var selectedTime by remember { mutableStateOf(SelectedTime.get()) } + + TimePicker( + selectedTime = selectedTime, + onSelectedTimeChanged = { selectedTime = it } + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/fknives/android/compose/learning/theme/AppTheme.kt b/app/src/main/java/org/fknives/android/compose/learning/theme/AppTheme.kt new file mode 100644 index 0000000..7345dba --- /dev/null +++ b/app/src/main/java/org/fknives/android/compose/learning/theme/AppTheme.kt @@ -0,0 +1,45 @@ +package org.fknives.android.compose.learning.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material.MaterialTheme +import androidx.compose.material.darkColors +import androidx.compose.material.lightColors +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +@Composable +fun AppTheme(content: @Composable () -> Unit) = MaterialTheme( + + colors = if (isSystemInDarkTheme()) { + darkColors( + primary = Color(0xFF7C43BD), + primaryVariant = Color(0xFF7C43BD), + secondary = Color(0xFF50B04A), + secondaryVariant = Color(0xFF50B04A), + background = Color(0xFF000000), + surface = Color(0xFF1A1A1A), + error = Color(0xFFFF5050), + onPrimary = Color(0xFFFFFFFF), + onSecondary = Color(0xFFFFFFFF), + onBackground = Color(0xFFFFFFFF), + onSurface = Color(0xFFFFFFFF), + onError = Color(0xFF000000) + ) + } else { + lightColors( + primary = Color(0xFF7C43BD), + primaryVariant = Color(0xFF7C43BD), + secondary = Color(0xFF50B04A), + secondaryVariant = Color(0xFF50B04A), + background = Color(0xFFFFFFFF), + surface = Color(0xFFA1A1A1), + error = Color(0xFFFF5050), + onPrimary = Color(0xFFFFFFFF), + onSecondary = Color(0xFFFFFFFF), + onBackground = Color(0xFFFFFFFF), + onSurface = Color(0xFFFFFFFF), + onError = Color(0xFF000000) + ) + }, + content = content +) \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..ce3279c --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Compose Learning + \ No newline at end of file diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000..fa0f996 --- /dev/null +++ b/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000..9ee9997 --- /dev/null +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..9e1669d --- /dev/null +++ b/build.gradle @@ -0,0 +1,13 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + id 'com.android.application' version '7.2.0' apply false + id 'com.android.library' version '7.2.0' apply false + id 'org.jetbrains.kotlin.android' version "1.6.10" apply false +} + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + kotlinOptions.freeCompilerArgs += ["-Xuse-experimental=kotlin.Experimental"] +} + +apply from: "gradlescripts/configs.gradle" +apply from: "gradlescripts/versions.gradle" \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..3c5031e --- /dev/null +++ b/gradle.properties @@ -0,0 +1,23 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..cf377a5 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Jun 13 17:23:04 EEST 2022 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/gradlescripts/configs.gradle b/gradlescripts/configs.gradle new file mode 100644 index 0000000..4bdc9f0 --- /dev/null +++ b/gradlescripts/configs.gradle @@ -0,0 +1,9 @@ +project.ext { + defaultCompileSdkVersion = 32 + defaultMinSdkVersion = 23 + defaultTargetSdkVersion = 32 + defaultVersionCode = 1 + defaultVersionName = "1.0" + compileCompatibility = JavaVersion.VERSION_1_8 + kotlinJvmTarget = "1.8" +} \ No newline at end of file diff --git a/gradlescripts/versions.gradle b/gradlescripts/versions.gradle new file mode 100644 index 0000000..ad7374c --- /dev/null +++ b/gradlescripts/versions.gradle @@ -0,0 +1,7 @@ +project.ext { + compose_version = "1.2.0-beta03" + kotlin_coroutine_version = "1.5.2" + appcompat_version = "1.4.0" + kotlin_version = "1.6.10" + composeKotlinCompilerExtensionVersion = "1.1.1" +} \ No newline at end of file diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/picker/.gitignore b/picker/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/picker/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/picker/build.gradle b/picker/build.gradle new file mode 100644 index 0000000..659ef34 --- /dev/null +++ b/picker/build.gradle @@ -0,0 +1,51 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'kotlin-kapt' +} + +android { + namespace 'org.fknives.android.compose.picker' + compileSdk defaultCompileSdkVersion + + defaultConfig { + minSdk defaultMinSdkVersion + targetSdk defaultTargetSdkVersion + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + buildFeatures { + compose true + } + composeOptions { + kotlinCompilerExtensionVersion composeKotlinCompilerExtensionVersion + } + compileOptions { + sourceCompatibility compileCompatibility + targetCompatibility compileCompatibility + } + kotlinOptions { + jvmTarget = kotlinJvmTarget + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutine_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutine_version" + + api "androidx.compose.material:material:$compose_version" + api "androidx.compose.animation:animation:$compose_version" + implementation "androidx.compose.ui:ui-tooling:$compose_version" + implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" + + debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" +} \ No newline at end of file diff --git a/picker/consumer-rules.pro b/picker/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/picker/proguard-rules.pro b/picker/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/picker/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/picker/src/androidTest/java/org/fknives/android/compose/picker/ExampleInstrumentedTest.kt b/picker/src/androidTest/java/org/fknives/android/compose/picker/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..903fe16 --- /dev/null +++ b/picker/src/androidTest/java/org/fknives/android/compose/picker/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package org.fknives.android.compose.picker + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("org.fknives.android.compose.picker.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/picker/src/main/AndroidManifest.xml b/picker/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a5918e6 --- /dev/null +++ b/picker/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/picker/src/main/java/org/fknives/android/compose/picker/number/NumberPicker.kt b/picker/src/main/java/org/fknives/android/compose/picker/number/NumberPicker.kt new file mode 100644 index 0000000..57e68cf --- /dev/null +++ b/picker/src/main/java/org/fknives/android/compose/picker/number/NumberPicker.kt @@ -0,0 +1,53 @@ +package org.fknives.android.compose.picker.number + +import androidx.compose.material.LocalTextStyle +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import org.fknives.android.compose.picker.text.util.TextPickerDefaults +import org.fknives.android.compose.picker.text.TextPickerState + +@Composable +fun NumberPicker( + modifier: Modifier = Modifier, + config: NumberPickerConfig, + selectedValue: Int, + onSelectedChange: (Int) -> Unit, + onIndexDifferenceChanging: (Int) -> Unit = TextPickerDefaults.onIndexDifferenceChanging, + textStyle: TextStyle = LocalTextStyle.current, + roundAround: Boolean = TextPickerDefaults.roundAround, +) { + NumberPicker( + config = config, + selectedValue = selectedValue, + onSelectedChange = onSelectedChange, + onIndexDifferenceChanging = onIndexDifferenceChanging + ) { + CustomInnerTextPicker( + modifier = modifier, + textStyle = textStyle, + roundAround = roundAround + ) + } +} + +@Composable +fun NumberPicker( + config: NumberPickerConfig, + selectedValue: Int, + onSelectedChange: (Int) -> Unit, + onIndexDifferenceChanging: (Int) -> Unit = TextPickerDefaults.onIndexDifferenceChanging, + state: TextPickerState = rememberNumberPickerState(selectedValue = selectedValue, config = config), + timePicker: @Composable NumberPickerScope.() -> Unit = { StandardInnerTextPicker() } +) { + require(selectedValue >= config.minimum) { "Selected Value($selectedValue) is less than Minimum (${config.minimum})!" } + require(selectedValue <= config.maximum) { "Selected Value($selectedValue) is more than Maximum (${config.maximum})!" } + val numberPickerScope = rememberNumberPickerScope( + state, + config, + onIndexDifferenceChanging, + onSelectedChange + ) + + timePicker(numberPickerScope) +} \ No newline at end of file diff --git a/picker/src/main/java/org/fknives/android/compose/picker/number/NumberPickerConfig.kt b/picker/src/main/java/org/fknives/android/compose/picker/number/NumberPickerConfig.kt new file mode 100644 index 0000000..c98446a --- /dev/null +++ b/picker/src/main/java/org/fknives/android/compose/picker/number/NumberPickerConfig.kt @@ -0,0 +1,36 @@ +package org.fknives.android.compose.picker.number + +import androidx.compose.runtime.Immutable + +@Immutable +data class NumberPickerConfig( + val maximum: Int, + val minimum: Int = 0, + val reversedOrder: Boolean = false, + val skipInBetween: Int = 0 +) { + init { + require(skipInBetween >= 0) { "Skip In Between cannot be negative!" } + } + + companion object { + val configMinutePicker get() = NumberPickerConfig( + minimum = 0, + maximum = 59, + reversedOrder = false, + skipInBetween = 0 + ) + val configHourPicker24 get() = NumberPickerConfig( + minimum = 0, + maximum = 23, + reversedOrder = false, + skipInBetween = 0 + ) + val configHourPicker12 get() = NumberPickerConfig( + minimum = 1, + maximum = 12, + reversedOrder = false, + skipInBetween = 0 + ) + } +} \ No newline at end of file diff --git a/picker/src/main/java/org/fknives/android/compose/picker/number/NumberPickerScope.kt b/picker/src/main/java/org/fknives/android/compose/picker/number/NumberPickerScope.kt new file mode 100644 index 0000000..f6736ad --- /dev/null +++ b/picker/src/main/java/org/fknives/android/compose/picker/number/NumberPickerScope.kt @@ -0,0 +1,49 @@ +package org.fknives.android.compose.picker.number + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.remember +import org.fknives.android.compose.picker.text.util.TextPickerDefaults +import org.fknives.android.compose.picker.text.TextPickerState + +@Immutable +data class NumberPickerScopeImpl( + override val state: TextPickerState, + override val onIndexDifferenceChanging: (Int) -> Unit, + override val onSelectedIndexChange: (Int) -> Unit, + override val textForIndex: (Int) -> String, +) : NumberPickerScope { + override val selectedIndex: Int get() = state.selected + override val itemCount: Int get() = state.itemCount +} + +@Composable +fun rememberNumberPickerScope( + state: TextPickerState, + config: NumberPickerConfig, + onSelectedChange: (Int) -> Unit, + onIndexDifferenceChanging: (Int) -> Unit = TextPickerDefaults.onIndexDifferenceChanging, + vararg keys: Any? +): NumberPickerScope { + val indexToNumber = rememberNumberPickerIndexToNumber(config = config) + + return remember(state, onIndexDifferenceChanging, config, keys) { + + NumberPickerScopeImpl( + state = state, + textForIndex = { "${indexToNumber(it)}" }, + onIndexDifferenceChanging = onIndexDifferenceChanging, + onSelectedIndexChange = { onSelectedChange(indexToNumber(it)) } + ) + } +} + +@Immutable +interface NumberPickerScope { + val state: TextPickerState + val onIndexDifferenceChanging: (Int) -> Unit + val onSelectedIndexChange: (Int) -> Unit + val textForIndex: (Int) -> String + val selectedIndex: Int get() = state.selected + val itemCount: Int get() = state.itemCount +} \ No newline at end of file diff --git a/picker/src/main/java/org/fknives/android/compose/picker/number/NumberPickerScopeExtension.kt b/picker/src/main/java/org/fknives/android/compose/picker/number/NumberPickerScopeExtension.kt new file mode 100644 index 0000000..84cb4d3 --- /dev/null +++ b/picker/src/main/java/org/fknives/android/compose/picker/number/NumberPickerScopeExtension.kt @@ -0,0 +1,55 @@ +package org.fknives.android.compose.picker.number + +import androidx.compose.material.LocalTextStyle +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import org.fknives.android.compose.picker.text.LinedTextPicker +import org.fknives.android.compose.picker.text.TextPicker +import org.fknives.android.compose.picker.text.util.TextPickerDefaults + +@Composable +fun NumberPickerScope.CustomInnerTextPicker( + modifier: Modifier = Modifier, + textStyle: TextStyle = LocalTextStyle.current, + roundAround: Boolean = TextPickerDefaults.roundAround +) { + TextPicker( + modifier = modifier, + textStyle = textStyle, + roundAround = roundAround, + textForIndex = textForIndex, + itemCount = itemCount, + selectedIndex = selectedIndex, + onSelectedIndexChange = onSelectedIndexChange, + onIndexDifferenceChanging = onIndexDifferenceChanging, + state = state + ) +} + +@Composable +fun NumberPickerScope.StandardInnerTextPicker(modifier: Modifier = Modifier) { + TextPicker( + modifier = modifier, + textForIndex = textForIndex, + itemCount = itemCount, + selectedIndex = selectedIndex, + onSelectedIndexChange = onSelectedIndexChange, + onIndexDifferenceChanging = onIndexDifferenceChanging, + state = state + ) +} + +@Composable +fun NumberPickerScope.LinedInnerTextPicker(modifier: Modifier = Modifier) { + LinedTextPicker( + modifier = modifier, + textForIndex = textForIndex, + itemCount = itemCount, + selected = selectedIndex, + onSelectedChange = onSelectedIndexChange, + onIndexChanging = onIndexDifferenceChanging, + state = state + ) +} + diff --git a/picker/src/main/java/org/fknives/android/compose/picker/number/NumberPickerStateHelper.kt b/picker/src/main/java/org/fknives/android/compose/picker/number/NumberPickerStateHelper.kt new file mode 100644 index 0000000..0f45266 --- /dev/null +++ b/picker/src/main/java/org/fknives/android/compose/picker/number/NumberPickerStateHelper.kt @@ -0,0 +1,36 @@ +package org.fknives.android.compose.picker.number + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import org.fknives.android.compose.picker.text.TextPickerState +import org.fknives.android.compose.picker.text.util.rememberTextPickerState + + +@Composable +fun rememberNumberPickerIndexToNumber(config: NumberPickerConfig): (Int) -> Int = + remember(config) { + if (config.reversedOrder) { + { index: Int -> + config.maximum - index - config.skipInBetween * index + } + } else { + { index: Int -> + config.minimum + index + config.skipInBetween * index + } + } + } + +private fun valueToIndex(config: NumberPickerConfig, value: Int): Int = + if (config.reversedOrder) { + (config.maximum - value) / (config.skipInBetween + 1) + } else { + (value - config.minimum) / (config.skipInBetween + 1) + } + +@Composable +fun rememberNumberPickerState(selectedValue: Int, config: NumberPickerConfig): TextPickerState { + val selected = valueToIndex(value = selectedValue, config = config) + + val itemCount = (config.maximum - config.minimum) / (config.skipInBetween + 1) + 1 + return rememberTextPickerState(selected = selected, itemCount = itemCount) +} \ No newline at end of file diff --git a/picker/src/main/java/org/fknives/android/compose/picker/text/LinedTextPicker.kt b/picker/src/main/java/org/fknives/android/compose/picker/text/LinedTextPicker.kt new file mode 100644 index 0000000..15229ed --- /dev/null +++ b/picker/src/main/java/org/fknives/android/compose/picker/text/LinedTextPicker.kt @@ -0,0 +1,44 @@ +package org.fknives.android.compose.picker.text + +import androidx.compose.material.LocalTextStyle +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.MeasurePolicy +import androidx.compose.ui.text.TextStyle +import org.fknives.android.compose.picker.text.content.* +import org.fknives.android.compose.picker.text.util.TextPickerDefaults +import org.fknives.android.compose.picker.text.util.rememberTextPickerAnimator +import org.fknives.android.compose.picker.text.util.rememberTextPickerState + +@Composable +fun LinedTextPicker( + modifier: Modifier = Modifier, + textForIndex: (Int) -> String, + itemCount: Int, + selected: Int, + onSelectedChange: (Int) -> Unit, + textStyle: TextStyle = LocalTextStyle.current, + roundAround: Boolean = TextPickerDefaults.roundAround, + onIndexChanging: (Int) -> Unit = TextPickerDefaults.onIndexDifferenceChanging, + state: TextPickerState = rememberTextPickerState(selected = selected, itemCount = itemCount), + animator: TextPickerAnimator = rememberTextPickerAnimator(roundAround = roundAround, onIndexDifferenceChanging = onIndexChanging), + textPickerContent: TextPickerContent = LinedTextPickerContent(), + textPickerMeasurePolicy: MeasurePolicy = remember(state) { LinedTextPickerMeasurePolicy(state) }, + pickerItem: @Composable (text: String, translation: Float) -> Unit = defaultNumberPickerTextAlphaModifier() +) = + TextPicker( + modifier = modifier, + textForIndex = textForIndex, + itemCount = itemCount, + selectedIndex = selected, + onSelectedIndexChange = onSelectedChange, + textStyle = textStyle, + roundAround = roundAround, + onIndexDifferenceChanging = onIndexChanging, + state = state, + animator = animator, + textPickerContent = textPickerContent, + textPickerMeasurePolicy = textPickerMeasurePolicy, + pickerItem = pickerItem, + ) \ No newline at end of file diff --git a/picker/src/main/java/org/fknives/android/compose/picker/text/TextPicker.kt b/picker/src/main/java/org/fknives/android/compose/picker/text/TextPicker.kt new file mode 100644 index 0000000..fbd692f --- /dev/null +++ b/picker/src/main/java/org/fknives/android/compose/picker/text/TextPicker.kt @@ -0,0 +1,98 @@ +package org.fknives.android.compose.picker.text + +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.draggable +import androidx.compose.foundation.gestures.rememberDraggableState +import androidx.compose.material.LocalTextStyle +import androidx.compose.material.ProvideTextStyle +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.MeasurePolicy +import androidx.compose.ui.text.TextStyle +import org.fknives.android.compose.picker.text.content.DefaultTextPickerContent +import org.fknives.android.compose.picker.text.content.DefaultTextPickerMeasurePolicy +import org.fknives.android.compose.picker.text.content.defaultNumberPickerTextAlphaModifier +import org.fknives.android.compose.picker.text.util.TextPickerDefaults +import org.fknives.android.compose.picker.text.util.rememberDefaultMoveUnsafeToProperIndex +import org.fknives.android.compose.picker.text.util.rememberTextPickerAnimator +import org.fknives.android.compose.picker.text.util.rememberTextPickerState +import org.fknives.android.compose.picker.text.util.rememberWrappedTextForIndex + +@Composable +fun TextPicker( + modifier: Modifier = Modifier, + textForIndex: (Int) -> String, + itemCount: Int, + selectedIndex: Int, + onSelectedIndexChange: (Int) -> Unit, + textStyle: TextStyle = LocalTextStyle.current, + roundAround: Boolean = TextPickerDefaults.roundAround, + onIndexDifferenceChanging: (Int) -> Unit = TextPickerDefaults.onIndexDifferenceChanging, + state: TextPickerState = rememberTextPickerState(selected = selectedIndex, itemCount = itemCount), + animator: TextPickerAnimator = rememberTextPickerAnimator(roundAround = roundAround, onIndexDifferenceChanging = onIndexDifferenceChanging), + textPickerContent: TextPickerContent = DefaultTextPickerContent(), + textPickerMeasurePolicy: MeasurePolicy = remember(state) { DefaultTextPickerMeasurePolicy(state) }, + pickerItem: @Composable (text: String, translation: Float) -> Unit = defaultNumberPickerTextAlphaModifier() +) { + + val moveUnsafeToProperIndex: (Int) -> Int = rememberDefaultMoveUnsafeToProperIndex(itemCount = itemCount, roundAround = roundAround) + val rememberTextForIndex = rememberWrappedTextForIndex(itemCount = itemCount, roundAround = roundAround, textForIndex = textForIndex) + + Layout( + modifier = modifier + .clipToBounds() + .draggable( + orientation = Orientation.Vertical, + onDragStopped = { velocity -> + animator.flingAndSnap(this, state, velocity) { change -> + val index = moveUnsafeToProperIndex(selectedIndex + change) + state.onPreviouslySelectedChange(index) + onSelectedIndexChange(moveUnsafeToProperIndex(selectedIndex + change)) + } + }, + state = rememberDraggableState { + animator.onDeltaY(state, it) + } + ), + content = { + if (state.previousSelected != selectedIndex) { + LaunchedEffect(selectedIndex) { + animator.snapToIndex(itemCount = itemCount, state = state) + state.onPreviouslySelectedChange(selectedIndex) + } + } + + ProvideTextStyle(textStyle) { + textPickerContent.Content( + textForIndex = rememberTextForIndex, + item = pickerItem, + moveUnsafeToProperIndex = moveUnsafeToProperIndex, + selected = moveUnsafeToProperIndex(selectedIndex + state.indexOffset), + before1TranslatePercent = state.before1TranslatePercent, + itemTranslatePercent = state.itemTranslatePercent, + after1TranslatePercent = state.after1TranslatePercent, + after2TranslatePercent = state.after2TranslatePercent + ) + } + }, + measurePolicy = textPickerMeasurePolicy + ) +} + +fun interface TextPickerContent { + @Composable + fun Content( + textForIndex: (Int) -> String, + selected: Int, + moveUnsafeToProperIndex: (Int) -> Int, + before1TranslatePercent: Float, + itemTranslatePercent: Float, + after1TranslatePercent: Float, + after2TranslatePercent: Float, + item: @Composable (text: String, translation: Float) -> Unit + ) +} diff --git a/picker/src/main/java/org/fknives/android/compose/picker/text/TextPickerAnimator.kt b/picker/src/main/java/org/fknives/android/compose/picker/text/TextPickerAnimator.kt new file mode 100644 index 0000000..72f7ada --- /dev/null +++ b/picker/src/main/java/org/fknives/android/compose/picker/text/TextPickerAnimator.kt @@ -0,0 +1,117 @@ +package org.fknives.android.compose.picker.text + +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.DecayAnimationSpec +import androidx.compose.animation.core.animate +import androidx.compose.animation.core.calculateTargetValue +import androidx.compose.animation.core.spring +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import org.fknives.android.compose.picker.text.util.OffsetLimiter +import org.fknives.android.compose.picker.text.util.TextPickerDefaults +import kotlin.math.abs +import kotlin.math.roundToInt + +class TextPickerAnimator( + private val flingDecaySpec: DecayAnimationSpec, + private val flingAnimationSpec: AnimationSpec = spring(0.75f), + private val velocityMultiplier: Float, + private val offsetLimiter: OffsetLimiter, + private val onIndexDifferenceChanging: (Int) -> Unit = TextPickerDefaults.onIndexDifferenceChanging +) { + + private var animationJob by mutableStateOf(null) + + fun onDeltaY(state: TextPickerState, deltaY: Float) { + modifyOffset(state, state.offset + deltaY) + } + + suspend fun snapToIndex(itemCount: Int, state: TextPickerState) { + if (state.previousSelected == state.selected) return + val scrollingIndexChange = findIndexChangeForProperScrollingAnimation( + fromIndex = state.previousSelected, + toIndex = state.selected, + itemCount = itemCount + ) + val offset = scrollingIndexChange * state.itemSize + animateOffset(initialValue = offset, targetValue = 0f, state = state) + } + + fun flingAndSnap(scope: CoroutineScope, state: TextPickerState, rawVelocity: Float, onIndexChange: (Int) -> Unit) { + val velocity = rawVelocity * velocityMultiplier + val flingOffsetValue = flingDecaySpec.calculateTargetValue(0f, velocity) + val endFlingValue = state.offset + flingOffsetValue + val limitedEndFlingValue = offsetLimiter.limit(endFlingValue, state) + val changeIndex = -(limitedEndFlingValue / state.itemSize).roundToInt() + val offsetTarget = -changeIndex * state.itemSize + animationJob?.cancel() + animationJob = scope.launch { + animateOffset(initialValue = state.offset, targetValue = offsetTarget, velocity = velocity, state = state) + onIndexChange(changeIndex) + modifyOffset(state, 0f) + } + } + + private suspend fun animateOffset(state: TextPickerState, initialValue: Float, targetValue: Float, velocity: Float = 0f) { + animate( + initialValue = initialValue, + targetValue = targetValue, + animationSpec = flingAnimationSpec, + initialVelocity = velocity + ) { value, _ -> + modifyOffset(state, value) + } + } + + private fun modifyOffset(state: TextPickerState, offset: Float) { + val animationLimitedOffset = offsetLimiter.overshootLimit(offset, state) + state.onOffsetChange(animationLimitedOffset) + if (onIndexDifferenceChanging !== TextPickerDefaults.onIndexDifferenceChanging) { + val hardLimitedOffset = offsetLimiter.limit(offset, state) + val fullIndex = (-hardLimitedOffset / state.itemSize).toInt() + val halfIndexChange = ((-hardLimitedOffset - fullIndex * state.itemSize)*2 / state.itemSize).toInt() + val index = halfIndexChange + fullIndex + onIndexDifferenceChanging(index) + } + } + + companion object { + + private fun findIndexChangeForProperScrollingAnimation(fromIndex: Int, toIndex: Int, itemCount: Int): Int { + val indexDifference = toIndex - fromIndex + return if (shouldNotScrollByDistance(indexDifference = indexDifference)) { + 0 + } else if (shouldScrollUpByDistance(indexDifference = indexDifference, itemCount = itemCount)) { + calculateIndexChangeScrollingUp(indexDifference = indexDifference, itemCount = itemCount) + } else { + calculateIndexChangeScrollingDown(indexDifference = indexDifference, itemCount = itemCount) + } + } + + private fun shouldNotScrollByDistance(indexDifference: Int) = indexDifference == 0 + + private fun calculateIndexChangeScrollingUp(indexDifference: Int, itemCount: Int): Int = + if (indexDifference < 0) { + indexDifference + } else { + indexDifference - itemCount + } + + private fun calculateIndexChangeScrollingDown(indexDifference: Int, itemCount: Int): Int = + if (indexDifference > 0) { + indexDifference + } else { + indexDifference + itemCount + } + + private fun shouldScrollUpByDistance(indexDifference: Int, itemCount: Int): Boolean { + val scrollUpIndexDifference = calculateIndexChangeScrollingUp(indexDifference = indexDifference, itemCount = itemCount) + val scrollUpDistance = abs(scrollUpIndexDifference) + return scrollUpDistance < itemCount / 2 + } + } +} \ No newline at end of file diff --git a/picker/src/main/java/org/fknives/android/compose/picker/text/TextPickerState.kt b/picker/src/main/java/org/fknives/android/compose/picker/text/TextPickerState.kt new file mode 100644 index 0000000..b1d407c --- /dev/null +++ b/picker/src/main/java/org/fknives/android/compose/picker/text/TextPickerState.kt @@ -0,0 +1,48 @@ +package org.fknives.android.compose.picker.text + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue + +class TextPickerState( + itemSizeState: MutableState, + previousSelectedState: MutableState, + val selected: Int, + val itemCount: Int +) { + var offset by mutableStateOf(0f) + private set + + var itemSize by itemSizeState + private set + + var previousSelected by previousSelectedState + private set + + val translateOffset by derivedStateOf { + (offset % itemSize) - (animationShiftOffset * itemSize) + } + val indexOffset by derivedStateOf { + (-offset / itemSize).toInt() - animationShiftOffset + } + private val animationShiftOffset get() = if (offset > 0) 1 else 0 + + val itemTranslatePercent by derivedStateOf { translateOffset / itemSize / 2f + 1f } + val before1TranslatePercent by derivedStateOf { itemTranslatePercent - 0.5f } + val after1TranslatePercent by derivedStateOf { 1.5f - itemTranslatePercent } + val after2TranslatePercent by derivedStateOf { 1f - itemTranslatePercent } + + fun onItemSizeChange(itemSize: Float) { + this.itemSize = itemSize + } + + fun onPreviouslySelectedChange(index: Int) { + previousSelected = index + } + + fun onOffsetChange(offset: Float) { + this.offset = offset + } +} diff --git a/picker/src/main/java/org/fknives/android/compose/picker/text/content/DefaultTextPickerContent.kt b/picker/src/main/java/org/fknives/android/compose/picker/text/content/DefaultTextPickerContent.kt new file mode 100644 index 0000000..995f92f --- /dev/null +++ b/picker/src/main/java/org/fknives/android/compose/picker/text/content/DefaultTextPickerContent.kt @@ -0,0 +1,39 @@ +package org.fknives.android.compose.picker.text.content + +import androidx.compose.runtime.Composable +import org.fknives.android.compose.picker.text.TextPickerContent + +class DefaultTextPickerContent : TextPickerContent { + + @Composable + override fun Content( + textForIndex: (Int) -> String, + selected: Int, + moveUnsafeToProperIndex: (Int) -> Int, + before1TranslatePercent: Float, + itemTranslatePercent: Float, + after1TranslatePercent: Float, + after2TranslatePercent: Float, + item: @Composable (text: String, translation: Float) -> Unit + ) { + val before1 = moveUnsafeToProperIndex(selected - 1) + val after1 = moveUnsafeToProperIndex(selected + 1) + val after2 = moveUnsafeToProperIndex(selected + 2) + item( + text = textForIndex(before1), + translation = before1TranslatePercent + ) + item( + text = textForIndex(selected), + translation = itemTranslatePercent + ) + item( + text = textForIndex(after1), + translation = after1TranslatePercent + ) + item( + text = textForIndex(after2), + translation = after2TranslatePercent + ) + } +} \ No newline at end of file diff --git a/picker/src/main/java/org/fknives/android/compose/picker/text/content/DefaultTextPickerLabel.kt b/picker/src/main/java/org/fknives/android/compose/picker/text/content/DefaultTextPickerLabel.kt new file mode 100644 index 0000000..2a524f6 --- /dev/null +++ b/picker/src/main/java/org/fknives/android/compose/picker/text/content/DefaultTextPickerLabel.kt @@ -0,0 +1,39 @@ +package org.fknives.android.compose.picker.text.content + +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.text.style.TextAlign +import org.fknives.android.compose.picker.text.util.TextPickerDefaults + +@Composable +fun DefaultNumberPickerText( + text: String, + modifier: Modifier +) { + Text( + text = text, + modifier = modifier, + textAlign = TextAlign.Center + ) +} + +fun defaultNumberPickerTextAlphaModifier( + modifier: Modifier = Modifier, + unselectedAlpha: Float = TextPickerDefaults.unselectedAlpha, + selectedAlpha: Float = TextPickerDefaults.selectedAlpha, + numberPickerLabel: @Composable (text: String, alpha: Float) -> Unit = { text, alpha -> + DefaultNumberPickerText(modifier = modifier.alpha(alpha), text = text) + } +): @Composable (String, Float) -> Unit = { text: String, translation: Float -> + val calculatedAlpha = if (translation < 0.5f) { + val between0And1Proportionally = (translation * 2) + between0And1Proportionally * unselectedAlpha + } else { + val between0And1Proportionally = (translation - 0.5f) * 2 + between0And1Proportionally * (selectedAlpha - unselectedAlpha) + unselectedAlpha + } + + numberPickerLabel(alpha = calculatedAlpha, text = text) +} \ No newline at end of file diff --git a/picker/src/main/java/org/fknives/android/compose/picker/text/content/DefaultTextPickerMeasurePolicy.kt b/picker/src/main/java/org/fknives/android/compose/picker/text/content/DefaultTextPickerMeasurePolicy.kt new file mode 100644 index 0000000..f99bff1 --- /dev/null +++ b/picker/src/main/java/org/fknives/android/compose/picker/text/content/DefaultTextPickerMeasurePolicy.kt @@ -0,0 +1,43 @@ +package org.fknives.android.compose.picker.text.content + +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.MeasurePolicy +import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.MeasureScope +import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.unit.Constraints +import org.fknives.android.compose.picker.text.TextPickerState + +open class DefaultTextPickerMeasurePolicy(private val state: TextPickerState) : MeasurePolicy { + + open val numberOfItemsToPlace = 4 + open val numberOfHeightMeasuringItems = 3 + + override fun MeasureScope.measure(measurables: List, constraints: Constraints): MeasureResult { + val placeables = measurables.map { measurable -> + measurable.measure(constraints) + } + + val placeablesHeight = measureHeight(placeables) + val minWidth = measureWidth(placeables) + state.onItemSizeChange(placeablesHeight / numberOfHeightMeasuringItems.toFloat()) + + return layout(minWidth, placeablesHeight) { + var yPosition = state.translateOffset.toInt() + + placeables.take(numberOfItemsToPlace).forEach { placeable -> + placeable.placeRelative(x = 0, y = yPosition) + yPosition += placeable.height + } + placementAfterItems(placeables, state) + } + } + + open fun Placeable.PlacementScope.placementAfterItems(placeables: List, state: TextPickerState) = Unit + + open fun measureWidth(placeables: List) = + placeables.take(numberOfItemsToPlace).maxOf { it.measuredWidth } + + open fun measureHeight(placeables: List) = + placeables.take(numberOfHeightMeasuringItems).sumOf { it.measuredHeight } +} \ No newline at end of file diff --git a/picker/src/main/java/org/fknives/android/compose/picker/text/content/LinedContent.kt b/picker/src/main/java/org/fknives/android/compose/picker/text/content/LinedContent.kt new file mode 100644 index 0000000..bccec77 --- /dev/null +++ b/picker/src/main/java/org/fknives/android/compose/picker/text/content/LinedContent.kt @@ -0,0 +1,75 @@ +package org.fknives.android.compose.picker.text.content + +import androidx.compose.material.Divider +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.unit.Dp +import org.fknives.android.compose.picker.text.TextPickerContent +import org.fknives.android.compose.picker.text.util.TextPickerDefaults +import org.fknives.android.compose.picker.text.TextPickerState + +class LinedTextPickerContent( + private val dividerModifier: Modifier = Modifier, + private val dividerColor: @Composable () -> Color = { MaterialTheme.colors.onSurface.copy(alpha = TextPickerDefaults.dividerAlpha) }, + private val dividerThickness: Dp = TextPickerDefaults.dividerThickness, + private val dividerStartIndent: Dp = TextPickerDefaults.dividerStartIndent +) : TextPickerContent { + + @Composable + override fun Content( + textForIndex: (Int) -> String, + selected: Int, + moveUnsafeToProperIndex: (Int) -> Int, + before1TranslatePercent: Float, + itemTranslatePercent: Float, + after1TranslatePercent: Float, + after2TranslatePercent: Float, + item: @Composable (text: String, translation: Float) -> Unit + ) { + val before1 = moveUnsafeToProperIndex(selected - 1) + val after1 = moveUnsafeToProperIndex(selected + 1) + val after2 = moveUnsafeToProperIndex(selected + 2) + item( + text = textForIndex(before1), + translation = before1TranslatePercent + ) + item( + text = textForIndex(selected), + translation = itemTranslatePercent + ) + item( + text = textForIndex(after1), + translation = after1TranslatePercent + ) + item( + text = textForIndex(after2), + translation = after2TranslatePercent + ) + Divider( + modifier = dividerModifier, + color = dividerColor(), + thickness = dividerThickness, + startIndent = dividerStartIndent, + ) + Divider( + modifier = dividerModifier, + color = dividerColor(), + thickness = dividerThickness, + startIndent = dividerStartIndent, + ) + } +} + +class LinedTextPickerMeasurePolicy(state: TextPickerState) : DefaultTextPickerMeasurePolicy(state = state) { + + override fun Placeable.PlacementScope.placementAfterItems(placeables: List, state: TextPickerState) { + val dividers = placeables.drop(numberOfItemsToPlace) + dividers.forEachIndexed { index, placeable -> + val y = ((index + 1) * state.itemSize) - placeable.measuredHeight / 2f + placeable.placeRelative(x = 0, y = y.toInt()) + } + } +} \ No newline at end of file diff --git a/picker/src/main/java/org/fknives/android/compose/picker/text/util/LapCounterByIndexOfDifference.kt b/picker/src/main/java/org/fknives/android/compose/picker/text/util/LapCounterByIndexOfDifference.kt new file mode 100644 index 0000000..19b186a --- /dev/null +++ b/picker/src/main/java/org/fknives/android/compose/picker/text/util/LapCounterByIndexOfDifference.kt @@ -0,0 +1,36 @@ +package org.fknives.android.compose.picker.text.util + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember + +fun createLapsCounterByOnIndexDifference(itemCount: Int, selectedIndex: Int): (Int) -> Int = { indexDifference -> + lapCounterByIndexOfDifference(itemCount = itemCount, indexDifference = indexDifference, selectedIndex = selectedIndex) +} + +fun lapCounterByIndexOfDifference( + itemCount: Int, + selectedIndex: Int, + indexDifference: Int +): Int { + var changedIndex = selectedIndex + indexDifference + var counter = 0 + if (changedIndex < 0) { + counter-- + while (changedIndex <= -itemCount) { + counter-- + changedIndex += itemCount + } + } else { + while (changedIndex >= itemCount) { + counter++ + changedIndex -= itemCount + } + } + + return counter +} + +@Composable +fun rememberLapsCounterByOnIndexDifference(selectedIndex: Int, itemCount: Int) = remember(selectedIndex, itemCount) { + createLapsCounterByOnIndexDifference(selectedIndex = selectedIndex, itemCount = itemCount) +} \ No newline at end of file diff --git a/picker/src/main/java/org/fknives/android/compose/picker/text/util/OffsetLimiter.kt b/picker/src/main/java/org/fknives/android/compose/picker/text/util/OffsetLimiter.kt new file mode 100644 index 0000000..903db30 --- /dev/null +++ b/picker/src/main/java/org/fknives/android/compose/picker/text/util/OffsetLimiter.kt @@ -0,0 +1,42 @@ +package org.fknives.android.compose.picker.text.util + +import org.fknives.android.compose.picker.text.TextPickerState +import kotlin.math.max +import kotlin.math.min + +fun interface OffsetLimiter { + + fun limit(offset: Float, state: TextPickerState): Float + + fun overshootLimit(offset: Float, state: TextPickerState): Float = + limit(offset, state) + + class NoLimit : OffsetLimiter { + override fun limit(offset: Float, state: TextPickerState): Float = offset + } + + class MinMaxLimit : OffsetLimiter { + + override fun limit(offset: Float, state: TextPickerState): Float { + val max = maximalOffset(state) + val min = minimalOffset(state) + return max(min(offset, max), min) + } + + private fun maximalOffset(state: TextPickerState) = state.selected * state.itemSize + + private fun minimalOffset(state: TextPickerState) = (state.selected + 1 - state.itemCount) * state.itemSize + + override fun overshootLimit(offset: Float, state: TextPickerState): Float { + val max = maximalOffset(state) + state.itemSize / 2 + val min = minimalOffset(state) - state.itemSize / 2 + return max(min(offset, max), min) + } + } + + companion object { + + fun default(roundAround: Boolean): OffsetLimiter = + if (roundAround) NoLimit() else MinMaxLimit() + } +} \ No newline at end of file diff --git a/picker/src/main/java/org/fknives/android/compose/picker/text/util/TextPickerDefaults.kt b/picker/src/main/java/org/fknives/android/compose/picker/text/util/TextPickerDefaults.kt new file mode 100644 index 0000000..c17f8ff --- /dev/null +++ b/picker/src/main/java/org/fknives/android/compose/picker/text/util/TextPickerDefaults.kt @@ -0,0 +1,15 @@ +package org.fknives.android.compose.picker.text.util + +import androidx.compose.ui.unit.dp + +object TextPickerDefaults { + const val selected = 0 + const val velocityMultiplier = 0.3f + const val unselectedAlpha = 0.6f + const val selectedAlpha = 1f + internal val onIndexDifferenceChanging: (Int) -> Unit = {} + const val roundAround = true + const val dividerAlpha = 0.12f + val dividerThickness = 2.dp + val dividerStartIndent = 0.dp +} \ No newline at end of file diff --git a/picker/src/main/java/org/fknives/android/compose/picker/text/util/TextPickerStateHelper.kt b/picker/src/main/java/org/fknives/android/compose/picker/text/util/TextPickerStateHelper.kt new file mode 100644 index 0000000..5ef3d74 --- /dev/null +++ b/picker/src/main/java/org/fknives/android/compose/picker/text/util/TextPickerStateHelper.kt @@ -0,0 +1,97 @@ +package org.fknives.android.compose.picker.text.util + +import androidx.compose.animation.splineBasedDecay +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Density +import org.fknives.android.compose.picker.text.TextPickerAnimator +import org.fknives.android.compose.picker.text.TextPickerState + +@Composable +fun rememberTextPickerState(selected: Int, itemCount: Int): TextPickerState { + require(selected >= 0) { "Selected value ($selected) is less than 0!" } + require(selected < itemCount) { "Selected value ($selected) is more or equal to ItemCount = $itemCount!" } + + val itemSizeState = remember { mutableStateOf(1f) } + val previousSelected = remember { mutableStateOf(selected) } + return remember(selected, itemCount) { + TextPickerState( + itemSizeState = itemSizeState, + previousSelectedState = previousSelected, + selected = selected, + itemCount = itemCount + ) + } +} + +@Composable +fun rememberTextPickerAnimator( + roundAround: Boolean, + onIndexDifferenceChanging: (Int) -> Unit = TextPickerDefaults.onIndexDifferenceChanging, + velocityMultiplier: Float = TextPickerDefaults.velocityMultiplier, + density: Density = LocalDensity.current, +): TextPickerAnimator { + require(velocityMultiplier > 0) { "0 Velocity would not work" } + val offsetLimiter = remember(roundAround) { OffsetLimiter.default(roundAround) } + return rememberTextPickerAnimator( + offsetLimiter = offsetLimiter, + velocityMultiplier = velocityMultiplier, + density = density, + onIndexDifferenceChanging = onIndexDifferenceChanging + ) +} + +@Composable +fun rememberTextPickerAnimator( + offsetLimiter: OffsetLimiter, + velocityMultiplier: Float = TextPickerDefaults.velocityMultiplier, + density: Density = LocalDensity.current, + onIndexDifferenceChanging: (Int) -> Unit = TextPickerDefaults.onIndexDifferenceChanging +): TextPickerAnimator { + require(velocityMultiplier > 0) { "0 Velocity would not work" } + return remember(offsetLimiter, velocityMultiplier, density, onIndexDifferenceChanging) { + TextPickerAnimator( + offsetLimiter = offsetLimiter, + flingDecaySpec = splineBasedDecay(density), + velocityMultiplier = velocityMultiplier, + onIndexDifferenceChanging = onIndexDifferenceChanging + ) + } +} + +@Composable +fun rememberDefaultMoveUnsafeToProperIndex(itemCount: Int, roundAround: Boolean) = + if (roundAround) { + remember(itemCount) { createMoveUnsafeToProperIndex(itemCount = itemCount) } + } else { + remember { { index: Int -> index } } + } + + +fun createMoveUnsafeToProperIndex(itemCount: Int) = moveUnsafeToProperIndex@{ unsafeIndex: Int -> + moveUnsafeToProperIndex(unsafeIndex, itemCount) +} + +fun moveUnsafeToProperIndex(unsafeIndex: Int, itemCount: Int) : Int { + var index = unsafeIndex + while (index < 0) { + index += itemCount + } + while (index >= itemCount) { + index -= itemCount + } + return index +} + +@Composable +fun rememberWrappedTextForIndex(itemCount: Int, roundAround: Boolean, textForIndex: (Int) -> String): (Int) -> String = + if (roundAround) { + textForIndex + } else { + remember(itemCount, textForIndex) { + { index -> if (index < 0 || index >= itemCount) "" else textForIndex(index) } + } + } + diff --git a/picker/src/main/java/org/fknives/android/compose/picker/time/DefaultAMPMList.kt b/picker/src/main/java/org/fknives/android/compose/picker/time/DefaultAMPMList.kt new file mode 100644 index 0000000..2f5a3d4 --- /dev/null +++ b/picker/src/main/java/org/fknives/android/compose/picker/time/DefaultAMPMList.kt @@ -0,0 +1,14 @@ +package org.fknives.android.compose.picker.time + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import java.text.DateFormatSymbols +import java.util.Locale + +fun defaultAmPmStrings(locale: Locale = Locale.getDefault()) = + DateFormatSymbols.getInstance(locale).amPmStrings.toList() + +@Composable +fun rememberDefaultAMPMList(locale: Locale = Locale.getDefault()) = remember(locale) { + DateFormatSymbols.getInstance(locale).amPmStrings.toList() +} \ No newline at end of file diff --git a/picker/src/main/java/org/fknives/android/compose/picker/time/SelectedTime.kt b/picker/src/main/java/org/fknives/android/compose/picker/time/SelectedTime.kt new file mode 100644 index 0000000..86df882 --- /dev/null +++ b/picker/src/main/java/org/fknives/android/compose/picker/time/SelectedTime.kt @@ -0,0 +1,28 @@ +package org.fknives.android.compose.picker.time + +import androidx.compose.runtime.Immutable +import java.util.Calendar + +@Immutable +data class SelectedTime( + val hour: Int, + val minute: Int, + val isAM: Boolean +) { + + companion object { + fun get(time: Long = System.currentTimeMillis()): SelectedTime { + val calendar = Calendar.getInstance() + calendar.timeInMillis = time + val hour = calendar.get(Calendar.HOUR) + val minute = calendar.get(Calendar.MINUTE) + val isAm = calendar.get(Calendar.AM_PM) == Calendar.AM + + return SelectedTime( + hour = if (hour == 0) 12 else hour, + minute = minute, + isAM = isAm + ) + } + } +} diff --git a/picker/src/main/java/org/fknives/android/compose/picker/time/TimePicker.kt b/picker/src/main/java/org/fknives/android/compose/picker/time/TimePicker.kt new file mode 100644 index 0000000..4bc5303 --- /dev/null +++ b/picker/src/main/java/org/fknives/android/compose/picker/time/TimePicker.kt @@ -0,0 +1,54 @@ +package org.fknives.android.compose.picker.time + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import org.fknives.android.compose.picker.number.NumberPickerConfig +import org.fknives.android.compose.picker.number.rememberNumberPickerScope +import org.fknives.android.compose.picker.number.rememberNumberPickerState + +@Composable +fun TimePicker( + timePickersMinWidth: Dp = 40.dp, + selectedTime: SelectedTime, + amPm: List = rememberDefaultAMPMList(), + onSelectedTimeChanged: (SelectedTime) -> Unit, + timePickers: @Composable (TimePickerScope) -> Unit = { StandardTimePickers(it) } +) { + val hourConfig = remember { NumberPickerConfig.configHourPicker12 } + val hourState = rememberNumberPickerState(selectedValue = selectedTime.hour, config = hourConfig) + val hourScope = rememberNumberPickerScope( + state = hourState, + config = hourConfig, + onSelectedChange = { + onSelectedTimeChanged(selectedTime.copy(hour = it)) + } + ) + + val minuteConfig = remember { NumberPickerConfig.configMinutePicker } + val minuteState = rememberNumberPickerState(selectedValue = selectedTime.minute, config = minuteConfig) + val minuteScope = rememberNumberPickerScope( + state = minuteState, + config = minuteConfig, + onSelectedChange = { + onSelectedTimeChanged(selectedTime.copy(minute = it)) + } + ) + + val amORpmScope = rememberAMorPMPickerScope( + listOfAMorPM = amPm, + isAM = selectedTime.isAM, + onAMSelectedChange = { + onSelectedTimeChanged(selectedTime.copy(isAM = it)) + }) + + val scope = rememberTimePickerScope( + timePickerMinWidth = timePickersMinWidth, + hoursPickerScope = hourScope, + minutesPickerScope = minuteScope, + amORpmPickerScope = amORpmScope + ) + + timePickers(scope) +} \ No newline at end of file diff --git a/picker/src/main/java/org/fknives/android/compose/picker/time/TimePickerScope.kt b/picker/src/main/java/org/fknives/android/compose/picker/time/TimePickerScope.kt new file mode 100644 index 0000000..2804b9b --- /dev/null +++ b/picker/src/main/java/org/fknives/android/compose/picker/time/TimePickerScope.kt @@ -0,0 +1,73 @@ +package org.fknives.android.compose.picker.time + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.remember +import androidx.compose.ui.unit.Dp +import org.fknives.android.compose.picker.number.NumberPickerScope +import org.fknives.android.compose.picker.text.util.TextPickerDefaults + +@Immutable +interface TimePickerScope { + val timePickerMinWidth: Dp + val hoursPickerScope: NumberPickerScope + val minutesPickerScope: NumberPickerScope + val amORpmPickerScope: AMorPMPickerScope +} + +@Immutable +data class TimePickerScopeImpl( + override val timePickerMinWidth: Dp, + override val hoursPickerScope: NumberPickerScope, + override val minutesPickerScope: NumberPickerScope, + override val amORpmPickerScope: AMorPMPickerScope +) : TimePickerScope + +@Immutable +interface AMorPMPickerScope { + val listOfAMorPM: List + val isAM: Boolean + val onAMSelectedChange: (Boolean) -> Unit + val onIndexDifferenceChanging: (Int) -> Unit +} + +@Immutable +data class AMorPMPickerScopeImpl( + override val listOfAMorPM: List, + override val isAM: Boolean, + override val onAMSelectedChange: (Boolean) -> Unit, + override val onIndexDifferenceChanging: (Int) -> Unit = TextPickerDefaults.onIndexDifferenceChanging +) : AMorPMPickerScope + +@Composable +fun rememberAMorPMPickerScope( + listOfAMorPM: List, + isAM: Boolean, + onAMSelectedChange: (Boolean) -> Unit, + onIndexDifferenceChanging: (Int) -> Unit = TextPickerDefaults.onIndexDifferenceChanging +): AMorPMPickerScope { + + return remember(listOfAMorPM, isAM, onAMSelectedChange, onIndexDifferenceChanging) { + AMorPMPickerScopeImpl( + listOfAMorPM = listOfAMorPM, + isAM = isAM, + onAMSelectedChange = onAMSelectedChange, + onIndexDifferenceChanging = onIndexDifferenceChanging + ) + } +} + +@Composable +fun rememberTimePickerScope( + timePickerMinWidth: Dp, + hoursPickerScope: NumberPickerScope, + minutesPickerScope: NumberPickerScope, + amORpmPickerScope: AMorPMPickerScope +) = remember(timePickerMinWidth, hoursPickerScope, minutesPickerScope, amORpmPickerScope) { + TimePickerScopeImpl( + timePickerMinWidth = timePickerMinWidth, + hoursPickerScope = hoursPickerScope, + minutesPickerScope = minutesPickerScope, + amORpmPickerScope = amORpmPickerScope + ) +} \ No newline at end of file diff --git a/picker/src/main/java/org/fknives/android/compose/picker/time/TimePickerScopeExtensions.kt b/picker/src/main/java/org/fknives/android/compose/picker/time/TimePickerScopeExtensions.kt new file mode 100644 index 0000000..daf64ac --- /dev/null +++ b/picker/src/main/java/org/fknives/android/compose/picker/time/TimePickerScopeExtensions.kt @@ -0,0 +1,72 @@ +package org.fknives.android.compose.picker.time + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.material.LocalTextStyle +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import org.fknives.android.compose.picker.number.CustomInnerTextPicker +import org.fknives.android.compose.picker.text.TextPicker +import org.fknives.android.compose.picker.text.util.TextPickerDefaults + +@Composable +fun StandardTimePickers(scope: TimePickerScope) { + Row { + scope.apply { + HourPicker() + MinutePicker() + IsAMorPMPicker() + } + } +} + +@Composable +fun TimePickerScope.HourPicker( + modifier: Modifier = Modifier, + textStyle: TextStyle = LocalTextStyle.current, + roundAround: Boolean = TextPickerDefaults.roundAround +) { + hoursPickerScope.apply { + CustomInnerTextPicker( + modifier = modifier.defaultMinSize(timePickerMinWidth), + textStyle = textStyle, + roundAround = roundAround + ) + } +} + +@Composable +fun TimePickerScope.MinutePicker( + modifier: Modifier = Modifier, + textStyle: TextStyle = LocalTextStyle.current, + roundAround: Boolean = TextPickerDefaults.roundAround +) { + minutesPickerScope.apply { + CustomInnerTextPicker( + modifier = modifier.defaultMinSize(timePickerMinWidth), + textStyle = textStyle, + roundAround = roundAround + ) + } +} + +@Composable +fun TimePickerScope.IsAMorPMPicker( + modifier: Modifier = Modifier, + textStyle: TextStyle = LocalTextStyle.current, + roundAround: Boolean = false +) { + amORpmPickerScope.apply { + TextPicker( + modifier = modifier.defaultMinSize(timePickerMinWidth), + textStyle = textStyle, + roundAround = roundAround, + textForIndex = listOfAMorPM::get, + itemCount = listOfAMorPM.size, + selectedIndex = if (isAM) 0 else 1, + onSelectedIndexChange = { onAMSelectedChange(it == 0) }, + onIndexDifferenceChanging = onIndexDifferenceChanging, + ) + } +} diff --git a/picker/src/main/java/org/fknives/android/compose/picker/time/clock/ClockPicker.kt b/picker/src/main/java/org/fknives/android/compose/picker/time/clock/ClockPicker.kt new file mode 100644 index 0000000..6226f77 --- /dev/null +++ b/picker/src/main/java/org/fknives/android/compose/picker/time/clock/ClockPicker.kt @@ -0,0 +1,97 @@ +package org.fknives.android.compose.picker.time.clock + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import org.fknives.android.compose.picker.number.NumberPickerConfig +import org.fknives.android.compose.picker.number.rememberNumberPickerScope +import org.fknives.android.compose.picker.number.rememberNumberPickerState +import org.fknives.android.compose.picker.text.util.rememberLapsCounterByOnIndexDifference +import org.fknives.android.compose.picker.time.SelectedTime +import org.fknives.android.compose.picker.time.StandardTimePickers +import org.fknives.android.compose.picker.time.TimePickerScope +import org.fknives.android.compose.picker.time.rememberAMorPMPickerScope +import org.fknives.android.compose.picker.time.rememberDefaultAMPMList +import org.fknives.android.compose.picker.time.rememberTimePickerScope +import kotlin.math.abs + +@Composable +fun ClockTimePicker( + timePickerMinWidth: Dp = 40.dp, + selectedTime: SelectedTime, + amPm: List = rememberDefaultAMPMList(), + onSelectedTimeChanged: (SelectedTime) -> Unit, + timePickers: @Composable (TimePickerScope) -> Unit = { StandardTimePickers(it) } +) { + var changingIsAM by remember(selectedTime.isAM) { mutableStateOf(selectedTime.isAM) } + var changingHour by remember(selectedTime.hour) { mutableStateOf(0) } + val hoursLapCounterByIndex = rememberLapsCounterByOnIndexDifference(selectedIndex = selectedTime.hour - 1, itemCount = 12) + val minutesLapCounterByIndex = rememberLapsCounterByOnIndexDifference(selectedIndex = selectedTime.minute, itemCount = 60) + + val hourConfig = remember { NumberPickerConfig.configHourPicker12 } + val hourPickerState = rememberNumberPickerState(selectedValue = selectedTime.hour + changingHour, config = hourConfig) + val hourScope = rememberNumberPickerScope( + state = hourPickerState, + config = hourConfig, + onIndexDifferenceChanging = { + if (changingHour == 0) { + val lapsCounter = hoursLapCounterByIndex(it) + changingIsAM = isAMResultByHourLapsCounter(lapsCounter, selectedTime.isAM) + } + }, + onSelectedChange = { + onSelectedTimeChanged(selectedTime.copy(hour = it, isAM = changingIsAM)) + }, + keys = arrayOf(changingHour, hoursLapCounterByIndex, changingIsAM) + ) + val minuteConfig = remember { NumberPickerConfig.configMinutePicker } + val minutePickerState = rememberNumberPickerState(selectedValue = selectedTime.minute, config = minuteConfig) + val minutesScope = rememberNumberPickerScope( + state = minutePickerState, + config = minuteConfig, + onIndexDifferenceChanging = { + val hourIndexDifference = minutesLapCounterByIndex(it) + var hourDifference = hourIndexDifference + while (selectedTime.hour + hourDifference <= 0) { + hourDifference += 12 + } + while (selectedTime.hour + hourDifference > 12) { + hourDifference -= 12 + } + val lapsCounter = hoursLapCounterByIndex(hourIndexDifference) + changingIsAM = isAMResultByHourLapsCounter(lapsCounter, selectedTime.isAM) + changingHour = hourDifference + }, + onSelectedChange = { + onSelectedTimeChanged(selectedTime.copy(hour = selectedTime.hour + changingHour, minute = it, isAM = changingIsAM)) + }, + keys = arrayOf(changingHour, hoursLapCounterByIndex, changingIsAM) + ) + val amORpmScope = rememberAMorPMPickerScope( + listOfAMorPM = amPm, + isAM = changingIsAM, + onAMSelectedChange = { + onSelectedTimeChanged(selectedTime.copy(hour = selectedTime.hour + changingHour, isAM = it)) + } + ) + + val scope = rememberTimePickerScope( + timePickerMinWidth = timePickerMinWidth, + hoursPickerScope = hourScope, + minutesPickerScope = minutesScope, + amORpmPickerScope = amORpmScope + ) + + timePickers(scope) +} + +private fun isAMResultByHourLapsCounter(hourLapCounter: Int, selectedTimeIsAm: Boolean): Boolean = + if (abs(hourLapCounter) % 2 == 0) { + selectedTimeIsAm + } else { + !selectedTimeIsAm + } \ No newline at end of file diff --git a/picker/src/test/java/org/fknives/android/compose/picker/ExampleUnitTest.kt b/picker/src/test/java/org/fknives/android/compose/picker/ExampleUnitTest.kt new file mode 100644 index 0000000..24ddb03 --- /dev/null +++ b/picker/src/test/java/org/fknives/android/compose/picker/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package org.fknives.android.compose.picker + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..4995d36 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,17 @@ +pluginManagement { + repositories { + gradlePluginPortal() + google() + mavenCentral() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} +rootProject.name = "Compose Learning" +include ':app' +include ':picker'