├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ ├── colors.xml
│ │ │ │ └── styles.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
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── drawable-v24
│ │ │ │ └── ic_launcher_foreground.xml
│ │ │ ├── layout
│ │ │ │ └── activity_main.xml
│ │ │ └── drawable
│ │ │ │ └── ic_launcher_background.xml
│ │ ├── java
│ │ │ └── de
│ │ │ │ └── timonknispel
│ │ │ │ └── ktloadingbutton
│ │ │ │ ├── NetStatus.kt
│ │ │ │ └── MainActivity.kt
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── de
│ │ │ └── timonknispel
│ │ │ └── ktloadingbutton
│ │ │ └── ExampleUnitTest.kt
│ └── androidTest
│ │ └── java
│ │ └── de
│ │ └── timonknispel
│ │ └── ktloadingbutton
│ │ └── ExampleInstrumentedTest.kt
├── proguard-rules.pro
└── build.gradle
├── ktloadingbutton
├── .gitignore
├── consumer-rules.pro
├── src
│ ├── main
│ │ ├── res
│ │ │ └── values
│ │ │ │ ├── strings.xml
│ │ │ │ └── attrs.xml
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── de
│ │ │ └── timonknispel
│ │ │ └── ktloadingbutton
│ │ │ └── KTLoadingButton.kt
│ ├── test
│ │ └── java
│ │ │ └── de
│ │ │ └── timonknispel
│ │ │ └── ktloadingbutton
│ │ │ └── ExampleUnitTest.kt
│ └── androidTest
│ │ └── java
│ │ └── de
│ │ └── timonknispel
│ │ └── ktloadingbutton
│ │ └── ExampleInstrumentedTest.kt
├── proguard-rules.pro
└── build.gradle
├── error.gif
├── progress.gif
├── success.gif
├── settings.gradle
├── new-example-app.png
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .idea
├── codeStyles
│ ├── codeStyleConfig.xml
│ └── Project.xml
├── runConfigurations.xml
└── checkstyle-idea.xml
├── ktlint.gradle
├── LICENSE
├── gradle.properties
├── gradlew.bat
├── .gitignore
├── README.md
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/ktloadingbutton/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/ktloadingbutton/consumer-rules.pro:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/error.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timonknispel/KTLoadingButton/HEAD/error.gif
--------------------------------------------------------------------------------
/progress.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timonknispel/KTLoadingButton/HEAD/progress.gif
--------------------------------------------------------------------------------
/success.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timonknispel/KTLoadingButton/HEAD/success.gif
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':ktloadingbutton'
2 | rootProject.name='KTLoadingButton'
3 |
--------------------------------------------------------------------------------
/new-example-app.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timonknispel/KTLoadingButton/HEAD/new-example-app.png
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | KTLoadingButton
3 |
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timonknispel/KTLoadingButton/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/ktloadingbutton/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | KTLoadingButton
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timonknispel/KTLoadingButton/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timonknispel/KTLoadingButton/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timonknispel/KTLoadingButton/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timonknispel/KTLoadingButton/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timonknispel/KTLoadingButton/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timonknispel/KTLoadingButton/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timonknispel/KTLoadingButton/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timonknispel/KTLoadingButton/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timonknispel/KTLoadingButton/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timonknispel/KTLoadingButton/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/ktloadingbutton/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/java/de/timonknispel/ktloadingbutton/NetStatus.kt:
--------------------------------------------------------------------------------
1 | package de.timonknispel.ktloadingbutton
2 |
3 | // simple enum to represent different status responses
4 | enum class NetStatus {
5 | LOADING, SUCCESS, ERROR
6 | }
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #008577
4 | #00574B
5 | #D81B60
6 |
7 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Jan 24 12:54:05 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-5.4.1-all.zip
7 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/test/java/de/timonknispel/ktloadingbutton/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package de.timonknispel.ktloadingbutton
2 |
3 | import org.junit.Assert.assertEquals
4 | import org.junit.Test
5 |
6 | /**
7 | * Example local unit test, which will execute on the development machine (host).
8 | *
9 | * See [testing documentation](http://d.android.com/tools/testing).
10 | */
11 | class ExampleUnitTest {
12 | @Test
13 | fun addition_isCorrect() {
14 | assertEquals(4, 2 + 2)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/ktloadingbutton/src/test/java/de/timonknispel/ktloadingbutton/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package de.timonknispel.ktloadingbutton
2 |
3 | import org.junit.Assert.assertEquals
4 | import org.junit.Test
5 |
6 | /**
7 | * Example local unit test, which will execute on the development machine (host).
8 | *
9 | * See [testing documentation](http://d.android.com/tools/testing).
10 | */
11 | class ExampleUnitTest {
12 | @Test
13 | fun addition_isCorrect() {
14 | assertEquals(4, 2 + 2)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/checkstyle-idea.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/ktlint.gradle:
--------------------------------------------------------------------------------
1 | repositories {
2 | jcenter()
3 | }
4 |
5 | configurations {
6 | ktlint
7 | }
8 |
9 | dependencies {
10 | ktlint "com.pinterest:ktlint:0.35.0"
11 | }
12 |
13 | task ktlint(type: JavaExec, group: "verification") {
14 | description = "Check Kotlin code style."
15 | classpath = configurations.ktlint
16 | main = "com.pinterest.ktlint.Main"
17 | args "src/**/*.kt"
18 | }
19 |
20 | check.dependsOn ktlint
21 |
22 | task ktlintFormat(type: JavaExec, group: "formatting") {
23 | description = "Fix Kotlin code style deviations."
24 | classpath = configurations.ktlint
25 | main = "com.pinterest.ktlint.Main"
26 | args "-F", "src/**/*.kt"
27 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/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
22 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/de/timonknispel/ktloadingbutton/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package de.timonknispel.ktloadingbutton
2 |
3 | import androidx.test.ext.junit.runners.AndroidJUnit4
4 | import androidx.test.platform.app.InstrumentationRegistry
5 | import org.junit.Assert.assertEquals
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | /**
10 | * Instrumented test, which will execute on an Android device.
11 | *
12 | * See [testing documentation](http://d.android.com/tools/testing).
13 | */
14 | @RunWith(AndroidJUnit4::class)
15 | class ExampleInstrumentedTest {
16 | @Test
17 | fun useAppContext() {
18 | // Context of the app under test.
19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
20 | assertEquals("de.timonknispel.ktloadingbutton", appContext.packageName)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/ktloadingbutton/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
22 |
--------------------------------------------------------------------------------
/ktloadingbutton/src/androidTest/java/de/timonknispel/ktloadingbutton/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package de.timonknispel.ktloadingbutton
2 |
3 | import androidx.test.ext.junit.runners.AndroidJUnit4
4 | import androidx.test.platform.app.InstrumentationRegistry
5 | import org.junit.Assert.assertEquals
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | /**
10 | * Instrumented test, which will execute on an Android device.
11 | *
12 | * See [testing documentation](http://d.android.com/tools/testing).
13 | */
14 | @RunWith(AndroidJUnit4::class)
15 | class ExampleInstrumentedTest {
16 | @Test
17 | fun useAppContext() {
18 | // Context of the app under test.
19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
20 | assertEquals("de.timonknispel.ktloadingbutton.test", appContext.packageName)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/ktloadingbutton/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 timonknispel
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/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=-Xmx1536m
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
22 |
--------------------------------------------------------------------------------
/ktloadingbutton/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 | apply from: "../ktlint.gradle"
5 | apply plugin: 'com.github.dcendents.android-maven'
6 |
7 | group = 'com.github.timonknispel'
8 | android {
9 | compileSdkVersion 29
10 | buildToolsVersion "29.0.2"
11 |
12 |
13 | defaultConfig {
14 | minSdkVersion 21
15 | targetSdkVersion 29
16 | versionCode 4
17 | versionName "1.2.0"
18 |
19 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
20 | consumerProguardFiles 'consumer-rules.pro'
21 | }
22 |
23 | buildTypes {
24 | release {
25 | minifyEnabled false
26 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
27 | }
28 | }
29 |
30 | }
31 |
32 | dependencies {
33 | implementation fileTree(dir: 'libs', include: ['*.jar'])
34 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
35 | implementation 'androidx.appcompat:appcompat:1.1.0'
36 | implementation 'androidx.core:core-ktx:1.1.0'
37 | testImplementation 'junit:junit:4.12'
38 | androidTestImplementation 'androidx.test.ext:junit:1.1.1'
39 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
40 | }
41 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | apply plugin: 'kotlin-android'
4 |
5 | apply plugin: 'kotlin-android-extensions'
6 |
7 | apply from: "../ktlint.gradle"
8 |
9 | android {
10 | compileSdkVersion 29
11 | buildToolsVersion "29.0.2"
12 | defaultConfig {
13 | applicationId "de.timonknispel.ktloadingbutton"
14 | minSdkVersion 21
15 | targetSdkVersion 29
16 | versionCode 2
17 | versionName "2.0"
18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
19 | }
20 | buildTypes {
21 | release {
22 | minifyEnabled false
23 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
24 | }
25 | }
26 | }
27 |
28 | dependencies {
29 | implementation fileTree(dir: 'libs', include: ['*.jar'])
30 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
31 | implementation 'androidx.appcompat:appcompat:1.1.0'
32 | implementation 'androidx.core:core-ktx:1.1.0'
33 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
34 | testImplementation 'junit:junit:4.12'
35 | androidTestImplementation 'androidx.test.ext:junit:1.1.1'
36 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
37 | implementation project(path: ':ktloadingbutton')
38 | implementation "org.jetbrains.kotlin:kotlin-reflect:1.3.61"
39 | }
40 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/src/main/java/de/timonknispel/ktloadingbutton/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package de.timonknispel.ktloadingbutton
2 |
3 | import android.os.Bundle
4 | import android.os.Handler
5 | import androidx.appcompat.app.AppCompatActivity
6 | import androidx.lifecycle.MutableLiveData
7 | import androidx.lifecycle.Observer
8 | import kotlinx.android.synthetic.main.activity_main.*
9 | import kotlin.random.Random
10 |
11 | class MainActivity : AppCompatActivity() {
12 |
13 | private val tag = this.javaClass.simpleName
14 | private val handler = Handler()
15 | private var prog = 0F
16 |
17 | override fun onCreate(savedInstanceState: Bundle?) {
18 | super.onCreate(savedInstanceState)
19 | setContentView(R.layout.activity_main)
20 |
21 | // Example for normal loading
22 | normal_loading_button.setOnClickListener {
23 | initLiveDataWithResult().observe(this, Observer { status ->
24 | when (status) {
25 | NetStatus.LOADING -> normal_loading_button.startLoading()
26 | NetStatus.SUCCESS -> normal_loading_button.doResult(true)
27 | NetStatus.ERROR -> normal_loading_button.doResult(false)
28 | else -> {
29 | }
30 | }
31 | })
32 | }
33 |
34 | // Example for loading data with a progress
35 | progress_loading_button.setOnClickListener {
36 | initLiveDataProgress().observe(this, Observer {
37 | progress_loading_button.setProgress(it)
38 | progress_loading_percentage_text.text = "Progress: $it%"
39 | if (it == 100F) {
40 | prog = 0F
41 | handler.removeCallbacksAndMessages(null)
42 | progress_loading_button.doResult(true) {
43 | progress_loading_percentage_text.text = "Progress: 0%"
44 | }
45 | }
46 | })
47 | }
48 |
49 | // Example for normal loading with validation
50 | normal_validation_loading_button.apply {
51 | validation = { doValidation() }
52 | setOnClickListener { }
53 | }
54 |
55 | // Button for resetting the validation button
56 | reset_normal_validation_loading_button.setOnClickListener {
57 | normal_validation_loading_button.reset()
58 | }
59 | }
60 |
61 | private fun doValidation() = normal_validation_loading_switch.isChecked
62 |
63 | // Function for generating live data and later return a result
64 | private fun initLiveDataWithResult(): MutableLiveData {
65 | val liveData = MutableLiveData()
66 | liveData.value = NetStatus.LOADING
67 |
68 | Handler().postDelayed({
69 | liveData.postValue(if (normal_loading_switch.isChecked) NetStatus.SUCCESS else NetStatus.ERROR)
70 | }, Random.nextLong(3000, 5000))
71 |
72 | return liveData
73 | }
74 |
75 | // test function for generating some data for the button
76 | private fun initLiveDataProgress(): MutableLiveData {
77 | val liveData = MutableLiveData()
78 |
79 | // first set state to loading
80 | liveData.value = prog
81 |
82 | val runnableCode = object : Runnable {
83 | override fun run() {
84 | prog += 1F
85 | liveData.postValue(prog)
86 | handler.postDelayed(this, 50)
87 | }
88 | }
89 |
90 | handler.post(runnableCode)
91 | return liveData
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | xmlns:android
20 |
21 | ^$
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | xmlns:.*
31 |
32 | ^$
33 |
34 |
35 | BY_NAME
36 |
37 |
38 |
39 |
40 |
41 |
42 | .*:id
43 |
44 | http://schemas.android.com/apk/res/android
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | .*:name
54 |
55 | http://schemas.android.com/apk/res/android
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | name
65 |
66 | ^$
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | style
76 |
77 | ^$
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | .*
87 |
88 | ^$
89 |
90 |
91 | BY_NAME
92 |
93 |
94 |
95 |
96 |
97 |
98 | .*
99 |
100 | http://schemas.android.com/apk/res/android
101 |
102 |
103 | ANDROID_ATTRIBUTE_ORDER
104 |
105 |
106 |
107 |
108 |
109 |
110 | .*
111 |
112 | .*
113 |
114 |
115 | BY_NAME
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/kotlin,android,androidstudio
3 | # Edit at https://www.gitignore.io/?templates=kotlin,android,androidstudio
4 |
5 | ### Android ###
6 | # Built application files
7 | *.apk
8 | *.ap_
9 | *.aab
10 |
11 | # Files for the ART/Dalvik VM
12 | *.dex
13 |
14 | # Java class files
15 | *.class
16 |
17 | # Generated files
18 | bin/
19 | gen/
20 | out/
21 | release/
22 |
23 | # Gradle files
24 | .gradle/
25 | build/
26 |
27 | # Local configuration file (sdk path, etc)
28 | local.properties
29 |
30 | # Proguard folder generated by Eclipse
31 | proguard/
32 |
33 | # Log Files
34 | *.log
35 |
36 | # Android Studio Navigation editor temp files
37 | .navigation/
38 |
39 | # Android Studio captures folder
40 | captures/
41 |
42 | # IntelliJ
43 | *.iml
44 | .idea/workspace.xml
45 | .idea/tasks.xml
46 | .idea/gradle.xml
47 | .idea/assetWizardSettings.xml
48 | .idea/dictionaries
49 | .idea/libraries
50 | # Android Studio 3 in .gitignore file.
51 | .idea/caches
52 | .idea/modules.xml
53 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you
54 | .idea/navEditor.xml
55 |
56 | # Keystore files
57 | # Uncomment the following lines if you do not want to check your keystore files in.
58 | #*.jks
59 | #*.keystore
60 |
61 | # External native build folder generated in Android Studio 2.2 and later
62 | .externalNativeBuild
63 |
64 | # Google Services (e.g. APIs or Firebase)
65 | # google-services.json
66 |
67 | # Freeline
68 | freeline.py
69 | freeline/
70 | freeline_project_description.json
71 |
72 | # fastlane
73 | fastlane/report.xml
74 | fastlane/Preview.html
75 | fastlane/screenshots
76 | fastlane/test_output
77 | fastlane/readme.md
78 |
79 | # Version control
80 | vcs.xml
81 |
82 | # lint
83 | lint/intermediates/
84 | lint/generated/
85 | lint/outputs/
86 | lint/tmp/
87 | # lint/reports/
88 |
89 | ### Android Patch ###
90 | gen-external-apklibs
91 | output.json
92 |
93 | # Replacement of .externalNativeBuild directories introduced
94 | # with Android Studio 3.5.
95 | .cxx/
96 |
97 | ### Kotlin ###
98 | # Compiled class file
99 |
100 | # Log file
101 |
102 | # BlueJ files
103 | *.ctxt
104 |
105 | # Mobile Tools for Java (J2ME)
106 | .mtj.tmp/
107 |
108 | # Package Files #
109 | *.jar
110 | *.war
111 | *.nar
112 | *.ear
113 | *.zip
114 | *.tar.gz
115 | *.rar
116 |
117 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
118 | hs_err_pid*
119 |
120 | ### AndroidStudio ###
121 | # Covers files to be ignored for android development using Android Studio.
122 |
123 | # Built application files
124 |
125 | # Files for the ART/Dalvik VM
126 |
127 | # Java class files
128 |
129 | # Generated files
130 |
131 | # Gradle files
132 | .gradle
133 |
134 | # Signing files
135 | .signing/
136 |
137 | # Local configuration file (sdk path, etc)
138 |
139 | # Proguard folder generated by Eclipse
140 |
141 | # Log Files
142 |
143 | # Android Studio
144 | /*/build/
145 | /*/local.properties
146 | /*/out
147 | /*/*/build
148 | /*/*/production
149 | *.ipr
150 | *~
151 | *.swp
152 |
153 | # Android Patch
154 |
155 | # External native build folder generated in Android Studio 2.2 and later
156 |
157 | # NDK
158 | obj/
159 |
160 | # IntelliJ IDEA
161 | *.iws
162 | /out/
163 |
164 | # User-specific configurations
165 | .idea/caches/
166 | .idea/libraries/
167 | .idea/shelf/
168 | .idea/.name
169 | .idea/compiler.xml
170 | .idea/copyright/profiles_settings.xml
171 | .idea/encodings.xml
172 | .idea/misc.xml
173 | .idea/scopes/scope_settings.xml
174 | .idea/vcs.xml
175 | .idea/jsLibraryMappings.xml
176 | .idea/datasources.xml
177 | .idea/dataSources.ids
178 | .idea/sqlDataSources.xml
179 | .idea/dynamic.xml
180 | .idea/uiDesigner.xml
181 |
182 | # OS-specific files
183 | .DS_Store
184 | .DS_Store?
185 | ._*
186 | .Spotlight-V100
187 | .Trashes
188 | ehthumbs.db
189 | Thumbs.db
190 |
191 | # Legacy Eclipse project files
192 | .classpath
193 | .project
194 | .cproject
195 | .settings/
196 |
197 | # Mobile Tools for Java (J2ME)
198 |
199 | # Package Files #
200 |
201 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml)
202 |
203 | ## Plugin-specific files:
204 |
205 | # mpeltonen/sbt-idea plugin
206 | .idea_modules/
207 |
208 | # JIRA plugin
209 | atlassian-ide-plugin.xml
210 |
211 | # Mongo Explorer plugin
212 | .idea/mongoSettings.xml
213 |
214 | # Crashlytics plugin (for Android Studio and IntelliJ)
215 | com_crashlytics_export_strings.xml
216 | crashlytics.properties
217 | crashlytics-build.properties
218 | fabric.properties
219 |
220 | ### AndroidStudio Patch ###
221 |
222 | !/gradle/wrapper/gradle-wrapper.jar
223 |
224 | # End of https://www.gitignore.io/api/kotlin,android,androidstudio
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # KTLoadingButton
2 |
3 | Simple loading button for kotlin android apps.
4 | This button can show results in a nicely designed way to not block the ui while the user is waiting.
5 |
6 | [](https://jitpack.io/#timonknispel/KTLoadingButton)
7 | [](https://jitpack.io/#timonknispel/KTLoadingButton)
8 |
9 |
10 | [](https://github.com/timonknispel/KTLoadingButton/issues)
11 | 
12 |
13 | [](https://github.com/timonknispel/KTLoadingButton/blob/master/LICENSE)
14 |
15 |
16 | |SUCCESS| ERROR | PROGRESS |
17 | |--|--| -- |
18 | |  |  |  |
19 |
20 | 
21 | - You can set a validation function if the button should show the loading animation (e.g. if a text field is not filled correctly):
22 | ```kotlin
23 | test_button.validation = { // call a function for validation }
24 | ```
25 | 
26 |
27 | 
28 |
29 | ## Installation
30 | 1. Add the JitPack repository to your build file
31 |
32 | ```css
33 | allprojects {
34 | repositories {
35 | ...
36 | maven { url 'https://jitpack.io' }
37 | }
38 | }
39 | ```
40 |
41 | 2. Add the dependency
42 |
43 | ```css
44 | dependencies {
45 | implementation 'com.github.timonknispel:KTLoadingButton:XXXX'
46 | }
47 | ```
48 | Where XXXX need to be replaced by the version showen in the badge above.
49 |
50 | ## Usage
51 | ### Example needed? Look inside the app folder
52 |
53 | 1. Add the KTLoadingButton to your layout
54 |
55 | ```xml
56 |
62 | ```
63 | 2. DONE
64 | Now all you have to do is to start the button. This can be done in several ways.
65 |
66 | #### Option 1: INTERMEDIATE (DEFAULT)
67 | Automatically by setting a onClickListener :
68 | ```kotlin
69 | test_button.setOnClickListener {}
70 | ```
71 |
72 | Simply call :
73 | ```kotlin
74 | test_button.startLoading()
75 | ````
76 |
77 | This will start the loading animation of the button. By default it should now intermediate.
78 |
79 | When loading is done simply call:
80 | ```kotlin
81 | test_button.doResult(success: Boolean)
82 | ```
83 | This will stop the loading animation and start the result animation according to the given success.
84 | Optional you can add a callback if you want to know when the animation is done. It also returns the LoadingButton itself:
85 | ```kotlin
86 | test_button.doResult(success: Boolean) { it: KTLoadingButton ->
87 | // do stuff here
88 | }
89 | ```
90 |
91 | #### Option 2: PROGRESS
92 | Add the progressStyle option to your xml layout:
93 | ```xml
94 |
101 | ```
102 | All you have to do now is to call:
103 | ```kotlin
104 | test_button.setProgress(progress: Float)
105 | ```
106 | to show the progress.
107 |
108 | ## Good to know
109 |
110 | - You can add a callback to the button if you need the click event even if the button is in loading state. The return type decides if the click event should be passed to the LoadingButton:
111 | ```kotlin
112 | test_button.touchListener = {
113 | // do some stuff
114 | return [true|false]
115 | }
116 | ```
117 | This can be handy if for example want the user to cancel the loading process.
118 |
119 | - You can reset the button by calling:
120 | ```kotlin
121 | test_button.reset()
122 | ```
123 |
124 | - You can set a validation function if the button should show the loading animation (e.g. if a text field is not filled correctly):
125 | ```kotlin
126 | test_button.validation = { // call a function for validation }
127 | ```
128 |
129 | ## Customization
130 |
131 | ### In XML
132 | |Attribute| Value/s | Description| Required | Default |
133 | |--|--| -- | -- | -- |
134 | | buttonName | String | Sets the text for the button |[x]| |
135 | | buttonTextSize | Dimension (SP) | Sets the text size for the button |[]| 16sp |
136 | | allCaps | Boolean | If set to true all button text will be in caps |[]| true |
137 | | buttonColor | Color | Sets the color for the button text and progress |[]| #373737|
138 | | loadingBackgroundColor | Color | Sets the background color for a failed result |[]| buttonColor with transparency of 31% |
139 | | succeedColor | Color | Sets the background color for a success result |[]|#4CAF50 |
140 | | failedColor | Color | Sets the background color for a failed result |[]|#F44336 |
141 | | autoResetButtonAfterResult | Boolean | Decides if the button should reset itself after the result was displayed (after 1,5 seconds) |[]| true |
142 | | progressStyle | [INTERMEDIATE or PROGRESS] | Decides if the button should intermediate or display a progress |[]| INTERMEDIATE |
143 | | border_thickness | Dimension (DP) | sets the thickness of the outline border and progress circle |[]| 1dp |
144 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
18 |
19 |
28 |
29 |
38 |
39 |
48 |
49 |
58 |
59 |
68 |
69 |
79 |
80 |
81 |
90 |
91 |
100 |
101 |
110 |
111 |
120 |
121 |
129 |
130 |
131 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/ktloadingbutton/src/main/java/de/timonknispel/ktloadingbutton/KTLoadingButton.kt:
--------------------------------------------------------------------------------
1 | package de.timonknispel.ktloadingbutton
2 |
3 | import android.animation.Animator
4 | import android.animation.ValueAnimator
5 | import android.annotation.SuppressLint
6 | import android.content.Context
7 | import android.graphics.*
8 | import android.os.Handler
9 | import android.util.AttributeSet
10 | import android.util.Log
11 | import android.view.MotionEvent
12 | import android.view.View
13 | import android.view.animation.AccelerateInterpolator
14 | import androidx.core.graphics.ColorUtils
15 | import java.util.*
16 | import kotlin.math.ceil
17 | import kotlin.math.sqrt
18 |
19 | class KTLoadingButton : View {
20 |
21 | private var state: State = State.NONE
22 |
23 | private var isSucceed = true
24 | private var isDoResult = false
25 | private var isDoneWithResult = false
26 |
27 | private var buttonText = ""
28 | private var buttonColor = 0
29 | private var successColor = 0
30 | private var failColor = 0
31 | private var textSize = 0
32 | private var _loadingBGPaint = 0
33 | private var progressStyle = ProgressStyle.INTERMEDIATE
34 | private var borderThickness = 0F
35 |
36 | private var submitAnim: ValueAnimator? = null
37 | private var loadingAnim: ValueAnimator? = null
38 | private var resultAnim: ValueAnimator? = null
39 |
40 | private var mWidth: Int = 0
41 | private var mHeight: Int = 0
42 |
43 | private var maxWidth: Int = 0
44 | private var maxHeight: Int = 0
45 |
46 | private var x: Int = 0
47 | private var y: Int = 0
48 |
49 | private lateinit var bgPaint: Paint
50 | private lateinit var loadingPaint: Paint
51 | private lateinit var resultPaint: Paint
52 | private lateinit var textPaint: Paint
53 | private lateinit var loadingBGPaint: Paint
54 |
55 | private lateinit var buttonPath: Path
56 | private lateinit var loadPath: Path
57 | private lateinit var dst: Path
58 | private lateinit var pathMeasure: PathMeasure
59 | private lateinit var resultPath: Path
60 |
61 | private lateinit var circleLeft: RectF
62 | private lateinit var circleMid: RectF
63 | private lateinit var circleRight: RectF
64 |
65 | private var textWidth: Int = 0
66 | private var textHeight: Int = 0
67 |
68 | private var loadValue = 0F
69 | private var currentProgress = 0F
70 |
71 | private var resetHandler = Handler()
72 | private var shouldAutoResetAfterResult: Boolean = true
73 |
74 | var touchListener: ((loadingButton: KTLoadingButton) -> Boolean)? = null
75 | var validation: (() -> Boolean)? = null
76 |
77 | constructor(context: Context) : this(context, null)
78 | constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)
79 | constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
80 | context,
81 | attrs,
82 | defStyleAttr
83 | ) {
84 | val typedArray =
85 | context?.obtainStyledAttributes(attrs, R.styleable.KTLoadingButton, defStyleAttr, 0)
86 | if (typedArray != null) {
87 | if (typedArray.getString(R.styleable.KTLoadingButton_buttonName) != null) {
88 | buttonText =
89 | typedArray.getString(R.styleable.KTLoadingButton_buttonName).orEmpty()
90 | }
91 | if (typedArray.getBoolean(R.styleable.KTLoadingButton_allCaps, true)) {
92 | buttonText = buttonText.toUpperCase(Locale.getDefault())
93 | }
94 |
95 | buttonColor = typedArray.getColor(
96 | R.styleable.KTLoadingButton_buttonColor,
97 | Color.parseColor("#373737")
98 | )
99 | successColor = typedArray.getColor(
100 | R.styleable.KTLoadingButton_succeedColor,
101 | Color.parseColor("#4CAF50")
102 | )
103 | failColor = typedArray.getColor(
104 | R.styleable.KTLoadingButton_failedColor,
105 | Color.parseColor("#F44336")
106 | )
107 | _loadingBGPaint = typedArray.getColor(
108 | R.styleable.KTLoadingButton_loadingBackgroundColor,
109 | -1
110 | )
111 | textSize = sp2px(
112 | typedArray.getDimension(
113 | R.styleable.KTLoadingButton_buttonTextSize,
114 | 16F
115 | )
116 | )
117 | shouldAutoResetAfterResult = typedArray.getBoolean(
118 | R.styleable.KTLoadingButton_autoResetButtonAfterResult,
119 | true
120 | )
121 | borderThickness =
122 | typedArray.getDimension(
123 | R.styleable.KTLoadingButton_border_thickness,
124 | 1F
125 | )
126 | progressStyle =
127 | ProgressStyle.formID(
128 | typedArray.getInt(
129 | R.styleable.KTLoadingButton_progressStyle,
130 | ProgressStyle.INTERMEDIATE.id
131 | )
132 | )
133 | typedArray.recycle()
134 | }
135 | this.setLayerType(LAYER_TYPE_SOFTWARE, null)
136 | setup()
137 | }
138 |
139 | private fun setup() {
140 | bgPaint = Paint().apply {
141 | color = buttonColor
142 | style = Paint.Style.STROKE
143 | strokeWidth = dp2px(borderThickness)
144 | isAntiAlias = true
145 | }
146 |
147 | loadingPaint = Paint().apply {
148 | color = buttonColor
149 | style = Paint.Style.STROKE
150 | strokeWidth = dp2px(borderThickness)
151 | isAntiAlias = true
152 | }
153 |
154 | loadingBGPaint = Paint().apply {
155 | color = if (_loadingBGPaint != -1) _loadingBGPaint else ColorUtils.setAlphaComponent(
156 | buttonColor,
157 | 70
158 | )
159 | style = Paint.Style.STROKE
160 | strokeWidth = dp2px(borderThickness)
161 | isAntiAlias = true
162 | }
163 |
164 | resultPaint = Paint().apply {
165 | color = Color.WHITE
166 | style = Paint.Style.STROKE
167 | strokeWidth = textSize
168 | strokeCap = Paint.Cap.ROUND
169 | isAntiAlias = true
170 | }
171 |
172 | textPaint = Paint().apply {
173 | color = buttonColor
174 | strokeWidth = this@KTLoadingButton.textSize / 6F
175 | this.textSize = this@KTLoadingButton.textSize.toFloat()
176 | isAntiAlias = true
177 | }
178 |
179 | textWidth = getTextWidth(textPaint, buttonText)
180 | textHeight = getTextHeight(textPaint, buttonText)
181 |
182 | buttonPath = Path()
183 | loadPath = Path()
184 | resultPath = Path()
185 | dst = Path()
186 | circleMid = RectF()
187 | circleLeft = RectF()
188 | circleRight = RectF()
189 | pathMeasure = PathMeasure()
190 | }
191 |
192 | fun startLoading() {
193 | if (state == State.NONE) {
194 | startSubmitAnim()
195 | }
196 | }
197 |
198 | fun doResult(isSucceed: Boolean, onDone: ((loadingButton: KTLoadingButton) -> Unit)? = null) {
199 | if (state == State.NONE || state == State.RESULT || isDoResult) {
200 | return
201 | }
202 | isDoResult = true
203 | this.isSucceed = isSucceed
204 | if (state == State.LOADING) {
205 | startResultAnim(onDone)
206 | }
207 | }
208 |
209 | fun reset() {
210 | submitAnim?.cancel()
211 | loadingAnim?.cancel()
212 | resultAnim?.cancel()
213 | state = State.NONE
214 | mWidth = maxWidth
215 | mHeight = maxHeight
216 | isSucceed = false
217 | isDoResult = false
218 | currentProgress = 0f
219 | setup()
220 | invalidate()
221 | }
222 |
223 | fun setProgress(progress: Float) {
224 | if (progress < 0 || progress > 100) {
225 | return
226 | }
227 | currentProgress = (progress * 0.01).toFloat()
228 | if (progressStyle == ProgressStyle.PROGRESS) {
229 | if (state != State.LOADING) {
230 | startLoading()
231 | }
232 | invalidate()
233 | }
234 | }
235 |
236 | fun isLoading() = state == State.LOADING
237 |
238 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
239 | val widthMode = MeasureSpec.getMode(widthMeasureSpec)
240 | var widthSize = MeasureSpec.getSize(widthMeasureSpec)
241 | val heightMode = MeasureSpec.getMode(heightMeasureSpec)
242 | var heightSize = MeasureSpec.getSize(heightMeasureSpec)
243 |
244 | if (widthMode == MeasureSpec.AT_MOST) {
245 | widthSize = textWidth + 100 + (dp2px(borderThickness) * 2).toInt()
246 | }
247 |
248 | if (heightMode == MeasureSpec.AT_MOST) {
249 | heightSize = (textHeight * 3F).toInt() + (dp2px(borderThickness) * 2).toInt()
250 | }
251 |
252 | if (heightSize > widthSize) {
253 | heightSize = (widthSize * 0.25F).toInt() + (dp2px(borderThickness) * 2).toInt()
254 | }
255 |
256 | mWidth = widthSize - (dp2px(borderThickness) * 2).toInt()
257 | mHeight = heightSize - (dp2px(borderThickness) * 2).toInt()
258 | x = (widthSize * 0.5F).toInt()
259 | y = (heightSize * 0.5F).toInt()
260 | maxWidth = mWidth
261 | maxHeight = mHeight
262 |
263 | setMeasuredDimension(widthSize, heightSize)
264 | }
265 |
266 | override fun onDraw(canvas: Canvas) {
267 | super.onDraw(canvas)
268 | canvas.translate(x.toFloat(), y.toFloat())
269 | drawButton(canvas)
270 | if (state == State.NONE || state == State.SUBMIT && mWidth > textWidth) {
271 | drawButtonText(canvas)
272 | }
273 | if (state == State.LOADING) {
274 | drawLoading(canvas)
275 | }
276 | if (state == State.RESULT) {
277 | drawResult(canvas, isSucceed)
278 | }
279 | }
280 |
281 | private fun drawButton(canvas: Canvas) {
282 | buttonPath.reset()
283 | circleLeft.set(
284 | (-mWidth / 2).toFloat(),
285 | (-mHeight / 2).toFloat(),
286 | (-mWidth / 2 + mHeight).toFloat(),
287 | (mHeight / 2).toFloat()
288 | )
289 | buttonPath.arcTo(circleLeft, 90f, 180f)
290 | buttonPath.lineTo((mWidth / 2 - mHeight / 2).toFloat(), (-mHeight / 2).toFloat())
291 | circleRight.set(
292 | (mWidth / 2 - mHeight).toFloat(),
293 | (-mHeight / 2).toFloat(),
294 | (mWidth / 2).toFloat(),
295 | (mHeight / 2).toFloat()
296 | )
297 | buttonPath.arcTo(circleRight, 270f, 180f)
298 | buttonPath.lineTo((-mWidth / 2 + mHeight / 2).toFloat(), (mHeight / 2).toFloat())
299 | canvas.drawPath(buttonPath, bgPaint)
300 | }
301 |
302 | private fun drawLoading(canvas: Canvas) {
303 | dst.reset()
304 | circleMid.set(
305 | (-maxHeight / 2F),
306 | (-maxHeight / 2F),
307 | (maxHeight / 2F),
308 | (maxHeight / 2F)
309 | )
310 | loadPath.addArc(circleMid, 270F, 359.999f)
311 | pathMeasure.setPath(loadPath, true)
312 | var startD = 0f
313 | val stopD: Float
314 | if (progressStyle == ProgressStyle.INTERMEDIATE) {
315 | startD = (pathMeasure.length * loadValue)
316 | stopD = startD + ((pathMeasure.length / 2) * loadValue)
317 | } else {
318 | stopD = pathMeasure.length * currentProgress
319 | }
320 | pathMeasure.getSegment(startD, stopD, dst, true)
321 | canvas.drawPath(dst, loadingPaint)
322 | canvas.drawCircle(
323 | canvas.clipBounds.exactCenterX(),
324 | canvas.clipBounds.exactCenterY(),
325 | maxHeight / 2F,
326 | loadingBGPaint
327 | )
328 | }
329 |
330 | private fun drawResult(canvas: Canvas, isSucceed: Boolean) {
331 | if (isSucceed) {
332 | resultPath.moveTo((-mHeight / 6).toFloat(), 0f)
333 | resultPath.lineTo(0f, (-mHeight / 6 + (1 + sqrt(5.0)) * mHeight / 12).toFloat())
334 | resultPath.lineTo((mHeight / 6).toFloat(), (-mHeight / 6).toFloat())
335 | } else {
336 | resultPath.moveTo((-mHeight / 6).toFloat(), (mHeight / 6).toFloat())
337 | resultPath.lineTo((mHeight / 6).toFloat(), (-mHeight / 6).toFloat())
338 | resultPath.moveTo((-mHeight / 6).toFloat(), (-mHeight / 6).toFloat())
339 | resultPath.lineTo((mHeight / 6).toFloat(), (mHeight / 6).toFloat())
340 | }
341 | canvas.drawPath(resultPath, resultPaint)
342 | }
343 |
344 | private fun drawButtonText(canvas: Canvas) {
345 | textPaint.alpha = (mWidth - textWidth) * 255 / (maxWidth - textWidth)
346 | canvas.drawText(buttonText, (-textWidth / 2).toFloat(), getTextBaseLineOffset(), textPaint)
347 | }
348 |
349 | private fun startSubmitAnim() {
350 | state = State.SUBMIT
351 | submitAnim = ValueAnimator.ofInt(maxWidth, maxHeight)
352 | submitAnim?.apply {
353 | addUpdateListener { animation ->
354 | mWidth = animation.animatedValue as Int
355 | if (mWidth == mHeight) {
356 | bgPaint.color = Color.TRANSPARENT
357 | }
358 | invalidate()
359 | }
360 |
361 | duration = 300
362 | interpolator = AccelerateInterpolator()
363 |
364 | addListener(object : Animator.AnimatorListener {
365 | override fun onAnimationStart(animation: Animator) {
366 | }
367 |
368 | override fun onAnimationEnd(animation: Animator) {
369 | if (isDoResult) {
370 | startResultAnim(null)
371 | } else {
372 | startLoadingAnim()
373 | }
374 | }
375 |
376 | override fun onAnimationCancel(animation: Animator) {
377 | }
378 |
379 | override fun onAnimationRepeat(animation: Animator) {
380 | }
381 | })
382 | }?.start()
383 | }
384 |
385 | private fun startLoadingAnim() {
386 | state = State.LOADING
387 | if (progressStyle == ProgressStyle.PROGRESS) {
388 | invalidate()
389 | return
390 | }
391 | loadingAnim = ValueAnimator.ofFloat(0.0f, 1.0f).apply {
392 | addUpdateListener { animation ->
393 | loadValue = animation.animatedValue as Float
394 | invalidate()
395 | }
396 | duration = 2000
397 | repeatCount = ValueAnimator.INFINITE
398 | }
399 | loadingAnim?.start()
400 | }
401 |
402 | private fun startResultAnim(onDone: ((loadingButton: KTLoadingButton) -> Unit)?) {
403 | state = State.RESULT
404 | isDoneWithResult = false
405 | loadingAnim?.cancel()
406 |
407 | resultAnim = ValueAnimator.ofInt(maxHeight, maxWidth).apply {
408 | addUpdateListener { animation ->
409 | mWidth = animation.animatedValue as Int
410 | resultPaint.alpha = (mWidth - mHeight) * 255 / (maxWidth - maxHeight)
411 | if (mWidth == mHeight) {
412 | if (isSucceed) {
413 | bgPaint.color = successColor
414 | } else {
415 | bgPaint.color = failColor
416 | }
417 | bgPaint.style = Paint.Style.FILL_AND_STROKE
418 | }
419 | invalidate()
420 | }
421 | addListener(object : Animator.AnimatorListener {
422 | override fun onAnimationStart(animation: Animator) {
423 | }
424 |
425 | override fun onAnimationEnd(animation: Animator) {
426 | postDelayed({
427 | onDone?.invoke(this@KTLoadingButton)
428 | isDoneWithResult = true
429 | }, 500)
430 | if (shouldAutoResetAfterResult) {
431 | resetHandler.postDelayed({
432 | reset()
433 | }, 1500)
434 | }
435 | }
436 |
437 | override fun onAnimationCancel(animation: Animator) {
438 | }
439 |
440 | override fun onAnimationRepeat(animation: Animator) {
441 | }
442 | })
443 | duration = 300
444 | interpolator = AccelerateInterpolator()
445 | }
446 | resultAnim?.start()
447 | }
448 |
449 | @SuppressLint("ClickableViewAccessibility")
450 | override fun onTouchEvent(event: MotionEvent): Boolean {
451 | when (event.action) {
452 | MotionEvent.ACTION_UP -> {
453 | if (touchListener?.invoke(this) == false){
454 | return true
455 | }
456 | when (state) {
457 | State.NONE -> {
458 | if (validation != null) {
459 | if (validation?.invoke() == true) {
460 | startSubmitAnim()
461 | } else {
462 | state = State.LOADING
463 | doResult(false)
464 | }
465 | } else {
466 | startSubmitAnim()
467 | }
468 | }
469 | State.RESULT -> if (isDoneWithResult) {
470 | resetHandler.removeCallbacksAndMessages(null)
471 | reset()
472 | return true
473 | }
474 | else -> return true
475 | }
476 |
477 | }
478 | }
479 | return super.onTouchEvent(event)
480 | }
481 |
482 | override fun onDetachedFromWindow() {
483 | super.onDetachedFromWindow()
484 | if (submitAnim != null) {
485 | submitAnim?.cancel()
486 | }
487 | if (loadingAnim != null) {
488 | loadingAnim?.cancel()
489 | }
490 | if (resultAnim != null) {
491 | resultAnim?.cancel()
492 | }
493 | }
494 |
495 | private fun sp2px(sp: Float): Int {
496 | val fontScale = context.resources.displayMetrics.scaledDensity
497 | return (sp * fontScale + 0.5f).toInt()
498 | }
499 |
500 | private fun dp2px(dp: Float): Float {
501 | val dpScale = context.resources.displayMetrics.density
502 | return (dp * dpScale)
503 | }
504 |
505 | private fun getTextWidth(paint: Paint, str: String?): Int {
506 | var mRet = 0
507 | if (!str.isNullOrEmpty()) {
508 | val len = str.length
509 | val widths = FloatArray(len)
510 | paint.getTextWidths(str, widths)
511 | for (j in 0 until len) {
512 | mRet += ceil(widths[j].toDouble()).toInt()
513 | }
514 | }
515 | return mRet
516 | }
517 |
518 | private fun getTextHeight(paint: Paint, str: String): Int {
519 | val rect = Rect()
520 | paint.getTextBounds(str, 0, str.length, rect)
521 | return rect.height()
522 | }
523 |
524 | private fun getTextBaseLineOffset(): Float {
525 | val fm = textPaint.fontMetrics
526 | return -(fm.bottom + fm.top) / 2
527 | }
528 | }
529 |
530 | enum class State {
531 | NONE, LOADING, RESULT, SUBMIT;
532 | }
533 |
534 | enum class ProgressStyle(val id: Int) {
535 | PROGRESS(1), INTERMEDIATE(0);
536 |
537 | companion object {
538 | fun formID(id: Int) = enumValues().find { it.id == id } ?: INTERMEDIATE
539 | }
540 | }
541 |
--------------------------------------------------------------------------------