Initial implementation

This commit is contained in:
Gergely Hegedus 2022-06-14 10:24:02 +03:00
parent 256c243ce0
commit 0b4fa9ece7
69 changed files with 2561 additions and 0 deletions

1
app/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

50
app/build.gradle Normal file
View file

@ -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")
}

21
app/proguard-rules.pro vendored Normal file
View file

@ -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

View file

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.DayNight.NoActionBar"
tools:targetApi="31">
<activity
android:name="org.fknives.android.compose.learning.MainActivity"
android:exported="true"
android:configChanges="colorMode|density|fontScale|fontWeightAdjustment|keyboard|keyboardHidden|layoutDirection|locale|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode"
android:label="Compose AWallet">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -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)
}
}
}
}
}

View file

@ -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)
}
)
}

View file

@ -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<Screens?>(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()
}
}
}

View file

@ -0,0 +1,7 @@
package org.fknives.android.compose.learning.navigation
enum class Screens {
TextPicker,
NumberPicker,
TimePicker
}

View file

@ -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()
}
}
}

View file

@ -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 },
)
}
}
}
}
}

View file

@ -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
)
}
}
}
}

View file

@ -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 }
)
}
}
}
}

View file

@ -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
)

View file

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View file

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View file

@ -0,0 +1,3 @@
<resources>
<string name="app_name">Compose Learning</string>
</resources>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older that API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

13
build.gradle Normal file
View file

@ -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"

23
gradle.properties Normal file
View file

@ -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

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View file

@ -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

View file

@ -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"
}

View file

@ -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"
}

185
gradlew vendored Executable file
View file

@ -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" "$@"

89
gradlew.bat vendored Normal file
View file

@ -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

1
picker/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

51
picker/build.gradle Normal file
View file

@ -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"
}

View file

21
picker/proguard-rules.pro vendored Normal file
View file

@ -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

View file

@ -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)
}
}

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View file

@ -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)
}

View file

@ -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
)
}
}

View file

@ -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
}

View file

@ -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
)
}

View file

@ -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)
}

View file

@ -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,
)

View file

@ -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
)
}

View file

@ -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<Float>,
private val flingAnimationSpec: AnimationSpec<Float> = spring(0.75f),
private val velocityMultiplier: Float,
private val offsetLimiter: OffsetLimiter,
private val onIndexDifferenceChanging: (Int) -> Unit = TextPickerDefaults.onIndexDifferenceChanging
) {
private var animationJob by mutableStateOf<Job?>(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
}
}
}

View file

@ -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<Float>,
previousSelectedState: MutableState<Int>,
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
}
}

View file

@ -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
)
}
}

View file

@ -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)
}

View file

@ -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<Measurable>, 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<Placeable>, state: TextPickerState) = Unit
open fun measureWidth(placeables: List<Placeable>) =
placeables.take(numberOfItemsToPlace).maxOf { it.measuredWidth }
open fun measureHeight(placeables: List<Placeable>) =
placeables.take(numberOfHeightMeasuringItems).sumOf { it.measuredHeight }
}

View file

@ -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<Placeable>, 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())
}
}
}

View file

@ -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)
}

View file

@ -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()
}
}

View file

@ -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
}

View file

@ -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) }
}
}

View file

@ -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()
}

View file

@ -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
)
}
}
}

View file

@ -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<String> = 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)
}

View file

@ -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<String>
val isAM: Boolean
val onAMSelectedChange: (Boolean) -> Unit
val onIndexDifferenceChanging: (Int) -> Unit
}
@Immutable
data class AMorPMPickerScopeImpl(
override val listOfAMorPM: List<String>,
override val isAM: Boolean,
override val onAMSelectedChange: (Boolean) -> Unit,
override val onIndexDifferenceChanging: (Int) -> Unit = TextPickerDefaults.onIndexDifferenceChanging
) : AMorPMPickerScope
@Composable
fun rememberAMorPMPickerScope(
listOfAMorPM: List<String>,
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
)
}

View file

@ -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,
)
}
}

View file

@ -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<String> = 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
}

View file

@ -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)
}
}

17
settings.gradle Normal file
View file

@ -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'