├── .gitignore ├── Gif_Demo.gif ├── README.md ├── Screen_recording.gif ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── tech │ │ └── hezy │ │ └── androidnumerictext │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── tech │ │ │ └── hezy │ │ │ └── androidnumerictext │ │ │ ├── AnimatedNumber.kt │ │ │ ├── MainActivity.kt │ │ │ ├── SampleActivity.kt │ │ │ └── ui │ │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ └── res │ │ ├── drawable │ │ ├── ic_launcher_background.xml │ │ └── ic_launcher_foreground.xml │ │ ├── layout │ │ └── layout_sample_activity.xml │ │ ├── mipmap-anydpi │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── tech │ └── hezy │ └── androidnumerictext │ └── ExampleUnitTest.kt ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── numeric-text ├── build.gradle.kts └── src │ └── main │ ├── java │ └── tech │ │ └── hezy │ │ └── numerictext │ │ ├── AnimatedNumberView.kt │ │ └── NumericTextTransition.kt │ └── res │ └── values │ └── attrs.xml └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /Gif_Demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZionHeZY/AndroidNumericText/371df5f88a85e9887e89d1f102e957cce1979def/Gif_Demo.gif -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AndroidNumericText 2 | 3 | ## Project Overview 4 | 5 | **AndroidNumericText** is a numeric input control library for Android applications, designed to provide convenient numeric input and formatting functionalities. It supports custom formats and styles, helping developers easily implement features related to numeric input. 6 | ![](Screen_recording.gif) ![](Gif_Demo.gif) 7 | 8 | ## Features 9 | 10 | - **Support for Integer and Decimal Input**: Flexibly handle different types of numeric input. 11 | - **Custom Numeric Formats**: Display formats like thousands separators, currency symbols, etc. 12 | - **Input Restrictions**: Set maximum value, minimum value, and number of decimal places. 13 | - **Simple and Easy-to-Use API**: Convenient for quick integration into projects. 14 | - **Custom Styles and Themes**: Meet different UI design requirements. 15 | - **iOS 17 SwiftUI contentTransition(.numericText()). 16 | 17 | ## Usage 18 | Compose: 19 | 20 | ```kotlin 21 | AnimatedNumber(number = number, modifier = Modifier.height(45.dp)) 22 | ``` 23 | ### Add the Control in Layout File 24 | 25 | ```xml 26 | 36 | ``` 37 | 38 | ### Configure in Code 39 | 40 | ```kotlin 41 | val animatedNumberView = findViewById(R.id.animatedNumberView) 42 | animatedNumberView.setNumber(currentNumber) 43 | ``` 44 | 45 | ## Contribution Guidelines 46 | 47 | We welcome feedback and suggestions for this project. You can contribute by submitting Issues or Pull Requests. Please refer to the [CONTRIBUTING.md](CONTRIBUTING.md) file for detailed contribution guidelines. 48 | 49 | ## License 50 | 51 | This project is licensed under the MIT License. For more information, please see the [LICENSE](LICENSE) file. 52 | 53 | ## Contact 54 | 55 | If you have any questions or suggestions, please contact the project maintainer: 56 | 57 | - **Email**: he0564@gmail.com 58 | - **GitHub**: [ZionHeZY](https://github.com/ZionHeZY) 59 | 60 | ## Acknowledgments 61 | 62 | Thank you to all the developers and users who have contributed to this project. 63 | -------------------------------------------------------------------------------- /Screen_recording.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZionHeZY/AndroidNumericText/371df5f88a85e9887e89d1f102e957cce1979def/Screen_recording.gif -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("kotlin-android") 4 | } 5 | 6 | android { 7 | namespace = "tech.hezy.androidnumerictext" 8 | compileSdk = 34 9 | 10 | defaultConfig { 11 | applicationId = "tech.hezy.androidnumerictext" 12 | minSdk = 26 13 | targetSdk = 34 14 | versionCode = 1 15 | versionName = "1.0" 16 | } 17 | 18 | buildTypes { 19 | release { 20 | isMinifyEnabled = false 21 | proguardFiles( 22 | getDefaultProguardFile("proguard-android-optimize.txt"), 23 | "proguard-rules.pro" 24 | ) 25 | } 26 | } 27 | 28 | buildFeatures { 29 | compose = true 30 | } 31 | 32 | composeOptions { 33 | kotlinCompilerExtensionVersion = "1.5.1" 34 | } 35 | 36 | compileOptions { 37 | sourceCompatibility = JavaVersion.VERSION_1_8 38 | targetCompatibility = JavaVersion.VERSION_1_8 39 | } 40 | 41 | kotlinOptions { 42 | jvmTarget = "1.8" 43 | } 44 | } 45 | 46 | dependencies { 47 | implementation(project(":numeric-text")) 48 | 49 | val composeBom = platform("androidx.compose:compose-bom:2024.02.00") 50 | implementation(composeBom) 51 | 52 | implementation("androidx.core:core-ktx:1.12.0") 53 | implementation("androidx.appcompat:appcompat:1.6.1") 54 | implementation("androidx.activity:activity-compose:1.8.2") 55 | implementation("androidx.constraintlayout:constraintlayout:2.1.4") 56 | 57 | // Compose 58 | implementation("androidx.compose.runtime:runtime") 59 | implementation("androidx.compose.ui:ui") 60 | implementation("androidx.compose.foundation:foundation") 61 | implementation("androidx.compose.material3:material3") 62 | implementation("androidx.compose.ui:ui-tooling-preview") 63 | debugImplementation("androidx.compose.ui:ui-tooling") 64 | } -------------------------------------------------------------------------------- /app/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 -------------------------------------------------------------------------------- /app/src/androidTest/java/tech/hezy/androidnumerictext/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package tech.hezy.androidnumerictext 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("tech.hezy.androidnumerictext", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/java/tech/hezy/androidnumerictext/AnimatedNumber.kt: -------------------------------------------------------------------------------- 1 | package tech.hezy.androidnumerictext 2 | 3 | import androidx.compose.animation.AnimatedContent 4 | import androidx.compose.animation.SizeTransform 5 | import androidx.compose.animation.fadeIn 6 | import androidx.compose.animation.fadeOut 7 | import androidx.compose.animation.slideInVertically 8 | import androidx.compose.animation.slideOutVertically 9 | import androidx.compose.animation.togetherWith 10 | import androidx.compose.foundation.layout.Box 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.LaunchedEffect 14 | import androidx.compose.runtime.getValue 15 | import androidx.compose.runtime.mutableIntStateOf 16 | import androidx.compose.runtime.remember 17 | import androidx.compose.runtime.setValue 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.unit.sp 21 | 22 | @Composable 23 | fun AnimatedNumber( 24 | number: Int, 25 | modifier: Modifier = Modifier 26 | ) { 27 | var oldNumber by remember { mutableIntStateOf(number) } 28 | var newNumber by remember { mutableIntStateOf(number) } 29 | 30 | LaunchedEffect(number) { 31 | oldNumber = newNumber 32 | newNumber = number 33 | } 34 | 35 | Box( 36 | contentAlignment = Alignment.Center, 37 | modifier = modifier 38 | ) { 39 | AnimatedContent( 40 | targetState = newNumber, 41 | transitionSpec = { 42 | if (newNumber > oldNumber) { 43 | (slideInVertically { it } + fadeIn()).togetherWith(slideOutVertically { -it } + fadeOut()) 44 | } else { 45 | (slideInVertically { -it } + fadeIn()).togetherWith(slideOutVertically { it } + fadeOut()) 46 | }.using(SizeTransform(clip = false)) 47 | }, label = "" 48 | ) { targetNumber -> 49 | Text( 50 | text = "$targetNumber", 51 | fontSize = 36.sp 52 | ) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/tech/hezy/androidnumerictext/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package tech.hezy.androidnumerictext 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.foundation.layout.Arrangement 7 | import androidx.compose.foundation.layout.Column 8 | import androidx.compose.foundation.layout.Spacer 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.foundation.layout.height 11 | import androidx.compose.material3.Button 12 | import androidx.compose.material3.MaterialTheme 13 | import androidx.compose.material3.Surface 14 | import androidx.compose.material3.Text 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.runtime.getValue 17 | import androidx.compose.runtime.mutableIntStateOf 18 | import androidx.compose.runtime.remember 19 | import androidx.compose.runtime.rememberCoroutineScope 20 | import androidx.compose.runtime.setValue 21 | import androidx.compose.ui.Alignment 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.tooling.preview.Preview 24 | import androidx.compose.ui.unit.dp 25 | import kotlinx.coroutines.launch 26 | 27 | class MainActivity : ComponentActivity() { 28 | override fun onCreate(savedInstanceState: Bundle?) { 29 | super.onCreate(savedInstanceState) 30 | setContent { 31 | AnimatedNumberApp() 32 | } 33 | } 34 | } 35 | 36 | @Composable 37 | fun AnimatedNumberApp() { 38 | Surface( 39 | modifier = Modifier.fillMaxSize(), 40 | color = MaterialTheme.colorScheme.background 41 | ) { 42 | AnimatedNumberDemo() 43 | } 44 | } 45 | 46 | 47 | @Composable 48 | fun AnimatedNumberDemo() { 49 | var number by remember { mutableIntStateOf(0) } 50 | val coroutineScope = rememberCoroutineScope() 51 | 52 | Column( 53 | horizontalAlignment = Alignment.CenterHorizontally, 54 | verticalArrangement = Arrangement.Center, 55 | modifier = Modifier.fillMaxSize() 56 | ) { 57 | AnimatedNumber(number = number, modifier = Modifier.height(45.dp)) 58 | 59 | Spacer(modifier = Modifier.height(56.dp)) 60 | 61 | Button(onClick = { 62 | coroutineScope.launch { 63 | number++ 64 | } 65 | }) { 66 | Text(text = "Increase") 67 | } 68 | Spacer(modifier = Modifier.height(8.dp)) 69 | Button(onClick = { 70 | coroutineScope.launch { 71 | number-- 72 | } 73 | }) { 74 | Text(text = "Decrease") 75 | } 76 | } 77 | } 78 | 79 | @Preview(showBackground = true) 80 | @Composable 81 | fun DefaultPreview() { 82 | AnimatedNumberApp() 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/tech/hezy/androidnumerictext/SampleActivity.kt: -------------------------------------------------------------------------------- 1 | package tech.hezy.androidnumerictext 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.appcompat.app.AppCompatActivity 6 | import tech.hezy.numerictext.AnimatedNumberView 7 | 8 | class SampleActivity : AppCompatActivity() { 9 | 10 | private var currentNumber = 0 11 | 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | setContentView(R.layout.layout_sample_activity) 15 | 16 | val animatedNumberView = findViewById(R.id.animatedNumberView) 17 | 18 | animatedNumberView.setNumber(currentNumber) 19 | 20 | findViewById(R.id.buttonIncrease).setOnClickListener { 21 | currentNumber += 1 22 | animatedNumberView.setNumber(currentNumber) 23 | } 24 | 25 | findViewById(R.id.buttonDecrease).setOnClickListener { 26 | currentNumber -= 1 27 | animatedNumberView.setNumber(currentNumber) 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/hezy/androidnumerictext/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package tech.hezy.androidnumerictext.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple80 = Color(0xFFD0BCFF) 6 | val PurpleGrey80 = Color(0xFFCCC2DC) 7 | val Pink80 = Color(0xFFEFB8C8) 8 | 9 | val Purple40 = Color(0xFF6650a4) 10 | val PurpleGrey40 = Color(0xFF625b71) 11 | val Pink40 = Color(0xFF7D5260) -------------------------------------------------------------------------------- /app/src/main/java/tech/hezy/androidnumerictext/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package tech.hezy.androidnumerictext.ui.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.darkColorScheme 8 | import androidx.compose.material3.dynamicDarkColorScheme 9 | import androidx.compose.material3.dynamicLightColorScheme 10 | import androidx.compose.material3.lightColorScheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.platform.LocalContext 13 | 14 | private val DarkColorScheme = darkColorScheme( 15 | primary = Purple80, 16 | secondary = PurpleGrey80, 17 | tertiary = Pink80 18 | ) 19 | 20 | private val LightColorScheme = lightColorScheme( 21 | primary = Purple40, 22 | secondary = PurpleGrey40, 23 | tertiary = Pink40 24 | 25 | /* Other default colors to override 26 | background = Color(0xFFFFFBFE), 27 | surface = Color(0xFFFFFBFE), 28 | onPrimary = Color.White, 29 | onSecondary = Color.White, 30 | onTertiary = Color.White, 31 | onBackground = Color(0xFF1C1B1F), 32 | onSurface = Color(0xFF1C1B1F), 33 | */ 34 | ) 35 | 36 | @Composable 37 | fun AndroidNumericTextTheme( 38 | darkTheme: Boolean = isSystemInDarkTheme(), 39 | // Dynamic color is available on Android 12+ 40 | dynamicColor: Boolean = true, 41 | content: @Composable () -> Unit 42 | ) { 43 | val colorScheme = when { 44 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 45 | val context = LocalContext.current 46 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 47 | } 48 | 49 | darkTheme -> DarkColorScheme 50 | else -> LightColorScheme 51 | } 52 | 53 | MaterialTheme( 54 | colorScheme = colorScheme, 55 | typography = Typography, 56 | content = content 57 | ) 58 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/hezy/androidnumerictext/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package tech.hezy.androidnumerictext.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_sample_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 17 | 18 |