├── 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
│ │ │ ├── drawable
│ │ │ │ └── ic_launcher_background.xml
│ │ │ └── layout
│ │ │ │ └── activity_main.xml
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── com
│ │ │ └── jem
│ │ │ └── MainActivity.kt
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── jem
│ │ │ └── ExampleUnitTest.kt
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── jem
│ │ ├── ExampleInstrumentedTest.kt
│ │ ├── RubberSeekBarTest.kt
│ │ └── RubberRangePickerTest.kt
├── proguard-rules.pro
└── build.gradle
├── rubberpicker
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ └── values
│ │ │ │ ├── strings.xml
│ │ │ │ └── attrs.xml
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── com
│ │ │ └── jem
│ │ │ └── rubberpicker
│ │ │ ├── ElasticBehavior.kt
│ │ │ ├── Utils.kt
│ │ │ ├── RubberSeekBar.kt
│ │ │ └── RubberRangePicker.kt
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── jem
│ │ │ └── rubberpicker
│ │ │ └── ExampleUnitTest.java
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── jem
│ │ └── rubberpicker
│ │ └── ExampleInstrumentedTest.java
├── proguard-rules.pro
└── build.gradle
├── settings.gradle
├── .gitattributes
├── RubberPicker-Demo.gif
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .idea
├── codeStyles
│ └── codeStyleConfig.xml
└── runConfigurations.xml
├── .github
└── workflows
│ └── android.yml
├── gradle.properties
├── LICENSE
├── .gitignore
├── gradlew.bat
├── gradlew
└── README.md
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/rubberpicker/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':rubberpicker'
2 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/RubberPicker-Demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chrisvin/RubberPicker/HEAD/RubberPicker-Demo.gif
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | RubberPicker
3 |
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chrisvin/RubberPicker/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/rubberpicker/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | RubberPicker
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chrisvin/RubberPicker/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chrisvin/RubberPicker/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chrisvin/RubberPicker/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chrisvin/RubberPicker/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chrisvin/RubberPicker/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chrisvin/RubberPicker/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/Chrisvin/RubberPicker/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/Chrisvin/RubberPicker/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/Chrisvin/RubberPicker/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/Chrisvin/RubberPicker/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/rubberpicker/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/rubberpicker/src/main/java/com/jem/rubberpicker/ElasticBehavior.kt:
--------------------------------------------------------------------------------
1 | package com.jem.rubberpicker
2 |
3 | enum class ElasticBehavior {
4 | LINEAR, CUBIC, RIGID
5 | }
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #008577
4 | #00574B
5 | #D81B60
6 |
7 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Oct 14 16:51:44 CEST 2019
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 |
--------------------------------------------------------------------------------
/rubberpicker/src/main/java/com/jem/rubberpicker/Utils.kt:
--------------------------------------------------------------------------------
1 | package com.jem.rubberpicker
2 |
3 | import android.content.Context
4 | import android.util.TypedValue
5 |
6 | fun convertDpToPx(context: Context, dpValue: Float): Float {
7 | return TypedValue.applyDimension(
8 | TypedValue.COMPLEX_UNIT_DIP,
9 | dpValue,
10 | context.resources.displayMetrics
11 | )
12 | }
13 |
14 |
--------------------------------------------------------------------------------
/.github/workflows/android.yml:
--------------------------------------------------------------------------------
1 | name: Android CI
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: ubuntu-latest
9 |
10 | steps:
11 | - uses: actions/checkout@v1
12 | - name: set up JDK 1.8
13 | uses: actions/setup-java@v1
14 | with:
15 | java-version: 1.8
16 | - name: Make Gradle executable
17 | run: chmod +x ./gradlew
18 | - name: Build with Gradle
19 | run: ./gradlew build
20 |
--------------------------------------------------------------------------------
/app/src/test/java/com/jem/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.jem
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 | }
18 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/rubberpicker/src/test/java/com/jem/rubberpicker/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.jem.rubberpicker;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/jem/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.jem
2 |
3 | import android.support.test.InstrumentationRegistry
4 | import android.support.test.runner.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.getTargetContext()
22 | assertEquals("com.jem", appContext.packageName)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/rubberpicker/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 |
--------------------------------------------------------------------------------
/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 | # Kotlin code style for this project: "official" or "obsolete":
15 | kotlin.code.style=official
16 |
--------------------------------------------------------------------------------
/rubberpicker/src/androidTest/java/com/jem/rubberpicker/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.jem.rubberpicker;
2 |
3 | import android.content.Context;
4 | import android.support.test.InstrumentationRegistry;
5 | import android.support.test.runner.AndroidJUnit4;
6 |
7 | import org.junit.Test;
8 | import org.junit.runner.RunWith;
9 |
10 | import static org.junit.Assert.*;
11 |
12 | /**
13 | * Instrumented test, which will execute on an Android device.
14 | *
15 | * @see Testing documentation
16 | */
17 | @RunWith(AndroidJUnit4.class)
18 | public class ExampleInstrumentedTest {
19 | @Test
20 | public void useAppContext() {
21 | // Context of the app under test.
22 | Context appContext = InstrumentationRegistry.getTargetContext();
23 |
24 | assertEquals("com.jem.rubberpicker.test", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Jem
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.
--------------------------------------------------------------------------------
/rubberpicker/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 |
4 | android {
5 | compileSdkVersion 28
6 |
7 |
8 |
9 | defaultConfig {
10 | minSdkVersion 15
11 | targetSdkVersion 28
12 | versionCode 1
13 | versionName "1.0"
14 |
15 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
16 |
17 | }
18 |
19 | buildTypes {
20 | release {
21 | minifyEnabled false
22 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
23 | }
24 | }
25 |
26 | }
27 |
28 | dependencies {
29 | implementation fileTree(dir: 'libs', include: ['*.jar'])
30 |
31 | implementation 'com.android.support:appcompat-v7:28.0.0'
32 | testImplementation 'junit:junit:4.12'
33 | androidTestImplementation 'com.android.support.test:runner:1.0.2'
34 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
35 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
36 | implementation 'com.android.support:support-dynamic-animation:28.0.0'
37 | }
38 | repositories {
39 | mavenCentral()
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 | android {
8 | compileSdkVersion 28
9 | defaultConfig {
10 | applicationId "com.jem"
11 | minSdkVersion 15
12 | targetSdkVersion 28
13 | versionCode 1
14 | versionName "1.0"
15 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
16 | }
17 | buildTypes {
18 | release {
19 | minifyEnabled false
20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
21 | }
22 | }
23 | }
24 |
25 | dependencies {
26 | implementation fileTree(dir: 'libs', include: ['*.jar'])
27 | implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
28 | implementation 'com.android.support:appcompat-v7:28.0.0'
29 | implementation 'com.android.support.constraint:constraint-layout:1.1.3'
30 | testImplementation 'junit:junit:4.12'
31 | androidTestImplementation 'com.android.support.test:runner:1.0.2'
32 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
33 |
34 | // implementation project(":rubberpicker")
35 | implementation 'com.github.Chrisvin:rubberpicker:v1.2'
36 | }
37 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.ap_
4 | *.aab
5 |
6 | # Files for the ART/Dalvik VM
7 | *.dex
8 |
9 | # Java class files
10 | *.class
11 |
12 | # Generated files
13 | bin/
14 | gen/
15 | out/
16 |
17 | # Gradle files
18 | .gradle/
19 | build/
20 |
21 | # Local configuration file (sdk path, etc)
22 | local.properties
23 |
24 | # Proguard folder generated by Eclipse
25 | proguard/
26 |
27 | # Log Files
28 | *.log
29 |
30 | # Android Studio Navigation editor temp files
31 | .navigation/
32 |
33 | # Android Studio captures folder
34 | captures/
35 |
36 | # IntelliJ
37 | *.iml
38 | .idea/workspace.xml
39 | .idea/tasks.xml
40 | .idea/gradle.xml
41 | .idea/assetWizardSettings.xml
42 | .idea/dictionaries
43 | .idea/libraries
44 | .idea/caches
45 | .idea/misc.xml
46 | .idea/modules.xml
47 | .idea/codeStyles/Project.xml
48 | .idea/vcs.xml
49 |
50 | # Keystore files
51 | # Uncomment the following lines if you do not want to check your keystore files in.
52 | #*.jks
53 | #*.keystore
54 |
55 | # External native build folder generated in Android Studio 2.2 and later
56 | .externalNativeBuild
57 |
58 | # Google Services (e.g. APIs or Firebase)
59 | google-services.json
60 |
61 | # Freeline
62 | freeline.py
63 | freeline/
64 | freeline_project_description.json
65 |
66 | # fastlane
67 | fastlane/report.xml
68 | fastlane/Preview.html
69 | fastlane/screenshots
70 | fastlane/test_output
71 | fastlane/readme.md
72 |
--------------------------------------------------------------------------------
/rubberpicker/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 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/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/androidTest/java/com/jem/RubberSeekBarTest.kt:
--------------------------------------------------------------------------------
1 | package com.jem
2 |
3 | import android.support.test.InstrumentationRegistry
4 | import android.support.test.filters.LargeTest
5 | import android.support.test.runner.AndroidJUnit4
6 | import com.jem.rubberpicker.ElasticBehavior
7 | import com.jem.rubberpicker.RubberSeekBar
8 | import junit.framework.Assert.assertEquals
9 | import org.junit.Before
10 | import org.junit.Test
11 | import org.junit.runner.RunWith
12 |
13 | @RunWith(AndroidJUnit4::class)
14 | @LargeTest
15 | class RubberSeekBarTest {
16 |
17 | private lateinit var rubberSeekBar: RubberSeekBar
18 |
19 | @Before
20 | fun setUp() {
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | rubberSeekBar = RubberSeekBar(appContext)
23 | }
24 |
25 | @Test
26 | fun getCurrentValue() {
27 | if (true) {
28 | //TODO: Convert this unit test to espresso UI test and then remove this if check
29 | // Since `setCurrentValue()` is tightly coupled with UI, and can't be tested without it.
30 | return
31 | }
32 |
33 | assertEquals(0, rubberSeekBar.getCurrentValue())
34 | rubberSeekBar.setCurrentValue(50)
35 | assertEquals(50, rubberSeekBar.getCurrentValue())
36 | rubberSeekBar.setCurrentValue(19)
37 | assertEquals(19, rubberSeekBar.getCurrentValue())
38 | rubberSeekBar.setCurrentValue(63)
39 | assertEquals(63, rubberSeekBar.getCurrentValue())
40 | rubberSeekBar.setCurrentValue(96)
41 | assertEquals(96, rubberSeekBar.getCurrentValue())
42 |
43 | rubberSeekBar.setMax(85)
44 | assertEquals(85, rubberSeekBar.getCurrentValue())
45 | rubberSeekBar.setCurrentValue(3)
46 | assertEquals(3, rubberSeekBar.getCurrentValue())
47 | rubberSeekBar.setMin(39)
48 | assertEquals(39, rubberSeekBar.getCurrentValue())
49 | rubberSeekBar.setCurrentValue(47)
50 | assertEquals(47, rubberSeekBar.getCurrentValue())
51 | }
52 |
53 | @Test
54 | fun getMin() {
55 | assertEquals(0, rubberSeekBar.getMin())
56 | rubberSeekBar.setMin(13)
57 | assertEquals(13, rubberSeekBar.getMin())
58 | rubberSeekBar.setMin(79)
59 | assertEquals(79, rubberSeekBar.getMin())
60 | }
61 |
62 | @Test
63 | fun getMax() {
64 | assertEquals(100, rubberSeekBar.getMax())
65 | rubberSeekBar.setMax(66)
66 | assertEquals(66, rubberSeekBar.getMax())
67 | rubberSeekBar.setMax(11)
68 | assertEquals(11, rubberSeekBar.getMax())
69 | }
70 |
71 | @Test
72 | fun getElasticBehavior() {
73 | rubberSeekBar.setElasticBehavior(ElasticBehavior.RIGID)
74 | assertEquals(ElasticBehavior.RIGID, rubberSeekBar.getElasticBehavior())
75 | rubberSeekBar.setElasticBehavior(ElasticBehavior.CUBIC)
76 | assertEquals(ElasticBehavior.CUBIC, rubberSeekBar.getElasticBehavior())
77 | rubberSeekBar.setElasticBehavior(ElasticBehavior.LINEAR)
78 | assertEquals(ElasticBehavior.LINEAR, rubberSeekBar.getElasticBehavior())
79 | }
80 |
81 | @Test
82 | fun getDampingRation() {
83 | rubberSeekBar.setDampingRatio(1.01f)
84 | assertEquals(1.01f, rubberSeekBar.getDampingRation())
85 | rubberSeekBar.setDampingRatio(4.76f)
86 | assertEquals(4.76f, rubberSeekBar.getDampingRation())
87 | }
88 |
89 | @Test
90 | fun getStiffness() {
91 | rubberSeekBar.setStiffness(2.34f)
92 | assertEquals(2.34f, rubberSeekBar.getStiffness())
93 | rubberSeekBar.setStiffness(4.32f)
94 | assertEquals(4.32f, rubberSeekBar.getStiffness())
95 | }
96 |
97 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/jem/RubberRangePickerTest.kt:
--------------------------------------------------------------------------------
1 | package com.jem
2 |
3 | import android.support.test.InstrumentationRegistry
4 | import android.support.test.filters.LargeTest
5 | import android.support.test.runner.AndroidJUnit4
6 | import com.jem.rubberpicker.ElasticBehavior
7 | import com.jem.rubberpicker.RubberRangePicker
8 | import junit.framework.Assert.assertEquals
9 | import org.junit.Before
10 | import org.junit.Test
11 | import org.junit.runner.RunWith
12 |
13 | @RunWith(AndroidJUnit4::class)
14 | @LargeTest
15 | class RubberRangePickerTest {
16 |
17 | private lateinit var rubberRangePicker: RubberRangePicker
18 |
19 | @Before
20 | fun setUp() {
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | rubberRangePicker = RubberRangePicker(appContext)
23 | }
24 |
25 | @Test
26 | fun getCurrentValue() {
27 | if (true) {
28 | //TODO: Convert this unit test to espresso UI test and then remove this if check
29 | // Since `setCurrentValue()` is tightly coupled with UI, and can't be tested without it.
30 | return
31 | }
32 |
33 | assertEquals(0, rubberRangePicker.getCurrentStartValue())
34 | assertEquals(100, rubberRangePicker.getCurrentEndValue())
35 | rubberRangePicker.setCurrentStartValue(50)
36 | assertEquals(50, rubberRangePicker.getCurrentStartValue())
37 | rubberRangePicker.setCurrentStartValue(19)
38 | assertEquals(19, rubberRangePicker.getCurrentStartValue())
39 | rubberRangePicker.setCurrentEndValue(63)
40 | assertEquals(63, rubberRangePicker.getCurrentEndValue())
41 | rubberRangePicker.setCurrentEndValue(96)
42 | assertEquals(96, rubberRangePicker.getCurrentEndValue())
43 |
44 | rubberRangePicker.setMax(85)
45 | assertEquals(85, rubberRangePicker.getCurrentEndValue())
46 | assertEquals(19, rubberRangePicker.getCurrentStartValue())
47 |
48 | rubberRangePicker.setCurrentStartValue(3)
49 | assertEquals(3, rubberRangePicker.getCurrentStartValue())
50 | rubberRangePicker.setCurrentEndValue(30)
51 | assertEquals(30, rubberRangePicker.getCurrentEndValue())
52 |
53 | rubberRangePicker.setMin(39)
54 | assertEquals(39, rubberRangePicker.getCurrentStartValue())
55 | assertEquals(39, rubberRangePicker.getCurrentEndValue())
56 | rubberRangePicker.setCurrentEndValue(47)
57 | assertEquals(47, rubberRangePicker.getCurrentEndValue())
58 | }
59 |
60 | @Test
61 | fun getMin() {
62 | assertEquals(0, rubberRangePicker.getMin())
63 | rubberRangePicker.setMin(17)
64 | assertEquals(17, rubberRangePicker.getMin())
65 | rubberRangePicker.setMin(93)
66 | assertEquals(93, rubberRangePicker.getMin())
67 | }
68 |
69 | @Test
70 | fun getMax() {
71 | assertEquals(100, rubberRangePicker.getMax())
72 | rubberRangePicker.setMax(58)
73 | assertEquals(58, rubberRangePicker.getMax())
74 | rubberRangePicker.setMax(29)
75 | assertEquals(29, rubberRangePicker.getMax())
76 | }
77 |
78 | @Test
79 | fun getElasticBehavior() {
80 | rubberRangePicker.setElasticBehavior(ElasticBehavior.RIGID)
81 | assertEquals(ElasticBehavior.RIGID, rubberRangePicker.getElasticBehavior())
82 | rubberRangePicker.setElasticBehavior(ElasticBehavior.CUBIC)
83 | assertEquals(ElasticBehavior.CUBIC, rubberRangePicker.getElasticBehavior())
84 | rubberRangePicker.setElasticBehavior(ElasticBehavior.LINEAR)
85 | assertEquals(ElasticBehavior.LINEAR, rubberRangePicker.getElasticBehavior())
86 | }
87 |
88 | @Test
89 | fun getDampingRation() {
90 | rubberRangePicker.setDampingRatio(1.81f)
91 | assertEquals(1.81f, rubberRangePicker.getDampingRation())
92 | rubberRangePicker.setDampingRatio(7.77f)
93 | assertEquals(7.77f, rubberRangePicker.getDampingRation())
94 | }
95 |
96 | @Test
97 | fun getStiffness() {
98 | rubberRangePicker.setStiffness(1.134f)
99 | assertEquals(1.134f, rubberRangePicker.getStiffness())
100 | rubberRangePicker.setStiffness(4.311f)
101 | assertEquals(4.311f, rubberRangePicker.getStiffness())
102 | }
103 |
104 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
10 |
12 |
14 |
16 |
18 |
20 |
22 |
24 |
26 |
28 |
30 |
32 |
34 |
36 |
38 |
40 |
42 |
44 |
46 |
48 |
50 |
52 |
54 |
56 |
58 |
60 |
62 |
64 |
66 |
68 |
70 |
72 |
74 |
75 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # RubberPicker
2 |
3 | [](https://opensource.org/licenses/MIT) [](https://jitpack.io/#Chrisvin/RubberPicker) [](https://android-arsenal.com/api?level=15) []( https://android-arsenal.com/details/1/7867 )
4 |
5 |

6 |
7 | RubberPicker library contains the `RubberSeekBar` and `RubberRangePicker`, inspired by [Cuberto's rubber-range-picker](https://github.com/Cuberto/rubber-range-picker).
8 |
9 | ## Getting started
10 | ### Setting up the dependency
11 | 1. Add the JitPack repository to your root build.gradle at the end of repositories:
12 | ```
13 | allprojects {
14 | repositories {
15 | ...
16 | maven { url 'https://jitpack.io' }
17 | }
18 | }
19 | ```
20 | 2. Add the RubberPicker dependency in the build.gradle:
21 | ```
22 | implementation 'com.github.Chrisvin:RubberPicker:v1.5'
23 | ```
24 |
25 | ### Demo app
26 | To run the demo project, clone the repository and run it via Android Studio.
27 |
28 | ## Usage
29 | ### Adding directly in layout.xml
30 | ```
31 |
46 |
47 |
48 |
63 | ```
64 | ### Adding/Modifying programmatically
65 | ```kotlin
66 | val rubberSeekBar = RubberSeekBar(this)
67 | rubberSeekBar.setMin(20)
68 | rubberSeekBar.setMax(80)
69 | rubberSeekBar.setElasticBehavior(ElasticBehavior.CUBIC)
70 | rubberSeekBar.setDampingRatio(0.4F)
71 | rubberSeekBar.setStiffness(1000F)
72 | rubberSeekBar.setStretchRange(50f)
73 | rubberSeekBar.setThumbRadius(32f)
74 | rubberSeekBar.setNormalTrackWidth(2f)
75 | rubberSeekBar.setHighlightTrackWidth(4f)
76 | rubberSeekBar.setNormalTrackColor(Color.GRAY)
77 | rubberSeekBar.setHighlightTrackColor(Color.BLUE)
78 | rubberSeekBar.setHighlightThumbOnTouchColor(Color.CYAN)
79 | rubberSeekBar.setDefaultThumbInsideColor(Color.WHITE)
80 |
81 | val currentValue = rubberSeekBar.getCurrentValue()
82 | rubberSeekBar.setCurrentValue(currentValue + 10)
83 | rubberSeekBar.setOnRubberSeekBarChangeListener(object : RubberSeekBar.OnRubberSeekBarChangeListener {
84 | override fun onProgressChanged(seekBar: RubberSeekBar, value: Int, fromUser: Boolean) {}
85 | override fun onStartTrackingTouch(seekBar: RubberSeekBar) {}
86 | override fun onStopTrackingTouch(seekBar: RubberSeekBar) {}
87 | })
88 |
89 |
90 | //Similarly for RubberRangePicker
91 | val rubberRangePicker = RubberRangePicker(this)
92 | rubberRangePicker.setMin(20)
93 | ...
94 | rubberRangePicker.setHighlightThumbOnTouchColor(Color.CYAN)
95 |
96 | val startThumbValue = rubberRangePicker.getCurrentStartValue()
97 | rubberRangePicker.setCurrentStartValue(startThumbValue + 10)
98 | val endThumbValue = rubberRangePicker.getCurrentEndValue()
99 | rubberRangePicker.setCurrentEndValue(endThumbValue + 10)
100 | rubberRangePicker.setOnRubberRangePickerChangeListener(object: RubberRangePicker.OnRubberRangePickerChangeListener{
101 | override fun onProgressChanged(rangePicker: RubberRangePicker, startValue: Int, endValue: Int, fromUser: Boolean) {}
102 | override fun onStartTrackingTouch(rangePicker: RubberRangePicker, isStartThumb: Boolean) {}
103 | override fun onStopTrackingTouch(rangePicker: RubberRangePicker, isStartThumb: Boolean) {}
104 | })
105 | ```
106 |
107 | ## Todo
108 | - [ ] Refactor code to remove redundant code between RubberSeekBar & RubberRangePicker.
109 | - [ ] Add step attribute, make necessary UI adjustments for step based value increments.
110 | - [ ] Current library overcomes view clipping by setting the parent layout's clipChildren & clipToPadding as false. Find a better alternative to overcome view clipping.
111 |
112 | ## Bugs and Feedback
113 | For bugs, questions and discussions please use the [Github Issues](https://github.com/Chrisvin/RubberPicker/issues).
114 |
115 | ## License
116 | ```
117 | MIT License
118 |
119 | Copyright (c) 2019 Jem
120 |
121 | Permission is hereby granted, free of charge, to any person obtaining a copy
122 | of this software and associated documentation files (the "Software"), to deal
123 | in the Software without restriction, including without limitation the rights
124 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
125 | copies of the Software, and to permit persons to whom the Software is
126 | furnished to do so, subject to the following conditions:
127 |
128 | The above copyright notice and this permission notice shall be included in all
129 | copies or substantial portions of the Software.
130 |
131 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
132 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
133 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
134 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
135 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
136 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
137 | SOFTWARE.
138 | ```
139 |
--------------------------------------------------------------------------------
/app/src/main/java/com/jem/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.jem
2 |
3 | import android.os.Bundle
4 | import android.os.Handler
5 | import android.support.v7.app.AppCompatActivity
6 | import android.util.Log
7 | import android.widget.RadioButton
8 | import android.widget.SeekBar
9 | import com.jem.rubberpicker.ElasticBehavior
10 | import com.jem.rubberpicker.RubberRangePicker
11 | import com.jem.rubberpicker.RubberSeekBar
12 | import kotlinx.android.synthetic.main.activity_main.*
13 | import android.support.v4.os.HandlerCompat.postDelayed
14 |
15 |
16 |
17 | class MainActivity : AppCompatActivity() {
18 |
19 | override fun onCreate(savedInstanceState: Bundle?) {
20 | super.onCreate(savedInstanceState)
21 | setContentView(R.layout.activity_main)
22 |
23 | elasticBehavior.setOnCheckedChangeListener { _, checkedId ->
24 | when (findViewById(checkedId).text) {
25 | "Cubic" -> {
26 | rubberSeekBar.setElasticBehavior(ElasticBehavior.CUBIC)
27 | rubberRangePicker.setElasticBehavior(ElasticBehavior.CUBIC)
28 | }
29 | "Linear" -> {
30 | rubberSeekBar.setElasticBehavior(ElasticBehavior.LINEAR)
31 | rubberRangePicker.setElasticBehavior(ElasticBehavior.LINEAR)
32 | }
33 | "Rigid" -> {
34 | rubberSeekBar.setElasticBehavior(ElasticBehavior.RIGID)
35 | rubberRangePicker.setElasticBehavior(ElasticBehavior.RIGID)
36 | }
37 | }
38 | }
39 |
40 | stretchRange.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener{
41 | override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
42 | stretchRangeValue.text = progress.toString()
43 | rubberSeekBar.setStretchRange(progress.toFloat())
44 | rubberRangePicker.setStretchRange(progress.toFloat())
45 | }
46 | override fun onStartTrackingTouch(seekBar: SeekBar?) {}
47 | override fun onStopTrackingTouch(seekBar: SeekBar?) {}
48 | })
49 |
50 | dampingRatio.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener{
51 | override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
52 | dampingRatioValue.text = (progress.toFloat()/10).toString()
53 | rubberSeekBar.setDampingRatio(progress.toFloat()/10)
54 | rubberRangePicker.setDampingRatio(progress.toFloat()/10)
55 | }
56 | override fun onStartTrackingTouch(seekBar: SeekBar?) {}
57 | override fun onStopTrackingTouch(seekBar: SeekBar?) {}
58 | })
59 |
60 | stiffness.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener{
61 | override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
62 | val progressValue = if (progress!=0) progress * 50 else 1
63 | stiffnessValue.text = (progressValue).toString()
64 | rubberSeekBar.setStiffness(progressValue.toFloat())
65 | rubberRangePicker.setStiffness(progressValue.toFloat())
66 | }
67 | override fun onStartTrackingTouch(seekBar: SeekBar?) {}
68 | override fun onStopTrackingTouch(seekBar: SeekBar?) {}
69 | })
70 |
71 | defaultThumbRadius.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener{
72 | override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
73 | val progressValue = if (progress == 0) 1 else progress
74 | defaultThumbRadiusValue.text = progressValue.toString()
75 | rubberSeekBar.setThumbRadius(progressValue.toFloat())
76 | rubberRangePicker.setThumbRadius(progressValue.toFloat())
77 | }
78 | override fun onStartTrackingTouch(seekBar: SeekBar?) {}
79 | override fun onStopTrackingTouch(seekBar: SeekBar?) {}
80 | })
81 |
82 | normalTrackWidth.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener{
83 | override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
84 | normalTrackWidthValue.text = progress.toString()
85 | rubberSeekBar.setNormalTrackWidth(progress.toFloat())
86 | rubberRangePicker.setNormalTrackWidth(progress.toFloat())
87 | }
88 | override fun onStartTrackingTouch(seekBar: SeekBar?) {}
89 | override fun onStopTrackingTouch(seekBar: SeekBar?) {}
90 | })
91 |
92 | highlightTrackWidth.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener{
93 | override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
94 | highlightTrackWidthValue.text = progress.toString()
95 | rubberSeekBar.setHighlightTrackWidth(progress.toFloat())
96 | rubberRangePicker.setHighlightTrackWidth(progress.toFloat())
97 | }
98 | override fun onStartTrackingTouch(seekBar: SeekBar?) {}
99 | override fun onStopTrackingTouch(seekBar: SeekBar?) {}
100 | })
101 |
102 | rubberSeekBar.setOnRubberSeekBarChangeListener(object: RubberSeekBar.OnRubberSeekBarChangeListener{
103 | override fun onProgressChanged(seekBar: RubberSeekBar, value: Int, fromUser: Boolean) {
104 | rubberSeekBarValue.text = value.toString()
105 | }
106 | override fun onStartTrackingTouch(seekBar: RubberSeekBar) {}
107 | override fun onStopTrackingTouch(seekBar: RubberSeekBar) {}
108 | })
109 |
110 | rubberRangePicker.setOnRubberRangePickerChangeListener(object: RubberRangePicker.OnRubberRangePickerChangeListener{
111 | override fun onProgressChanged(rangePicker: RubberRangePicker, startValue: Int, endValue: Int, fromUser: Boolean) {
112 | rubberRangePickerStartValue.text = startValue.toString()
113 | rubberRangePickerEndValue.text = endValue.toString()
114 | }
115 | override fun onStartTrackingTouch(rangePicker: RubberRangePicker, isStartThumb: Boolean) {}
116 | override fun onStopTrackingTouch(rangePicker: RubberRangePicker, isStartThumb: Boolean) {}
117 | })
118 | }
119 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
15 |
16 |
26 |
36 |
46 |
56 |
57 |
67 |
77 |
87 |
88 |
94 |
95 |
102 |
106 |
110 |
114 |
115 |
116 |
126 |
127 |
138 |
139 |
149 |
150 |
161 |
162 |
172 |
173 |
182 |
183 |
194 |
195 |
205 |
206 |
217 |
218 |
228 |
229 |
240 |
241 |
251 |
252 |
263 |
264 |
270 |
271 |
279 |
280 |
289 |
297 |
298 |
299 |
310 |
311 |
320 |
328 |
336 |
344 |
345 |
--------------------------------------------------------------------------------
/rubberpicker/src/main/java/com/jem/rubberpicker/RubberSeekBar.kt:
--------------------------------------------------------------------------------
1 | package com.jem.rubberpicker
2 |
3 | import android.content.Context
4 | import android.graphics.Canvas
5 | import android.graphics.Color
6 | import android.graphics.Paint
7 | import android.graphics.Path
8 | import android.graphics.drawable.Drawable
9 | import android.support.animation.FloatValueHolder
10 | import android.support.animation.SpringAnimation
11 | import android.support.animation.SpringForce
12 | import android.util.AttributeSet
13 | import android.view.MotionEvent
14 | import android.view.View
15 | import android.view.ViewGroup
16 | import java.util.concurrent.ArrayBlockingQueue
17 | import kotlin.math.absoluteValue
18 |
19 |
20 | class RubberSeekBar : View {
21 |
22 | private val paint: Paint by lazy {
23 | val tempPaint = Paint()
24 | tempPaint.style = Paint.Style.STROKE
25 | tempPaint.color = normalTrackColor
26 | tempPaint.strokeWidth = 5f
27 | tempPaint.isAntiAlias = true
28 | tempPaint
29 | }
30 | private var path: Path = Path()
31 | private var springAnimation: SpringAnimation? = null
32 | private var thumbX: Float = -1f
33 | private var thumbY: Float = -1f
34 | private val initialControlXPositionQueue = ArrayBlockingQueue(1)
35 | // Used to determine the start and end points of the track.
36 | // Useful for drawing and also for other calculations.
37 | private val trackStartX: Float
38 | get() {
39 | return if (drawableThumb != null) {
40 | setDrawableHalfWidthAndHeight()
41 | drawableThumbHalfWidth.toFloat()
42 | } else {
43 | drawableThumbRadius
44 | }
45 | }
46 | private val trackEndX: Float
47 | get() {
48 | return if (drawableThumb != null) {
49 | setDrawableHalfWidthAndHeight()
50 | width - drawableThumbHalfWidth.toFloat()
51 | } else {
52 | width - drawableThumbRadius
53 | }
54 | }
55 | private val trackY: Float
56 | get() {
57 | return height.toFloat() / 2
58 | }
59 |
60 | private var x1: Float = 0f
61 | private var y1: Float = 0f
62 | private var x2: Float = 0f
63 | private var y2: Float = 0f
64 |
65 | private var stretchRange: Float = -1f
66 |
67 | private var elasticBehavior: ElasticBehavior = ElasticBehavior.CUBIC
68 |
69 | private var drawableThumb: Drawable? = null
70 | private var drawableThumbHalfWidth = 0
71 | private var drawableThumbHalfHeight = 0
72 | private var drawableThumbSelected: Boolean = false
73 |
74 | private var drawableThumbRadius: Float = 0.0f
75 | private var normalTrackWidth: Float = 0.0f
76 | private var highlightTrackWidth: Float = 0.0f
77 |
78 | private var normalTrackColor: Int = 0
79 | private var highlightTrackColor: Int = 0
80 | private var highlightThumbOnTouchColor: Int = 0
81 | private var defaultThumbInsideColor: Int = 0
82 | private var dampingRatio: Float = 0f
83 | private var stiffness: Float = 0f
84 |
85 | private var minValue: Int = 0
86 | private var maxValue: Int = 100
87 |
88 | private var onChangeListener: OnRubberSeekBarChangeListener? = null
89 |
90 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) :
91 | super(context, attrs, defStyleAttr) {
92 | init(attrs)
93 | }
94 |
95 | constructor(context: Context, attrs: AttributeSet?) :
96 | super(context, attrs) {
97 | init(attrs)
98 | }
99 |
100 | constructor(context: Context) :
101 | super(context) {
102 | init(null)
103 | }
104 |
105 | private fun init(attrs: AttributeSet?) {
106 | stretchRange = convertDpToPx(context, 24f)
107 | drawableThumbRadius = convertDpToPx(context, 16f)
108 | normalTrackWidth = convertDpToPx(context, 2f)
109 | highlightTrackWidth = convertDpToPx(context, 4f)
110 | normalTrackColor = Color.GRAY
111 | highlightTrackColor = 0xFF38ACEC.toInt()
112 | highlightThumbOnTouchColor = 0xFF82CAFA.toInt()
113 | defaultThumbInsideColor = Color.WHITE
114 | dampingRatio = SpringForce.DAMPING_RATIO_HIGH_BOUNCY
115 | stiffness = SpringForce.STIFFNESS_LOW
116 |
117 | attrs?.let {
118 | val typedArray = context.obtainStyledAttributes(attrs, R.styleable.RubberSeekBar, 0, 0)
119 | stretchRange = typedArray.getDimensionPixelSize(
120 | R.styleable.RubberSeekBar_stretchRange,
121 | convertDpToPx(context, 24f).toInt()
122 | ).toFloat()
123 | drawableThumbRadius = typedArray.getDimensionPixelSize(
124 | R.styleable.RubberSeekBar_defaultThumbRadius,
125 | convertDpToPx(context, 16f).toInt()
126 | ).toFloat()
127 | normalTrackWidth = typedArray.getDimensionPixelSize(
128 | R.styleable.RubberSeekBar_normalTrackWidth,
129 | convertDpToPx(context, 2f).toInt()
130 | ).toFloat()
131 | highlightTrackWidth = typedArray.getDimensionPixelSize(
132 | R.styleable.RubberSeekBar_highlightTrackWidth,
133 | convertDpToPx(context, 4f).toInt()
134 | ).toFloat()
135 | drawableThumb = typedArray.getDrawable(R.styleable.RubberSeekBar_thumbDrawable)
136 | normalTrackColor =
137 | typedArray.getColor(R.styleable.RubberSeekBar_normalTrackColor, Color.GRAY)
138 | highlightTrackColor = typedArray.getColor(
139 | R.styleable.RubberSeekBar_highlightTrackColor,
140 | 0xFF38ACEC.toInt()
141 | )
142 | highlightThumbOnTouchColor =
143 | typedArray.getColor(
144 | R.styleable.RubberSeekBar_highlightDefaultThumbOnTouchColor,
145 | 0xFF82CAFA.toInt()
146 | )
147 | defaultThumbInsideColor =
148 | typedArray.getColor(
149 | R.styleable.RubberSeekBar_defaultThumbInsideColor,
150 | Color.WHITE
151 | )
152 | dampingRatio =
153 | typedArray.getFloat(
154 | R.styleable.RubberSeekBar_dampingRatio,
155 | SpringForce.DAMPING_RATIO_HIGH_BOUNCY
156 | )
157 | stiffness =
158 | typedArray.getFloat(R.styleable.RubberSeekBar_stiffness, SpringForce.STIFFNESS_LOW)
159 | minValue = typedArray.getInt(R.styleable.RubberSeekBar_minValue, 0)
160 | maxValue = typedArray.getInt(R.styleable.RubberSeekBar_maxValue, 100)
161 | elasticBehavior = typedArray.getInt(R.styleable.RubberSeekBar_elasticBehavior, 1).run {
162 | when (this) {
163 | 0 -> ElasticBehavior.LINEAR
164 | 1 -> ElasticBehavior.CUBIC
165 | 2 -> ElasticBehavior.RIGID
166 | else -> ElasticBehavior.CUBIC
167 | }
168 | }
169 | if (typedArray.hasValue(R.styleable.RubberSeekBar_initialValue)) {
170 | setCurrentValue(typedArray.getInt(R.styleable.RubberSeekBar_initialValue, minValue))
171 | }
172 | typedArray.recycle()
173 | }
174 | }
175 |
176 | override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
177 | super.onLayout(changed, left, top, right, bottom)
178 | if (thumbX < trackStartX) {
179 | if (initialControlXPositionQueue.isEmpty()) {
180 | thumbX = trackStartX
181 | } else {
182 | setCurrentValue(initialControlXPositionQueue.poll())
183 | }
184 | thumbY = trackY
185 | }
186 | }
187 |
188 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
189 | val minimumHeight: Int = if (drawableThumb != null) {
190 | setDrawableHalfWidthAndHeight()
191 | drawableThumbHalfHeight * 2
192 | } else {
193 | (drawableThumbRadius * 2).toInt()
194 | }
195 | setMeasuredDimension(
196 | getDefaultSize(suggestedMinimumWidth, widthMeasureSpec),
197 | measureDimension(minimumHeight + paddingTop + paddingBottom, heightMeasureSpec)
198 | )
199 | }
200 |
201 | private fun measureDimension(desiredSize: Int, measureSpec: Int): Int {
202 | var result: Int
203 | val specMode = MeasureSpec.getMode(measureSpec)
204 | val specSize = MeasureSpec.getSize(measureSpec)
205 |
206 | if (specMode == MeasureSpec.EXACTLY) {
207 | result = specSize
208 | } else {
209 | result = desiredSize
210 | if (specMode == MeasureSpec.AT_MOST) {
211 | result = Math.min(result, specSize)
212 | }
213 | }
214 |
215 | return result
216 | }
217 |
218 |
219 | override fun onDraw(canvas: Canvas?) {
220 | super.onDraw(canvas)
221 | // TODO - Try to figure out a better way to overcome view clipping
222 | // Workaround since Region.Op.REPLACE won't work in Android P & above.
223 | // Region.Op.REPLACE also doesn't work properly (at times) even in devices below Android P.
224 | (parent as? ViewGroup)?.clipChildren = false
225 | (parent as? ViewGroup)?.clipToPadding = false
226 |
227 | drawTrack(canvas)
228 | drawThumb(canvas)
229 | }
230 |
231 | private fun drawThumb(canvas: Canvas?) {
232 | if (elasticBehavior == ElasticBehavior.RIGID) {
233 | thumbY = trackY
234 | }
235 | if (drawableThumb != null) {
236 | canvas?.let {
237 | canvas.translate(thumbX, thumbY)
238 | drawableThumb?.draw(it)
239 | }
240 | } else {
241 | paint.color = highlightTrackColor
242 | paint.style = Paint.Style.FILL
243 | canvas?.drawCircle(thumbX, thumbY, drawableThumbRadius, paint)
244 | if (drawableThumbSelected) {
245 | paint.color = highlightThumbOnTouchColor
246 | } else {
247 | paint.color = defaultThumbInsideColor
248 | }
249 | canvas?.drawCircle(thumbX, thumbY, drawableThumbRadius - highlightTrackWidth, paint)
250 | paint.style = Paint.Style.STROKE
251 | }
252 | }
253 |
254 | private fun drawTrack(canvas: Canvas?) {
255 | if (thumbY == trackY) {
256 | drawRigidTrack(canvas)
257 | return
258 | }
259 |
260 | path.reset()
261 | path.moveTo(trackStartX, trackY)
262 |
263 | when (elasticBehavior) {
264 | ElasticBehavior.LINEAR -> drawLinearTrack(canvas)
265 | ElasticBehavior.CUBIC -> drawBezierTrack(canvas)
266 | ElasticBehavior.RIGID -> drawRigidTrack(canvas)
267 | }
268 | }
269 |
270 | private fun drawRigidTrack(canvas: Canvas?) {
271 | paint.color = highlightTrackColor
272 | paint.strokeWidth = highlightTrackWidth
273 | canvas?.drawLine(trackStartX, trackY, thumbX, trackY, paint)
274 | paint.color = normalTrackColor
275 | paint.strokeWidth = normalTrackWidth
276 | canvas?.drawLine(thumbX, trackY, trackEndX, trackY, paint)
277 | }
278 |
279 | private fun drawBezierTrack(canvas: Canvas?) {
280 | x1 = (thumbX + trackStartX) / 2
281 | y1 = height.toFloat() / 2
282 | x2 = x1
283 | y2 = thumbY
284 | path.cubicTo(x1, y1, x2, y2, thumbX, thumbY)
285 | paint.color = highlightTrackColor
286 | paint.strokeWidth = highlightTrackWidth
287 | canvas?.drawPath(path, paint)
288 |
289 | path.reset()
290 | path.moveTo(thumbX, thumbY)
291 | x1 = (thumbX + trackEndX) / 2
292 | y1 = thumbY
293 | x2 = x1
294 | y2 = height.toFloat() / 2
295 | path.cubicTo(x1, y1, x2, y2, trackEndX, trackY)
296 | paint.color = normalTrackColor
297 | paint.strokeWidth = normalTrackWidth
298 | canvas?.drawPath(path, paint)
299 | }
300 |
301 | private fun drawLinearTrack(canvas: Canvas?) {
302 | paint.color = highlightTrackColor
303 | paint.strokeWidth = highlightTrackWidth
304 | path.lineTo(thumbX, thumbY)
305 | canvas?.drawPath(path, paint)
306 |
307 | path.reset()
308 | path.moveTo(thumbX, thumbY)
309 | paint.color = normalTrackColor
310 | paint.strokeWidth = normalTrackWidth
311 | path.lineTo(trackEndX, height.toFloat() / 2)
312 | canvas?.drawPath(path, paint)
313 | }
314 |
315 | override fun onTouchEvent(event: MotionEvent?): Boolean {
316 | if (event == null) {
317 | return super.onTouchEvent(event)
318 | }
319 | val x = event.x
320 | val y = event.y
321 | when (event.action) {
322 | MotionEvent.ACTION_DOWN -> {
323 | if (isTouchPointInDrawableThumb(x, y)) {
324 | springAnimation?.cancel()
325 | drawableThumbSelected = true
326 | // thumbX = x.coerceHorizontal()
327 | // thumbY = y.coerceVertical().coerceToStretchRange(thumbX)
328 | onChangeListener?.onStartTrackingTouch(this)
329 | // onChangeListener?.onProgressChanged(this, getCurrentValue(), true)
330 | invalidate()
331 | return true
332 | }
333 | }
334 | MotionEvent.ACTION_MOVE -> {
335 | if (drawableThumbSelected) {
336 | thumbX = x.coerceHorizontal()
337 | thumbY = y.coerceVertical().coerceToStretchRange(thumbX)
338 | onChangeListener?.onProgressChanged(this, getCurrentValue(), true)
339 | invalidate()
340 | return true
341 | }
342 | }
343 | MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
344 | if (drawableThumbSelected) {
345 | drawableThumbSelected = false
346 | // thumbX = x.coerceHorizontal()
347 | // thumbY = y.coerceVertical().coerceToStretchRange(thumbX)
348 | // onChangeListener?.onProgressChanged(this, getCurrentValue(), true)
349 | onChangeListener?.onStopTrackingTouch(this)
350 | springAnimation =
351 | SpringAnimation(FloatValueHolder(trackY))
352 | .setStartValue(thumbY)
353 | .setSpring(
354 | SpringForce(trackY)
355 | .setDampingRatio(dampingRatio)
356 | .setStiffness(stiffness)
357 | )
358 | .addUpdateListener { _, value, _ ->
359 | thumbY = value
360 | invalidate()
361 | }
362 | .addEndListener { _, _, _, _ ->
363 | thumbY = trackY
364 | invalidate()
365 | }
366 | springAnimation?.start()
367 | return true
368 | }
369 | }
370 | }
371 | return super.onTouchEvent(event)
372 | }
373 |
374 | private fun isTouchPointInDrawableThumb(x: Float, y: Float): Boolean {
375 | if (drawableThumb != null) {
376 | drawableThumb?.let {
377 | setDrawableHalfWidthAndHeight()
378 | if (x > thumbX - drawableThumbHalfWidth && x < thumbX + drawableThumbHalfWidth &&
379 | y > thumbY - drawableThumbHalfHeight && x < thumbY + drawableThumbHalfHeight
380 | ) {
381 | return true
382 | }
383 | }
384 | } else {
385 | if ((x - thumbX) * (x - thumbX) +
386 | (y - thumbY) * (y - thumbY) <= drawableThumbRadius * drawableThumbRadius
387 | ) {
388 | return true
389 | }
390 | }
391 | return false
392 | }
393 |
394 | private fun setDrawableHalfWidthAndHeight() {
395 | if (drawableThumbHalfWidth != 0 && drawableThumbHalfHeight != 0) {
396 | return
397 | }
398 | drawableThumb?.let {
399 | drawableThumbHalfWidth = (it.bounds.right - it.bounds.left).absoluteValue / 2
400 | drawableThumbHalfHeight = (it.bounds.bottom - it.bounds.top).absoluteValue / 2
401 | }
402 | }
403 |
404 | private fun Float.coerceHorizontal(): Float {
405 | return this.coerceAtMost(trackEndX).coerceAtLeast(trackStartX)
406 | }
407 |
408 | private fun Float.coerceVertical(): Float {
409 | return this.coerceAtMost(trackY + stretchRange).coerceAtLeast(trackY - stretchRange)
410 | }
411 |
412 | private fun Float.coerceToStretchRange(x: Float): Float {
413 | return if (this <= height / 2) {
414 | this.coerceAtLeast(
415 | if (x <= width / 2) {
416 | -(((2 * (stretchRange + height / 2) - height) * (x - trackStartX)) / (width - (2 * trackStartX))) + (height / 2)
417 | } else {
418 | -(((2 * (stretchRange + height / 2) - height) * (x - trackEndX)) / (width - (2 * trackEndX))) + (height / 2)
419 | }
420 | )
421 | } else {
422 | this.coerceAtMost(
423 | if (x <= width / 2) {
424 | (((2 * (stretchRange + height / 2) - height) * (x - trackStartX)) / (width - (2 * trackStartX))) + (height / 2)
425 | } else {
426 | (((2 * (stretchRange + height / 2) - height) * (x - trackEndX)) / (width - (2 * trackEndX))) + (height / 2)
427 | }
428 | )
429 | }
430 | }
431 |
432 | //region Public functions
433 |
434 | //region Getter functions
435 |
436 | fun getCurrentValue(): Int {
437 | if (thumbX <= trackStartX) {
438 | return minValue
439 | } else if (thumbX >= trackEndX) {
440 | return maxValue
441 | }
442 | return Math.round(((thumbX - trackStartX) / (trackEndX - trackStartX)) * (maxValue - minValue)) + minValue
443 | }
444 |
445 | fun getMin(): Int {
446 | return minValue
447 | }
448 |
449 | fun getMax(): Int {
450 | return maxValue
451 | }
452 |
453 | fun getElasticBehavior(): ElasticBehavior {
454 | return elasticBehavior
455 | }
456 |
457 | fun getDampingRation(): Float {
458 | return dampingRatio
459 | }
460 |
461 | fun getStiffness(): Float {
462 | return stiffness
463 | }
464 |
465 | //endregion
466 |
467 | //region Setter functions
468 |
469 | /**
470 | * Set the Elastic Behavior for the SeekBar.
471 | */
472 | fun setElasticBehavior(elasticBehavior: ElasticBehavior) {
473 | this.elasticBehavior = elasticBehavior
474 | if (elasticBehavior == ElasticBehavior.RIGID) {
475 | springAnimation?.cancel()
476 | }
477 | invalidate()
478 | }
479 |
480 | /**
481 | * Set the maximum Stretch Range in dp.
482 | */
483 | @Throws(IllegalArgumentException::class)
484 | fun setStretchRange(stretchRangeInDp: Float) {
485 | if (stretchRangeInDp < 0) {
486 | throw IllegalArgumentException("Stretch range value can not be negative")
487 | }
488 | this.stretchRange = convertDpToPx(context, stretchRangeInDp)
489 | invalidate()
490 | }
491 |
492 | @Throws(IllegalArgumentException::class, IllegalStateException::class)
493 | fun setThumbRadius(dpValue: Float) {
494 | if (dpValue <= 0) {
495 | throw IllegalArgumentException("Thumb radius must be non-negative")
496 | }
497 | if (drawableThumb != null) {
498 | throw IllegalStateException("Thumb radius can not be set when drawable is used as thumb")
499 | }
500 | val oldY = trackY
501 | val oldThumbValue = getCurrentValue()
502 | drawableThumbRadius = convertDpToPx(context, dpValue)
503 | setCurrentValue(oldThumbValue)
504 | thumbY = (thumbY * drawableThumbRadius) / oldY
505 | if (springAnimation?.isRunning == true) springAnimation?.animateToFinalPosition(
506 | drawableThumbRadius
507 | )
508 | invalidate()
509 | requestLayout()
510 | }
511 |
512 | fun setNormalTrackWidth(dpValue: Float) {
513 | normalTrackWidth = convertDpToPx(context, dpValue)
514 | invalidate()
515 | }
516 |
517 | fun setHighlightTrackWidth(dpValue: Float) {
518 | highlightTrackWidth = convertDpToPx(context, dpValue)
519 | invalidate()
520 | }
521 |
522 | fun setNormalTrackColor(value: Int) {
523 | normalTrackColor = value
524 | invalidate()
525 | }
526 |
527 | fun setHighlightTrackColor(value: Int) {
528 | highlightTrackColor = value
529 | invalidate()
530 | }
531 |
532 | fun setHighlightThumbOnTouchColor(value: Int) {
533 | highlightThumbOnTouchColor = value
534 | invalidate()
535 | }
536 |
537 | fun setDefaultThumbInsideColor(value: Int) {
538 | defaultThumbInsideColor = value
539 | invalidate()
540 | }
541 |
542 | @Throws(java.lang.IllegalArgumentException::class)
543 | fun setDampingRatio(value: Float) {
544 | if (value < 0.0f) {
545 | throw IllegalArgumentException("Damping ratio must be non-negative")
546 | }
547 | dampingRatio = value
548 | springAnimation?.spring?.dampingRatio = dampingRatio
549 | if (springAnimation?.isRunning == true) springAnimation?.animateToFinalPosition(trackY)
550 | invalidate()
551 | }
552 |
553 | @Throws(java.lang.IllegalArgumentException::class)
554 | fun setStiffness(value: Float) {
555 | if (value <= 0.0f) {
556 | throw IllegalArgumentException("Spring stiffness constant must be positive.")
557 | }
558 | stiffness = value
559 | springAnimation?.spring?.stiffness = stiffness
560 | if (springAnimation?.isRunning == true) springAnimation?.animateToFinalPosition(trackY)
561 | invalidate()
562 | }
563 |
564 | @Throws(java.lang.IllegalArgumentException::class)
565 | fun setMin(value: Int) {
566 | if (value >= maxValue) {
567 | throw java.lang.IllegalArgumentException("Min value must be smaller than max value")
568 | }
569 | val oldValue = getCurrentValue()
570 | minValue = value
571 | if (minValue > oldValue) {
572 | setCurrentValue(minValue)
573 | } else {
574 | setCurrentValue(oldValue)
575 | }
576 | }
577 |
578 | @Throws(java.lang.IllegalArgumentException::class)
579 | fun setMax(value: Int) {
580 | if (value <= minValue) {
581 | throw java.lang.IllegalArgumentException("Max value must be greater than min value")
582 | }
583 | val oldValue = getCurrentValue()
584 | maxValue = value
585 | if (maxValue < oldValue) {
586 | setCurrentValue(maxValue)
587 | } else {
588 | setCurrentValue(oldValue)
589 | }
590 | }
591 |
592 | fun setCurrentValue(value: Int) {
593 | val validValue = value.coerceAtLeast(minValue).coerceAtMost(maxValue)
594 | if (trackEndX < 0) {
595 | //If this function gets called before the view gets layed out and learns what it's width value is
596 | if (initialControlXPositionQueue.isNotEmpty()) {
597 | //Incase this is called multiple times, always use the latest value
598 | initialControlXPositionQueue.clear()
599 | }
600 | initialControlXPositionQueue.offer(validValue)
601 | return
602 | }
603 | thumbX =
604 | (((validValue - minValue).toFloat() / (maxValue - minValue)) * (trackEndX - trackStartX)) + trackStartX
605 | onChangeListener?.onProgressChanged(this, getCurrentValue(), false)
606 | invalidate()
607 | }
608 |
609 | fun setOnRubberSeekBarChangeListener(listener: OnRubberSeekBarChangeListener) {
610 | onChangeListener = listener
611 | }
612 | //endregion
613 |
614 | //endregion
615 |
616 | // TODO - Fill out the necessary comments and descriptions
617 |
618 | //region Interfaces
619 | /**
620 | * Based on the SeekBar.onSeekBarChangeListener
621 | */
622 | interface OnRubberSeekBarChangeListener {
623 | fun onProgressChanged(seekBar: RubberSeekBar, value: Int, fromUser: Boolean)
624 | fun onStartTrackingTouch(seekBar: RubberSeekBar)
625 | fun onStopTrackingTouch(seekBar: RubberSeekBar)
626 | }
627 | //endregion
628 | }
--------------------------------------------------------------------------------
/rubberpicker/src/main/java/com/jem/rubberpicker/RubberRangePicker.kt:
--------------------------------------------------------------------------------
1 | package com.jem.rubberpicker
2 |
3 |
4 | import android.content.Context
5 | import android.graphics.Canvas
6 | import android.graphics.Color
7 | import android.graphics.Paint
8 | import android.graphics.Path
9 | import android.graphics.drawable.Drawable
10 | import android.support.animation.FloatValueHolder
11 | import android.support.animation.SpringAnimation
12 | import android.support.animation.SpringForce
13 | import android.util.AttributeSet
14 | import android.view.MotionEvent
15 | import android.view.View
16 | import android.view.ViewGroup
17 | import java.util.concurrent.ArrayBlockingQueue
18 | import kotlin.math.absoluteValue
19 | import kotlin.reflect.KMutableProperty0
20 |
21 |
22 | class RubberRangePicker : View {
23 |
24 | private val paint: Paint by lazy {
25 | val tempPaint = Paint()
26 | tempPaint.style = Paint.Style.STROKE
27 | tempPaint.color = normalTrackColor
28 | tempPaint.strokeWidth = 5f
29 | tempPaint.isAntiAlias = true
30 | tempPaint
31 | }
32 | private var path: Path = Path()
33 | private var startThumbSpringAnimation: SpringAnimation? = null
34 | private var endThumbSpringAnimation: SpringAnimation? = null
35 | private var startThumbX: Float = -1f
36 | private var startThumbY: Float = -1f
37 | private var endThumbX: Float = -1f
38 | private var endThumbY: Float = -1f
39 | private val initialStartThumbXPositionQueue = ArrayBlockingQueue(1)
40 | private val initialEndThumbXPositionQueue = ArrayBlockingQueue(1)
41 | // Used to determine the start and end points of the track.
42 | // Useful for drawing and also for other calculations.
43 | private val trackStartX: Float
44 | get() {
45 | return if (drawableThumb != null) {
46 | setDrawableHalfWidthAndHeight()
47 | drawableThumbHalfWidth
48 | } else {
49 | drawableThumbRadius
50 | }
51 | }
52 | private val trackEndX: Float
53 | get() {
54 | return if (drawableThumb != null) {
55 | setDrawableHalfWidthAndHeight()
56 | width - drawableThumbHalfWidth
57 | } else {
58 | width - drawableThumbRadius
59 | }
60 | }
61 | private val trackY: Float
62 | get() {
63 | return height.toFloat() / 2
64 | }
65 |
66 | private var x1: Float = 0f
67 | private var y1: Float = 0f
68 | private var x2: Float = 0f
69 | private var y2: Float = 0f
70 |
71 | private var stretchRange: Float = -1f
72 |
73 | private var elasticBehavior: ElasticBehavior = ElasticBehavior.CUBIC
74 |
75 | private var drawableThumb: Drawable? = null
76 | private var drawableThumbHalfWidth: Float = 0f
77 | private var drawableThumbHalfHeight: Float = 0f
78 | private var startDrawableThumbSelected: Boolean = false
79 | private var endDrawableThumbSelected: Boolean = false
80 |
81 | private var drawableThumbRadius: Float = 0.0f
82 | private var normalTrackWidth: Float = 0.0f
83 | private var highlightTrackWidth: Float = 0.0f
84 |
85 | private var normalTrackColor: Int = 0
86 | private var highlightTrackColor: Int = 0
87 | private var highlightThumbOnTouchColor: Int = 0
88 | private var defaultThumbInsideColor: Int = 0
89 | private var dampingRatio: Float = 0f
90 | private var stiffness: Float = 0f
91 |
92 | private var minValue: Int = 0
93 | private var maxValue: Int = 100
94 |
95 | private var onChangeListener: OnRubberRangePickerChangeListener? = null
96 |
97 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) :
98 | super(context, attrs, defStyleAttr) {
99 | init(attrs)
100 | }
101 |
102 | constructor(context: Context, attrs: AttributeSet?) :
103 | super(context, attrs) {
104 | init(attrs)
105 | }
106 |
107 | constructor(context: Context) :
108 | super(context) {
109 | init(null)
110 | }
111 |
112 | private fun init(attrs: AttributeSet?) {
113 | stretchRange = convertDpToPx(context, 24f)
114 | drawableThumbRadius = convertDpToPx(context, 16f)
115 | normalTrackWidth = convertDpToPx(context, 2f)
116 | highlightTrackWidth = convertDpToPx(context, 4f)
117 | normalTrackColor = Color.GRAY
118 | highlightTrackColor = 0xFF38ACEC.toInt()
119 | highlightThumbOnTouchColor = 0xFF82CAFA.toInt()
120 | defaultThumbInsideColor = Color.WHITE
121 | dampingRatio = SpringForce.DAMPING_RATIO_HIGH_BOUNCY
122 | stiffness = SpringForce.STIFFNESS_LOW
123 |
124 | attrs?.let {
125 | val typedArray =
126 | context.obtainStyledAttributes(attrs, R.styleable.RubberRangePicker, 0, 0)
127 | stretchRange = typedArray.getDimensionPixelSize(
128 | R.styleable.RubberRangePicker_stretchRange,
129 | convertDpToPx(context, 24f).toInt()
130 | ).toFloat()
131 | drawableThumbRadius = typedArray.getDimensionPixelSize(
132 | R.styleable.RubberRangePicker_defaultThumbRadius,
133 | convertDpToPx(context, 16f).toInt()
134 | ).toFloat()
135 | normalTrackWidth = typedArray.getDimensionPixelSize(
136 | R.styleable.RubberRangePicker_normalTrackWidth,
137 | convertDpToPx(context, 2f).toInt()
138 | ).toFloat()
139 | highlightTrackWidth = typedArray.getDimensionPixelSize(
140 | R.styleable.RubberRangePicker_highlightTrackWidth,
141 | convertDpToPx(context, 4f).toInt()
142 | ).toFloat()
143 | drawableThumb = typedArray.getDrawable(R.styleable.RubberRangePicker_thumbDrawable)
144 | normalTrackColor =
145 | typedArray.getColor(R.styleable.RubberRangePicker_normalTrackColor, Color.GRAY)
146 | highlightTrackColor =
147 | typedArray.getColor(
148 | R.styleable.RubberRangePicker_highlightTrackColor,
149 | 0xFF38ACEC.toInt()
150 | )
151 | highlightThumbOnTouchColor =
152 | typedArray.getColor(
153 | R.styleable.RubberRangePicker_highlightDefaultThumbOnTouchColor,
154 | 0xFF82CAFA.toInt()
155 | )
156 | defaultThumbInsideColor =
157 | typedArray.getColor(
158 | R.styleable.RubberRangePicker_defaultThumbInsideColor,
159 | Color.WHITE
160 | )
161 | dampingRatio =
162 | typedArray.getFloat(
163 | R.styleable.RubberRangePicker_dampingRatio,
164 | SpringForce.DAMPING_RATIO_HIGH_BOUNCY
165 | )
166 | stiffness = typedArray.getFloat(
167 | R.styleable.RubberRangePicker_stiffness,
168 | SpringForce.STIFFNESS_LOW
169 | )
170 | minValue = typedArray.getInt(R.styleable.RubberRangePicker_minValue, 0)
171 | maxValue = typedArray.getInt(R.styleable.RubberRangePicker_maxValue, 100)
172 | elasticBehavior =
173 | typedArray.getInt(R.styleable.RubberRangePicker_elasticBehavior, 1).run {
174 | when (this) {
175 | 0 -> ElasticBehavior.LINEAR
176 | 1 -> ElasticBehavior.CUBIC
177 | 2 -> ElasticBehavior.RIGID
178 | else -> ElasticBehavior.CUBIC
179 | }
180 | }
181 | if (typedArray.hasValue(R.styleable.RubberRangePicker_initialStartValue)) {
182 | setCurrentStartValue(typedArray.getInt(R.styleable.RubberRangePicker_initialStartValue, minValue))
183 | }
184 | if (typedArray.hasValue(R.styleable.RubberRangePicker_initialEndValue)) {
185 | setCurrentEndValue(typedArray.getInt(R.styleable.RubberRangePicker_initialEndValue, minValue))
186 | }
187 | typedArray.recycle()
188 | }
189 | }
190 |
191 | override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
192 | super.onLayout(changed, left, top, right, bottom)
193 | if (startThumbX < trackStartX || !initialStartThumbXPositionQueue.isEmpty()) {
194 | if (initialStartThumbXPositionQueue.isEmpty()) {
195 | startThumbX = trackStartX
196 | } else {
197 | setCurrentStartValue(initialStartThumbXPositionQueue.poll())
198 | }
199 | startThumbY = trackY
200 | }
201 | if (endThumbX < trackStartX || !initialEndThumbXPositionQueue.isEmpty()) {
202 | if (initialEndThumbXPositionQueue.isEmpty()) {
203 | endThumbX = trackStartX + getThumbWidth()
204 | } else {
205 | setCurrentEndValue(initialEndThumbXPositionQueue.poll())
206 | }
207 | endThumbY = trackY
208 | }
209 | }
210 |
211 | private fun getThumbWidth(): Float {
212 | return if (drawableThumb != null) {
213 | setDrawableHalfWidthAndHeight()
214 | 2 * drawableThumbHalfWidth
215 | } else {
216 | 2 * drawableThumbRadius
217 | }
218 | }
219 |
220 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
221 | val minimumHeight: Int = if (drawableThumb != null) {
222 | setDrawableHalfWidthAndHeight()
223 | (drawableThumbHalfHeight * 2).toInt()
224 | } else {
225 | (drawableThumbRadius * 2).toInt()
226 | }
227 | setMeasuredDimension(
228 | getDefaultSize(suggestedMinimumWidth, widthMeasureSpec),
229 | measureDimension(minimumHeight + paddingTop + paddingBottom, heightMeasureSpec)
230 | )
231 | }
232 |
233 | private fun measureDimension(desiredSize: Int, measureSpec: Int): Int {
234 | var result: Int
235 | val specMode = MeasureSpec.getMode(measureSpec)
236 | val specSize = MeasureSpec.getSize(measureSpec)
237 |
238 | if (specMode == MeasureSpec.EXACTLY) {
239 | result = specSize
240 | } else {
241 | result = desiredSize
242 | if (specMode == MeasureSpec.AT_MOST) {
243 | result = Math.min(result, specSize)
244 | }
245 | }
246 |
247 | return result
248 | }
249 |
250 |
251 | override fun onDraw(canvas: Canvas?) {
252 | super.onDraw(canvas)
253 | // TODO - Try to figure out a better way to overcome view clipping
254 | // Workaround since Region.Op.REPLACE won't work in Android P & above.
255 | // Region.Op.REPLACE also doesn't work properly (at times) even in devices below Android P.
256 | (parent as? ViewGroup)?.clipChildren = false
257 | (parent as? ViewGroup)?.clipToPadding = false
258 |
259 | drawTrack(canvas)
260 | drawThumb(canvas)
261 | }
262 |
263 | private fun drawThumb(canvas: Canvas?) {
264 | if (elasticBehavior == ElasticBehavior.RIGID) {
265 | startThumbY = trackY
266 | endThumbY = trackY
267 | }
268 | if (drawableThumb != null) {
269 | canvas?.let {
270 | canvas.translate(startThumbX, startThumbY)
271 | drawableThumb?.draw(it)
272 | canvas.translate(endThumbX, endThumbY)
273 | drawableThumb?.draw(it)
274 | }
275 | } else {
276 | drawThumbCircles(canvas, startThumbX, startThumbY, startDrawableThumbSelected)
277 | drawThumbCircles(canvas, endThumbX, endThumbY, endDrawableThumbSelected)
278 | }
279 | }
280 |
281 | private fun drawThumbCircles(
282 | canvas: Canvas?,
283 | posX: Float,
284 | posY: Float,
285 | thumbSelected: Boolean
286 | ) {
287 | paint.color = highlightTrackColor
288 | paint.style = Paint.Style.FILL
289 | canvas?.drawCircle(posX, posY, drawableThumbRadius, paint)
290 | if (thumbSelected) {
291 | paint.color = highlightThumbOnTouchColor
292 | } else {
293 | paint.color = defaultThumbInsideColor
294 | }
295 | canvas?.drawCircle(posX, posY, drawableThumbRadius - highlightTrackWidth, paint)
296 | paint.style = Paint.Style.STROKE
297 | }
298 |
299 | private fun drawTrack(canvas: Canvas?) {
300 | if (startThumbY == trackY && endThumbY == trackY) {
301 | drawRigidTrack(canvas)
302 | return
303 | }
304 |
305 | path.reset()
306 | path.moveTo(trackStartX, trackY)
307 |
308 | when (elasticBehavior) {
309 | ElasticBehavior.LINEAR -> drawLinearTrack(canvas)
310 | ElasticBehavior.CUBIC -> drawBezierTrack(canvas)
311 | ElasticBehavior.RIGID -> drawRigidTrack(canvas)
312 | }
313 | }
314 |
315 | private fun drawRigidTrack(canvas: Canvas?) {
316 | paint.color = normalTrackColor
317 | paint.strokeWidth = normalTrackWidth
318 | canvas?.drawLine(trackStartX, trackY, startThumbX, trackY, paint)
319 | paint.color = highlightTrackColor
320 | paint.strokeWidth = highlightTrackWidth
321 | canvas?.drawLine(startThumbX, trackY, endThumbX, trackY, paint)
322 | paint.color = normalTrackColor
323 | paint.strokeWidth = normalTrackWidth
324 | canvas?.drawLine(endThumbX, trackY, trackEndX, trackY, paint)
325 | }
326 |
327 | private fun drawBezierTrack(canvas: Canvas?) {
328 | x1 = (startThumbX + trackStartX) / 2
329 | y1 = trackY
330 | x2 = x1
331 | y2 = startThumbY
332 | path.cubicTo(x1, y1, x2, y2, startThumbX, startThumbY)
333 | paint.color = normalTrackColor
334 | paint.strokeWidth = normalTrackWidth
335 | canvas?.drawPath(path, paint)
336 |
337 | path.reset()
338 | path.moveTo(startThumbX, startThumbY)
339 | x1 = (startThumbX + endThumbX) / 2
340 | y1 = startThumbY
341 | x2 = x1
342 | y2 = endThumbY
343 | path.cubicTo(x1, y1, x2, y2, endThumbX, endThumbY)
344 | paint.color = highlightTrackColor
345 | paint.strokeWidth = highlightTrackWidth
346 | canvas?.drawPath(path, paint)
347 |
348 | path.reset()
349 | path.moveTo(endThumbX, endThumbY)
350 | x1 = (endThumbX + trackEndX) / 2
351 | y1 = endThumbY
352 | x2 = x1
353 | y2 = trackY
354 | path.cubicTo(x1, y1, x2, y2, trackEndX, trackY)
355 | paint.color = normalTrackColor
356 | paint.strokeWidth = normalTrackWidth
357 | canvas?.drawPath(path, paint)
358 | }
359 |
360 | private fun drawLinearTrack(canvas: Canvas?) {
361 | paint.color = normalTrackColor
362 | paint.strokeWidth = normalTrackWidth
363 | path.lineTo(startThumbX, startThumbY)
364 | canvas?.drawPath(path, paint)
365 |
366 | path.reset()
367 | path.moveTo(startThumbX, startThumbY)
368 | paint.color = highlightTrackColor
369 | paint.strokeWidth = highlightTrackWidth
370 | path.lineTo(endThumbX, endThumbY)
371 | canvas?.drawPath(path, paint)
372 |
373 | path.reset()
374 | path.moveTo(endThumbX, endThumbY)
375 | paint.color = normalTrackColor
376 | paint.strokeWidth = normalTrackWidth
377 | path.lineTo(trackEndX, trackY)
378 | canvas?.drawPath(path, paint)
379 | }
380 |
381 | override fun onTouchEvent(event: MotionEvent?): Boolean {
382 | if (event == null) {
383 | return super.onTouchEvent(event)
384 | }
385 | var thumbXReference: KMutableProperty0? = null
386 | var thumbYReference: KMutableProperty0? = null
387 | var thumbSelectedReference: KMutableProperty0? = null
388 | var thumbSpringAnimationReference: KMutableProperty0? = null
389 | var startX: Float = trackStartX
390 | var endX: Float = trackEndX
391 | if (startDrawableThumbSelected) {
392 | thumbXReference = this::startThumbX
393 | thumbYReference = this::startThumbY
394 | thumbSelectedReference = this::startDrawableThumbSelected
395 | thumbSpringAnimationReference = this::startThumbSpringAnimation
396 | endX = endThumbX - getThumbWidth()
397 | } else if (endDrawableThumbSelected) {
398 | thumbXReference = this::endThumbX
399 | thumbYReference = this::endThumbY
400 | thumbSelectedReference = this::endDrawableThumbSelected
401 | thumbSpringAnimationReference = this::endThumbSpringAnimation
402 | startX = startThumbX + getThumbWidth()
403 | }
404 | val x = event.x
405 | val y = event.y
406 | when (event.action) {
407 | MotionEvent.ACTION_DOWN -> {
408 | if (isTouchPointInDrawableThumb(x, y, startThumbX, startThumbY)) {
409 | thumbXReference = this::startThumbX
410 | thumbYReference = this::startThumbY
411 | thumbSelectedReference = this::startDrawableThumbSelected
412 | thumbSpringAnimationReference = this::startThumbSpringAnimation
413 |
414 | } else if (isTouchPointInDrawableThumb(x, y, endThumbX, endThumbY)) {
415 | thumbXReference = this::endThumbX
416 | thumbYReference = this::endThumbY
417 | thumbSelectedReference = this::endDrawableThumbSelected
418 | thumbSpringAnimationReference = this::endThumbSpringAnimation
419 | }
420 | if (thumbXReference != null) {
421 | thumbSpringAnimationReference?.get()?.cancel()
422 | thumbSelectedReference?.set(true)
423 | // thumbXReference.set(x.coerceHorizontal(startX, endX))
424 | // thumbYReference?.set(y.coerceVertical().coerceToStretchRange(thumbXReference.get(), startX, endX))
425 | onChangeListener?.onStartTrackingTouch(this, startX == trackStartX)
426 | // onChangeListener?.onProgressChanged(this, getCurrentStartValue(), getCurrentEndValue(), true)
427 | invalidate()
428 | return true
429 | }
430 | }
431 | MotionEvent.ACTION_MOVE -> {
432 | if (thumbSelectedReference?.get() == true && thumbXReference != null) {
433 | thumbXReference.set(
434 | x
435 | .coerceAtLeast(if (startX == trackStartX) trackStartX else trackStartX + getThumbWidth())
436 | .coerceAtMost(if (startX == trackStartX) trackEndX - getThumbWidth() else trackEndX)
437 | )
438 | adjustStartEndThumbXPositions(startX == trackStartX)
439 | if (endThumbX - startThumbX > getThumbWidth() - 1 && endThumbX - startThumbX < getThumbWidth() + 1) {
440 | thumbYReference?.set(trackY)
441 | } else {
442 | thumbYReference?.set(
443 | y
444 | .coerceAtMost(trackY + stretchRange)
445 | .coerceAtLeast(trackY - stretchRange)
446 | .coerceToStretchRange(thumbXReference.get(), startX, endX)
447 | )
448 | }
449 | onChangeListener?.onProgressChanged(
450 | this,
451 | getCurrentStartValue(),
452 | getCurrentEndValue(),
453 | true
454 | )
455 | invalidate()
456 | return true
457 | }
458 | }
459 | MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
460 | if (thumbSelectedReference?.get() == true && thumbXReference != null && thumbYReference != null) {
461 | thumbSelectedReference.set(false)
462 | // thumbXReference.set(x.coerceHorizontal(startX, endX))
463 | // thumbYReference.set(y.coerceVertical().coerceToStretchRange(thumbXReference.get(), startX, endX))
464 | // onChangeListener?.onProgressChanged(this, getCurrentStartValue(), getCurrentEndValue(), true)
465 | onChangeListener?.onStopTrackingTouch(this, startX == trackStartX)
466 | thumbSpringAnimationReference?.set(
467 | SpringAnimation(FloatValueHolder(trackY))
468 | .setStartValue(thumbYReference.get())
469 | .setSpring(
470 | SpringForce(trackY)
471 | .setDampingRatio(dampingRatio)
472 | .setStiffness(stiffness)
473 | )
474 | .addUpdateListener { _, value, _ ->
475 | thumbYReference.set(value)
476 | invalidate()
477 | }
478 | .addEndListener { _, _, _, _ ->
479 | thumbYReference.set(trackY)
480 | invalidate()
481 | })
482 | thumbSpringAnimationReference?.get()?.start()
483 | return true
484 | }
485 | }
486 | }
487 | return super.onTouchEvent(event)
488 | }
489 |
490 | private fun adjustStartEndThumbXPositions(basedOnStartThumb: Boolean) {
491 | if (basedOnStartThumb) {
492 | endThumbX = endThumbX.coerceAtLeast(startThumbX + getThumbWidth())
493 | } else {
494 | startThumbX = startThumbX.coerceAtMost(endThumbX - getThumbWidth())
495 | }
496 | }
497 |
498 | private fun isTouchPointInDrawableThumb(
499 | x: Float,
500 | y: Float,
501 | thumbX: Float,
502 | thumbY: Float
503 | ): Boolean {
504 | if (drawableThumb != null) {
505 | drawableThumb?.let {
506 | setDrawableHalfWidthAndHeight()
507 | if (x > thumbX - drawableThumbHalfWidth && x < thumbX + drawableThumbHalfWidth &&
508 | y > thumbY - drawableThumbHalfHeight && x < thumbY + drawableThumbHalfHeight
509 | ) {
510 | return true
511 | }
512 | }
513 | } else {
514 | if ((x - thumbX) * (x - thumbX) +
515 | (y - thumbY) * (y - thumbY) <= drawableThumbRadius * drawableThumbRadius
516 | ) {
517 | return true
518 | }
519 | }
520 | return false
521 | }
522 |
523 | private fun setDrawableHalfWidthAndHeight() {
524 | if (drawableThumbHalfWidth != 0f && drawableThumbHalfHeight != 0f) {
525 | return
526 | }
527 | drawableThumb?.let {
528 | drawableThumbHalfWidth =
529 | ((it.bounds.right - it.bounds.left).absoluteValue).toFloat() / 2
530 | drawableThumbHalfHeight =
531 | ((it.bounds.bottom - it.bounds.top).absoluteValue).toFloat() / 2
532 | }
533 | }
534 |
535 | private fun Float.coerceToStretchRange(x: Float, startX: Float, endX: Float): Float {
536 | return if (this <= height / 2) {
537 | this.coerceAtLeast(
538 | when {
539 | x < (endX + startX) / 2 -> -(((2 * (stretchRange + height / 2) - height) * (x - startX)) / ((endX + startX) - (2 * startX))) + (height / 2)
540 | x > (endX + startX) / 2 -> -(((2 * (stretchRange + height / 2) - height) * (x - endX)) / ((endX + startX) - (2 * endX))) + (height / 2)
541 | else -> trackY
542 | }
543 | )
544 | } else {
545 | this.coerceAtMost(
546 | when {
547 | x < (endX + startX) / 2 -> (((2 * (stretchRange + height / 2) - height) * (x - startX)) / ((endX + startX) - (2 * startX))) + (height / 2)
548 | x > (endX + startX) / 2 -> (((2 * (stretchRange + height / 2) - height) * (x - endX)) / ((endX + startX) - (2 * endX))) + (height / 2)
549 | else -> trackY
550 | }
551 | )
552 | }
553 | }
554 |
555 | //region Public functions
556 |
557 | //region Getter functions
558 |
559 | fun getCurrentStartValue(): Int {
560 | if (startThumbX <= trackStartX) {
561 | return minValue
562 | } else if (startThumbX >= trackEndX - getThumbWidth()) {
563 | return maxValue
564 | }
565 | return Math.round((((startThumbX - trackStartX) / ((trackEndX - getThumbWidth()) - trackStartX)) * (maxValue - minValue))) + minValue
566 | }
567 |
568 | fun getCurrentEndValue(): Int {
569 | if (endThumbX <= trackStartX + getThumbWidth()) {
570 | return minValue
571 | } else if (endThumbX >= trackEndX) {
572 | return maxValue
573 | }
574 | return Math.round((((endThumbX - (trackStartX + getThumbWidth())) / (trackEndX - (trackStartX + getThumbWidth()))) * (maxValue - minValue))) + minValue
575 | }
576 |
577 | fun getMin(): Int {
578 | return minValue
579 | }
580 |
581 | fun getMax(): Int {
582 | return maxValue
583 | }
584 |
585 | fun getElasticBehavior(): ElasticBehavior {
586 | return elasticBehavior
587 | }
588 |
589 | fun getDampingRation(): Float {
590 | return dampingRatio
591 | }
592 |
593 | fun getStiffness(): Float {
594 | return stiffness
595 | }
596 |
597 | //endregion
598 |
599 | //region Setter functions
600 |
601 | /**
602 | * Set the Elastic Behavior for the RangePicker.
603 | */
604 | fun setElasticBehavior(elasticBehavior: ElasticBehavior) {
605 | this.elasticBehavior = elasticBehavior
606 | if (elasticBehavior == ElasticBehavior.RIGID) {
607 | startThumbSpringAnimation?.cancel()
608 | endThumbSpringAnimation?.cancel()
609 | }
610 | invalidate()
611 | }
612 |
613 | /**
614 | * Set the maximum Stretch Range in dp.
615 | */
616 | @Throws(IllegalArgumentException::class)
617 | fun setStretchRange(stretchRangeInDp: Float) {
618 | if (stretchRangeInDp < 0) {
619 | throw IllegalArgumentException("Stretch range value can not be negative")
620 | }
621 | this.stretchRange = convertDpToPx(context, stretchRangeInDp)
622 | invalidate()
623 | }
624 |
625 | @Throws(IllegalArgumentException::class, IllegalStateException::class)
626 | fun setThumbRadius(dpValue: Float) {
627 | if (dpValue <= 0) {
628 | throw IllegalArgumentException("Thumb radius must be non-negative")
629 | }
630 | if (drawableThumb != null) {
631 | throw IllegalStateException("Thumb radius can not be set when drawable is used as thumb")
632 | }
633 | val oldY = trackY
634 | val oldStartThumbX = getCurrentStartValue()
635 | val oldEndThumbX = getCurrentEndValue()
636 | drawableThumbRadius = convertDpToPx(context, dpValue)
637 | setCurrentStartValue(oldStartThumbX)
638 | startThumbY = (startThumbY * drawableThumbRadius) / oldY
639 | if (startThumbSpringAnimation?.isRunning == true) startThumbSpringAnimation?.animateToFinalPosition(
640 | drawableThumbRadius
641 | )
642 | setCurrentEndValue(oldEndThumbX)
643 | endThumbY = (endThumbY * drawableThumbRadius) / oldY
644 | if (endThumbSpringAnimation?.isRunning == true) endThumbSpringAnimation?.animateToFinalPosition(
645 | drawableThumbRadius
646 | )
647 | invalidate()
648 | requestLayout()
649 | }
650 |
651 | fun setNormalTrackWidth(dpValue: Float) {
652 | normalTrackWidth = convertDpToPx(context, dpValue)
653 | invalidate()
654 | }
655 |
656 | fun setHighlightTrackWidth(dpValue: Float) {
657 | highlightTrackWidth = convertDpToPx(context, dpValue)
658 | invalidate()
659 | }
660 |
661 | fun setNormalTrackColor(value: Int) {
662 | normalTrackColor = value
663 | invalidate()
664 | }
665 |
666 | fun setHighlightTrackColor(value: Int) {
667 | highlightTrackColor = value
668 | invalidate()
669 | }
670 |
671 | fun setHighlightThumbOnTouchColor(value: Int) {
672 | highlightThumbOnTouchColor = value
673 | invalidate()
674 | }
675 |
676 | fun setDefaultThumbInsideColor(value: Int) {
677 | defaultThumbInsideColor = value
678 | invalidate()
679 | }
680 |
681 | @Throws(java.lang.IllegalArgumentException::class)
682 | fun setDampingRatio(value: Float) {
683 | if (value < 0.0f) {
684 | throw IllegalArgumentException("Damping ratio must be non-negative")
685 | }
686 | dampingRatio = value
687 | startThumbSpringAnimation?.spring?.dampingRatio = dampingRatio
688 | endThumbSpringAnimation?.spring?.dampingRatio = dampingRatio
689 | if (startThumbSpringAnimation?.isRunning == true) startThumbSpringAnimation?.animateToFinalPosition(
690 | trackY
691 | )
692 | if (endThumbSpringAnimation?.isRunning == true) endThumbSpringAnimation?.animateToFinalPosition(
693 | trackY
694 | )
695 | invalidate()
696 | }
697 |
698 | @Throws(java.lang.IllegalArgumentException::class)
699 | fun setStiffness(value: Float) {
700 | if (value <= 0.0f) {
701 | throw IllegalArgumentException("Spring stiffness constant must be positive")
702 | }
703 | stiffness = value
704 | startThumbSpringAnimation?.spring?.stiffness = stiffness
705 | endThumbSpringAnimation?.spring?.stiffness = stiffness
706 | if (startThumbSpringAnimation?.isRunning == true) startThumbSpringAnimation?.animateToFinalPosition(
707 | trackY
708 | )
709 | if (endThumbSpringAnimation?.isRunning == true) endThumbSpringAnimation?.animateToFinalPosition(
710 | trackY
711 | )
712 | invalidate()
713 | }
714 |
715 | @Throws(java.lang.IllegalArgumentException::class)
716 | fun setMin(value: Int) {
717 | if (value >= maxValue) {
718 | throw java.lang.IllegalArgumentException("Min value must be smaller than max value")
719 | }
720 | val oldStartValue = getCurrentStartValue()
721 | val oldEndValue = getCurrentEndValue()
722 | minValue = value
723 | if (minValue > oldStartValue) {
724 | setCurrentEndValue(oldEndValue)
725 | setCurrentStartValue(minValue)
726 | } else {
727 | setCurrentEndValue(oldEndValue)
728 | setCurrentStartValue(oldStartValue)
729 | }
730 | }
731 |
732 | @Throws(java.lang.IllegalArgumentException::class)
733 | fun setMax(value: Int) {
734 | if (value <= minValue) {
735 | throw java.lang.IllegalArgumentException("Max value must be greater than min value")
736 | }
737 | val oldStartValue = getCurrentStartValue()
738 | val oldEndValue = getCurrentEndValue()
739 | maxValue = value
740 | if (maxValue < oldEndValue) {
741 | setCurrentStartValue(oldStartValue)
742 | setCurrentEndValue(maxValue)
743 | } else {
744 | setCurrentStartValue(oldStartValue)
745 | setCurrentEndValue(oldEndValue)
746 | }
747 | }
748 |
749 | fun setCurrentStartValue(value: Int) {
750 | val validValue = value.coerceAtLeast(minValue).coerceAtMost(maxValue)
751 | if (trackEndX < 0) {
752 | //If this function gets called before the view gets layed out and learns what it's width value is
753 | if (initialStartThumbXPositionQueue.isNotEmpty()) {
754 | //Incase this is called multiple times, always use the latest value
755 | initialStartThumbXPositionQueue.clear()
756 | }
757 | initialStartThumbXPositionQueue.offer(validValue)
758 | return
759 | }
760 | startThumbX =
761 | (((validValue - minValue).toFloat() / (maxValue - minValue)) * ((trackEndX - getThumbWidth()) - trackStartX)) + trackStartX
762 | adjustStartEndThumbXPositions(true)
763 | onChangeListener?.onProgressChanged(
764 | this,
765 | getCurrentStartValue(),
766 | getCurrentEndValue(),
767 | false
768 | )
769 | invalidate()
770 | }
771 |
772 | fun setCurrentEndValue(value: Int) {
773 | val validValue = value.coerceAtLeast(minValue).coerceAtMost(maxValue)
774 | if (trackEndX < 0) {
775 | //If this function gets called before the view gets layed out and learns what it's width value is
776 | if (initialEndThumbXPositionQueue.isNotEmpty()) {
777 | //Incase this is called multiple times, always use the latest value
778 | initialEndThumbXPositionQueue.clear()
779 | }
780 | initialEndThumbXPositionQueue.offer(validValue)
781 | return
782 | }
783 | endThumbX =
784 | (((validValue - minValue).toFloat() / (maxValue - minValue)) * (trackEndX - (trackStartX + getThumbWidth()))) + (trackStartX + getThumbWidth())
785 | adjustStartEndThumbXPositions(false)
786 | onChangeListener?.onProgressChanged(
787 | this,
788 | getCurrentStartValue(),
789 | getCurrentEndValue(),
790 | false
791 | )
792 | invalidate()
793 | }
794 |
795 | fun setOnRubberRangePickerChangeListener(listener: OnRubberRangePickerChangeListener) {
796 | onChangeListener = listener
797 | }
798 | //endregion
799 | //endregion
800 |
801 | // TODO - Fill out the necessary comments and descriptions
802 |
803 | //region Interfaces
804 | /**
805 | * Based on the RubberSeekBar.onSeekBarChangeListener
806 | */
807 | interface OnRubberRangePickerChangeListener {
808 | fun onProgressChanged(
809 | rangePicker: RubberRangePicker,
810 | startValue: Int,
811 | endValue: Int,
812 | fromUser: Boolean
813 | )
814 |
815 | fun onStartTrackingTouch(rangePicker: RubberRangePicker, isStartThumb: Boolean)
816 | fun onStopTrackingTouch(rangePicker: RubberRangePicker, isStartThumb: Boolean)
817 | }
818 | //endregion
819 | }
--------------------------------------------------------------------------------