├── .gitignore ├── art ├── sample_24hours.png ├── sample_ampm.png ├── sample_list.png ├── sample_number.png └── showcase.gif ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts └── src │ └── main │ └── kotlin │ ├── CommonPlugin.kt │ ├── Versions.kt │ └── XProject.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── lib ├── build.gradle.kts ├── rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── com │ └── chargemap │ └── compose │ └── numberpicker │ ├── HoursNumberPicker.kt │ ├── ListItemPicker.kt │ └── NumberPicker.kt ├── publish.sh ├── readme.md ├── sample ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── kotlin │ └── com │ │ └── chargemap │ │ └── android │ │ └── sample │ │ ├── MainActivity.kt │ │ ├── MainActivityUI.kt │ │ └── PreviewComponents.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── layout │ └── activity.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ ├── colors.xml │ ├── strings.xml │ └── themes.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.apk 2 | *.ap_ 3 | 4 | **/generated.* 5 | *_generated.* 6 | 7 | *.iml 8 | *.dex 9 | *.class 10 | bin/ 11 | gen/ 12 | .gradle/ 13 | build/ 14 | 15 | local.properties 16 | proguard/ 17 | .metadata/ 18 | *.DS_Store 19 | Thumbs.db 20 | 21 | .idea/ 22 | report.xml 23 | -------------------------------------------------------------------------------- /art/sample_24hours.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChargeMap/Compose-NumberPicker/cd71609331c2285f392c7561a3cf6bea5b422a72/art/sample_24hours.png -------------------------------------------------------------------------------- /art/sample_ampm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChargeMap/Compose-NumberPicker/cd71609331c2285f392c7561a3cf6bea5b422a72/art/sample_ampm.png -------------------------------------------------------------------------------- /art/sample_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChargeMap/Compose-NumberPicker/cd71609331c2285f392c7561a3cf6bea5b422a72/art/sample_list.png -------------------------------------------------------------------------------- /art/sample_number.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChargeMap/Compose-NumberPicker/cd71609331c2285f392c7561a3cf6bea5b422a72/art/sample_number.png -------------------------------------------------------------------------------- /art/showcase.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChargeMap/Compose-NumberPicker/cd71609331c2285f392c7561a3cf6bea5b422a72/art/showcase.gif -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | 7 | dependencies { 8 | classpath("com.android.tools.build:gradle:4.1.3") 9 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}") 10 | classpath("com.vanniktech:gradle-maven-publish-plugin:0.15.1") 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | task("clean", Delete::class) { 22 | delete = setOf(rootProject.buildDir) 23 | } -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `java-gradle-plugin` 3 | `kotlin-dsl` 4 | } 5 | 6 | gradlePlugin { 7 | plugins { 8 | register("common") { 9 | id = "common" 10 | implementationClass = "CommonPlugin" 11 | } 12 | } 13 | } 14 | 15 | repositories { 16 | google() 17 | jcenter() 18 | } 19 | 20 | dependencies { 21 | implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10") 22 | implementation("com.android.tools.build:gradle:7.1.2") 23 | } -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/CommonPlugin.kt: -------------------------------------------------------------------------------- 1 | import com.android.build.gradle.BaseExtension 2 | import org.gradle.api.JavaVersion 3 | import org.gradle.api.Plugin 4 | import org.gradle.api.Project 5 | 6 | class CommonPlugin : Plugin { 7 | 8 | override fun apply(project: Project) { 9 | project.extensions.findByType(BaseExtension::class.java)?.apply { 10 | compileSdkVersion(31) 11 | 12 | defaultConfig { 13 | versionCode = project.gradleProperties.getProperty("VERSION_CODE").toString().toInt() 14 | versionName = project.gradleProperties.getProperty("VERSION_NAME").toString() 15 | minSdk = 21 16 | targetSdk = 31 17 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 18 | vectorDrawables.useSupportLibrary = true 19 | } 20 | 21 | listOf( 22 | "debug", 23 | "test", 24 | "release", 25 | "androidTest", 26 | "main" 27 | ).forEach { 28 | sourceSets.getByName(it).java.srcDirs("src/$it/kotlin") 29 | } 30 | 31 | compileOptions.sourceCompatibility = JavaVersion.VERSION_1_8 32 | compileOptions.targetCompatibility = JavaVersion.VERSION_1_8 33 | 34 | testOptions.unitTests.isReturnDefaultValues = false 35 | 36 | composeOptions { 37 | kotlinCompilerExtensionVersion = Versions.compose 38 | } 39 | 40 | buildFeatures.compose = true 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/Versions.kt: -------------------------------------------------------------------------------- 1 | object Versions { 2 | const val gradle = "7.1.2" 3 | const val kotlin = "1.6.10" 4 | const val compose = "1.1.0" 5 | } -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/XProject.kt: -------------------------------------------------------------------------------- 1 | import org.gradle.api.Project 2 | import java.io.File 3 | import java.util.* 4 | 5 | fun readFile(path: String): File { 6 | val file = File(path) 7 | if (!file.canRead()) throw Throwable("Could not read file : $file") 8 | return file 9 | } 10 | 11 | fun readProperties(path: String): Properties = Properties().apply { load(readFile(path).reader()) } 12 | 13 | val Project.gradleProperties get() = readProperties("${rootProject.rootDir}/gradle.properties") 14 | val Project.commonGradleProperties get() = readProperties("${System.getProperty("user.home")}/.gradle/gradle.properties") 15 | val Project.localProperties get() = readProperties("${rootProject.rootDir}/local.properties") -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | ##################### 2 | ## PROPERTIES 3 | ##################### 4 | 5 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 6 | android.useAndroidX=true 7 | android.enableJetifier=true 8 | kotlin.code.style=official 9 | 10 | ##################### 11 | ## PUBLISHING 12 | ##################### 13 | 14 | VERSION_NAME=1.0.5 15 | VERSION_CODE=105 16 | 17 | GROUP=com.chargemap.compose 18 | POM_ARTIFACT_ID=numberpicker 19 | 20 | POM_NAME=numberpicker 21 | POM_PACKAGING=aar 22 | 23 | POM_DESCRIPTION=Jetpack Compose Number Picker 24 | POM_INCEPTION_YEAR=2022 25 | 26 | POM_URL=https://github.com/chargemap/compose-numberpicker 27 | POM_SCM_URL=https://github.com/chargemap/compose-numberpicker 28 | POM_SCM_CONNECTION=scm:git@github.com:chargemap/compose-numberpicker.git 29 | POM_SCM_DEV_CONNECTION=scm:git@github.com:chargemap/compose-numberpicker.git 30 | 31 | POM_LICENCE_NAME=The Apache Software License, Version 2.0 32 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt 33 | POM_LICENCE_DIST=repo 34 | 35 | POM_DEVELOPER_ID=ChargemapMobile 36 | POM_DEVELOPER_NAME=ChargemapMobile 37 | POM_DEVELOPER_URL=https://github.com/ChargemapMobile -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChargeMap/Compose-NumberPicker/cd71609331c2285f392c7561a3cf6bea5b422a72/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Oct 31 12:21:00 CET 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /lib/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | id("kotlin-android") 4 | id("common") 5 | id("com.vanniktech.maven.publish") 6 | } 7 | 8 | mavenPublish { 9 | sonatypeHost = com.vanniktech.maven.publish.SonatypeHost.S01 10 | } 11 | 12 | dependencies { 13 | api("org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlin}") 14 | api("androidx.compose.material:material:${Versions.compose}") 15 | 16 | testImplementation("junit:junit:4.13.2") 17 | } 18 | 19 | repositories { 20 | google() 21 | mavenCentral() 22 | } -------------------------------------------------------------------------------- /lib/rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChargeMap/Compose-NumberPicker/cd71609331c2285f392c7561a3cf6bea5b422a72/lib/rules.pro -------------------------------------------------------------------------------- /lib/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/src/main/kotlin/com/chargemap/compose/numberpicker/HoursNumberPicker.kt: -------------------------------------------------------------------------------- 1 | package com.chargemap.compose.numberpicker 2 | 3 | import androidx.compose.foundation.layout.Row 4 | import androidx.compose.material.LocalTextStyle 5 | import androidx.compose.material.MaterialTheme 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Alignment 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.Color 10 | import androidx.compose.ui.text.TextStyle 11 | import kotlin.math.abs 12 | 13 | sealed interface Hours { 14 | val hours: Int 15 | val minutes: Int 16 | } 17 | 18 | data class FullHours( 19 | override val hours: Int, 20 | override val minutes: Int, 21 | ) : Hours 22 | 23 | data class AMPMHours( 24 | override val hours: Int, 25 | override val minutes: Int, 26 | val dayTime: DayTime 27 | ) : Hours { 28 | enum class DayTime { 29 | AM, 30 | PM; 31 | } 32 | } 33 | 34 | @Composable 35 | fun HoursNumberPicker( 36 | modifier: Modifier = Modifier, 37 | value: Hours, 38 | leadingZero: Boolean = true, 39 | hoursRange: Iterable = when (value) { 40 | is FullHours -> (0..23) 41 | is AMPMHours -> (1..12) 42 | }, 43 | minutesRange: Iterable = (0..59), 44 | hoursDivider: (@Composable () -> Unit)? = null, 45 | minutesDivider: (@Composable () -> Unit)? = null, 46 | onValueChange: (Hours) -> Unit, 47 | dividersColor: Color = MaterialTheme.colors.primary, 48 | textStyle: TextStyle = LocalTextStyle.current, 49 | ) { 50 | when (value) { 51 | is FullHours -> 52 | FullHoursNumberPicker( 53 | modifier = modifier, 54 | value = value, 55 | leadingZero = leadingZero, 56 | hoursRange = hoursRange, 57 | minutesRange = minutesRange, 58 | hoursDivider = hoursDivider, 59 | minutesDivider = minutesDivider, 60 | onValueChange = onValueChange, 61 | dividersColor = dividersColor, 62 | textStyle = textStyle, 63 | ) 64 | is AMPMHours -> 65 | AMPMHoursNumberPicker( 66 | modifier = modifier, 67 | value = value, 68 | leadingZero = leadingZero, 69 | hoursRange = hoursRange, 70 | minutesRange = minutesRange, 71 | hoursDivider = hoursDivider, 72 | minutesDivider = minutesDivider, 73 | onValueChange = onValueChange, 74 | dividersColor = dividersColor, 75 | textStyle = textStyle, 76 | ) 77 | } 78 | } 79 | 80 | @Composable 81 | fun FullHoursNumberPicker( 82 | modifier: Modifier = Modifier, 83 | value: FullHours, 84 | leadingZero: Boolean = true, 85 | hoursRange: Iterable, 86 | minutesRange: Iterable = (0..59), 87 | hoursDivider: (@Composable () -> Unit)? = null, 88 | minutesDivider: (@Composable () -> Unit)? = null, 89 | onValueChange: (Hours) -> Unit, 90 | dividersColor: Color = MaterialTheme.colors.primary, 91 | textStyle: TextStyle = LocalTextStyle.current, 92 | ) { 93 | Row( 94 | modifier = modifier, 95 | verticalAlignment = Alignment.CenterVertically, 96 | ) { 97 | NumberPicker( 98 | modifier = Modifier.weight(1f), 99 | label = { 100 | "${if (leadingZero && abs(it) < 10) "0" else ""}$it" 101 | }, 102 | value = value.hours, 103 | onValueChange = { 104 | onValueChange(value.copy(hours = it)) 105 | }, 106 | dividersColor = dividersColor, 107 | textStyle = textStyle, 108 | range = hoursRange 109 | ) 110 | 111 | hoursDivider?.invoke() 112 | 113 | NumberPicker( 114 | modifier = Modifier.weight(1f), 115 | label = { 116 | "${if (leadingZero && abs(it) < 10) "0" else ""}$it" 117 | }, 118 | value = value.minutes, 119 | onValueChange = { 120 | onValueChange(value.copy(minutes = it)) 121 | }, 122 | dividersColor = dividersColor, 123 | textStyle = textStyle, 124 | range = minutesRange 125 | ) 126 | 127 | minutesDivider?.invoke() 128 | } 129 | } 130 | 131 | @Composable 132 | fun AMPMHoursNumberPicker( 133 | modifier: Modifier = Modifier, 134 | value: AMPMHours, 135 | leadingZero: Boolean = true, 136 | hoursRange: Iterable, 137 | minutesRange: Iterable = (0..59), 138 | hoursDivider: (@Composable () -> Unit)? = null, 139 | minutesDivider: (@Composable () -> Unit)? = null, 140 | onValueChange: (Hours) -> Unit, 141 | dividersColor: Color = MaterialTheme.colors.primary, 142 | textStyle: TextStyle = LocalTextStyle.current, 143 | ) { 144 | Row( 145 | modifier = modifier, 146 | verticalAlignment = Alignment.CenterVertically, 147 | ) { 148 | NumberPicker( 149 | modifier = Modifier.weight(1f), 150 | value = value.hours, 151 | label = { 152 | "${if (leadingZero && abs(it) < 10) "0" else ""}$it" 153 | }, 154 | onValueChange = { 155 | onValueChange(value.copy(hours = it)) 156 | }, 157 | dividersColor = dividersColor, 158 | textStyle = textStyle, 159 | range = hoursRange 160 | ) 161 | 162 | hoursDivider?.invoke() 163 | 164 | NumberPicker( 165 | modifier = Modifier.weight(1f), 166 | label = { 167 | "${if (leadingZero && abs(it) < 10) "0" else ""}$it" 168 | }, 169 | value = value.minutes, 170 | onValueChange = { 171 | onValueChange(value.copy(minutes = it)) 172 | }, 173 | dividersColor = dividersColor, 174 | textStyle = textStyle, 175 | range = minutesRange 176 | ) 177 | 178 | minutesDivider?.invoke() 179 | 180 | NumberPicker( 181 | value = when (value.dayTime) { 182 | AMPMHours.DayTime.AM -> 0 183 | else -> 1 184 | }, 185 | label = { 186 | when (it) { 187 | 0 -> "AM" 188 | else -> "PM" 189 | } 190 | }, 191 | onValueChange = { 192 | onValueChange( 193 | value.copy( 194 | dayTime = when (it) { 195 | 0 -> AMPMHours.DayTime.AM 196 | else -> AMPMHours.DayTime.PM 197 | } 198 | ) 199 | ) 200 | }, 201 | dividersColor = dividersColor, 202 | textStyle = textStyle, 203 | range = (0..1) 204 | ) 205 | } 206 | } -------------------------------------------------------------------------------- /lib/src/main/kotlin/com/chargemap/compose/numberpicker/ListItemPicker.kt: -------------------------------------------------------------------------------- 1 | package com.chargemap.compose.numberpicker 2 | 3 | import androidx.compose.animation.core.* 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.gestures.Orientation 6 | import androidx.compose.foundation.gestures.detectTapGestures 7 | import androidx.compose.foundation.gestures.draggable 8 | import androidx.compose.foundation.gestures.rememberDraggableState 9 | import androidx.compose.foundation.layout.* 10 | import androidx.compose.material.LocalTextStyle 11 | import androidx.compose.material.MaterialTheme 12 | import androidx.compose.material.ProvideTextStyle 13 | import androidx.compose.material.Text 14 | import androidx.compose.runtime.* 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.draw.alpha 18 | import androidx.compose.ui.graphics.Color 19 | import androidx.compose.ui.input.pointer.pointerInput 20 | import androidx.compose.ui.layout.Layout 21 | import androidx.compose.ui.platform.LocalDensity 22 | import androidx.compose.ui.text.TextStyle 23 | import androidx.compose.ui.text.style.TextAlign 24 | import androidx.compose.ui.unit.IntOffset 25 | import androidx.compose.ui.unit.dp 26 | import kotlinx.coroutines.launch 27 | import kotlin.math.abs 28 | import kotlin.math.roundToInt 29 | 30 | private fun getItemIndexForOffset( 31 | range: List, 32 | value: T, 33 | offset: Float, 34 | halfNumbersColumnHeightPx: Float 35 | ): Int { 36 | val indexOf = range.indexOf(value) - (offset / halfNumbersColumnHeightPx).toInt() 37 | return maxOf(0, minOf(indexOf, range.count() - 1)) 38 | } 39 | 40 | @Composable 41 | fun ListItemPicker( 42 | modifier: Modifier = Modifier, 43 | label: (T) -> String = { it.toString() }, 44 | value: T, 45 | onValueChange: (T) -> Unit, 46 | dividersColor: Color = MaterialTheme.colors.primary, 47 | list: List, 48 | textStyle: TextStyle = LocalTextStyle.current, 49 | ) { 50 | val minimumAlpha = 0.3f 51 | val verticalMargin = 8.dp 52 | val numbersColumnHeight = 80.dp 53 | val halfNumbersColumnHeight = numbersColumnHeight / 2 54 | val halfNumbersColumnHeightPx = with(LocalDensity.current) { halfNumbersColumnHeight.toPx() } 55 | 56 | val coroutineScope = rememberCoroutineScope() 57 | 58 | val animatedOffset = remember { Animatable(0f) } 59 | .apply { 60 | val index = list.indexOf(value) 61 | val offsetRange = remember(value, list) { 62 | -((list.count() - 1) - index) * halfNumbersColumnHeightPx to 63 | index * halfNumbersColumnHeightPx 64 | } 65 | updateBounds(offsetRange.first, offsetRange.second) 66 | } 67 | 68 | val coercedAnimatedOffset = animatedOffset.value % halfNumbersColumnHeightPx 69 | 70 | val indexOfElement = getItemIndexForOffset(list, value, animatedOffset.value, halfNumbersColumnHeightPx) 71 | 72 | var dividersWidth by remember { mutableStateOf(0.dp) } 73 | 74 | Layout( 75 | modifier = modifier 76 | .draggable( 77 | orientation = Orientation.Vertical, 78 | state = rememberDraggableState { deltaY -> 79 | coroutineScope.launch { 80 | animatedOffset.snapTo(animatedOffset.value + deltaY) 81 | } 82 | }, 83 | onDragStopped = { velocity -> 84 | coroutineScope.launch { 85 | val endValue = animatedOffset.fling( 86 | initialVelocity = velocity, 87 | animationSpec = exponentialDecay(frictionMultiplier = 20f), 88 | adjustTarget = { target -> 89 | val coercedTarget = target % halfNumbersColumnHeightPx 90 | val coercedAnchors = 91 | listOf(-halfNumbersColumnHeightPx, 0f, halfNumbersColumnHeightPx) 92 | val coercedPoint = coercedAnchors.minByOrNull { abs(it - coercedTarget) }!! 93 | val base = halfNumbersColumnHeightPx * (target / halfNumbersColumnHeightPx).toInt() 94 | coercedPoint + base 95 | } 96 | ).endState.value 97 | 98 | val result = list.elementAt( 99 | getItemIndexForOffset(list, value, endValue, halfNumbersColumnHeightPx) 100 | ) 101 | onValueChange(result) 102 | animatedOffset.snapTo(0f) 103 | } 104 | } 105 | ) 106 | .padding(vertical = numbersColumnHeight / 3 + verticalMargin * 2), 107 | content = { 108 | Box( 109 | modifier 110 | .width(dividersWidth) 111 | .height(2.dp) 112 | .background(color = dividersColor) 113 | ) 114 | Box( 115 | modifier = Modifier 116 | .padding(vertical = verticalMargin, horizontal = 20.dp) 117 | .offset { IntOffset(x = 0, y = coercedAnimatedOffset.roundToInt()) } 118 | ) { 119 | val baseLabelModifier = Modifier.align(Alignment.Center) 120 | ProvideTextStyle(textStyle) { 121 | if (indexOfElement > 0) 122 | Label( 123 | text = label(list.elementAt(indexOfElement - 1)), 124 | modifier = baseLabelModifier 125 | .offset(y = -halfNumbersColumnHeight) 126 | .alpha(maxOf(minimumAlpha, coercedAnimatedOffset / halfNumbersColumnHeightPx)) 127 | ) 128 | Label( 129 | text = label(list.elementAt(indexOfElement)), 130 | modifier = baseLabelModifier 131 | .alpha( 132 | (maxOf( 133 | minimumAlpha, 134 | 1 - abs(coercedAnimatedOffset) / halfNumbersColumnHeightPx 135 | )) 136 | ) 137 | ) 138 | if (indexOfElement < list.count() - 1) 139 | Label( 140 | text = label(list.elementAt(indexOfElement + 1)), 141 | modifier = baseLabelModifier 142 | .offset(y = halfNumbersColumnHeight) 143 | .alpha(maxOf(minimumAlpha, -coercedAnimatedOffset / halfNumbersColumnHeightPx)) 144 | ) 145 | } 146 | } 147 | Box( 148 | modifier 149 | .width(dividersWidth) 150 | .height(2.dp) 151 | .background(color = dividersColor) 152 | ) 153 | } 154 | ) { measurables, constraints -> 155 | // Don't constrain child views further, measure them with given constraints 156 | // List of measured children 157 | val placeables = measurables.map { measurable -> 158 | // Measure each children 159 | measurable.measure(constraints) 160 | } 161 | 162 | dividersWidth = placeables 163 | .drop(1) 164 | .first() 165 | .width 166 | .toDp() 167 | 168 | // Set the size of the layout as big as it can 169 | layout(dividersWidth.toPx().toInt(), placeables 170 | .sumOf { 171 | it.height 172 | } 173 | ) { 174 | // Track the y co-ord we have placed children up to 175 | var yPosition = 0 176 | 177 | // Place children in the parent layout 178 | placeables.forEach { placeable -> 179 | 180 | // Position item on the screen 181 | placeable.placeRelative(x = 0, y = yPosition) 182 | 183 | // Record the y co-ord placed up to 184 | yPosition += placeable.height 185 | } 186 | } 187 | } 188 | } 189 | 190 | @Composable 191 | private fun Label(text: String, modifier: Modifier) { 192 | Text( 193 | modifier = modifier.pointerInput(Unit) { 194 | detectTapGestures(onLongPress = { 195 | // FIXME: Empty to disable text selection 196 | }) 197 | }, 198 | text = text, 199 | textAlign = TextAlign.Center, 200 | ) 201 | } 202 | 203 | private suspend fun Animatable.fling( 204 | initialVelocity: Float, 205 | animationSpec: DecayAnimationSpec, 206 | adjustTarget: ((Float) -> Float)?, 207 | block: (Animatable.() -> Unit)? = null, 208 | ): AnimationResult { 209 | val targetValue = animationSpec.calculateTargetValue(value, initialVelocity) 210 | val adjustedTarget = adjustTarget?.invoke(targetValue) 211 | return if (adjustedTarget != null) { 212 | animateTo( 213 | targetValue = adjustedTarget, 214 | initialVelocity = initialVelocity, 215 | block = block 216 | ) 217 | } else { 218 | animateDecay( 219 | initialVelocity = initialVelocity, 220 | animationSpec = animationSpec, 221 | block = block, 222 | ) 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /lib/src/main/kotlin/com/chargemap/compose/numberpicker/NumberPicker.kt: -------------------------------------------------------------------------------- 1 | package com.chargemap.compose.numberpicker 2 | 3 | import androidx.compose.material.LocalTextStyle 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.graphics.Color 8 | import androidx.compose.ui.text.TextStyle 9 | 10 | @Composable 11 | fun NumberPicker( 12 | modifier: Modifier = Modifier, 13 | label: (Int) -> String = { 14 | it.toString() 15 | }, 16 | value: Int, 17 | onValueChange: (Int) -> Unit, 18 | dividersColor: Color = MaterialTheme.colors.primary, 19 | range: Iterable, 20 | textStyle: TextStyle = LocalTextStyle.current, 21 | ) { 22 | ListItemPicker( 23 | modifier = modifier, 24 | label = label, 25 | value = value, 26 | onValueChange = onValueChange, 27 | dividersColor = dividersColor, 28 | list = range.toList(), 29 | textStyle = textStyle 30 | ) 31 | } -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | VERSION_NAME="$1" 4 | VERSION_CODE="${VERSION_NAME//./}" 5 | 6 | sed -e "14s/.*/VERSION_NAME=$VERSION_NAME/" -i '' gradle.properties 7 | sed -e "15s/.*/VERSION_CODE=$VERSION_CODE/" -i '' gradle.properties 8 | 9 | git add . 10 | git commit -m "$1" 11 | git tag -a $1 -m "$1" 12 | git push --follow-tags 13 | 14 | ./gradlew clean 15 | ./gradlew build 16 | ./gradlew uploadArchives --no-daemon --no-parallel -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | > [!WARNING] 2 | > This repository is archived, it is now avalaible at https://github.com/charge-map/Compose-NumberPicker 3 | 4 | # Jetpack Compose Number Picker 5 | 6 | Android library providing a Number Picker for Jetpack Compose. 7 | 8 | [![chargemap](https://github.com/chargemap.png?size=50)](https://chargemap.com) 9 | 10 | [![Maven version](https://img.shields.io/maven-central/v/com.chargemap.compose/numberpicker?style=for-the-badge)](https://mvnrepository.com/artifact/com.chargemap.compose/numberpicker) 11 | 12 | ![License MIT](https://img.shields.io/badge/MIT-9E9F9F?style=flat-square&label=License) 13 | ![Android minimuml version](https://img.shields.io/badge/21+-9E9F9F?style=flat-square&label=Minimum&logo=android) 14 | 15 | ## Showcase 16 | 17 | 18 | 19 | ## Installation 20 | 21 | In your **module** *build.gradle* : 22 | 23 | ``` 24 | dependencies { 25 | implementation "com.chargemap.compose:numberpicker:latestVersion" 26 | } 27 | ``` 28 | 29 | ## Usage 30 | 31 | ### Simple NumberPicker 32 | 33 | 34 | 35 | ``` 36 | var pickerValue by remember { mutableStateOf(0) } 37 | 38 | NumberPicker( 39 | value = pickerValue, 40 | range = 0..10, 41 | onValueChange = { 42 | pickerValue = it 43 | } 44 | ) 45 | 46 | ``` 47 | 48 | ### 24 hours HoursNumberPicker 49 | 50 | 51 | 52 | ``` 53 | var pickerValue by remember { mutableStateOf(FullHours(12, 43)) } 54 | 55 | HoursNumberPicker( 56 | dividersColor = MaterialTheme.colors.primary, 57 | leadingZero = false, 58 | value = pickerValue, 59 | onValueChange = { 60 | pickerValue = it 61 | }, 62 | hoursDivider = { 63 | Text( 64 | modifier = Modifier.size(24.dp), 65 | textAlign = TextAlign.Center, 66 | text = ":" 67 | ) 68 | } 69 | ) 70 | 71 | ``` 72 | 73 | ### AM/PM HoursNumberPicker 74 | 75 | 76 | 77 | ``` 78 | var pickerValue by remember { mutableStateOf(AMPMHours(9, 12, AMPMHours.DayTime.PM )) } 79 | 80 | HoursNumberPicker( 81 | dividersColor = MaterialTheme.colors.primary, 82 | value = pickerValue, 83 | onValueChange = { 84 | pickerValue = it 85 | }, 86 | hoursDivider = { 87 | Text( 88 | modifier = Modifier.padding(horizontal = 8.dp), 89 | textAlign = TextAlign.Center, 90 | text = "hours" 91 | ) 92 | }, 93 | minutesDivider = { 94 | Text( 95 | modifier = Modifier.padding(horizontal = 8.dp), 96 | textAlign = TextAlign.Center, 97 | text = "minutes" 98 | ) 99 | } 100 | ) 101 | 102 | ``` 103 | 104 | ### List Picker 105 | 106 | 107 | 108 | ``` 109 | val possibleValues = listOf("🍎", "🍊", "🍉", "🥭", "🍈", "🍇", "🍍") 110 | var state by remember { mutableStateOf(possibleValues[0]) } 111 | ListItemPicker( 112 | label = { it }, 113 | value = state, 114 | onValueChange = { state = it }, 115 | list = possibleValues 116 | ) 117 | 118 | ``` 119 | 120 | ## Contributors 121 | 122 | | [![chargemap](https://github.com/chargemap.png?size=50)](https://github.com/chargemap) | [Chargemap](https://github.com/chargemap) | Author | 123 | |--------------|--------------|--------------| 124 | | [![pandasys](https://github.com/pandasys.png?size=50)](https://github.com/pandasys) | [Eric A. Snell](https://github.com/pandasys) | [Pull Request](https://github.com/ChargeMap/Compose-NumberPicker/pull/2) | 125 | | [![pandasys](https://github.com/cjrcodes.png?size=50)](https://github.com/pandasys) | [Christian R](https://github.com/cjrcodes) | [Pull Request](https://github.com/ChargeMap/Compose-NumberPicker/pull/8) | 126 | -------------------------------------------------------------------------------- /sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /sample/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("kotlin-android") 4 | id("common") 5 | } 6 | 7 | android { 8 | // JAVA 11 9 | compileOptions { 10 | sourceCompatibility = JavaVersion.VERSION_11 11 | targetCompatibility = JavaVersion.VERSION_11 12 | } 13 | 14 | project.tasks.withType { 15 | kotlinOptions { 16 | jvmTarget = JavaVersion.VERSION_11.toString() 17 | } 18 | } 19 | 20 | buildFeatures.compose = true 21 | 22 | composeOptions { 23 | kotlinCompilerExtensionVersion = Versions.compose 24 | } 25 | } 26 | 27 | dependencies { 28 | implementation(project(":lib")) 29 | 30 | implementation("com.google.android.material:material:1.5.0") 31 | implementation("androidx.appcompat:appcompat:1.4.1") 32 | implementation("androidx.activity:activity-compose:1.4.0") 33 | implementation("androidx.compose.ui:ui-tooling:${Versions.compose}") 34 | 35 | testImplementation("junit:junit:4.13.2") 36 | } -------------------------------------------------------------------------------- /sample/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/chargemap/android/sample/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.chargemap.android.sample 2 | 3 | import android.os.Bundle 4 | import androidx.activity.compose.setContent 5 | import androidx.appcompat.app.AppCompatActivity 6 | 7 | class MainActivity : AppCompatActivity() { 8 | 9 | override fun onCreate(savedInstanceState: Bundle?) { 10 | super.onCreate(savedInstanceState) 11 | setContent { 12 | MainActivityUI() 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/chargemap/android/sample/MainActivityUI.kt: -------------------------------------------------------------------------------- 1 | package com.chargemap.android.sample 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.foundation.rememberScrollState 5 | import androidx.compose.foundation.verticalScroll 6 | import androidx.compose.material.MaterialTheme 7 | import androidx.compose.material.Scaffold 8 | import androidx.compose.material.Text 9 | import androidx.compose.material.TopAppBar 10 | import androidx.compose.runtime.* 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.res.stringResource 13 | import androidx.compose.ui.text.style.TextAlign 14 | import androidx.compose.ui.unit.dp 15 | import com.chargemap.compose.numberpicker.* 16 | 17 | @Composable 18 | fun MainActivityUI() { 19 | 20 | val scrollState = rememberScrollState() 21 | 22 | MaterialTheme { 23 | Scaffold( 24 | topBar = { 25 | TopAppBar(title = { Text(stringResource(id = R.string.app_name)) }) 26 | } 27 | ) { 28 | Column( 29 | modifier = Modifier 30 | .fillMaxWidth() 31 | .verticalScroll(scrollState) 32 | ) { 33 | Column( 34 | Modifier 35 | .fillMaxWidth() 36 | .padding(16.dp) 37 | ) { 38 | NumberPicker() 39 | HoursNumberPicker1() 40 | HoursNumberPicker2() 41 | HoursNumberPicker3() 42 | HoursNumberPicker4() 43 | DoublesPicker() 44 | FruitPicker() 45 | IntRangePicker() 46 | } 47 | } 48 | } 49 | } 50 | } 51 | 52 | @Composable 53 | private fun NumberPicker() { 54 | var state by remember { mutableStateOf(0) } 55 | NumberPicker( 56 | value = state, 57 | range = 0..10, 58 | onValueChange = { 59 | state = it 60 | } 61 | ) 62 | } 63 | 64 | @Composable 65 | private fun HoursNumberPicker1() { 66 | var state by remember { mutableStateOf(FullHours(12, 43)) } 67 | HoursNumberPicker( 68 | modifier = Modifier 69 | .fillMaxWidth() 70 | .padding(vertical = 16.dp), leadingZero = true, 71 | 72 | dividersColor = MaterialTheme.colors.error, 73 | value = state, 74 | onValueChange = { 75 | state = it 76 | }, 77 | hoursDivider = { 78 | Text( 79 | modifier = Modifier.size(24.dp), 80 | textAlign = TextAlign.Center, 81 | text = ":" 82 | ) 83 | } 84 | ) 85 | } 86 | 87 | @Composable 88 | private fun HoursNumberPicker2() { 89 | var state by remember { mutableStateOf(AMPMHours(9, 43, AMPMHours.DayTime.PM)) } 90 | HoursNumberPicker( 91 | modifier = Modifier 92 | .fillMaxWidth() 93 | .padding(vertical = 16.dp), leadingZero = true, 94 | 95 | dividersColor = MaterialTheme.colors.secondary, 96 | value = state, 97 | onValueChange = { 98 | state = it 99 | }, 100 | hoursDivider = { 101 | Text( 102 | modifier = Modifier.size(24.dp), 103 | textAlign = TextAlign.Center, 104 | text = ":" 105 | ) 106 | }, 107 | minutesDivider = { 108 | Spacer( 109 | modifier = Modifier.size(24.dp), 110 | ) 111 | } 112 | ) 113 | } 114 | 115 | @Composable 116 | private fun HoursNumberPicker3() { 117 | var state by remember { mutableStateOf(FullHours(9, 20)) } 118 | 119 | HoursNumberPicker( 120 | modifier = Modifier 121 | .fillMaxWidth() 122 | .padding(vertical = 16.dp), leadingZero = true, 123 | 124 | value = state, 125 | onValueChange = { 126 | state = it 127 | }, 128 | minutesRange = IntProgression.fromClosedRange(0, 50, 10), 129 | hoursDivider = { 130 | Text( 131 | modifier = Modifier.padding(horizontal = 8.dp), 132 | textAlign = TextAlign.Center, 133 | text = "h" 134 | ) 135 | }, 136 | minutesDivider = { 137 | Text( 138 | modifier = Modifier.padding(horizontal = 8.dp), 139 | textAlign = TextAlign.Center, 140 | text = "m" 141 | ) 142 | } 143 | ) 144 | } 145 | 146 | @Composable 147 | private fun HoursNumberPicker4() { 148 | var state by remember { mutableStateOf(FullHours(11, 36)) } 149 | HoursNumberPicker( 150 | modifier = Modifier 151 | .fillMaxWidth() 152 | .padding(vertical = 16.dp), leadingZero = true, 153 | 154 | value = state, 155 | onValueChange = { 156 | state = it 157 | }, 158 | hoursDivider = { 159 | Text( 160 | modifier = Modifier.padding(horizontal = 8.dp), 161 | textAlign = TextAlign.Center, 162 | text = "hours" 163 | ) 164 | }, 165 | minutesDivider = { 166 | Text( 167 | modifier = Modifier.padding(horizontal = 8.dp), 168 | textAlign = TextAlign.Center, 169 | text = "minutes" 170 | ) 171 | } 172 | ) 173 | } 174 | 175 | @Composable 176 | private fun DoublesPicker() { 177 | val possibleValues = generateSequence(0.5f) { it + 0.25f } 178 | .takeWhile { it <= 5f } 179 | .toList() 180 | var state by remember { mutableStateOf(possibleValues[0]) } 181 | ListItemPicker( 182 | label = { it.toString() }, 183 | value = state, 184 | onValueChange = { state = it }, 185 | list = possibleValues 186 | ) 187 | } 188 | 189 | @Composable 190 | private fun FruitPicker() { 191 | val possibleValues = listOf("🍎", "🍊", "🍉", "🥭", "🍈", "🍇", "🍍") 192 | var state by remember { mutableStateOf(possibleValues[0]) } 193 | ListItemPicker( 194 | label = { it }, 195 | value = state, 196 | onValueChange = { state = it }, 197 | list = possibleValues 198 | ) 199 | } 200 | 201 | @Composable 202 | private fun IntRangePicker() { 203 | val possibleValues = (-5..10).toList() 204 | var value by remember { mutableStateOf(possibleValues[0]) } 205 | ListItemPicker( 206 | label = { it.toString() }, 207 | value = value, 208 | onValueChange = { value = it }, 209 | list = possibleValues 210 | ) 211 | } -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/chargemap/android/sample/PreviewComponents.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.foundation.layout.Spacer 2 | import androidx.compose.foundation.layout.fillMaxWidth 3 | import androidx.compose.foundation.layout.padding 4 | import androidx.compose.foundation.layout.size 5 | import androidx.compose.material.MaterialTheme 6 | import androidx.compose.material.Text 7 | import androidx.compose.runtime.* 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.Color 10 | import androidx.compose.ui.text.TextStyle 11 | import androidx.compose.ui.text.style.TextAlign 12 | import androidx.compose.ui.tooling.preview.Preview 13 | import androidx.compose.ui.unit.dp 14 | import com.chargemap.compose.numberpicker.* 15 | 16 | @Preview 17 | @Composable 18 | private fun NumberPickerPreview() { 19 | var state by remember { mutableStateOf(0) } 20 | NumberPicker( 21 | value = state, 22 | range = 0..10, 23 | onValueChange = { 24 | state = it 25 | }, 26 | textStyle = TextStyle(Color.White) 27 | ) 28 | } 29 | 30 | @Preview 31 | @Composable 32 | private fun HoursNumberPicker1Preview() { 33 | var state by remember { mutableStateOf(FullHours(12, 0)) } 34 | HoursNumberPicker( 35 | modifier = Modifier 36 | .fillMaxWidth() 37 | .padding(vertical = 16.dp), 38 | leadingZero = true, 39 | dividersColor = MaterialTheme.colors.error, 40 | value = state, 41 | onValueChange = { 42 | state = it 43 | }, 44 | hoursDivider = { 45 | Text( 46 | modifier = Modifier.size(24.dp), 47 | textAlign = TextAlign.Center, 48 | text = ":" 49 | ) 50 | }, 51 | textStyle = TextStyle(Color.White) 52 | ) 53 | } 54 | 55 | @Preview 56 | @Composable 57 | private fun HoursNumberPicker2Preview() { 58 | var state by remember { mutableStateOf(AMPMHours(9, 0, AMPMHours.DayTime.PM)) } 59 | HoursNumberPicker( 60 | modifier = Modifier 61 | .fillMaxWidth() 62 | .padding(vertical = 16.dp), 63 | leadingZero = true, 64 | dividersColor = MaterialTheme.colors.secondary, 65 | value = state, 66 | onValueChange = { 67 | state = it 68 | }, 69 | hoursDivider = { 70 | Text( 71 | modifier = Modifier.size(24.dp), 72 | textAlign = TextAlign.Center, 73 | text = ":" 74 | ) 75 | }, 76 | minutesDivider = { 77 | Spacer( 78 | modifier = Modifier.size(24.dp), 79 | ) 80 | }, 81 | textStyle = TextStyle(Color.White) 82 | ) 83 | } 84 | 85 | @Preview 86 | @Composable 87 | private fun HoursNumberPicker3Preview() { 88 | var state by remember { mutableStateOf(FullHours(9, 20)) } 89 | 90 | HoursNumberPicker( 91 | modifier = Modifier 92 | .fillMaxWidth() 93 | .padding(vertical = 16.dp), 94 | value = state, 95 | onValueChange = { 96 | state = it 97 | }, 98 | leadingZero = true, 99 | 100 | minutesRange = IntProgression.fromClosedRange(0, 50, 10), 101 | hoursDivider = { 102 | Text( 103 | modifier = Modifier.padding(horizontal = 8.dp), 104 | textAlign = TextAlign.Center, 105 | text = "h" 106 | ) 107 | }, 108 | minutesDivider = { 109 | Text( 110 | modifier = Modifier.padding(horizontal = 8.dp), 111 | textAlign = TextAlign.Center, 112 | text = "m" 113 | ) 114 | }, 115 | textStyle = TextStyle(Color.White) 116 | ) 117 | } 118 | 119 | @Preview 120 | @Composable 121 | private fun HoursNumberPicker4Preview() { 122 | var state by remember { mutableStateOf(FullHours(11, 36)) } 123 | HoursNumberPicker( 124 | modifier = Modifier 125 | .fillMaxWidth() 126 | .padding(vertical = 16.dp), 127 | leadingZero = true, 128 | value = state, 129 | onValueChange = { 130 | state = it 131 | }, 132 | hoursDivider = { 133 | Text( 134 | modifier = Modifier.padding(horizontal = 8.dp), 135 | textAlign = TextAlign.Center, 136 | text = "hours" 137 | ) 138 | }, 139 | minutesDivider = { 140 | Text( 141 | modifier = Modifier.padding(horizontal = 8.dp), 142 | textAlign = TextAlign.Center, 143 | text = "minutes" 144 | ) 145 | }, 146 | textStyle = TextStyle(Color.White) 147 | ) 148 | } 149 | 150 | @Preview 151 | @Composable 152 | private fun DoublesPickerPreview() { 153 | val possibleValues = generateSequence(0.5f) { it + 0.25f } 154 | .takeWhile { it <= 5f } 155 | .toList() 156 | var state by remember { mutableStateOf(possibleValues[0]) } 157 | ListItemPicker( 158 | label = { it.toString() }, 159 | value = state, 160 | onValueChange = { state = it }, 161 | list = possibleValues, 162 | textStyle = TextStyle(Color.White) 163 | ) 164 | } 165 | 166 | @Preview 167 | @Composable 168 | private fun FruitPickerPreview() { 169 | val possibleValues = listOf("🍎", "🍊", "🍉", "🥭", "🍈", "🍇", "🍍") 170 | var state by remember { mutableStateOf(possibleValues[0]) } 171 | ListItemPicker( 172 | label = { it }, 173 | value = state, 174 | onValueChange = { state = it }, 175 | list = possibleValues 176 | ) 177 | } 178 | 179 | @Preview 180 | @Composable 181 | private fun IntRangePickerPreview() { 182 | val possibleValues = (-5..10).toList() 183 | var value by remember { mutableStateOf(possibleValues[0]) } 184 | ListItemPicker( 185 | label = { it.toString() }, 186 | value = value, 187 | onValueChange = { value = it }, 188 | list = possibleValues, 189 | textStyle = TextStyle(Color.White) 190 | ) 191 | } -------------------------------------------------------------------------------- /sample/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChargeMap/Compose-NumberPicker/cd71609331c2285f392c7561a3cf6bea5b422a72/sample/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChargeMap/Compose-NumberPicker/cd71609331c2285f392c7561a3cf6bea5b422a72/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChargeMap/Compose-NumberPicker/cd71609331c2285f392c7561a3cf6bea5b422a72/sample/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChargeMap/Compose-NumberPicker/cd71609331c2285f392c7561a3cf6bea5b422a72/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChargeMap/Compose-NumberPicker/cd71609331c2285f392c7561a3cf6bea5b422a72/sample/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChargeMap/Compose-NumberPicker/cd71609331c2285f392c7561a3cf6bea5b422a72/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChargeMap/Compose-NumberPicker/cd71609331c2285f392c7561a3cf6bea5b422a72/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChargeMap/Compose-NumberPicker/cd71609331c2285f392c7561a3cf6bea5b422a72/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChargeMap/Compose-NumberPicker/cd71609331c2285f392c7561a3cf6bea5b422a72/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChargeMap/Compose-NumberPicker/cd71609331c2285f392c7561a3cf6bea5b422a72/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | 7 | #FF03DAC5 8 | #FF018786 9 | 10 | #FF000000 11 | #FFFFFFFF 12 | -------------------------------------------------------------------------------- /sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Template 3 | -------------------------------------------------------------------------------- /sample/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ":sample", ":lib" 2 | rootProject.name = "Compose-NumberPicker" --------------------------------------------------------------------------------