├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── colors.xml │ │ │ │ └── themes.xml │ │ │ ├── font │ │ │ │ └── secular.ttf │ │ │ ├── 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 │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── values-night │ │ │ │ └── themes.xml │ │ │ ├── layout │ │ │ │ └── activity_main.xml │ │ │ ├── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ │ └── drawable │ │ │ │ └── ic_launcher_background.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── dev │ │ │ └── dayaonweb │ │ │ └── incrementdecrementbutton │ │ │ └── MainActivity.kt │ ├── test │ │ └── java │ │ │ └── dev │ │ │ └── dayaonweb │ │ │ └── incrementdecrementbutton │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── dev │ │ └── dayaonweb │ │ └── incrementdecrementbutton │ │ └── ExampleInstrumentedTest.kt ├── proguard-rules.pro └── build.gradle ├── incrementdecrementbutton ├── .gitignore ├── consumer-rules.pro ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── dev │ │ │ │ └── dayaonweb │ │ │ │ └── incrementdecrementbutton │ │ │ │ ├── animations │ │ │ │ ├── AnimationType.kt │ │ │ │ ├── BaseAnimation.kt │ │ │ │ └── Animation.kt │ │ │ │ ├── util │ │ │ │ └── Extension.kt │ │ │ │ ├── composable │ │ │ │ └── IncrementDecrementButton.kt │ │ │ │ └── IncrementDecrementButton.kt │ │ └── res │ │ │ ├── drawable │ │ │ ├── ic_baseline_remove_24.xml │ │ │ └── ic_baseline_add_24.xml │ │ │ ├── values │ │ │ └── attrs.xml │ │ │ └── layout │ │ │ └── increment_decrement_button_layout.xml │ ├── test │ │ └── java │ │ │ └── dev │ │ │ └── dayaonweb │ │ │ └── incrementdecrementbutton │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── dev │ │ └── dayaonweb │ │ └── incrementdecrementbutton │ │ └── ExampleInstrumentedTest.kt ├── proguard-rules.pro └── build.gradle ├── screenshots ├── btn_basic.gif ├── btn_horizontal.gif ├── btn_vertical.gif ├── compose_vs_xml.mp4 ├── Screenshot_20220209_224432.png └── Screenshot_20220220_025721_colored.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── settings.gradle ├── gradle.properties ├── gradlew.bat ├── gradlew └── README.md /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /incrementdecrementbutton/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /incrementdecrementbutton/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /screenshots/btn_basic.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Damercy/IncrementDecrementButton/HEAD/screenshots/btn_basic.gif -------------------------------------------------------------------------------- /screenshots/btn_horizontal.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Damercy/IncrementDecrementButton/HEAD/screenshots/btn_horizontal.gif -------------------------------------------------------------------------------- /screenshots/btn_vertical.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Damercy/IncrementDecrementButton/HEAD/screenshots/btn_vertical.gif -------------------------------------------------------------------------------- /screenshots/compose_vs_xml.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Damercy/IncrementDecrementButton/HEAD/screenshots/compose_vs_xml.mp4 -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | IncrementDecrementButton 3 | -------------------------------------------------------------------------------- /app/src/main/res/font/secular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Damercy/IncrementDecrementButton/HEAD/app/src/main/res/font/secular.ttf -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Damercy/IncrementDecrementButton/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /screenshots/Screenshot_20220209_224432.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Damercy/IncrementDecrementButton/HEAD/screenshots/Screenshot_20220209_224432.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Damercy/IncrementDecrementButton/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Damercy/IncrementDecrementButton/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Damercy/IncrementDecrementButton/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Damercy/IncrementDecrementButton/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Damercy/IncrementDecrementButton/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /screenshots/Screenshot_20220220_025721_colored.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Damercy/IncrementDecrementButton/HEAD/screenshots/Screenshot_20220220_025721_colored.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Damercy/IncrementDecrementButton/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Damercy/IncrementDecrementButton/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Damercy/IncrementDecrementButton/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Damercy/IncrementDecrementButton/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Damercy/IncrementDecrementButton/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /incrementdecrementbutton/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /incrementdecrementbutton/src/main/java/dev/dayaonweb/incrementdecrementbutton/animations/AnimationType.kt: -------------------------------------------------------------------------------- 1 | package dev.dayaonweb.incrementdecrementbutton.animations 2 | 3 | enum class AnimationType { 4 | VERTICAL, 5 | HORIZONTAL, 6 | FADE 7 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Dec 18 01:17:40 IST 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /.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 | .idea 17 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | dependencyResolutionManagement { 2 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 3 | repositories { 4 | google() 5 | mavenCentral() 6 | jcenter() // Warning: this repository is going to shut down soon 7 | } 8 | } 9 | rootProject.name = "IncrementDecrementButton" 10 | include ':app' 11 | include ':incrementdecrementbutton' 12 | -------------------------------------------------------------------------------- /incrementdecrementbutton/src/main/res/drawable/ic_baseline_remove_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /incrementdecrementbutton/src/main/res/drawable/ic_baseline_add_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | #E3E3E3 11 | -------------------------------------------------------------------------------- /app/src/test/java/dev/dayaonweb/incrementdecrementbutton/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package dev.dayaonweb.incrementdecrementbutton 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /incrementdecrementbutton/src/test/java/dev/dayaonweb/incrementdecrementbutton/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package dev.dayaonweb.incrementdecrementbutton 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /incrementdecrementbutton/src/main/java/dev/dayaonweb/incrementdecrementbutton/animations/BaseAnimation.kt: -------------------------------------------------------------------------------- 1 | package dev.dayaonweb.incrementdecrementbutton.animations 2 | 3 | import android.animation.Animator 4 | import android.view.View 5 | 6 | interface BaseAnimation { 7 | var duration: Long 8 | var targetView: View 9 | var shouldReverse: Boolean 10 | val signPrefix: Int 11 | get() = if (shouldReverse) -1 else 1 12 | 13 | fun animate(animationType: AnimationType, animationListener: Animator.AnimatorListener? = null) 14 | } -------------------------------------------------------------------------------- /incrementdecrementbutton/src/main/java/dev/dayaonweb/incrementdecrementbutton/util/Extension.kt: -------------------------------------------------------------------------------- 1 | package dev.dayaonweb.incrementdecrementbutton.util 2 | 3 | import android.content.res.Resources 4 | import android.content.res.TypedArray 5 | import android.util.TypedValue 6 | 7 | inline fun > TypedArray.getEnum(index: Int, default: T) = 8 | getInt(index, -1).let { 9 | if (it >= 0) enumValues()[it] else default 10 | } 11 | 12 | val Number.toPx 13 | get() = TypedValue.applyDimension( 14 | TypedValue.COMPLEX_UNIT_DIP, 15 | this.toFloat(), 16 | Resources.getSystem().displayMetrics 17 | ) -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /incrementdecrementbutton/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/dev/dayaonweb/incrementdecrementbutton/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package dev.dayaonweb.incrementdecrementbutton 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("dev.dayaonweb.incrementdecrementbutton", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /incrementdecrementbutton/src/androidTest/java/dev/dayaonweb/incrementdecrementbutton/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package dev.dayaonweb.incrementdecrementbutton 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("dev.dayaonweb.incrementdecrementbutton.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | -------------------------------------------------------------------------------- /app/src/main/java/dev/dayaonweb/incrementdecrementbutton/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package dev.dayaonweb.incrementdecrementbutton 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.compose.material.MaterialTheme 6 | import androidx.compose.ui.graphics.Color 7 | import androidx.compose.ui.platform.ComposeView 8 | import dev.dayaonweb.incrementdecrementbutton.animations.AnimationType 9 | import dev.dayaonweb.incrementdecrementbutton.composable.IncrementDecrementButton 10 | 11 | class MainActivity : AppCompatActivity() { 12 | private lateinit var composeView: ComposeView 13 | override fun onCreate(savedInstanceState: Bundle?) { 14 | super.onCreate(savedInstanceState) 15 | setContentView(R.layout.activity_main) 16 | composeView = findViewById(R.id.compose_view) 17 | composeView.setContent { 18 | MaterialTheme { 19 | IncrementDecrementButton( 20 | contentColor = Color.Black, 21 | animationType = AnimationType.HORIZONTAL, 22 | 23 | ) 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /incrementdecrementbutton/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 20 | 21 | 22 | 30 | 31 | -------------------------------------------------------------------------------- /incrementdecrementbutton/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | compileSdk 32 8 | 9 | defaultConfig { 10 | minSdk 21 11 | targetSdk 32 12 | versionName "1.1.0" 13 | versionCode 101000 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | consumerProguardFiles "consumer-rules.pro" 16 | } 17 | 18 | buildFeatures { 19 | compose true 20 | } 21 | 22 | composeOptions { 23 | kotlinCompilerExtensionVersion '1.2.0-alpha08' 24 | } 25 | 26 | buildTypes { 27 | release { 28 | minifyEnabled false 29 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 30 | } 31 | } 32 | compileOptions { 33 | sourceCompatibility JavaVersion.VERSION_1_8 34 | targetCompatibility JavaVersion.VERSION_1_8 35 | } 36 | kotlinOptions { 37 | jvmTarget = '1.8' 38 | } 39 | } 40 | 41 | dependencies { 42 | 43 | implementation 'androidx.core:core-ktx:1.7.0' 44 | implementation 'androidx.appcompat:appcompat:1.4.1' 45 | implementation 'com.google.android.material:material:1.5.0' 46 | 47 | // Compose dependencies 48 | implementation 'androidx.activity:activity-compose:1.6.0-alpha01' 49 | implementation 'androidx.compose.material:material:1.2.0-alpha08' 50 | implementation 'androidx.compose.animation:animation:1.2.0-alpha08' 51 | implementation 'androidx.compose.ui:ui-tooling:1.2.0-alpha08' 52 | 53 | testImplementation 'junit:junit:4.13.2' 54 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 55 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 56 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | compileSdk 32 8 | 9 | defaultConfig { 10 | applicationId "dev.dayaonweb.incrementdecrementbutton" 11 | minSdk 21 12 | targetSdk 32 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | buildFeatures { 20 | compose true 21 | } 22 | 23 | composeOptions { 24 | kotlinCompilerExtensionVersion '1.2.0-alpha08' 25 | } 26 | 27 | buildTypes { 28 | release { 29 | minifyEnabled false 30 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 31 | } 32 | } 33 | compileOptions { 34 | sourceCompatibility JavaVersion.VERSION_1_8 35 | targetCompatibility JavaVersion.VERSION_1_8 36 | } 37 | kotlinOptions { 38 | jvmTarget = '1.8' 39 | } 40 | } 41 | 42 | dependencies { 43 | 44 | implementation 'androidx.core:core-ktx:1.7.0' 45 | implementation 'androidx.appcompat:appcompat:1.4.1' 46 | implementation 'com.google.android.material:material:1.5.0' 47 | implementation 'androidx.constraintlayout:constraintlayout:2.1.3' 48 | 49 | 50 | // Compose dependencies 51 | implementation 'androidx.activity:activity-compose:1.6.0-alpha01' 52 | implementation 'androidx.compose.material:material:1.2.0-alpha08' 53 | implementation 'androidx.compose.animation:animation:1.2.0-alpha08' 54 | implementation 'androidx.compose.ui:ui-tooling:1.2.0-alpha08' 55 | 56 | testImplementation 'junit:junit:4.13.2' 57 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 58 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 59 | 60 | implementation(project(path: ":incrementdecrementbutton")) 61 | } -------------------------------------------------------------------------------- /incrementdecrementbutton/src/main/res/layout/increment_decrement_button_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 17 | 18 | 23 | 24 | 33 | 34 | 43 | 44 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /incrementdecrementbutton/src/main/java/dev/dayaonweb/incrementdecrementbutton/animations/Animation.kt: -------------------------------------------------------------------------------- 1 | package dev.dayaonweb.incrementdecrementbutton.animations 2 | 3 | import android.animation.Animator 4 | import android.view.View 5 | 6 | class Animation( 7 | override var targetView: View, 8 | override var shouldReverse: Boolean = false, 9 | override var duration: Long = 500L 10 | ) : BaseAnimation { 11 | 12 | private fun crossFade(view: View) { 13 | view.apply { 14 | alpha = 0f 15 | animate() 16 | .alpha(1.0f) 17 | .setDuration(500) 18 | .start() 19 | 20 | } 21 | } 22 | 23 | private fun translateTopToDown(view: View, listener: Animator.AnimatorListener) { 24 | val finalTranslateValue = signPrefix * 1000.0f 25 | val initialTranslateValue = 0f 26 | view.apply { 27 | translationY = initialTranslateValue 28 | animate() 29 | .translationY(finalTranslateValue) 30 | .setDuration(duration / 2) 31 | .setListener(object : Animator.AnimatorListener { 32 | override fun onAnimationStart(p0: Animator?) = Unit 33 | override fun onAnimationEnd(p0: Animator?) { 34 | translationY = -finalTranslateValue 35 | animate() 36 | .translationY(initialTranslateValue) 37 | .setDuration(duration / 2) 38 | .setListener(listener) 39 | .start() 40 | } 41 | 42 | override fun onAnimationCancel(p0: Animator?) = Unit 43 | override fun onAnimationRepeat(p0: Animator?) = Unit 44 | 45 | }) 46 | .start() 47 | } 48 | } 49 | 50 | private fun translateLeftToRight(view: View, listener: Animator.AnimatorListener) { 51 | val finalTranslateValue = signPrefix * 100.0f 52 | val initialTranslateValue = 0f 53 | view.apply { 54 | translationX = initialTranslateValue 55 | animate() 56 | .translationX(finalTranslateValue) 57 | .setDuration(duration / 2) 58 | .setListener(object : Animator.AnimatorListener { 59 | override fun onAnimationStart(p0: Animator?) = Unit 60 | override fun onAnimationEnd(p0: Animator?) { 61 | translationX = -finalTranslateValue 62 | animate() 63 | .translationX(initialTranslateValue) 64 | .setDuration(duration / 2) 65 | .setListener(listener) 66 | .start() 67 | } 68 | 69 | override fun onAnimationCancel(p0: Animator?) = Unit 70 | override fun onAnimationRepeat(p0: Animator?) = Unit 71 | 72 | }) 73 | .start() 74 | } 75 | } 76 | 77 | 78 | override fun animate( 79 | animationType: AnimationType, 80 | animationListener: Animator.AnimatorListener? 81 | ) { 82 | when (animationType) { 83 | AnimationType.FADE -> crossFade(targetView) 84 | AnimationType.HORIZONTAL -> animationListener?.let { animListener -> 85 | translateLeftToRight( 86 | targetView, 87 | animListener 88 | ) 89 | } 90 | AnimationType.VERTICAL -> animationListener?.let { animListener -> 91 | translateTopToDown( 92 | targetView, 93 | animListener 94 | ) 95 | } 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IncrementDecrementButton 2 | 3 | Zomato/Swiggy like increment decrement button for android. Available as a composable as well as XML based view library! 4 | 5 | ![Kotlin](https://img.shields.io/badge/kotlin-%230095D5.svg?style=for-the-badge&logo=kotlin&logoColor=white) ![Android](https://img.shields.io/badge/Android-3DDC84?style=for-the-badge&logo=android&logoColor=white) [![GitHub license](https://badgen.net/github/license/Naereen/Strapdown.js)](https://github.com/Naereen/StrapDown.js/blob/master/LICENSE) 6 | 7 | https://user-images.githubusercontent.com/24220261/167204923-4cd365d2-b2c7-4d49-86e0-395085591a27.mp4 8 | 9 | 10 | ### Screenshots 11 | 12 | 13 | 14 | 15 | ### Requirements 16 | - API level 21+ 17 | 18 | 19 | ### Installation 20 | Step 1. Add the JitPack repository to your build file. Add it in your root build.gradle at the end of repositories: 21 | ```groovy 22 | allprojects { 23 | repositories { 24 | ... 25 | maven { url 'https://jitpack.io' } 26 | } 27 | } 28 | ``` 29 | Step 2. Add the dependency in app's buid.gradle: 30 | ```groovy 31 | dependencies { implementation 'com.github.Damercy:IncrementDecrementButton:2.0.0' } 32 | ``` 33 | 34 | 35 | For gradle versions **7.X.X+**, you may face issues related to unable to resolve library on syncing projects. In that case, please update project's `settings.gradle` as follows: 36 | ```groovy 37 | dependencyResolutionManagement { 38 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 39 | repositories { 40 | google() 41 | mavenCentral() 42 | jcenter() // Warning: this repository is going to shut down soon 43 | maven { url "https://jitpack.io" } // Add jitpack dependency here instead 44 | } 45 | } 46 | rootProject.name = "Your awesome app" 47 | include ':app' 48 | ``` 49 | ### Usage 50 | #### XML 51 | For a basic use case, you can simply plugin the library in your layout as follows: 52 | ```xml 53 | 54 | 60 | 61 | 62 | 63 | 72 | 73 | 82 | 83 | 84 | 85 | 86 | ``` 87 | 88 | The following attributes are implemented as of now: 89 | | Attribute | Format | Description | 90 | | ------------------ | ----------- | ----------- | 91 | |`app:decrementText` | string | The text to set for decrement button. Default is `-` | 92 | | `app:incrementText` | string | The text to set for increment button. Default is `+` | 93 | | `app:middleText` | string | The text to set for the middle view. Default is `ADD` | 94 | | `app:fontFamilyRes` | reference | The font family to set for the texts | 95 | | `app:fontSizeRes` | integer | The font size to set for the texts. Defaults to 16sp | 96 | | `app:cornerRadius` | dimension | The corner radius of the button. Defaults to 8dp | 97 | | `app:animationType` | enum | The type of animation to set on text change. Either of `FADE`,`HORIZONTAL` or `VERTICAL`. Defaults to `FADE` | 98 | | `app:animationDuration` | integer | The duration for animation. Defauls to `500ms` | 99 | | `app:textColor` | color | The color to set for texts | 100 | | `app:buttonBackground` | reference | The color to set as background for button | 101 | | `app:borderStrokeColor` | color | The border stroke color to set for button. Defaults to `white` | 102 | | `app:borderStrokeWidth` | integer | The border width to set as background for button. Defaults to `0` hence no border is shown | 103 | 104 | Programatically: 105 | ```kotlin 106 | class MainActivity : AppCompatActivity() { 107 | override fun onCreate(savedInstanceState: Bundle?) { 108 | super.onCreate(savedInstanceState) 109 | setContentView(R.layout.activity_main) 110 | val button: IncrementDecrementButton = findViewById(R.id.inc_dec_btn) 111 | 112 | // available functions 113 | button.setIncrementButtonText() 114 | button.setDecrementButtonText() 115 | button.getCurrentValue() // Get current value (amount) 116 | button.setFontFamily() 117 | } 118 | } 119 | ``` 120 | #### Composable 121 | Simply use the composable as shown below. 122 | ```kotlin 123 | MaterialTheme { 124 | // Some other composables 125 | IncrementDecrementButton() 126 | } 127 | ``` 128 | The IncrementDecrementButton is highly customizable. You can pass in your `Modifier` with appropriate styling as required or use the available methods provided. 129 | 130 | You can even pass in your own composables for the decrement (left), middle (center) & increment (right) places of the button. The default values of IncrementDecrementButton is as follows: 131 | ```kotlin 132 | @Composable 133 | fun IncrementDecrementButton( 134 | modifier: Modifier = Modifier, 135 | fontFamily: FontFamily = FontFamily(typeface = Typeface.DEFAULT), 136 | fontSize: TextUnit = 16.0.sp, 137 | cornerRadius: Dp = 8.dp, 138 | animationType: AnimationType = AnimationType.FADE, 139 | backgroundColor: Color = MaterialTheme.colors.background, 140 | contentColor: Color = MaterialTheme.colors.contentColorFor(backgroundColor), 141 | borderStroke: BorderStroke = BorderStroke(0.dp, Color.White), 142 | value: Int = 0, 143 | onDecrementClick: (Int) -> Unit = {}, 144 | onIncrementClick: (Int) -> Unit = {}, 145 | onMiddleClick: (Int) -> Unit = {}, 146 | decrementComposable: @Composable (cb: (Int) -> Unit) -> Unit = { cb -> 147 | DefaultDecrementComposable( 148 | modifier = modifier, 149 | textColor = contentColor, 150 | fontFamily = fontFamily, 151 | fontSize = fontSize, 152 | backgroundColor = backgroundColor, 153 | cornerRadius = cornerRadius, 154 | borderStroke = borderStroke, 155 | onDecrementClick = { cb(-1) } 156 | ) 157 | }, 158 | incrementComposable: @Composable (cb: (Int) -> Unit) -> Unit = { cb -> 159 | DefaultIncrementComposable( 160 | modifier = modifier, 161 | textColor = contentColor, 162 | fontFamily = fontFamily, 163 | fontSize = fontSize, 164 | backgroundColor = backgroundColor, 165 | cornerRadius = cornerRadius, 166 | borderStroke = borderStroke, 167 | onIncrementClick = { cb(-1) } 168 | ) 169 | }, 170 | middleComposable: @Composable (Int, cb: (Int) -> Unit) -> Unit = { buttonValue, cb -> 171 | DefaultMiddleComposable( 172 | modifier = modifier, 173 | textColor = contentColor, 174 | fontFamily = fontFamily, 175 | fontSize = fontSize, 176 | backgroundColor = backgroundColor, 177 | borderStroke = borderStroke, 178 | onMiddleClick = { cb(-1) }, 179 | value = buttonValue 180 | ) 181 | }, 182 | ) 183 | ``` 184 | 185 | Please read the [release notes](https://github.com/Damercy/IncrementDecrementButton/releases/tag/2.0.0) for details on available methods & changes. 186 | 187 | ### Try sample app 188 | You can download the [sample apk](https://github.com/Damercy/IncrementDecrementButton/releases/download/2.0.0/sample.apk) to see this library in action. 189 | 190 | ### Hit the ⭐ if this library helped you in your projects 😄 191 | -------------------------------------------------------------------------------- /incrementdecrementbutton/src/main/java/dev/dayaonweb/incrementdecrementbutton/composable/IncrementDecrementButton.kt: -------------------------------------------------------------------------------- 1 | package dev.dayaonweb.incrementdecrementbutton.composable 2 | 3 | import android.annotation.SuppressLint 4 | import android.graphics.Typeface 5 | import androidx.compose.animation.* 6 | import androidx.compose.foundation.BorderStroke 7 | import androidx.compose.foundation.background 8 | import androidx.compose.foundation.border 9 | import androidx.compose.foundation.clickable 10 | import androidx.compose.foundation.layout.Row 11 | import androidx.compose.foundation.layout.defaultMinSize 12 | import androidx.compose.foundation.layout.wrapContentHeight 13 | import androidx.compose.foundation.shape.RoundedCornerShape 14 | import androidx.compose.material.MaterialTheme 15 | import androidx.compose.material.Scaffold 16 | import androidx.compose.material.Text 17 | import androidx.compose.material.contentColorFor 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.runtime.getValue 20 | import androidx.compose.runtime.mutableStateOf 21 | import androidx.compose.runtime.saveable.rememberSaveable 22 | import androidx.compose.runtime.setValue 23 | import androidx.compose.ui.Modifier 24 | import androidx.compose.ui.draw.clip 25 | import androidx.compose.ui.graphics.Color 26 | import androidx.compose.ui.text.font.FontFamily 27 | import androidx.compose.ui.text.intl.Locale 28 | import androidx.compose.ui.text.style.TextAlign 29 | import androidx.compose.ui.text.toUpperCase 30 | import androidx.compose.ui.tooling.preview.Preview 31 | import androidx.compose.ui.unit.Dp 32 | import androidx.compose.ui.unit.TextUnit 33 | import androidx.compose.ui.unit.dp 34 | import androidx.compose.ui.unit.sp 35 | import dev.dayaonweb.incrementdecrementbutton.animations.AnimationType 36 | 37 | 38 | @OptIn(ExperimentalAnimationApi::class) 39 | @Composable 40 | fun IncrementDecrementButton( 41 | modifier: Modifier = Modifier, 42 | fontFamily: FontFamily = FontFamily(typeface = Typeface.DEFAULT), 43 | fontSize: TextUnit = 16.0.sp, 44 | cornerRadius: Dp = 8.dp, 45 | animationType: AnimationType = AnimationType.FADE, 46 | backgroundColor: Color = MaterialTheme.colors.background, 47 | contentColor: Color = MaterialTheme.colors.contentColorFor(backgroundColor), 48 | borderStroke: BorderStroke = BorderStroke(0.dp, Color.White), 49 | value: Int = 0, 50 | onDecrementClick: (Int) -> Unit = {}, 51 | onIncrementClick: (Int) -> Unit = {}, 52 | onMiddleClick: (Int) -> Unit = {}, 53 | decrementComposable: @Composable (cb: (Int) -> Unit) -> Unit = { cb -> 54 | DefaultDecrementComposable( 55 | modifier = modifier, 56 | textColor = contentColor, 57 | fontFamily = fontFamily, 58 | fontSize = fontSize, 59 | backgroundColor = backgroundColor, 60 | cornerRadius = cornerRadius, 61 | borderStroke = borderStroke, 62 | onDecrementClick = { cb(-1) } 63 | ) 64 | }, 65 | incrementComposable: @Composable (cb: (Int) -> Unit) -> Unit = { cb -> 66 | DefaultIncrementComposable( 67 | modifier = modifier, 68 | textColor = contentColor, 69 | fontFamily = fontFamily, 70 | fontSize = fontSize, 71 | backgroundColor = backgroundColor, 72 | cornerRadius = cornerRadius, 73 | borderStroke = borderStroke, 74 | onIncrementClick = { cb(-1) } 75 | ) 76 | }, 77 | middleComposable: @Composable (Int, cb: (Int) -> Unit) -> Unit = { buttonValue, cb -> 78 | DefaultMiddleComposable( 79 | modifier = modifier, 80 | textColor = contentColor, 81 | fontFamily = fontFamily, 82 | fontSize = fontSize, 83 | backgroundColor = backgroundColor, 84 | borderStroke = borderStroke, 85 | onMiddleClick = { cb(-1) }, 86 | value = buttonValue 87 | ) 88 | }, 89 | ) { 90 | var buttonValue by rememberSaveable { mutableStateOf(value) } 91 | var isDecrement by rememberSaveable { mutableStateOf(false) } 92 | 93 | Row { 94 | decrementComposable { 95 | isDecrement = true 96 | if (buttonValue <= 0) 97 | buttonValue = 0 98 | else 99 | buttonValue-- 100 | onDecrementClick(buttonValue) 101 | } 102 | AnimatedContent( 103 | targetState = buttonValue, 104 | transitionSpec = { 105 | getAnimationSpec(animationType, isDecrement) 106 | } 107 | ) { value -> 108 | middleComposable(value) { 109 | isDecrement = false 110 | buttonValue++ 111 | onMiddleClick(value) 112 | } 113 | } 114 | incrementComposable { 115 | isDecrement = false 116 | buttonValue++ 117 | onIncrementClick(buttonValue) 118 | } 119 | } 120 | } 121 | 122 | @OptIn(ExperimentalAnimationApi::class) 123 | fun getAnimationSpec( 124 | animationType: AnimationType, 125 | isDecrement: Boolean 126 | ): ContentTransform { 127 | val inverseConstant = if (isDecrement) -1 else 1 128 | return when (animationType) { 129 | AnimationType.FADE -> { 130 | fadeIn() with fadeOut() 131 | } 132 | AnimationType.VERTICAL -> { 133 | slideInVertically { 134 | inverseConstant * it 135 | } with slideOutVertically { inverseConstant * -it } 136 | } 137 | else -> { 138 | slideInHorizontally { inverseConstant * it } with slideOutHorizontally { inverseConstant * -it } 139 | } 140 | } 141 | } 142 | 143 | 144 | @Composable 145 | fun DefaultDecrementComposable( 146 | modifier: Modifier = Modifier, 147 | textColor: Color, 148 | backgroundColor: Color, 149 | fontFamily: FontFamily, 150 | fontSize: TextUnit, 151 | cornerRadius: Dp, 152 | borderStroke: BorderStroke, 153 | onDecrementClick: () -> Unit 154 | ) { 155 | val shape = RoundedCornerShape( 156 | topStart = cornerRadius, 157 | bottomStart = cornerRadius 158 | ) 159 | val buttonModifier = modifier 160 | .background( 161 | color = backgroundColor, 162 | shape = shape 163 | ) 164 | .clip( 165 | shape = shape 166 | ) 167 | .clickable { 168 | onDecrementClick() 169 | } 170 | .border( 171 | border = borderStroke, 172 | shape = shape 173 | ) 174 | .defaultMinSize(minWidth = 64.dp, minHeight = 36.dp) 175 | .wrapContentHeight() 176 | 177 | Text( 178 | text = "-", 179 | color = textColor, 180 | fontFamily = fontFamily, 181 | fontSize = fontSize, 182 | textAlign = TextAlign.Center, 183 | modifier = buttonModifier 184 | ) 185 | } 186 | 187 | @Composable 188 | fun DefaultIncrementComposable( 189 | modifier: Modifier = Modifier, 190 | textColor: Color, 191 | backgroundColor: Color, 192 | fontFamily: FontFamily, 193 | fontSize: TextUnit, 194 | cornerRadius: Dp, 195 | borderStroke: BorderStroke, 196 | onIncrementClick: () -> Unit 197 | ) { 198 | val shape = RoundedCornerShape( 199 | topEnd = cornerRadius, 200 | bottomEnd = cornerRadius 201 | ) 202 | val buttonModifier = modifier 203 | .background( 204 | color = backgroundColor, 205 | shape = shape 206 | ) 207 | .clip( 208 | shape = shape 209 | ) 210 | .clickable { 211 | onIncrementClick() 212 | } 213 | .border( 214 | border = borderStroke, 215 | shape = shape 216 | ) 217 | .defaultMinSize(minWidth = 64.dp, minHeight = 36.dp) 218 | .wrapContentHeight() 219 | 220 | 221 | Text( 222 | text = "+", 223 | color = textColor, 224 | fontFamily = fontFamily, 225 | fontSize = fontSize, 226 | textAlign = TextAlign.Center, 227 | modifier = buttonModifier 228 | ) 229 | 230 | } 231 | 232 | @Composable 233 | fun DefaultMiddleComposable( 234 | modifier: Modifier = Modifier, 235 | textColor: Color, 236 | backgroundColor: Color, 237 | fontFamily: FontFamily, 238 | fontSize: TextUnit, 239 | borderStroke: BorderStroke, 240 | value: Int, 241 | onMiddleClick: () -> Unit 242 | ) { 243 | 244 | val buttonModifier = modifier 245 | .background( 246 | color = backgroundColor, 247 | ) 248 | .clickable { 249 | onMiddleClick() 250 | } 251 | .border( 252 | border = borderStroke 253 | ) 254 | .defaultMinSize(minWidth = 64.dp, minHeight = 36.dp) 255 | .wrapContentHeight() 256 | 257 | 258 | Text( 259 | text = if (value == 0) "Add".toUpperCase(Locale.current) else value.toString() 260 | .toUpperCase( 261 | Locale.current 262 | ), 263 | color = textColor, 264 | fontFamily = fontFamily, 265 | fontSize = fontSize, 266 | textAlign = TextAlign.Center, 267 | modifier = buttonModifier 268 | ) 269 | } 270 | 271 | @SuppressLint("UnusedMaterialScaffoldPaddingParameter") 272 | @Preview 273 | @Composable 274 | fun IncrementDecrementButtonPreview() { 275 | Scaffold { 276 | IncrementDecrementButton() 277 | } 278 | } -------------------------------------------------------------------------------- /incrementdecrementbutton/src/main/java/dev/dayaonweb/incrementdecrementbutton/IncrementDecrementButton.kt: -------------------------------------------------------------------------------- 1 | package dev.dayaonweb.incrementdecrementbutton 2 | 3 | import android.animation.Animator 4 | import android.content.Context 5 | import android.util.AttributeSet 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import androidx.annotation.* 9 | import androidx.constraintlayout.widget.ConstraintLayout 10 | import androidx.core.content.res.ResourcesCompat 11 | import androidx.core.text.isDigitsOnly 12 | import com.google.android.material.button.MaterialButton 13 | import com.google.android.material.card.MaterialCardView 14 | import com.google.android.material.textview.MaterialTextView 15 | import dev.dayaonweb.incrementdecrementbutton.animations.Animation 16 | import dev.dayaonweb.incrementdecrementbutton.animations.AnimationType 17 | import dev.dayaonweb.incrementdecrementbutton.util.getEnum 18 | import dev.dayaonweb.incrementdecrementbutton.util.toPx 19 | import kotlin.time.Duration 20 | 21 | @Suppress("MemberVisibilityCanBePrivate", "unused") 22 | class IncrementDecrementButton @JvmOverloads constructor( 23 | context: Context, 24 | attrs: AttributeSet? = null, 25 | defStyle: Int = 0, 26 | defStyleRes: Int = 0 27 | ) : ConstraintLayout(context, attrs, defStyle, defStyleRes) { 28 | 29 | // attributes 30 | private var fontFamily: Int 31 | private var fontSize: Int 32 | private var decrementText: String 33 | private var borderStrokeWidth: Int 34 | private var borderStrokeColor: Int 35 | private var textColor: Int 36 | private var incrementText: String 37 | private var middleText: String 38 | private var cornerRadius: Float 39 | private var background: Int 40 | private var animationDuration: Long 41 | private var animationType: AnimationType 42 | 43 | // views 44 | private lateinit var btnIncrement: MaterialButton 45 | private lateinit var btnDecrement: MaterialButton 46 | private lateinit var btnText: MaterialTextView 47 | private lateinit var btnRoot: MaterialCardView 48 | 49 | var value = 0 50 | private var previousValue = value 51 | private lateinit var animation: Animation 52 | 53 | 54 | init { 55 | context.theme.obtainStyledAttributes( 56 | attrs, 57 | R.styleable.IncrementDecrementButton, 58 | defStyle, 59 | defStyleRes 60 | ).apply { 61 | fontFamily = getResourceId(R.styleable.IncrementDecrementButton_fontFamilyRes, -1) 62 | fontSize = getInt(R.styleable.IncrementDecrementButton_fontSizeRes, DEFAULT_FONT_SIZE) 63 | decrementText = getString(R.styleable.IncrementDecrementButton_decrementText) 64 | ?: DEFAULT_DECREMENT_TEXT 65 | incrementText = getString(R.styleable.IncrementDecrementButton_incrementText) 66 | ?: DEFAULT_INCREMENT_TEXT 67 | middleText = 68 | getString(R.styleable.IncrementDecrementButton_middleText) ?: DEFAULT_MIDDLE_TEXT 69 | cornerRadius = 70 | getDimension(R.styleable.IncrementDecrementButton_cornerRadius, DEFAULT_CORNER_SIZE) 71 | borderStrokeColor = getColor( 72 | R.styleable.IncrementDecrementButton_borderStrokeColor, 73 | ResourcesCompat.getColor(resources, android.R.color.white, null) 74 | ) 75 | textColor = getColor( 76 | R.styleable.IncrementDecrementButton_textColor, 77 | ResourcesCompat.getColor(resources, android.R.color.black, null) 78 | ) 79 | borderStrokeWidth = getInt(R.styleable.IncrementDecrementButton_borderStrokeWidth, 0) 80 | background = getResourceId(R.styleable.IncrementDecrementButton_buttonBackground, -1) 81 | animationType = 82 | getEnum(R.styleable.IncrementDecrementButton_animationType, AnimationType.FADE) 83 | animationDuration = 84 | getInt( 85 | R.styleable.IncrementDecrementButton_animationDuration, 86 | DEFAULT_ANIMATION_DURATION 87 | ).toLong() 88 | } 89 | LayoutInflater.from(context).inflate(R.layout.increment_decrement_button_layout, this, true) 90 | initializeIncDecButton() 91 | } 92 | 93 | 94 | // public setters 95 | fun setFontFamily(@FontRes fontRes: Int) { 96 | if (fontRes == -1) return 97 | val font = ResourcesCompat.getFont(context, fontRes) 98 | btnIncrement.typeface = font 99 | btnDecrement.typeface = font 100 | btnText.typeface = font 101 | invalidateLayout() 102 | } 103 | 104 | fun setFontSize(@Size size: Int) { 105 | if (size < 0) return 106 | fontSize = size 107 | btnIncrement.textSize = fontSize.toFloat() 108 | btnDecrement.textSize = fontSize.toFloat() 109 | btnText.textSize = fontSize.toFloat() 110 | invalidateLayout() 111 | } 112 | 113 | fun setButtonBackgroundColor(@ColorRes color: Int) { 114 | if (color == -1) return 115 | background = color 116 | btnRoot.setCardBackgroundColor(ResourcesCompat.getColor(resources, background, null)) 117 | invalidateLayout() 118 | } 119 | 120 | private fun setButtonTextColor(@ColorInt color: Int) { 121 | textColor = color 122 | btnIncrement.setTextColor(textColor) 123 | btnDecrement.setTextColor(textColor) 124 | btnText.setTextColor(textColor) 125 | invalidateLayout() 126 | } 127 | 128 | fun setIncrementButtonText(text: String) { 129 | incrementText = text 130 | setResourceText(btnIncrement, incrementText) 131 | invalidateLayout() 132 | } 133 | 134 | fun setDecrementButtonText(text: String) { 135 | decrementText = text 136 | setResourceText(btnDecrement, decrementText) 137 | invalidateLayout() 138 | } 139 | 140 | fun setMiddleText(text: String) { 141 | middleText = text 142 | setResourceText(btnText, middleText) 143 | invalidateLayout() 144 | } 145 | 146 | fun setCornerRadius(@Dimension radius: Float) { 147 | cornerRadius = radius 148 | btnRoot.radius = cornerRadius 149 | invalidateLayout() 150 | } 151 | 152 | fun setBorderStrokeWidth(width: Int) { 153 | borderStrokeWidth = width 154 | btnRoot.strokeWidth = borderStrokeWidth 155 | invalidateLayout() 156 | } 157 | 158 | fun setBorderStrokeColor(@ColorRes color: Int) { 159 | borderStrokeColor = color 160 | btnRoot.strokeColor = borderStrokeColor 161 | invalidateLayout() 162 | } 163 | 164 | fun setAnimation(animation: AnimationType) { 165 | animationType = animation 166 | } 167 | 168 | fun setAnimationDuration(duration: Duration) { 169 | animationDuration = duration.inWholeMilliseconds 170 | } 171 | 172 | 173 | /** 174 | * ****************************For release v2.0******************************* 175 | 176 | fun setColor(@ColorRes color: Int) { 177 | 178 | } 179 | 180 | fun setIncrementButtonColor(@ColorRes color: Int) { 181 | 182 | } 183 | 184 | fun setDecrementButtonColor(@ColorRes color: Int) { 185 | 186 | } 187 | 188 | fun setMiddleColor(@ColorRes color: Int) { 189 | 190 | } 191 | 192 | fun setIncrementButtonDrawable(@DrawableRes drawableRes: Int) { 193 | 194 | } 195 | 196 | fun setDecrementButtonDrawable(@DrawableRes drawableRes: Int) { 197 | 198 | } 199 | 200 | fun toggleRipple(isEnabled: Boolean) { 201 | enableRipple = isEnabled 202 | 203 | } 204 | 205 | * ****************************For release v2.0******************************* 206 | */ 207 | 208 | // public getters 209 | fun getCurrentValue() = value 210 | 211 | private fun initializeIncDecButton() { 212 | btnIncrement = findViewById(R.id.btn_increment) 213 | btnDecrement = findViewById(R.id.btn_decrement) 214 | btnText = findViewById(R.id.btn_text) 215 | btnRoot = findViewById(R.id.btn_root) 216 | animation = Animation(btnText, duration = animationDuration) 217 | setFontFamily(fontFamily) 218 | setButtonBackgroundColor(background) 219 | setIncrementButtonText(incrementText) 220 | setDecrementButtonText(decrementText) 221 | setMiddleText(middleText) 222 | setButtonTextColor(textColor) 223 | setFontSize(fontSize) 224 | setCornerRadius(cornerRadius) 225 | setBorderStrokeColor(borderStrokeColor) 226 | setBorderStrokeWidth(borderStrokeWidth) 227 | attachListeners() 228 | } 229 | 230 | private fun attachListeners() { 231 | btnIncrement.setOnClickListener { 232 | previousValue = value 233 | value++ 234 | onIncrement() 235 | } 236 | btnDecrement.setOnClickListener { 237 | previousValue = value 238 | value-- 239 | onDecrementRangeCheck() 240 | } 241 | btnText.setOnClickListener { 242 | if (btnText.text.isDigitsOnly()) 243 | return@setOnClickListener 244 | previousValue = value 245 | value++ 246 | onIncrement() 247 | } 248 | } 249 | 250 | private fun onIncrement() { 251 | if (value > 0) 252 | setResourceText( 253 | btnText, 254 | if (animationType != AnimationType.FADE) previousValue.toString() else value.toString() 255 | ) 256 | 257 | } 258 | 259 | private fun onDecrementRangeCheck() { 260 | if (value <= 0) { 261 | value = 0 262 | previousValue = value 263 | setResourceText(btnText, middleText, isDecrement = true) 264 | } else 265 | setResourceText( 266 | btnText, 267 | if (animationType != AnimationType.FADE) previousValue.toString() else value.toString(), 268 | isDecrement = true 269 | ) 270 | } 271 | 272 | private fun invalidateLayout() { 273 | invalidate() 274 | requestLayout() 275 | } 276 | 277 | private fun setResourceText( 278 | view: View, 279 | text: String, 280 | shouldAnimate: Boolean = true, 281 | isDecrement: Boolean = false 282 | ) { 283 | when (view) { 284 | is MaterialButton -> view.text = text 285 | is MaterialTextView -> { 286 | view.text = text 287 | if (value != 0 && shouldAnimate) { 288 | animation.shouldReverse = isDecrement 289 | animation.duration = animationDuration 290 | animation.animate(animationType, getListenerForAnimation(animationType)) 291 | } 292 | } 293 | } 294 | invalidateLayout() 295 | } 296 | 297 | 298 | private fun getListenerForAnimation(animationType: AnimationType): Animator.AnimatorListener? { 299 | return when (animationType) { 300 | AnimationType.FADE -> null 301 | else -> object : Animator.AnimatorListener { 302 | override fun onAnimationStart(p0: Animator?) = Unit 303 | override fun onAnimationEnd(p0: Animator?) { 304 | setResourceText(btnText, value.toString(), false) 305 | } 306 | 307 | override fun onAnimationCancel(p0: Animator?) = Unit 308 | override fun onAnimationRepeat(p0: Animator?) = Unit 309 | } 310 | } 311 | } 312 | 313 | 314 | companion object { 315 | private const val DEFAULT_FONT_SIZE = 16 316 | private val DEFAULT_CORNER_SIZE = 8.0f.toPx 317 | private const val DEFAULT_ANIMATION_DURATION = 500 318 | private const val DEFAULT_DECREMENT_TEXT = "-" 319 | private const val DEFAULT_INCREMENT_TEXT = "+" 320 | private const val DEFAULT_MIDDLE_TEXT = "ADD" 321 | } 322 | } --------------------------------------------------------------------------------