├── 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 | 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 | [![License: MIT](https://img.shields.io/badge/License-MIT-silver.svg)](https://opensource.org/licenses/MIT) [![](https://jitpack.io/v/Chrisvin/RubberPicker.svg)](https://jitpack.io/#Chrisvin/RubberPicker) [![API](https://img.shields.io/badge/API-15%2B-blue.svg?style=flat)](https://android-arsenal.com/api?level=15) [![Android Arsenal]( https://img.shields.io/badge/Android%20Arsenal-RubberPicker-gold.svg?style=flat )]( 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 | } --------------------------------------------------------------------------------