├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── it │ │ └── sephiroth │ │ └── android │ │ └── rangeseekbardemo │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── it │ │ │ └── sephiroth │ │ │ └── android │ │ │ └── rangeseekbardemo │ │ │ └── MainActivity.java │ └── res │ │ ├── layout │ │ └── activity_main.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 │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── it │ └── sephiroth │ └── android │ └── rangeseekbardemo │ └── ExampleUnitTest.java ├── art ├── screenshot1.png └── video.gif ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── rangeseekbar-library ├── .gitignore ├── build.gradle ├── gradle.properties ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── it │ │ └── sephiroth │ │ └── android │ │ └── library │ │ └── rangeseekbar │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── it │ │ │ └── sephiroth │ │ │ └── android │ │ │ └── library │ │ │ └── rangeseekbar │ │ │ ├── DrawableUtils.java │ │ │ ├── MathUtils.java │ │ │ ├── RangeProgressBar.java │ │ │ ├── RangeSeekBar.java │ │ │ ├── SephirothViewCompat.java │ │ │ └── ThemeUtils.java │ └── res │ │ ├── anim │ │ ├── sephiroth_rsb_seekbar_thumb_pressed_to_unpressed_thumb_animation.xml │ │ └── sephiroth_rsb_seekbar_thumb_unpressed_to_pressed_thumb_0_animation.xml │ │ ├── color │ │ ├── sephiroth_control_highlight_material.xml │ │ └── sephiroth_white_disabled_material.xml │ │ ├── drawable │ │ ├── sephiroth_control_background_32dp_material.xml │ │ ├── sephiroth_rsb_range_progress_material.xml │ │ ├── sephiroth_rsb_range_progress_material_inverted.xml │ │ ├── sephiroth_rsb_seekbar_thumb_material_anim.xml │ │ ├── sephiroth_rsb_seekbar_thumb_pressed_to_unpressed.xml │ │ ├── sephiroth_rsb_seekbar_thumb_pressed_to_unpressed_animation.xml │ │ ├── sephiroth_rsb_seekbar_thumb_unpressed_to_pressed.xml │ │ ├── sephiroth_rsb_seekbar_thumb_unpressed_to_pressed_animation.xml │ │ └── sephiroth_rsb_tick_mark_material.xml │ │ └── values │ │ ├── attrs.xml │ │ ├── dimens.xml │ │ ├── styles.xml │ │ └── themes.xml │ └── test │ └── java │ └── it │ └── sephiroth │ └── android │ └── library │ └── rangeseekbar │ └── ExampleUnitTest.java └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | # Android Studio Navigation editor temp files 30 | .navigation/ 31 | 32 | # Android Studio captures folder 33 | captures/ 34 | 35 | # Intellij 36 | *.iml 37 | .idea/ 38 | .idea/workspace.xml 39 | 40 | # Keystore files 41 | *.jks 42 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "JavaCheckStyle"] 2 | path = JavaCheckStyle 3 | url = git@github.com:sephiroth74/JavaCheckStyle.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Alessandro Crugnola 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Range SeekBar 2 | 3 | [ ![Download](https://api.bintray.com/packages/bintray/jcenter/it.sephiroth.android.library.rangeseekbar%3Arangeseekbar/images/download.svg) ](https://bintray.com/bintray/jcenter/it.sephiroth.android.library.rangeseekbar%3Arangeseekbar/_latestVersion) 4 | [![](https://jitpack.io/v/sephiroth74/RangeSeekbar.svg)](https://jitpack.io/#sephiroth74/RangeSeekbar) 5 | 6 | 7 | Similar to the Android built-in SeekBar, but it allows to edit a values in a range (start, end). 8 |
9 | 10 | 11 | 12 | 13 | --- 14 | 15 | ## Installation 16 | ### Maven 17 | Add the library dependency: 18 | 19 | implementation 'it.sephiroth.android.library.rangeseekbar:rangeseekbar:**version**' 20 | 21 | ### JitPack 22 | **Step 1.** Add the JitPack repository to your build file: 23 | 24 | allprojects { 25 | repositories { 26 | ... 27 | maven { url 'https://jitpack.io' } 28 | } 29 | } 30 | 31 | **Step 2.** Add the dependency to your project's build.gradle file: 32 | 33 | dependencies { 34 | implementation 'com.github.sephiroth74:RangeSeekBar:Tag' 35 | } 36 | 37 | To See the last release version: https://jitpack.io/private#sephiroth74/RangeSeekBar 38 | 39 | --- 40 | 41 | ## Usage 42 | 43 | layout: 44 | 45 | 60 | 61 | java: 62 | 63 | seekbar.setOnRangeSeekBarChangeListener(new RangeSeekBar.OnRangeSeekBarChangeListener() { 64 | @Override 65 | public void onProgressChanged( 66 | final RangeSeekBar seekBar, final int progressStart, final int progressEnd, final boolean fromUser) { } 67 | 68 | @Override 69 | public void onStartTrackingTouch(final RangeSeekBar seekBar) { } 70 | 71 | @Override 72 | public void onStopTrackingTouch(final RangeSeekBar seekBar) { } 73 | }); 74 | 75 | 76 | To see the list of all the available attributes, see [attrs.xml](./rangeseekbar-library/src/main/res/values/attrs.xml) 77 | 78 | 79 | --- 80 | 81 | ## License 82 | 83 | MIT License 84 | 85 | Copyright (c) 2017 Alessandro Crugnola 86 | 87 | Permission is hereby granted, free of charge, to any person obtaining a copy 88 | of this software and associated documentation files (the "Software"), to deal 89 | in the Software without restriction, including without limitation the rights 90 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 91 | copies of the Software, and to permit persons to whom the Software is 92 | furnished to do so, subject to the following conditions: 93 | 94 | The above copyright notice and this permission notice shall be included in all 95 | copies or substantial portions of the Software. 96 | 97 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 98 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 99 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 100 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 101 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 102 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 103 | SOFTWARE. 104 | 105 | --- 106 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | compileSdkVersion rootProject.ext.compileSdkVersion 7 | 8 | defaultConfig { 9 | applicationId "it.sephiroth.android.rangeseekbardemo" 10 | minSdkVersion rootProject.ext.minSdkVersion 11 | targetSdkVersion rootProject.ext.targetSdkVersion 12 | versionCode 1 13 | versionName "1.0" 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | } 16 | 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | 24 | compileOptions { 25 | sourceCompatibility 1.8 26 | targetCompatibility 1.8 27 | } 28 | } 29 | 30 | dependencies { 31 | implementation fileTree(dir: 'libs', include: ['*.jar']) 32 | implementation project(':rangeseekbar-library') 33 | 34 | implementation 'androidx.appcompat:appcompat:1.0.2' 35 | implementation 'androidx.annotation:annotation:1.0.1' 36 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 37 | implementation 'androidx.constraintlayout:constraintlayout-solver:1.1.3' 38 | implementation 'androidx.legacy:legacy-support-v4:1.0.0' 39 | implementation 'com.google.android.material:material:1.0.0' 40 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 41 | implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" 42 | 43 | implementation 'com.jakewharton.timber:timber:4.7.1' 44 | 45 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 46 | 47 | testImplementation 'junit:junit:4.12' 48 | 49 | } 50 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Volumes/MackintoshHDD/Users/sephiroth/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /app/src/androidTest/java/it/sephiroth/android/rangeseekbardemo/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package it.sephiroth.android.rangeseekbardemo; 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 | * Instrumentation 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() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("it.sephiroth.android.rangeseekbardemo", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/java/it/sephiroth/android/rangeseekbardemo/MainActivity.java: -------------------------------------------------------------------------------- 1 | package it.sephiroth.android.rangeseekbardemo; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.os.Bundle; 5 | import android.widget.TextView; 6 | 7 | import androidx.appcompat.app.AppCompatActivity; 8 | import it.sephiroth.android.library.rangeseekbar.RangeSeekBar; 9 | 10 | public class MainActivity extends AppCompatActivity { 11 | 12 | @Override 13 | protected void onCreate(Bundle savedInstanceState) { 14 | super.onCreate(savedInstanceState); 15 | setContentView(R.layout.activity_main); 16 | 17 | final RangeSeekBar seekBar1 = findViewById(R.id.rangeSeekBar); 18 | final TextView textView1 = findViewById(R.id.textView2); 19 | 20 | final RangeSeekBar seekBar2 = findViewById(R.id.rangeSeekBar2); 21 | final TextView textView2 = findViewById(R.id.textView3); 22 | 23 | final RangeSeekBar seekBar3 = findViewById(R.id.rangeSeekBar3); 24 | final TextView textView3 = findViewById(R.id.textView5); 25 | 26 | initializeSeekBar(seekBar1, textView1); 27 | initializeSeekBar(seekBar2, textView2); 28 | initializeSeekBar(seekBar3, textView3); 29 | 30 | } 31 | 32 | private void initializeSeekBar(RangeSeekBar seekbar, TextView textView) { 33 | seekbar.setOnRangeSeekBarChangeListener(new RangeSeekBar.OnRangeSeekBarChangeListener() { 34 | @Override 35 | public void onProgressChanged( 36 | final RangeSeekBar seekBar, final int progressStart, final int progressEnd, final boolean fromUser) { 37 | updateRangeText(textView, seekBar); 38 | } 39 | 40 | @Override 41 | public void onStartTrackingTouch(final RangeSeekBar seekBar) { } 42 | 43 | @Override 44 | public void onStopTrackingTouch(final RangeSeekBar seekBar) { } 45 | }); 46 | 47 | updateRangeText(textView, seekbar); 48 | } 49 | 50 | @SuppressLint ("SetTextI18n") 51 | private void updateRangeText(TextView textView, RangeSeekBar seekBar) { 52 | textView.setText(seekBar.getProgressStart() + " - " + seekBar.getProgressEnd()); 53 | 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 25 | 26 | 37 | 38 | 47 | 48 | 49 | 50 | 63 | 64 | 65 | 71 | 72 | 81 | 82 | 83 | 84 | 99 | 100 | 101 | 107 | 108 | 117 | 118 | 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sephiroth74/RangeSeekBar/f072e0ec1cd6c8c85f307d0cb792557459aee861/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sephiroth74/RangeSeekBar/f072e0ec1cd6c8c85f307d0cb792557459aee861/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sephiroth74/RangeSeekBar/f072e0ec1cd6c8c85f307d0cb792557459aee861/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sephiroth74/RangeSeekBar/f072e0ec1cd6c8c85f307d0cb792557459aee861/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sephiroth74/RangeSeekBar/f072e0ec1cd6c8c85f307d0cb792557459aee861/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sephiroth74/RangeSeekBar/f072e0ec1cd6c8c85f307d0cb792557459aee861/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sephiroth74/RangeSeekBar/f072e0ec1cd6c8c85f307d0cb792557459aee861/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sephiroth74/RangeSeekBar/f072e0ec1cd6c8c85f307d0cb792557459aee861/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sephiroth74/RangeSeekBar/f072e0ec1cd6c8c85f307d0cb792557459aee861/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sephiroth74/RangeSeekBar/f072e0ec1cd6c8c85f307d0cb792557459aee861/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #1976d2 4 | #63a4ff 5 | #004ba0 6 | #d32f2f 7 | #ff6659 8 | #9a0007 9 | #ffffff 10 | #ffffff 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | RangeSeekBar 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/test/java/it/sephiroth/android/rangeseekbardemo/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package it.sephiroth.android.rangeseekbardemo; 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() throws Exception { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /art/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sephiroth74/RangeSeekBar/f072e0ec1cd6c8c85f307d0cb792557459aee861/art/screenshot1.png -------------------------------------------------------------------------------- /art/video.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sephiroth74/RangeSeekBar/f072e0ec1cd6c8c85f307d0cb792557459aee861/art/video.gif -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | 3 | ext { 4 | kotlin_version = '1.3.11' 5 | } 6 | 7 | repositories { 8 | google() 9 | jcenter() 10 | } 11 | 12 | dependencies { 13 | classpath 'com.android.tools.build:gradle:3.4.0-alpha10' 14 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 15 | } 16 | } 17 | 18 | ext { 19 | compileSdkVersion = 28 20 | minSdkVersion = 23 21 | targetSdkVersion = 28 22 | } 23 | 24 | 25 | allprojects { 26 | repositories { 27 | jcenter() 28 | google() 29 | } 30 | } 31 | 32 | task clean(type: Delete) { 33 | delete rootProject.buildDir 34 | } 35 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | VERSION_NAME=1.1.0 2 | VERSION_CODE=3 3 | GROUP=it.sephiroth.android.library.rangeseekbar 4 | 5 | POM_DESCRIPTION=Android Range Seekbar 6 | POM_URL=https://github.com/sephiroth74/RangeSeekBar 7 | POM_SCM_URL=https://github.com/sephiroth74/RangeSeekBar 8 | POM_SCM_CONNECTION=scm:git@github.com:sephiroth74/RangeSeekBar.git 9 | POM_SCM_DEV_CONNECTION=scm:git@github.com:sephiroth74/RangeSeekBar.git 10 | POM_LICENCE_NAME=The MIT License (MIT) 11 | POM_LICENCE_URL=https://github.com/sephiroth74/RangeSeekBar/blob/master/LICENSE 12 | POM_LICENCE_DIST=repo 13 | POM_DEVELOPER_ID=sephiroth74 14 | POM_DEVELOPER_NAME=Alessandro Crugnola 15 | POM_DEVELOPER_EMAIL=alessandro.crugnola@gmail.com 16 | POM_DEVELOPER_URL=http://blog.sephiroth.it 17 | POM_DEVELOPER_ROLE=author 18 | 19 | android.useAndroidX=true 20 | android.enableJetifier=true 21 | 22 | org.gradle.daemon=true 23 | org.gradle.jvmargs=-Xmx1536M -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sephiroth74/RangeSeekBar/f072e0ec1cd6c8c85f307d0cb792557459aee861/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Feb 09 16:09:20 EST 2017 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.1-milestone-1-all.zip 7 | 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /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 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 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 Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /rangeseekbar-library/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /rangeseekbar-library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | apply plugin: 'kotlin-kapt' 5 | 6 | android { 7 | compileSdkVersion rootProject.ext.compileSdkVersion 8 | testBuildType "debug" 9 | 10 | defaultConfig { 11 | minSdkVersion rootProject.ext.minSdkVersion 12 | targetSdkVersion rootProject.ext.targetSdkVersion 13 | versionCode 1 14 | versionName VERSION_NAME 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 22 | } 23 | 24 | debug { 25 | testCoverageEnabled true 26 | } 27 | } 28 | 29 | compileOptions { 30 | sourceCompatibility JavaVersion.VERSION_1_8 31 | targetCompatibility JavaVersion.VERSION_1_8 32 | } 33 | 34 | lintOptions { 35 | disable "AppLinksAutoVerifyError", "AppLinksAutoVerifyWarning" 36 | xmlReport false 37 | textReport true 38 | htmlReport false 39 | textOutput "stdout" 40 | abortOnError false 41 | } 42 | } 43 | 44 | dependencies { 45 | implementation fileTree(dir: 'libs', include: ['*.jar']) 46 | implementation 'androidx.annotation:annotation:1.0.1' 47 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 48 | implementation 'com.google.android.material:material:1.0.0-rc01' 49 | implementation 'androidx.appcompat:appcompat:1.0.2' 50 | implementation 'androidx.core:core:1.0.1' 51 | 52 | implementation 'it.sephiroth.android.library.simplelogger:simple-logger:1.0.0' 53 | 54 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 55 | androidTestImplementation 'androidx.test.espresso:espresso-idling-resource:3.1.1' 56 | androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0' 57 | androidTestImplementation 'androidx.test:core:1.1.0' 58 | androidTestImplementation 'androidx.test:runner:1.1.1' 59 | androidTestImplementation 'androidx.test:rules:1.1.1' 60 | androidTestImplementation 'androidx.test.ext:junit:1.1.0' 61 | 62 | testImplementation 'androidx.test:core:1.1.0' 63 | testImplementation 'androidx.test:runner:1.1.1' 64 | testImplementation 'androidx.test:rules:1.1.1' 65 | testImplementation 'androidx.test.ext:junit:1.1.0' 66 | testImplementation 'androidx.test.espresso:espresso-core:3.1.1' 67 | testImplementation 'org.robolectric:robolectric:4.1' 68 | 69 | androidTestUtil 'androidx.test:orchestrator:1.1.1' 70 | } 71 | 72 | apply from: 'https://raw.githubusercontent.com/sephiroth74/gradle-mvn-push/master/gradle-mvn-push.gradle' 73 | -------------------------------------------------------------------------------- /rangeseekbar-library/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=Range Seekbar 2 | POM_ARTIFACT_ID=rangeseekbar 3 | POM_PACKAGING=aar -------------------------------------------------------------------------------- /rangeseekbar-library/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Volumes/MackintoshHDD/Users/sephiroth/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /rangeseekbar-library/src/androidTest/java/it/sephiroth/android/library/rangeseekbar/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package it.sephiroth.android.library.rangeseekbar; 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 | * Instrumentation 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() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("it.sephiroth.android.library.rangeseekbar.test", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /rangeseekbar-library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /rangeseekbar-library/src/main/java/it/sephiroth/android/library/rangeseekbar/DrawableUtils.java: -------------------------------------------------------------------------------- 1 | package it.sephiroth.android.library.rangeseekbar; 2 | 3 | import android.graphics.PorterDuff; 4 | import android.graphics.drawable.Drawable; 5 | import android.graphics.drawable.DrawableContainer; 6 | import android.graphics.drawable.DrawableWrapper; 7 | import android.graphics.drawable.ScaleDrawable; 8 | import android.os.Build; 9 | 10 | import java.util.Objects; 11 | 12 | import androidx.annotation.NonNull; 13 | 14 | public class DrawableUtils { 15 | private static final String VECTOR_DRAWABLE_CLAZZ_NAME 16 | = "android.graphics.drawable.VectorDrawable"; 17 | 18 | private DrawableUtils() { } 19 | 20 | public static PorterDuff.Mode parseTintMode(int value, PorterDuff.Mode defaultMode) { 21 | switch (value) { 22 | case 3: 23 | return PorterDuff.Mode.SRC_OVER; 24 | case 5: 25 | return PorterDuff.Mode.SRC_IN; 26 | case 9: 27 | return PorterDuff.Mode.SRC_ATOP; 28 | case 14: 29 | return PorterDuff.Mode.MULTIPLY; 30 | case 15: 31 | return PorterDuff.Mode.SCREEN; 32 | case 16: 33 | return PorterDuff.Mode.ADD; 34 | default: 35 | return defaultMode; 36 | } 37 | } 38 | 39 | static void fixDrawable(@NonNull final Drawable drawable) { 40 | if (Build.VERSION.SDK_INT == 21 41 | && VECTOR_DRAWABLE_CLAZZ_NAME.equals(drawable.getClass().getName())) { 42 | fixVectorDrawableTinting(drawable); 43 | } 44 | } 45 | 46 | private static void fixVectorDrawableTinting(final Drawable drawable) { 47 | final int[] originalState = drawable.getState(); 48 | if (originalState == null || originalState.length == 0) { 49 | drawable.setState(ThemeUtils.CHECKED_STATE_SET); 50 | } else { 51 | drawable.setState(ThemeUtils.EMPTY_STATE_SET); 52 | } 53 | drawable.setState(originalState); 54 | } 55 | 56 | public static boolean canSafelyMutateDrawable(@NonNull Drawable drawable) { 57 | if (drawable instanceof DrawableContainer) { 58 | // If we have a DrawableContainer, let's traverse it's child array 59 | final Drawable.ConstantState state = drawable.getConstantState(); 60 | if (state instanceof DrawableContainer.DrawableContainerState) { 61 | final DrawableContainer.DrawableContainerState containerState = 62 | (DrawableContainer.DrawableContainerState) state; 63 | for (final Drawable child : containerState.getChildren()) { 64 | if (!canSafelyMutateDrawable(child)) { 65 | return false; 66 | } 67 | } 68 | } 69 | 70 | } else if (drawable instanceof DrawableWrapper) { 71 | return canSafelyMutateDrawable( 72 | Objects.requireNonNull(((DrawableWrapper) drawable).getDrawable())); 73 | } else if (drawable instanceof ScaleDrawable) { 74 | return canSafelyMutateDrawable(Objects.requireNonNull(((ScaleDrawable) drawable).getDrawable())); 75 | } 76 | 77 | return true; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /rangeseekbar-library/src/main/java/it/sephiroth/android/library/rangeseekbar/MathUtils.java: -------------------------------------------------------------------------------- 1 | package it.sephiroth.android.library.rangeseekbar; 2 | 3 | class MathUtils { 4 | static int constrain(int amount, int low, int high) { 5 | return amount < low ? low : (amount > high ? high : amount); 6 | } 7 | 8 | static float constrain(float amount, float low, float high) { 9 | return amount < low ? low : (amount > high ? high : amount); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /rangeseekbar-library/src/main/java/it/sephiroth/android/library/rangeseekbar/RangeProgressBar.java: -------------------------------------------------------------------------------- 1 | package it.sephiroth.android.library.rangeseekbar; 2 | 3 | import android.animation.AnimatorSet; 4 | import android.animation.ValueAnimator; 5 | import android.content.Context; 6 | import android.content.res.ColorStateList; 7 | import android.content.res.TypedArray; 8 | import android.graphics.Canvas; 9 | import android.graphics.PorterDuff; 10 | import android.graphics.Rect; 11 | import android.graphics.Shader; 12 | import android.graphics.drawable.BitmapDrawable; 13 | import android.graphics.drawable.ClipDrawable; 14 | import android.graphics.drawable.Drawable; 15 | import android.graphics.drawable.LayerDrawable; 16 | import android.graphics.drawable.StateListDrawable; 17 | import android.os.Build; 18 | import android.os.Parcel; 19 | import android.os.Parcelable; 20 | import android.util.AttributeSet; 21 | import android.view.Gravity; 22 | import android.view.View; 23 | import android.view.accessibility.AccessibilityEvent; 24 | import android.view.animation.AnimationUtils; 25 | import android.view.animation.DecelerateInterpolator; 26 | import android.view.animation.Interpolator; 27 | 28 | import java.util.ArrayList; 29 | 30 | import androidx.annotation.InterpolatorRes; 31 | import androidx.annotation.NonNull; 32 | import androidx.annotation.Nullable; 33 | import androidx.core.graphics.drawable.DrawableCompat; 34 | import androidx.core.util.Pools; 35 | import it.sephiroth.android.library.simplelogger.LoggerFactory; 36 | import it.sephiroth.android.library.simplelogger.LoggerFactory.LoggerType; 37 | 38 | public class RangeProgressBar extends View { 39 | protected static LoggerFactory.Logger logger = 40 | LoggerFactory.getLogger("RangeProgressBar", BuildConfig.DEBUG ? LoggerType.Console : LoggerType.Null); 41 | 42 | private static final int TIMEOUT_SEND_ACCESSIBILITY_EVENT = 200; 43 | 44 | /** Interpolator used for smooth progress animations. */ 45 | private static final DecelerateInterpolator PROGRESS_ANIM_INTERPOLATOR = 46 | new DecelerateInterpolator(); 47 | 48 | /** Duration of smooth progress animations. */ 49 | private static final int PROGRESS_ANIM_DURATION = 80; 50 | 51 | protected int mMinMaxStepSize; 52 | protected int mProgressStartMaxValue; 53 | protected int mProgressEndMinValue; 54 | 55 | int mMinWidth; 56 | int mMaxWidth; 57 | int mMinHeight; 58 | int mMaxHeight; 59 | 60 | private int mProgressOffset; 61 | private int mEndProgress; 62 | private int mStartProgress; 63 | private int mMax; 64 | 65 | protected boolean mInitialProgressDone; 66 | 67 | private Drawable mProgressDrawable; 68 | private Drawable mCurrentDrawable; 69 | private ProgressTintInfo mProgressTintInfo; 70 | 71 | int mSampleWidth = 0; 72 | private boolean mNoInvalidate; 73 | private Interpolator mInterpolator; 74 | private RefreshProgressRunnable mRefreshProgressRunnable; 75 | private long mUiThreadId; 76 | 77 | private boolean mInDrawing; 78 | private boolean mAttached; 79 | private boolean mRefreshIsPosted; 80 | 81 | /** Value used to track progress animation, in the range [0...1]. */ 82 | private float mVisualStartProgress; 83 | private float mVisualEndProgress; 84 | 85 | boolean mMirrorForRtl = false; 86 | 87 | private boolean mAggregatedIsVisible; 88 | 89 | private final ArrayList mRefreshData = new ArrayList<>(); 90 | 91 | private AccessibilityEventSender mAccessibilityEventSender; 92 | private Drawable mProgressDrawableIndicator; 93 | private Rect mProgressIndicatorBounds; 94 | private int mComputedWidth; 95 | protected int mPaddingBottom; 96 | protected int mPaddingTop; 97 | protected int mPaddingLeft; 98 | protected int mPaddingRight; 99 | 100 | public RangeProgressBar(Context context) { 101 | this(context, null); 102 | } 103 | 104 | public RangeProgressBar(Context context, AttributeSet attrs) { 105 | this(context, attrs, R.attr.sephiroth_rangeProgressBarStyle); 106 | } 107 | 108 | public RangeProgressBar(Context context, AttributeSet attrs, int defStyleAttr) { 109 | super(context, attrs, defStyleAttr); 110 | 111 | mUiThreadId = Thread.currentThread().getId(); 112 | 113 | initProgressBar(); 114 | 115 | final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RangeProgressBar, defStyleAttr, 0); 116 | 117 | mNoInvalidate = true; 118 | 119 | final Drawable progressDrawable = a.getDrawable(R.styleable.RangeProgressBar_android_progressDrawable); 120 | if (progressDrawable != null) { 121 | if (needsTileify(progressDrawable)) { 122 | setProgressDrawableTiled(progressDrawable); 123 | } else { 124 | setProgressDrawable(progressDrawable); 125 | } 126 | } 127 | 128 | mMinWidth = a.getDimensionPixelSize(R.styleable.RangeProgressBar_android_minWidth, mMinWidth); 129 | mMaxWidth = a.getDimensionPixelSize(R.styleable.RangeProgressBar_android_maxWidth, mMaxWidth); 130 | mMinHeight = a.getDimensionPixelSize(R.styleable.RangeProgressBar_android_minHeight, mMinHeight); 131 | mMaxHeight = a.getDimensionPixelSize(R.styleable.RangeProgressBar_android_maxHeight, mMaxHeight); 132 | mMinMaxStepSize = a.getInteger(R.styleable.RangeProgressBar_range_progress_startEnd_minDiff, 0); 133 | mProgressOffset = a.getDimensionPixelSize(R.styleable.RangeProgressBar_range_progress_offset, 0); 134 | mProgressEndMinValue = a.getInteger(R.styleable.RangeProgressBar_range_progress_endMinValue, -1); 135 | mProgressStartMaxValue = a.getInteger(R.styleable.RangeProgressBar_range_progress_startMaxValue, -1); 136 | 137 | final int resID = a.getResourceId( 138 | R.styleable.RangeProgressBar_android_interpolator, 139 | android.R.anim.linear_interpolator 140 | ); // default to linear interpolator 141 | 142 | if (resID > 0) { 143 | setInterpolator(context, resID); 144 | } 145 | 146 | setMax(a.getInteger(R.styleable.RangeProgressBar_android_max, mMax)); 147 | 148 | mNoInvalidate = false; 149 | 150 | if (a.hasValue(R.styleable.RangeProgressBar_android_progressTintMode)) { 151 | if (mProgressTintInfo == null) { 152 | mProgressTintInfo = new ProgressTintInfo(); 153 | } 154 | mProgressTintInfo.mProgressTintMode = DrawableUtils.parseTintMode(a.getInt( 155 | R.styleable.RangeProgressBar_android_progressTintMode, -1), null); 156 | mProgressTintInfo.mHasProgressTintMode = true; 157 | } 158 | 159 | if (a.hasValue(R.styleable.RangeProgressBar_android_progressTint)) { 160 | if (mProgressTintInfo == null) { 161 | mProgressTintInfo = new ProgressTintInfo(); 162 | } 163 | mProgressTintInfo.mProgressTintList = a.getColorStateList( 164 | R.styleable.RangeProgressBar_android_progressTint); 165 | mProgressTintInfo.mHasProgressTint = true; 166 | } 167 | 168 | if (a.hasValue(R.styleable.RangeProgressBar_android_progressBackgroundTintMode)) { 169 | if (mProgressTintInfo == null) { 170 | mProgressTintInfo = new ProgressTintInfo(); 171 | } 172 | mProgressTintInfo.mProgressBackgroundTintMode = DrawableUtils.parseTintMode(a.getInt( 173 | R.styleable.RangeProgressBar_android_progressBackgroundTintMode, -1), null); 174 | mProgressTintInfo.mHasProgressBackgroundTintMode = true; 175 | } 176 | 177 | if (a.hasValue(R.styleable.RangeProgressBar_android_progressBackgroundTint)) { 178 | if (mProgressTintInfo == null) { 179 | mProgressTintInfo = new ProgressTintInfo(); 180 | } 181 | mProgressTintInfo.mProgressBackgroundTintList = a.getColorStateList( 182 | R.styleable.RangeProgressBar_android_progressBackgroundTint); 183 | mProgressTintInfo.mHasProgressBackgroundTint = true; 184 | } 185 | 186 | final int startProgress = a.getInteger(R.styleable.RangeProgressBar_range_progress_startValue, mStartProgress); 187 | final int endProgress = a.getInteger(R.styleable.RangeProgressBar_range_progress_endValue, mEndProgress); 188 | 189 | a.recycle(); 190 | 191 | applyProgressTints(); 192 | 193 | if (getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { 194 | setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); 195 | } 196 | 197 | setProgressStartEndBoundaries(mProgressStartMaxValue, mProgressEndMinValue); 198 | 199 | setInitialProgress( 200 | startProgress, 201 | endProgress 202 | ); 203 | 204 | mInitialProgressDone = true; 205 | } 206 | 207 | public int getMinMapStepSize() { 208 | return mMinMaxStepSize; 209 | } 210 | 211 | protected void setInitialProgress(final int startProgress, final int endProgress) { 212 | setProgress(startProgress, endProgress); 213 | } 214 | 215 | private static boolean needsTileify(Drawable dr) { 216 | if (dr instanceof LayerDrawable) { 217 | final LayerDrawable orig = (LayerDrawable) dr; 218 | final int N = orig.getNumberOfLayers(); 219 | for (int i = 0; i < N; i++) { 220 | if (needsTileify(orig.getDrawable(i))) { 221 | logger.debug("needsTileify!"); 222 | return true; 223 | } 224 | } 225 | return false; 226 | } 227 | 228 | if (dr instanceof StateListDrawable) { 229 | //throw new RuntimeException("StateListDrawable not supported"); 230 | return false; 231 | // final StateListDrawable in = (StateListDrawable) dr; 232 | // final int N = in.getStateCount(); 233 | // for (int i = 0; i < N; i++) { 234 | // if (needsTileify(in.getStateDrawable(i))) { 235 | // return true; 236 | // } 237 | // } 238 | // return false; 239 | } 240 | 241 | return dr instanceof BitmapDrawable; 242 | 243 | } 244 | 245 | private Drawable tileify(Drawable drawable, boolean clip) { 246 | logger.debug("tileify: " + drawable + ", clip: " + clip); 247 | // TODO: This is a terrible idea that potentially destroys any drawable 248 | // that extends any of these classes. We *really* need to remove this. 249 | 250 | if (drawable instanceof LayerDrawable) { 251 | final LayerDrawable orig = (LayerDrawable) drawable; 252 | final int N = orig.getNumberOfLayers(); 253 | final Drawable[] outDrawables = new Drawable[N]; 254 | 255 | for (int i = 0; i < N; i++) { 256 | final int id = orig.getId(i); 257 | outDrawables[i] = tileify( 258 | orig.getDrawable(i), 259 | (id == android.R.id.progress || id == android.R.id.secondaryProgress) 260 | ); 261 | } 262 | 263 | final LayerDrawable clone = new LayerDrawable(outDrawables); 264 | for (int i = 0; i < N; i++) { 265 | clone.setId(i, orig.getId(i)); 266 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 267 | clone.setLayerGravity(i, orig.getLayerGravity(i)); 268 | clone.setLayerWidth(i, orig.getLayerWidth(i)); 269 | clone.setLayerHeight(i, orig.getLayerHeight(i)); 270 | clone.setLayerInsetLeft(i, orig.getLayerInsetLeft(i)); 271 | clone.setLayerInsetRight(i, orig.getLayerInsetRight(i)); 272 | clone.setLayerInsetTop(i, orig.getLayerInsetTop(i)); 273 | clone.setLayerInsetBottom(i, orig.getLayerInsetBottom(i)); 274 | clone.setLayerInsetStart(i, orig.getLayerInsetStart(i)); 275 | clone.setLayerInsetEnd(i, orig.getLayerInsetEnd(i)); 276 | } 277 | } 278 | 279 | return clone; 280 | } 281 | 282 | if (drawable instanceof StateListDrawable) { 283 | throw new RuntimeException("StateListDrawable not supported"); 284 | // 285 | // final StateListDrawable in = (StateListDrawable) drawable; 286 | // final StateListDrawable out = new StateListDrawable(); 287 | // final int N = in.getStateCount(); 288 | // for (int i = 0; i < N; i++) { 289 | // out.addState(in.getStateSet(i), tileify(in.getStateDrawable(i), clip)); 290 | // } 291 | // 292 | // return out; 293 | } 294 | 295 | if (drawable instanceof BitmapDrawable) { 296 | final Drawable.ConstantState cs = drawable.getConstantState(); 297 | assert cs != null; 298 | final BitmapDrawable clone = (BitmapDrawable) cs.newDrawable(getResources()); 299 | clone.setTileModeXY(Shader.TileMode.REPEAT, Shader.TileMode.CLAMP); 300 | 301 | if (mSampleWidth <= 0) { 302 | mSampleWidth = clone.getIntrinsicWidth(); 303 | } 304 | 305 | if (clip) { 306 | return new ClipDrawable(clone, Gravity.START, ClipDrawable.HORIZONTAL); 307 | } else { 308 | return clone; 309 | } 310 | } 311 | 312 | return drawable; 313 | } 314 | 315 | private void initProgressBar() { 316 | mMax = 100; 317 | mEndProgress = 100; 318 | mStartProgress = 0; 319 | mMinWidth = 24; 320 | mMaxWidth = 48; 321 | mMinHeight = 24; 322 | mMaxHeight = 48; 323 | } 324 | 325 | private void swapCurrentDrawable(Drawable newDrawable) { 326 | final Drawable oldDrawable = mCurrentDrawable; 327 | mCurrentDrawable = newDrawable; 328 | 329 | if (oldDrawable != mCurrentDrawable) { 330 | if (oldDrawable != null) { 331 | oldDrawable.setVisible(false, false); 332 | } 333 | if (mCurrentDrawable != null) { 334 | mCurrentDrawable.setVisible(getWindowVisibility() == VISIBLE && isShown(), false); 335 | } 336 | } 337 | } 338 | 339 | public Drawable getProgressDrawable() { 340 | return mProgressDrawable; 341 | } 342 | 343 | public void setProgressDrawable(Drawable d) { 344 | if (mProgressDrawable != d) { 345 | if (mProgressDrawable != null) { 346 | mProgressDrawable.setCallback(null); 347 | unscheduleDrawable(mProgressDrawable); 348 | } 349 | 350 | mProgressDrawable = d; 351 | 352 | if (d != null) { 353 | d.setCallback(this); 354 | DrawableCompat.setLayoutDirection(d, getLayoutDirection()); 355 | if (d.isStateful()) { 356 | d.setState(getDrawableState()); 357 | } 358 | 359 | // Make sure the ProgressBar is always tall enough 360 | int drawableHeight = d.getMinimumHeight(); 361 | if (mMaxHeight < drawableHeight) { 362 | mMaxHeight = drawableHeight; 363 | requestLayout(); 364 | } 365 | 366 | applyProgressTints(); 367 | } 368 | 369 | swapCurrentDrawable(d); 370 | postInvalidate(); 371 | 372 | updateDrawableBounds(getWidth(), getHeight()); 373 | updateDrawableState(); 374 | 375 | doRefreshProgress(android.R.id.progress, mStartProgress, mEndProgress, false, false, false); 376 | } 377 | } 378 | 379 | private void applyProgressTints() { 380 | if (mProgressDrawable != null && mProgressTintInfo != null) { 381 | applyPrimaryProgressTint(); 382 | applyProgressBackgroundTint(); 383 | } 384 | } 385 | 386 | private void applyPrimaryProgressTint() { 387 | if (mProgressTintInfo.mHasProgressTint 388 | || mProgressTintInfo.mHasProgressTintMode) { 389 | final Drawable target = getTintTarget(android.R.id.progress, true); 390 | if (target != null) { 391 | if (mProgressTintInfo.mHasProgressTint) { 392 | DrawableCompat.setTintList(target, mProgressTintInfo.mProgressTintList); 393 | } 394 | if (mProgressTintInfo.mHasProgressTintMode) { 395 | DrawableCompat.setTintMode(target, mProgressTintInfo.mProgressTintMode); 396 | } 397 | 398 | // The drawable (or one of its children) may not have been 399 | // stateful before applying the tint, so let's try again. 400 | if (target.isStateful()) { 401 | target.setState(getDrawableState()); 402 | } 403 | } 404 | } 405 | } 406 | 407 | private void applyProgressBackgroundTint() { 408 | if (mProgressTintInfo.mHasProgressBackgroundTint 409 | || mProgressTintInfo.mHasProgressBackgroundTintMode) { 410 | final Drawable target = getTintTarget(android.R.id.background, false); 411 | if (target != null) { 412 | if (mProgressTintInfo.mHasProgressBackgroundTint) { 413 | DrawableCompat.setTintList(target, mProgressTintInfo.mProgressBackgroundTintList); 414 | } 415 | if (mProgressTintInfo.mHasProgressBackgroundTintMode) { 416 | DrawableCompat.setTintMode(target, mProgressTintInfo.mProgressBackgroundTintMode); 417 | } 418 | 419 | // The drawable (or one of its children) may not have been 420 | // stateful before applying the tint, so let's try again. 421 | if (target.isStateful()) { 422 | target.setState(getDrawableState()); 423 | } 424 | } 425 | } 426 | } 427 | 428 | @SuppressWarnings ("unused") 429 | public void setProgressTintList(@Nullable ColorStateList tint) { 430 | if (mProgressTintInfo == null) { 431 | mProgressTintInfo = new ProgressTintInfo(); 432 | } 433 | mProgressTintInfo.mProgressTintList = tint; 434 | mProgressTintInfo.mHasProgressTint = true; 435 | 436 | if (mProgressDrawable != null) { 437 | applyPrimaryProgressTint(); 438 | } 439 | } 440 | 441 | @SuppressWarnings ("unused") 442 | @Nullable 443 | public ColorStateList getProgressTintList() { 444 | return mProgressTintInfo != null ? mProgressTintInfo.mProgressTintList : null; 445 | } 446 | 447 | @SuppressWarnings ("unused") 448 | public void setProgressTintMode(@Nullable PorterDuff.Mode tintMode) { 449 | if (mProgressTintInfo == null) { 450 | mProgressTintInfo = new ProgressTintInfo(); 451 | } 452 | mProgressTintInfo.mProgressTintMode = tintMode; 453 | mProgressTintInfo.mHasProgressTintMode = true; 454 | 455 | if (mProgressDrawable != null) { 456 | applyPrimaryProgressTint(); 457 | } 458 | } 459 | 460 | @SuppressWarnings ("unused") 461 | @Nullable 462 | public PorterDuff.Mode getProgressTintMode() { 463 | return mProgressTintInfo != null ? mProgressTintInfo.mProgressTintMode : null; 464 | } 465 | 466 | @SuppressWarnings ("unused") 467 | public void setProgressBackgroundTintList(@Nullable ColorStateList tint) { 468 | if (mProgressTintInfo == null) { 469 | mProgressTintInfo = new ProgressTintInfo(); 470 | } 471 | mProgressTintInfo.mProgressBackgroundTintList = tint; 472 | mProgressTintInfo.mHasProgressBackgroundTint = true; 473 | 474 | if (mProgressDrawable != null) { 475 | applyProgressBackgroundTint(); 476 | } 477 | } 478 | 479 | @SuppressWarnings ("unused") 480 | @Nullable 481 | public ColorStateList getProgressBackgroundTintList() { 482 | return mProgressTintInfo != null ? mProgressTintInfo.mProgressBackgroundTintList : null; 483 | } 484 | 485 | @SuppressWarnings ("unused") 486 | public void setProgressBackgroundTintMode(@Nullable PorterDuff.Mode tintMode) { 487 | if (mProgressTintInfo == null) { 488 | mProgressTintInfo = new ProgressTintInfo(); 489 | } 490 | mProgressTintInfo.mProgressBackgroundTintMode = tintMode; 491 | mProgressTintInfo.mHasProgressBackgroundTintMode = true; 492 | 493 | if (mProgressDrawable != null) { 494 | applyProgressBackgroundTint(); 495 | } 496 | } 497 | 498 | @SuppressWarnings ("unused") 499 | @Nullable 500 | public PorterDuff.Mode getProgressBackgroundTintMode() { 501 | return mProgressTintInfo != null ? mProgressTintInfo.mProgressBackgroundTintMode : null; 502 | } 503 | 504 | @Nullable 505 | private Drawable getTintTarget(int layerId, boolean shouldFallback) { 506 | Drawable layer = null; 507 | 508 | final Drawable d = mProgressDrawable; 509 | if (d != null) { 510 | mProgressDrawable = d.mutate(); 511 | 512 | if (d instanceof LayerDrawable) { 513 | layer = ((LayerDrawable) d).findDrawableByLayerId(layerId); 514 | } 515 | 516 | if (shouldFallback && layer == null) { 517 | layer = d; 518 | } 519 | } 520 | 521 | return layer; 522 | } 523 | 524 | public void setProgressDrawableTiled(Drawable d) { 525 | if (d != null) { 526 | d = tileify(d, false); 527 | } 528 | 529 | setProgressDrawable(d); 530 | } 531 | 532 | Drawable getCurrentDrawable() { 533 | return mCurrentDrawable; 534 | } 535 | 536 | @Override 537 | protected boolean verifyDrawable(@NonNull Drawable who) { 538 | return who == mProgressDrawable || super.verifyDrawable(who); 539 | } 540 | 541 | @Override 542 | public void jumpDrawablesToCurrentState() { 543 | super.jumpDrawablesToCurrentState(); 544 | if (mProgressDrawable != null) { 545 | mProgressDrawable.jumpToCurrentState(); 546 | } 547 | } 548 | 549 | // WTF: why this method is hidden? 550 | // @Override 551 | // public void onResolveDrawables(int layoutDirection) { 552 | // final Drawable d = mCurrentDrawable; 553 | // if (d != null) { 554 | // d.setLayoutDirection(layoutDirection); 555 | // } 556 | // if (mIndeterminateDrawable != null) { 557 | // mIndeterminateDrawable.setLayoutDirection(layoutDirection); 558 | // } 559 | // if (mProgressDrawable != null) { 560 | // mProgressDrawable.setLayoutDirection(layoutDirection); 561 | // } 562 | // } 563 | 564 | @Override 565 | public void postInvalidate() { 566 | if (!mNoInvalidate) { 567 | super.postInvalidate(); 568 | } 569 | } 570 | 571 | private class RefreshProgressRunnable implements Runnable { 572 | public void run() { 573 | synchronized (RangeProgressBar.this) { 574 | final int count = mRefreshData.size(); 575 | for (int i = 0; i < count; i++) { 576 | final RefreshData rd = mRefreshData.get(i); 577 | doRefreshProgress(rd.id, rd.startValue, rd.endValue, rd.fromUser, true, rd.animate); 578 | rd.recycle(); 579 | } 580 | mRefreshData.clear(); 581 | mRefreshIsPosted = false; 582 | } 583 | } 584 | } 585 | 586 | @SuppressWarnings ("WeakerAccess") 587 | private static class RefreshData { 588 | private static final int POOL_MAX = 24; 589 | private static final Pools.SynchronizedPool sPool = 590 | new Pools.SynchronizedPool<>(POOL_MAX); 591 | 592 | public int id; 593 | public int startValue; 594 | public int endValue; 595 | public boolean fromUser; 596 | public boolean animate; 597 | 598 | public static RefreshData obtain(int id, int startValue, int endValue, boolean fromUser, boolean animate) { 599 | RefreshData rd = sPool.acquire(); 600 | if (rd == null) { 601 | rd = new RefreshData(); 602 | } 603 | rd.id = id; 604 | rd.startValue = startValue; 605 | rd.endValue = endValue; 606 | rd.fromUser = fromUser; 607 | rd.animate = animate; 608 | return rd; 609 | } 610 | 611 | public void recycle() { 612 | sPool.release(this); 613 | } 614 | } 615 | 616 | private synchronized void doRefreshProgress( 617 | int id, int startValue, int endValue, boolean fromUser, 618 | boolean callBackToApp, boolean animate) { 619 | 620 | logger.info("doRefreshProgress(%d, %d, %b, %b)", startValue, endValue, fromUser, animate); 621 | 622 | final float scale1 = mMax > 0 ? (float) startValue / mMax : 0; 623 | final float scale2 = mMax > 0 ? (float) endValue / mMax : 0; 624 | 625 | if (animate) { 626 | logger.verbose("start: %g to %g", mVisualStartProgress, scale1); 627 | logger.verbose("end: %g to %g", mVisualEndProgress, scale2); 628 | 629 | final ValueAnimator a1 = ValueAnimator.ofFloat(mVisualStartProgress, scale1); 630 | final ValueAnimator a2 = ValueAnimator.ofFloat(mVisualEndProgress, scale2); 631 | 632 | a2.addUpdateListener( 633 | animation -> setVisualProgress( 634 | android.R.id.progress, (float) a1.getAnimatedValue(), (float) a2.getAnimatedValue())); 635 | 636 | AnimatorSet set = new AnimatorSet(); 637 | set.playTogether(a1, a2); 638 | set.setDuration(PROGRESS_ANIM_DURATION); 639 | set.setInterpolator(PROGRESS_ANIM_INTERPOLATOR); 640 | set.start(); 641 | } else { 642 | setVisualProgress(id, scale1, scale2); 643 | } 644 | 645 | if (callBackToApp) { 646 | onProgressRefresh(fromUser, startValue, endValue); 647 | } 648 | } 649 | 650 | public void onProgressRefresh(boolean fromUser, int startValue, int endValue) { 651 | logger.debug("onProgressRefresh(%d, %d)", startValue, endValue); 652 | } 653 | 654 | private void setVisualProgress(int id, float progress1, float progress2) { 655 | logger.info("setVisualProgress(%g, %g)", progress1, progress2); 656 | mVisualStartProgress = progress1; 657 | mVisualEndProgress = progress2; 658 | invalidate(); 659 | onVisualProgressChanged(id, progress1, progress2); 660 | } 661 | 662 | public void onVisualProgressChanged(int id, float scale1, float scale2) { 663 | logger.debug("onVisualProgressChanged(%g, %g)", scale1, scale2); 664 | } 665 | 666 | private synchronized void refreshProgress( 667 | @SuppressWarnings ("SameParameterValue") int id, int startValue, int endValue, boolean fromUser, 668 | boolean animate) { 669 | if (mUiThreadId == Thread.currentThread().getId()) { 670 | doRefreshProgress(id, startValue, endValue, fromUser, true, animate); 671 | } else { 672 | if (mRefreshProgressRunnable == null) { 673 | mRefreshProgressRunnable = new RefreshProgressRunnable(); 674 | } 675 | 676 | final RefreshData rd = RefreshData.obtain(id, startValue, endValue, fromUser, animate); 677 | mRefreshData.add(rd); 678 | if (mAttached && !mRefreshIsPosted) { 679 | removeCallbacks(mRefreshProgressRunnable); 680 | post(mRefreshProgressRunnable); 681 | mRefreshIsPosted = true; 682 | } 683 | } 684 | } 685 | 686 | public synchronized void setProgress(int startValue, int endValue) { 687 | logger.info("setProgress(%d, %d)", startValue, endValue); 688 | setProgressInternal(startValue, endValue, false, false); 689 | } 690 | 691 | @SuppressWarnings ("unused") 692 | public void setProgress(int startValue, int endValue, boolean animate) { 693 | setProgressInternal(startValue, endValue, false, animate); 694 | } 695 | 696 | synchronized boolean setProgressInternal(int startValue, int endValue, boolean fromUser, boolean animate) { 697 | logger.info("setProgressInternal(%d, %d)", startValue, endValue); 698 | startValue = MathUtils.constrain(startValue, 0, MathUtils.constrain(endValue, 0, mMax)); 699 | endValue = MathUtils.constrain(endValue, startValue, mMax); 700 | 701 | if (startValue == mStartProgress && endValue == mEndProgress) { 702 | return false; 703 | } 704 | 705 | mEndProgress = endValue; 706 | mStartProgress = startValue; 707 | 708 | refreshProgress(android.R.id.progress, mStartProgress, mEndProgress, fromUser, animate); 709 | 710 | return true; 711 | } 712 | 713 | /** 714 | * Set the start max value and the end min value.
715 | * This will override the #setMinMaxStepSize 716 | */ 717 | public void setProgressStartEndBoundaries(int startMax, int endMin) { 718 | logger.info("setProgressStartEndBoundaries(%d, %d)", startMax, endMin); 719 | 720 | if (startMax > endMin) { 721 | throw new IllegalArgumentException("startMax cannot be greater than endMin"); 722 | } 723 | 724 | if (startMax > mMax) { 725 | throw new IllegalArgumentException("startMax cannot be greater max value"); 726 | } 727 | 728 | if (startMax != -1 || endMin != -1) { 729 | mMinMaxStepSize = 0; 730 | } 731 | 732 | mProgressStartMaxValue = startMax; 733 | mProgressEndMinValue = endMin; 734 | } 735 | 736 | public void setMinMaxStepSize(int value) { 737 | logger.info("setMinMaxStepSize(%d)", value); 738 | 739 | if (value > mMax) { 740 | throw new IllegalArgumentException("value cannot be greater than max value"); 741 | } 742 | 743 | if (value != 0) { 744 | mProgressEndMinValue = -1; 745 | mProgressStartMaxValue = -1; 746 | } 747 | 748 | mMinMaxStepSize = value; 749 | } 750 | 751 | public int getProgressStartMaxValue() { 752 | if (mProgressStartMaxValue != -1) { 753 | return mProgressStartMaxValue; 754 | } 755 | return getProgressEnd() - mMinMaxStepSize; 756 | } 757 | 758 | public int getProgressEndMinValue() { 759 | if (mProgressEndMinValue != -1) { 760 | return mProgressEndMinValue; 761 | } 762 | return getProgressStart() + mMinMaxStepSize; 763 | } 764 | 765 | public int getProgressEnd() { 766 | return mEndProgress; 767 | } 768 | 769 | public int getProgressStart() { 770 | return mStartProgress; 771 | } 772 | 773 | public synchronized int getMax() { 774 | return mMax; 775 | } 776 | 777 | public synchronized void setMax(int max) { 778 | logger.info("setMax(%d)", max); 779 | if (max < 0) { 780 | max = 0; 781 | } 782 | if (max != mMax) { 783 | mMax = max; 784 | postInvalidate(); 785 | 786 | if (mEndProgress > max) { 787 | mEndProgress = max; 788 | } 789 | refreshProgress(android.R.id.progress, mStartProgress, mEndProgress, false, false); 790 | } 791 | } 792 | 793 | @SuppressWarnings ("unused") 794 | public synchronized final void incrementEndValueBy(int diff) { 795 | setProgress(mStartProgress, mEndProgress + diff); 796 | } 797 | 798 | public void setInterpolator(Context context, @InterpolatorRes int resID) { 799 | setInterpolator(AnimationUtils.loadInterpolator(context, resID)); 800 | } 801 | 802 | public void setInterpolator(Interpolator interpolator) { 803 | mInterpolator = interpolator; 804 | } 805 | 806 | @SuppressWarnings ("unused") 807 | public Interpolator getInterpolator() { 808 | return mInterpolator; 809 | } 810 | 811 | @Override 812 | public void onVisibilityAggregated(boolean isVisible) { 813 | super.onVisibilityAggregated(isVisible); 814 | 815 | if (isVisible != mAggregatedIsVisible) { 816 | mAggregatedIsVisible = isVisible; 817 | 818 | if (mCurrentDrawable != null) { 819 | mCurrentDrawable.setVisible(isVisible, false); 820 | } 821 | } 822 | } 823 | 824 | @Override 825 | public void invalidateDrawable(@NonNull Drawable dr) { 826 | if (!mInDrawing) { 827 | if (verifyDrawable(dr)) { 828 | final Rect dirty = dr.getBounds(); 829 | final int scrollX = getScrollX() + getPaddingLeft(); 830 | final int scrollY = getScrollY() + getPaddingTop(); 831 | 832 | invalidate(dirty.left + scrollX, dirty.top + scrollY, 833 | dirty.right + scrollX, dirty.bottom + scrollY 834 | ); 835 | } else { 836 | super.invalidateDrawable(dr); 837 | } 838 | } 839 | } 840 | 841 | @Override 842 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 843 | updateDrawableBounds(w, h); 844 | } 845 | 846 | private void updateDrawableBounds(int w, int h) { 847 | mPaddingLeft = getPaddingLeft(); 848 | mPaddingRight = getPaddingRight(); 849 | mPaddingTop = getPaddingTop(); 850 | mPaddingBottom = getPaddingBottom(); 851 | 852 | w -= mPaddingRight + mPaddingLeft; 853 | h -= mPaddingTop + mPaddingBottom; 854 | 855 | int right = w; 856 | int bottom = h; 857 | int top = 0; 858 | int left = 0; 859 | 860 | mProgressDrawableIndicator = null; 861 | mProgressIndicatorBounds = null; 862 | mComputedWidth = w; 863 | 864 | if (mProgressDrawable != null) { 865 | mProgressDrawable.setBounds(left, top, right, bottom); 866 | mProgressDrawableIndicator = ((LayerDrawable) mProgressDrawable).findDrawableByLayerId(android.R.id.progress); 867 | mProgressIndicatorBounds = mProgressDrawableIndicator.getBounds(); 868 | } 869 | } 870 | 871 | @Override 872 | protected synchronized void onDraw(Canvas canvas) { 873 | super.onDraw(canvas); 874 | drawTrack(canvas); 875 | } 876 | 877 | protected boolean isLayoutRtl() { 878 | return (getLayoutDirection() == LAYOUT_DIRECTION_RTL); 879 | } 880 | 881 | void drawTrack(Canvas canvas) { 882 | final Drawable d = mProgressDrawable; 883 | if (d != null) { 884 | // Translate canvas so a indeterminate circular progress bar with padding 885 | // rotates properly in its animation 886 | final int saveCount = canvas.save(); 887 | 888 | if (isLayoutRtl() && mMirrorForRtl) { 889 | canvas.translate(getWidth() - mPaddingRight, mPaddingTop); 890 | canvas.scale(-1.0f, 1.0f); 891 | } else { 892 | canvas.translate(mPaddingLeft, mPaddingTop); 893 | } 894 | 895 | if (null != mProgressIndicatorBounds) { 896 | final int w = mComputedWidth - mProgressOffset; 897 | final int start = (int) (mVisualStartProgress * w); 898 | final int end = (int) (mVisualEndProgress * w); 899 | 900 | mProgressDrawableIndicator 901 | .setBounds( 902 | start, 903 | mProgressIndicatorBounds.top, 904 | mProgressOffset + end, 905 | mProgressIndicatorBounds.bottom 906 | ); 907 | } 908 | 909 | d.draw(canvas); 910 | canvas.restoreToCount(saveCount); 911 | } 912 | } 913 | 914 | public int getProgressOffset() { 915 | return mProgressOffset; 916 | } 917 | 918 | @SuppressWarnings ("unused") 919 | public void setProgressOffset(final int value) { 920 | logger.info("setProgressOffset(%d)", value); 921 | this.mProgressOffset = value; 922 | } 923 | 924 | @Override 925 | protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 926 | int dw = 0; 927 | int dh = 0; 928 | 929 | final Drawable d = mCurrentDrawable; 930 | if (d != null) { 931 | dw = Math.max(mMinWidth, Math.min(mMaxWidth, d.getIntrinsicWidth())); 932 | dh = Math.max(mMinHeight, Math.min(mMaxHeight, d.getIntrinsicHeight())); 933 | } 934 | 935 | updateDrawableState(); 936 | 937 | dw += getPaddingLeft() + getPaddingRight(); 938 | dh += getPaddingTop() + getPaddingBottom(); 939 | 940 | final int measuredWidth = resolveSizeAndState(dw, widthMeasureSpec, 0); 941 | final int measuredHeight = resolveSizeAndState(dh, heightMeasureSpec, 0); 942 | setMeasuredDimension(measuredWidth, measuredHeight); 943 | } 944 | 945 | @Override 946 | protected void drawableStateChanged() { 947 | super.drawableStateChanged(); 948 | updateDrawableState(); 949 | } 950 | 951 | private void updateDrawableState() { 952 | final int[] state = getDrawableState(); 953 | boolean changed = false; 954 | 955 | final Drawable progressDrawable = mProgressDrawable; 956 | if (progressDrawable != null && progressDrawable.isStateful()) { 957 | changed = progressDrawable.setState(state); 958 | } 959 | 960 | if (changed) { 961 | invalidate(); 962 | } 963 | } 964 | 965 | @Override 966 | public void drawableHotspotChanged(float x, float y) { 967 | super.drawableHotspotChanged(x, y); 968 | 969 | if (mProgressDrawable != null) { 970 | logger.verbose("setHotspot(%.2f, %.2f)", x, y); 971 | DrawableCompat.setHotspot(mProgressDrawable, x, y); 972 | } 973 | } 974 | 975 | static class SavedState extends BaseSavedState { 976 | int startValue; 977 | int endValue; 978 | 979 | SavedState(Parcelable superState) { 980 | super(superState); 981 | } 982 | 983 | private SavedState(Parcel in) { 984 | super(in); 985 | startValue = in.readInt(); 986 | endValue = in.readInt(); 987 | } 988 | 989 | @Override 990 | public void writeToParcel(Parcel out, int flags) { 991 | super.writeToParcel(out, flags); 992 | out.writeInt(startValue); 993 | out.writeInt(endValue); 994 | } 995 | 996 | public static final Parcelable.Creator CREATOR 997 | = new Parcelable.Creator() { 998 | public SavedState createFromParcel(Parcel in) { 999 | return new SavedState(in); 1000 | } 1001 | 1002 | public SavedState[] newArray(int size) { 1003 | return new SavedState[size]; 1004 | } 1005 | }; 1006 | } 1007 | 1008 | @Override 1009 | public Parcelable onSaveInstanceState() { 1010 | Parcelable superState = super.onSaveInstanceState(); 1011 | SavedState ss = new SavedState(superState); 1012 | 1013 | ss.startValue = mStartProgress; 1014 | ss.endValue = mEndProgress; 1015 | 1016 | return ss; 1017 | } 1018 | 1019 | @Override 1020 | public void onRestoreInstanceState(Parcelable state) { 1021 | SavedState ss = (SavedState) state; 1022 | super.onRestoreInstanceState(ss.getSuperState()); 1023 | setProgress(ss.startValue, ss.endValue); 1024 | } 1025 | 1026 | @Override 1027 | protected void onAttachedToWindow() { 1028 | super.onAttachedToWindow(); 1029 | 1030 | synchronized (this) { 1031 | final int count = mRefreshData.size(); 1032 | for (int i = 0; i < count; i++) { 1033 | final RefreshData rd = mRefreshData.get(i); 1034 | doRefreshProgress(rd.id, rd.startValue, rd.endValue, rd.fromUser, true, rd.animate); 1035 | rd.recycle(); 1036 | } 1037 | mRefreshData.clear(); 1038 | } 1039 | mAttached = true; 1040 | } 1041 | 1042 | @Override 1043 | protected void onDetachedFromWindow() { 1044 | if (mRefreshProgressRunnable != null) { 1045 | removeCallbacks(mRefreshProgressRunnable); 1046 | mRefreshIsPosted = false; 1047 | } 1048 | if (mAccessibilityEventSender != null) { 1049 | removeCallbacks(mAccessibilityEventSender); 1050 | } 1051 | super.onDetachedFromWindow(); 1052 | mAttached = false; 1053 | } 1054 | 1055 | @Override 1056 | public CharSequence getAccessibilityClassName() { 1057 | return RangeProgressBar.class.getName(); 1058 | } 1059 | 1060 | private class AccessibilityEventSender implements Runnable { 1061 | public void run() { 1062 | sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); 1063 | } 1064 | } 1065 | 1066 | private static class ProgressTintInfo { 1067 | ColorStateList mProgressTintList; 1068 | PorterDuff.Mode mProgressTintMode; 1069 | boolean mHasProgressTint; 1070 | boolean mHasProgressTintMode; 1071 | 1072 | ColorStateList mProgressBackgroundTintList; 1073 | PorterDuff.Mode mProgressBackgroundTintMode; 1074 | boolean mHasProgressBackgroundTint; 1075 | boolean mHasProgressBackgroundTintMode; 1076 | 1077 | } 1078 | } 1079 | -------------------------------------------------------------------------------- /rangeseekbar-library/src/main/java/it/sephiroth/android/library/rangeseekbar/RangeSeekBar.java: -------------------------------------------------------------------------------- 1 | 2 | package it.sephiroth.android.library.rangeseekbar; 3 | 4 | import android.annotation.SuppressLint; 5 | import android.content.Context; 6 | import android.content.res.ColorStateList; 7 | import android.content.res.TypedArray; 8 | import android.graphics.Canvas; 9 | import android.graphics.PorterDuff; 10 | import android.graphics.Rect; 11 | import android.graphics.Region.Op; 12 | import android.graphics.drawable.Drawable; 13 | import android.os.Build; 14 | import android.os.Bundle; 15 | import android.util.AttributeSet; 16 | import android.view.KeyEvent; 17 | import android.view.MotionEvent; 18 | import android.view.ViewConfiguration; 19 | import android.view.accessibility.AccessibilityNodeInfo; 20 | 21 | import androidx.annotation.NonNull; 22 | import androidx.annotation.Nullable; 23 | import androidx.core.graphics.drawable.DrawableCompat; 24 | import androidx.core.view.ViewCompat; 25 | 26 | @SuppressWarnings ("unused") 27 | public class RangeSeekBar extends RangeProgressBar { 28 | 29 | private int mInitialStartValue; 30 | private int mInitialEndValue; 31 | 32 | public interface OnRangeSeekBarChangeListener { 33 | 34 | void onProgressChanged(RangeSeekBar seekBar, int progressStart, int progressEnd, boolean fromUser); 35 | 36 | void onStartTrackingTouch(RangeSeekBar seekBar); 37 | 38 | void onStopTrackingTouch(RangeSeekBar seekBar); 39 | } 40 | 41 | private final Rect mTempRect1 = new Rect(); 42 | private final Rect mTempRect2 = new Rect(); 43 | 44 | private OnRangeSeekBarChangeListener mOnRangeSeekBarChangeListener; 45 | 46 | public enum WhichThumb { 47 | Start, End, None 48 | } 49 | 50 | private int mThumbWidth; 51 | private int mThumbHeight; 52 | private int mThumbOffset; 53 | private int mStepSize = 1; 54 | private int mThumbClipInset = 0; 55 | private Drawable mThumbStart; 56 | private Drawable mThumbEnd; 57 | private Drawable mTickMark; 58 | private ColorStateList mThumbTintList = null; 59 | private ColorStateList mTickMarkTintList = null; 60 | private PorterDuff.Mode mThumbTintMode = null; 61 | private PorterDuff.Mode mTickMarkTintMode = null; 62 | private boolean mHasThumbTint = false; 63 | private boolean mHasThumbTintMode = false; 64 | private boolean mHasTickMarkTint = false; 65 | private boolean mHasTickMarkTintMode = false; 66 | private boolean mSplitTrack; 67 | 68 | /** 69 | * On touch, this offset plus the scaled value from the position of the 70 | * touch will form the progress value. Usually 0. 71 | */ 72 | float mTouchProgressOffset; 73 | 74 | /** 75 | * Whether this is user seekable. 76 | */ 77 | boolean mIsUserSeekable = true; 78 | 79 | private int mKeyProgressIncrement = 1; 80 | private static final int NO_ALPHA = 0xFF; 81 | private float mDisabledAlpha; 82 | 83 | private int mScaledTouchSlop; 84 | private float mTouchDownX; 85 | private boolean mIsDragging; 86 | private WhichThumb mWhichThumb = WhichThumb.None; 87 | 88 | public RangeSeekBar(Context context) { 89 | this(context, null); 90 | } 91 | 92 | public RangeSeekBar(Context context, AttributeSet attrs) { 93 | this(context, attrs, R.attr.sephiroth_rangeSeekBarStyle); 94 | } 95 | 96 | public RangeSeekBar(Context context, AttributeSet attrs, int defStyleAttr) { 97 | this(context, attrs, defStyleAttr, 0); 98 | } 99 | 100 | public RangeSeekBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 101 | super(context, attrs, defStyleAttr); 102 | 103 | mInitialProgressDone = false; 104 | 105 | final TypedArray a = context.obtainStyledAttributes( 106 | attrs, R.styleable.RangeSeekBar, defStyleAttr, defStyleRes); 107 | 108 | Drawable thumb; 109 | Drawable thumb2; 110 | 111 | if (a.hasValue(R.styleable.RangeSeekBar_range_seekbar_leftThumb)) { 112 | thumb = a.getDrawable(R.styleable.RangeSeekBar_range_seekbar_leftThumb); 113 | } else { 114 | thumb = a.getDrawable(R.styleable.RangeSeekBar_android_thumb); 115 | } 116 | 117 | if (a.hasValue(R.styleable.RangeSeekBar_range_seekbar_rightThumb)) { 118 | thumb2 = a.getDrawable(R.styleable.RangeSeekBar_range_seekbar_rightThumb); 119 | } else { 120 | thumb2 = a.getDrawable(R.styleable.RangeSeekBar_android_thumb); 121 | } 122 | 123 | setThumb(thumb, WhichThumb.Start); 124 | setThumb(thumb2, WhichThumb.End); 125 | 126 | if (a.hasValue(R.styleable.RangeSeekBar_android_thumbTintMode)) { 127 | mThumbTintMode = DrawableUtils.parseTintMode(a.getInt( 128 | R.styleable.RangeSeekBar_android_thumbTintMode, -1), mThumbTintMode); 129 | mHasThumbTintMode = true; 130 | } 131 | 132 | if (a.hasValue(R.styleable.RangeSeekBar_android_thumbTint)) { 133 | mThumbTintList = a.getColorStateList(R.styleable.RangeSeekBar_android_thumbTint); 134 | mHasThumbTint = true; 135 | } 136 | 137 | if (Build.VERSION.SDK_INT >= 24) { 138 | final Drawable tickMark = a.getDrawable(R.styleable.RangeSeekBar_android_tickMark); 139 | logger.verbose("tickMark = %s", tickMark); 140 | 141 | setTickMark(tickMark); 142 | 143 | if (a.hasValue(R.styleable.RangeSeekBar_android_tickMarkTintMode)) { 144 | mTickMarkTintMode = DrawableUtils.parseTintMode(a.getInt( 145 | R.styleable.RangeSeekBar_android_tickMarkTintMode, -1), mTickMarkTintMode); 146 | mHasTickMarkTintMode = true; 147 | } 148 | 149 | if (a.hasValue(R.styleable.RangeSeekBar_android_tickMarkTint)) { 150 | mTickMarkTintList = a.getColorStateList(R.styleable.RangeSeekBar_android_tickMarkTint); 151 | mHasTickMarkTint = true; 152 | } 153 | } 154 | 155 | mSplitTrack = a.getBoolean(R.styleable.RangeSeekBar_android_splitTrack, false); 156 | 157 | if (a.hasValue(R.styleable.RangeSeekBar_range_seekbar_stepSize)) { 158 | mStepSize = a.getInt(R.styleable.RangeSeekBar_range_seekbar_stepSize, 1); 159 | } 160 | 161 | setMinMaxStepSize(getMinMapStepSize()); 162 | 163 | mThumbClipInset = a.getDimensionPixelSize(R.styleable.RangeSeekBar_range_seekbar_thumbInset, mThumbClipInset); 164 | 165 | final int thumbOffset = a.getDimensionPixelOffset( 166 | R.styleable.RangeSeekBar_android_thumbOffset, getThumbOffset()); 167 | setThumbOffset(thumbOffset); 168 | 169 | final boolean useDisabledAlpha = a.getBoolean(R.styleable.RangeSeekBar_range_seekbar_useDisabledAlpha, true); 170 | a.recycle(); 171 | 172 | if (useDisabledAlpha) { 173 | // final TypedArray ta = context.obtainStyledAttributes(attrs, android.R.styleable.Theme, 0, 0); 174 | // mDisabledAlpha = ta.getFloat(R.styleable.Theme_disabledAlpha, 0.5f); 175 | // ta.recycle(); 176 | 177 | // TODO: find out 178 | mDisabledAlpha = 0.5f; 179 | } else { 180 | mDisabledAlpha = 1.0f; 181 | } 182 | 183 | applyTickMarkTint(); 184 | 185 | mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 186 | 187 | setInitialProgress(mInitialStartValue, mInitialEndValue); 188 | 189 | setProgress(mInitialStartValue, mInitialEndValue); 190 | 191 | mInitialProgressDone = true; 192 | } 193 | 194 | private void adjustInitialProgressValues() { 195 | logger.info("adjustInitialProgressValues"); 196 | 197 | if (mProgressStartMaxValue != -1 || mProgressEndMinValue != -1) { 198 | if (mProgressStartMaxValue != -1) { 199 | mInitialStartValue = Math.min(mInitialStartValue, mProgressStartMaxValue); 200 | } 201 | 202 | if (mProgressEndMinValue != -1) { 203 | mInitialEndValue = Math.max(mInitialEndValue, mProgressEndMinValue); 204 | } 205 | } else if (mMinMaxStepSize != 0) { 206 | if (mInitialEndValue - mInitialStartValue < mMinMaxStepSize) { 207 | mInitialStartValue = Math.max(0, mInitialEndValue - mMinMaxStepSize); 208 | mInitialEndValue = Math.min(getMax(), mInitialStartValue + mMinMaxStepSize); 209 | } 210 | } 211 | } 212 | 213 | /** 214 | * Set the minimum step size when changing value 215 | * @param value step size 216 | */ 217 | public void setStepSize(final int value) { 218 | logger.info("setStepSize(%d)", value); 219 | 220 | mStepSize = value; 221 | setMinMaxStepSize(getMinMapStepSize()); 222 | setProgress(getProgressStart(), getProgressEnd()); 223 | } 224 | 225 | @Override 226 | public void setMinMaxStepSize(final int value) { 227 | super.setMinMaxStepSize(value); 228 | 229 | // be sure is a multiple of stepsize 230 | if (mMinMaxStepSize != 0) { 231 | if (mMinMaxStepSize % mStepSize != 0) { 232 | mMinMaxStepSize = Math.max(mStepSize, mMinMaxStepSize - (mMinMaxStepSize % mStepSize)); 233 | } 234 | } 235 | logger.info("setMinMaxStepSize(%d --> %d)", value, mMinMaxStepSize); 236 | 237 | if (mInitialProgressDone) { 238 | setProgress(getProgressStart(), getProgressEnd()); 239 | } 240 | } 241 | 242 | @Override 243 | public void setProgressStartEndBoundaries(final int startMax, final int endMin) { 244 | super.setProgressStartEndBoundaries(startMax, endMin); 245 | 246 | if (mInitialProgressDone) { 247 | setProgress(getProgressStart(), getProgressEnd()); 248 | } 249 | } 250 | 251 | @Override 252 | protected void setInitialProgress(int startProgress, int endProgress) { 253 | logger.info("setInitialProgress: %d - %d", startProgress, endProgress); 254 | mInitialStartValue = startProgress; 255 | mInitialEndValue = endProgress; 256 | adjustInitialProgressValues(); 257 | } 258 | 259 | @Override 260 | public void onProgressRefresh(final boolean fromUser, final int startValue, final int endValue) { 261 | super.onProgressRefresh(fromUser, startValue, endValue); 262 | 263 | if (mOnRangeSeekBarChangeListener != null) { 264 | mOnRangeSeekBarChangeListener.onProgressChanged(this, startValue, endValue, fromUser); 265 | } 266 | } 267 | 268 | public void setOnRangeSeekBarChangeListener(OnRangeSeekBarChangeListener l) { 269 | mOnRangeSeekBarChangeListener = l; 270 | } 271 | 272 | /** 273 | * Sets the thumb that will be drawn at the end of the progress meter within the SeekBar. 274 | *

275 | * If the thumb is a valid drawable (i.e. not null), half its width will be 276 | * used as the new thumb offset (@see #setThumbOffset(int)). 277 | * 278 | * @param thumb Drawable representing the thumb 279 | * @param which which thumb 280 | */ 281 | public void setThumb(Drawable thumb, WhichThumb which) { 282 | final boolean needUpdate; 283 | 284 | Drawable whichThumb = which == WhichThumb.Start ? mThumbStart : mThumbEnd; 285 | 286 | if (whichThumb != null && thumb != whichThumb) { 287 | whichThumb.setCallback(null); 288 | needUpdate = true; 289 | } else { 290 | needUpdate = false; 291 | } 292 | 293 | if (thumb != null) { 294 | thumb.setCallback(this); 295 | DrawableCompat.setLayoutDirection(thumb, ViewCompat.getLayoutDirection(this)); 296 | mThumbOffset = thumb.getIntrinsicWidth() / 2; 297 | mThumbWidth = thumb.getIntrinsicWidth(); 298 | mThumbHeight = thumb.getIntrinsicHeight(); 299 | 300 | if (needUpdate && 301 | (thumb.getIntrinsicWidth() != whichThumb.getIntrinsicWidth() 302 | || thumb.getIntrinsicHeight() != whichThumb.getIntrinsicHeight())) { 303 | requestLayout(); 304 | } 305 | } 306 | 307 | if (which == WhichThumb.Start) { 308 | mThumbStart = thumb; 309 | } else { 310 | mThumbEnd = thumb; 311 | } 312 | 313 | applyThumbTintInternal(which); 314 | invalidate(); 315 | 316 | if (needUpdate) { 317 | updateThumbAndTrackPos(getWidth(), getHeight()); 318 | if (thumb != null && thumb.isStateful()) { 319 | // Note that if the states are different this won't work. 320 | // For now, let's consider that an app bug. 321 | int[] state = getDrawableState(); 322 | thumb.setState(state); 323 | } 324 | } 325 | } 326 | 327 | public Drawable getThumbStart() { 328 | return mThumbStart; 329 | } 330 | 331 | public Drawable getThumbEnd() { 332 | return mThumbEnd; 333 | } 334 | 335 | public void setThumbTintList(@Nullable ColorStateList tint) { 336 | mThumbTintList = tint; 337 | mHasThumbTint = true; 338 | 339 | applyThumbTint(); 340 | } 341 | 342 | @Nullable 343 | public ColorStateList getThumbTintList() { 344 | return mThumbTintList; 345 | } 346 | 347 | public void setThumbTintMode(@Nullable PorterDuff.Mode tintMode) { 348 | mThumbTintMode = tintMode; 349 | mHasThumbTintMode = true; 350 | applyThumbTint(); 351 | } 352 | 353 | @Nullable 354 | public PorterDuff.Mode getThumbTintMode() { 355 | return mThumbTintMode; 356 | } 357 | 358 | private void applyThumbTint() { 359 | applyThumbTintInternal(WhichThumb.Start); 360 | applyThumbTintInternal(WhichThumb.End); 361 | } 362 | 363 | private void applyThumbTintInternal(final WhichThumb which) { 364 | Drawable thumb = which == WhichThumb.Start ? mThumbStart : mThumbEnd; 365 | 366 | if (thumb != null && (mHasThumbTint || mHasThumbTintMode)) { 367 | 368 | if (which == WhichThumb.Start) { 369 | mThumbStart = thumb.mutate(); 370 | thumb = mThumbStart; 371 | } else { 372 | mThumbEnd = thumb.mutate(); 373 | thumb = mThumbEnd; 374 | } 375 | 376 | if (mHasThumbTint) { 377 | DrawableCompat.setTintList(thumb, mThumbTintList); 378 | } 379 | 380 | if (mHasThumbTintMode) { 381 | DrawableCompat.setTintMode(thumb, mThumbTintMode); 382 | } 383 | 384 | if (thumb.isStateful()) { 385 | thumb.setState(getDrawableState()); 386 | } 387 | } 388 | } 389 | 390 | /** 391 | * @see #setThumbOffset(int) 392 | */ 393 | public int getThumbOffset() { 394 | return mThumbOffset; 395 | } 396 | 397 | /** 398 | * Sets the thumb offset that allows the thumb to extend out of the range of 399 | * the track. 400 | * 401 | * @param thumbOffset The offset amount in pixels. 402 | */ 403 | public void setThumbOffset(int thumbOffset) { 404 | mThumbOffset = thumbOffset; 405 | invalidate(); 406 | } 407 | 408 | /** 409 | * Specifies whether the track should be split by the thumb. When true, 410 | * the thumb's optical bounds will be clipped out of the track drawable, 411 | * then the thumb will be drawn into the resulting gap. 412 | * 413 | * @param splitTrack Whether the track should be split by the thumb 414 | */ 415 | public void setSplitTrack(boolean splitTrack) { 416 | mSplitTrack = splitTrack; 417 | invalidate(); 418 | } 419 | 420 | /** 421 | * Returns whether the track should be split by the thumb. 422 | */ 423 | public boolean getSplitTrack() { 424 | return mSplitTrack; 425 | } 426 | 427 | /** 428 | * Assign a new tick mark drawable 429 | * @param tickMark 430 | */ 431 | public void setTickMark(Drawable tickMark) { 432 | if (mTickMark != null) { 433 | mTickMark.setCallback(null); 434 | } 435 | 436 | mTickMark = tickMark; 437 | 438 | if (tickMark != null) { 439 | 440 | setProgressOffset(0); 441 | 442 | tickMark.setCallback(this); 443 | if (tickMark.isStateful()) { 444 | tickMark.setState(getDrawableState()); 445 | } 446 | 447 | final int w = tickMark.getIntrinsicWidth(); 448 | final int h = tickMark.getIntrinsicHeight(); 449 | final int halfW = w >= 0 ? w / 2 : 1; 450 | final int halfH = h >= 0 ? h / 2 : 1; 451 | tickMark.setBounds(-halfW, -halfH, halfW, halfH); 452 | applyTickMarkTint(); 453 | } 454 | invalidate(); 455 | } 456 | 457 | /** 458 | * @return the drawable displayed at each progress position 459 | */ 460 | public Drawable getTickMark() { 461 | return mTickMark; 462 | } 463 | 464 | /** 465 | * Applies a tint to the tick mark drawable. Does not modify the current tint 466 | * mode, which is {@link PorterDuff.Mode#SRC_IN} by default. 467 | *

468 | * Subsequent calls to {@link #setTickMark(Drawable)} will automatically 469 | * mutate the drawable and apply the specified tint and tint mode using 470 | * {@link Drawable#setTintList(ColorStateList)}. 471 | * 472 | * @param tint the tint to apply, may be {@code null} to clear tint 473 | * @attr ref android.R.styleable#SeekBar_tickMarkTint 474 | * @see #getTickMarkTintList() 475 | * @see Drawable#setTintList(ColorStateList) 476 | */ 477 | public void setTickMarkTintList(@Nullable ColorStateList tint) { 478 | mTickMarkTintList = tint; 479 | mHasTickMarkTint = true; 480 | 481 | applyTickMarkTint(); 482 | } 483 | 484 | /** 485 | * Returns the tint applied to the tick mark drawable, if specified. 486 | * 487 | * @return the tint applied to the tick mark drawable 488 | * @attr ref android.R.styleable#SeekBar_tickMarkTint 489 | * @see #setTickMarkTintList(ColorStateList) 490 | */ 491 | @Nullable 492 | public ColorStateList getTickMarkTintList() { 493 | return mTickMarkTintList; 494 | } 495 | 496 | /** 497 | * Specifies the blending mode used to apply the tint specified by 498 | * {@link #setTickMarkTintList(ColorStateList)}} to the tick mark drawable. The 499 | * default mode is {@link PorterDuff.Mode#SRC_IN}. 500 | * 501 | * @param tintMode the blending mode used to apply the tint, may be 502 | * {@code null} to clear tint 503 | * @attr ref android.R.styleable#SeekBar_tickMarkTintMode 504 | * @see #getTickMarkTintMode() 505 | * @see Drawable#setTintMode(PorterDuff.Mode) 506 | */ 507 | public void setTickMarkTintMode(@Nullable PorterDuff.Mode tintMode) { 508 | mTickMarkTintMode = tintMode; 509 | mHasTickMarkTintMode = true; 510 | 511 | applyTickMarkTint(); 512 | } 513 | 514 | /** 515 | * Returns the blending mode used to apply the tint to the tick mark drawable, 516 | * if specified. 517 | * 518 | * @return the blending mode used to apply the tint to the tick mark drawable 519 | * @attr ref android.R.styleable#SeekBar_tickMarkTintMode 520 | * @see #setTickMarkTintMode(PorterDuff.Mode) 521 | */ 522 | @Nullable 523 | public PorterDuff.Mode getTickMarkTintMode() { 524 | return mTickMarkTintMode; 525 | } 526 | 527 | protected void applyTickMarkTint() { 528 | if (mTickMark != null && (mHasTickMarkTint || mHasTickMarkTintMode)) { 529 | mTickMark = DrawableCompat.wrap(mTickMark.mutate()); 530 | 531 | if (mHasTickMarkTint) { 532 | DrawableCompat.setTintList(mTickMark, mTickMarkTintList); 533 | } 534 | 535 | if (mHasTickMarkTintMode) { 536 | DrawableCompat.setTintMode(mTickMark, mTickMarkTintMode); 537 | } 538 | 539 | // The drawable (or one of its children) may not have been 540 | // stateful before applying the tint, so let's try again. 541 | if (mTickMark.isStateful()) { 542 | mTickMark.setState(getDrawableState()); 543 | } 544 | } 545 | } 546 | 547 | public void setKeyProgressIncrement(int increment) { 548 | mKeyProgressIncrement = increment < 0 ? -increment : increment; 549 | } 550 | 551 | public float getKeyProgressIncrement() { 552 | return mKeyProgressIncrement; 553 | } 554 | 555 | public synchronized void setMax(int max) { 556 | super.setMax(max); 557 | 558 | if ((mKeyProgressIncrement == 0) || (getMax() / mKeyProgressIncrement > 20)) { 559 | setKeyProgressIncrement(getMax() / 20); 560 | } 561 | } 562 | 563 | @Override 564 | protected boolean verifyDrawable(@NonNull Drawable who) { 565 | return who == mThumbStart || who == mThumbEnd || who == mTickMark || super.verifyDrawable(who); 566 | } 567 | 568 | @Override 569 | public void jumpDrawablesToCurrentState() { 570 | super.jumpDrawablesToCurrentState(); 571 | 572 | if (mThumbStart != null) { 573 | mThumbStart.jumpToCurrentState(); 574 | } 575 | 576 | if (mThumbEnd != null) { 577 | mThumbEnd.jumpToCurrentState(); 578 | } 579 | 580 | if (mTickMark != null) { 581 | mTickMark.jumpToCurrentState(); 582 | } 583 | } 584 | 585 | @Override 586 | protected void drawableStateChanged() { 587 | super.drawableStateChanged(); 588 | 589 | logger.info("drawableStateChanged(%s)", mWhichThumb); 590 | 591 | final Drawable progressDrawable = getProgressDrawable(); 592 | if (progressDrawable != null && mDisabledAlpha < 1.0f) { 593 | progressDrawable.setAlpha(isEnabled() ? NO_ALPHA : (int) (NO_ALPHA * mDisabledAlpha)); 594 | } 595 | 596 | if (mWhichThumb != WhichThumb.None) { 597 | Drawable thumb = mWhichThumb == WhichThumb.Start ? mThumbStart : mThumbEnd; 598 | setDrawableState(thumb, getDrawableState()); 599 | } else { 600 | setDrawableState(mThumbStart, getDrawableState()); 601 | setDrawableState(mThumbEnd, getDrawableState()); 602 | } 603 | 604 | final Drawable tickMark = mTickMark; 605 | if (tickMark != null && tickMark.isStateful() 606 | && tickMark.setState(getDrawableState())) { 607 | invalidateDrawable(tickMark); 608 | } 609 | } 610 | 611 | protected void setDrawableState(final Drawable drawable, final int[] drawableState) { 612 | if (null != drawable && drawable.isStateful() && drawable.setState(drawableState)) { 613 | invalidateDrawable(drawable); 614 | } 615 | } 616 | 617 | @Override 618 | public void drawableHotspotChanged(float x, float y) { 619 | super.drawableHotspotChanged(x, y); 620 | 621 | if (mThumbStart != null) { 622 | logger.verbose("setHotspot(mThumbStart, %.2f, %.2f)", x, y); 623 | DrawableCompat.setHotspot(mThumbStart, x, y); 624 | } 625 | 626 | if (mThumbEnd != null) { 627 | logger.verbose("setHotspot(mThumbEnd, %.2f, %.2f)", x, y); 628 | DrawableCompat.setHotspot(mThumbEnd, x, y); 629 | } 630 | } 631 | 632 | @Override 633 | public void onVisualProgressChanged(int id, float scaleStart, float scaleEnd) { 634 | super.onVisualProgressChanged(id, scaleStart, scaleEnd); 635 | 636 | if (id == android.R.id.progress) { 637 | if (mThumbStart != null && mThumbEnd != null) { 638 | setThumbPos(getWidth(), mThumbStart, scaleStart, WhichThumb.Start, Integer.MIN_VALUE); 639 | setThumbPos(getWidth(), mThumbEnd, scaleEnd, WhichThumb.End, Integer.MIN_VALUE); 640 | invalidate(); 641 | } 642 | } 643 | } 644 | 645 | @Override 646 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 647 | super.onSizeChanged(w, h, oldw, oldh); 648 | 649 | updateThumbAndTrackPos(w, h); 650 | } 651 | 652 | private void updateThumbAndTrackPos(int w, int h) { 653 | final int paddedHeight = h - mPaddingTop - mPaddingBottom; 654 | final Drawable track = getCurrentDrawable(); 655 | final Drawable thumb = mThumbStart; 656 | 657 | // The max height does not incorporate padding, whereas the height 658 | // parameter does. 659 | final int trackHeight = Math.min(mMaxHeight, paddedHeight); 660 | final int thumbHeight = thumb == null ? 0 : thumb.getIntrinsicHeight(); 661 | 662 | // Apply offset to whichever item is taller. 663 | final int trackOffset; 664 | final int thumbOffset; 665 | if (thumbHeight > trackHeight) { 666 | final int offsetHeight = (paddedHeight - thumbHeight) / 2; 667 | trackOffset = offsetHeight + (thumbHeight - trackHeight) / 2; 668 | thumbOffset = offsetHeight; 669 | } else { 670 | final int offsetHeight = (paddedHeight - trackHeight) / 2; 671 | trackOffset = offsetHeight; 672 | thumbOffset = offsetHeight + (trackHeight - thumbHeight) / 2; 673 | } 674 | 675 | if (track != null) { 676 | final int trackWidth = w - mPaddingRight - mPaddingLeft; 677 | track.setBounds(0, trackOffset, trackWidth, trackOffset + trackHeight); 678 | } 679 | 680 | if (mThumbStart != null && mThumbEnd != null) { 681 | setThumbPos(w, mThumbStart, getScaleStart(), WhichThumb.Start, thumbOffset); 682 | setThumbPos(w, mThumbEnd, getScaleEnd(), WhichThumb.End, thumbOffset); 683 | } 684 | 685 | final Drawable background = getBackground(); 686 | 687 | if (background != null && thumb != null) { 688 | final Rect bounds = thumb.getBounds(); 689 | background.setBounds(bounds); 690 | logger.verbose("setHotspot(background, %d, %d)", bounds.centerX(), bounds.centerY()); 691 | DrawableCompat.setHotspotBounds( 692 | background, bounds.left, bounds.top, bounds.right, bounds.bottom); 693 | 694 | } 695 | } 696 | 697 | private float getScaleStart() { 698 | final float max = getMax(); 699 | return max > 0 ? getProgressStart() / max : 0; 700 | } 701 | 702 | private float getScaleEnd() { 703 | final float max = getMax(); 704 | return max > 0 ? getProgressEnd() / max : 0; 705 | } 706 | 707 | /** 708 | * Updates the thumb drawable bounds. 709 | * 710 | * @param w Width of the view, including padding 711 | * @param thumb Drawable used for the thumb 712 | * @param scale Current progress between 0 and 1 713 | * @param offset Vertical offset for centering. If set to Integer.MIN_VALUE, the current offset will be used. 714 | */ 715 | private void setThumbPos(int w, Drawable thumb, float scale, WhichThumb which, int offset) { 716 | logger.info("setThumbPos(%d, %g, %s, %d)", w, scale, which, offset); 717 | 718 | int available = (w - mPaddingLeft - mPaddingRight) - getProgressOffset(); 719 | available -= mThumbWidth; 720 | 721 | // The extra space for the thumb to move on the track 722 | available += mThumbOffset * 2; 723 | 724 | final int thumbPos = (int) (scale * available + 0.5f); 725 | 726 | final int top, bottom; 727 | if (offset == Integer.MIN_VALUE) { 728 | final Rect oldBounds = thumb.getBounds(); 729 | top = oldBounds.top; 730 | bottom = oldBounds.bottom; 731 | } else { 732 | top = offset; 733 | bottom = offset + mThumbHeight; 734 | } 735 | 736 | int left = thumbPos; 737 | int right = left + mThumbWidth; 738 | 739 | if (which == WhichThumb.End) { 740 | left += getProgressOffset(); 741 | right += getProgressOffset(); 742 | } 743 | 744 | final Drawable background = getBackground(); 745 | 746 | if (background != null && which == mWhichThumb) { 747 | final int offsetX = mPaddingLeft - mThumbOffset; 748 | final int offsetY = mPaddingTop; 749 | 750 | background.setBounds( 751 | left + offsetX, 752 | top + offsetY, 753 | right + offsetX, 754 | bottom + offsetY 755 | ); 756 | 757 | DrawableCompat.setHotspotBounds( 758 | background, 759 | left + offsetX, 760 | top + offsetY, 761 | right + offsetX, 762 | bottom + offsetY 763 | ); 764 | } 765 | 766 | thumb.setBounds(left, top, right, bottom); 767 | } 768 | 769 | @Override 770 | protected synchronized void onDraw(Canvas canvas) { 771 | super.onDraw(canvas); 772 | drawThumb(canvas); 773 | } 774 | 775 | @Override 776 | void drawTrack(Canvas canvas) { 777 | if (mThumbStart != null && mSplitTrack) { 778 | final Rect tempRect = mTempRect1; 779 | 780 | mThumbStart.copyBounds(tempRect); 781 | tempRect.offset(mPaddingLeft - mThumbOffset, mPaddingTop); 782 | tempRect.left += mThumbClipInset; 783 | tempRect.right -= mThumbClipInset; 784 | 785 | final int saveCount = canvas.save(); 786 | canvas.clipRect(tempRect, Op.DIFFERENCE); 787 | 788 | mThumbEnd.copyBounds(tempRect); 789 | tempRect.offset(mPaddingLeft - mThumbOffset, mPaddingTop); 790 | tempRect.left += mThumbClipInset; 791 | tempRect.right -= mThumbClipInset; 792 | 793 | canvas.clipRect(tempRect, Op.DIFFERENCE); 794 | 795 | super.drawTrack(canvas); 796 | drawTickMarks(canvas); 797 | canvas.restoreToCount(saveCount); 798 | 799 | } else { 800 | super.drawTrack(canvas); 801 | drawTickMarks(canvas); 802 | } 803 | } 804 | 805 | void drawTickMarks(Canvas canvas) { 806 | if (mTickMark != null) { 807 | final float count = getMax(); 808 | if (count > 1) { 809 | final float spacing = (getWidth() - (mPaddingLeft + mPaddingRight)) / (count / mStepSize); 810 | final int saveCount = canvas.save(); 811 | canvas.translate(mPaddingLeft, getHeight() / 2f); 812 | for (int i = 0; i <= count; i++) { 813 | mTickMark.draw(canvas); 814 | canvas.translate(spacing, 0); 815 | } 816 | canvas.restoreToCount(saveCount); 817 | } 818 | } 819 | } 820 | 821 | void drawThumb(Canvas canvas) { 822 | if (mThumbStart != null && mThumbEnd != null) { 823 | final int saveCount = canvas.save(); 824 | canvas.translate(mPaddingLeft - mThumbOffset, mPaddingTop); 825 | mThumbStart.draw(canvas); 826 | mThumbEnd.draw(canvas); 827 | canvas.restoreToCount(saveCount); 828 | } 829 | } 830 | 831 | @Override 832 | protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 833 | Drawable d = getCurrentDrawable(); 834 | 835 | int thumbHeight = mThumbStart == null ? 0 : mThumbStart.getIntrinsicHeight(); 836 | int dw = 0; 837 | int dh = 0; 838 | if (d != null) { 839 | dw = Math.max(mMinWidth, Math.min(mMaxWidth, d.getIntrinsicWidth())); 840 | dh = Math.max(mMinHeight, Math.min(mMaxHeight, d.getIntrinsicHeight())); 841 | dh = Math.max(thumbHeight, dh); 842 | } 843 | dw += mPaddingLeft + mPaddingRight; 844 | dh += mPaddingTop + mPaddingBottom; 845 | 846 | setMeasuredDimension( 847 | resolveSizeAndState(dw, widthMeasureSpec, 0), 848 | resolveSizeAndState(dh, heightMeasureSpec, 0) 849 | ); 850 | } 851 | 852 | @SuppressLint ("ClickableViewAccessibility") 853 | @Override 854 | public boolean onTouchEvent(MotionEvent event) { 855 | if (!mIsUserSeekable || !isEnabled()) { 856 | return false; 857 | } 858 | 859 | switch (event.getAction()) { 860 | case MotionEvent.ACTION_DOWN: 861 | logger.info("ACTION_DOWN"); 862 | if (SephirothViewCompat.isInScrollingContainer(this)) { 863 | logger.warn("isInScrollContainer"); 864 | mTouchDownX = event.getX(); 865 | } else { 866 | startDrag(event); 867 | } 868 | break; 869 | 870 | case MotionEvent.ACTION_MOVE: 871 | if (mIsDragging) { 872 | trackTouchEvent(event); 873 | } else { 874 | final float x = event.getX(); 875 | if (Math.abs(x - mTouchDownX) > mScaledTouchSlop) { 876 | startDrag(event); 877 | } 878 | } 879 | break; 880 | 881 | case MotionEvent.ACTION_UP: 882 | if (mIsDragging) { 883 | trackTouchEvent(event); 884 | onStopTrackingTouch(); 885 | setPressed(false); 886 | } else { 887 | // Touch up when we never crossed the touch slop threshold should 888 | // be interpreted as a tap-seek to that location. 889 | onStartTrackingTouch(); 890 | trackTouchEvent(event); 891 | onStopTrackingTouch(); 892 | performClick(); 893 | } 894 | // ProgressBar doesn't know to repaint the thumb drawable 895 | // in its inactive state when the touch stops (because the 896 | // value has not apparently changed) 897 | invalidate(); 898 | break; 899 | 900 | case MotionEvent.ACTION_CANCEL: 901 | if (mIsDragging) { 902 | onStopTrackingTouch(); 903 | setPressed(false); 904 | } 905 | invalidate(); // see above explanation 906 | break; 907 | } 908 | return true; 909 | } 910 | 911 | private WhichThumb getNearestThumb(float x, float y) { 912 | 913 | mThumbStart.copyBounds(mTempRect1); 914 | mTempRect1.inset(mTempRect1.width() / 4, mTempRect1.height() / 4); 915 | 916 | mThumbEnd.copyBounds(mTempRect2); 917 | mTempRect2.inset(mTempRect2.width() / 4, mTempRect2.height() / 4); 918 | 919 | float diff1 = Math.abs((x) - mTempRect1.centerX()); 920 | float diff2 = Math.abs((x) - mTempRect2.centerX()); 921 | 922 | if (mTempRect1.contains((int) x, (int) y)) { 923 | return WhichThumb.Start; 924 | } 925 | if (mTempRect2.contains((int) x, (int) y)) { 926 | return WhichThumb.End; 927 | } 928 | 929 | return diff1 < diff2 ? WhichThumb.Start : WhichThumb.End; 930 | } 931 | 932 | private void startDrag(MotionEvent event) { 933 | logger.info("startDrag"); 934 | 935 | if (null == mThumbStart || null == mThumbEnd) { 936 | logger.error("missing one of the thumbs!"); 937 | return; 938 | } 939 | 940 | mWhichThumb = getNearestThumb(event.getX() - (mPaddingLeft - mThumbOffset), event.getY()); 941 | logger.verbose("mWhichThumb: %s", mWhichThumb); 942 | 943 | setPressed(true); 944 | 945 | if (mWhichThumb != WhichThumb.None) { 946 | final Drawable thumb = mWhichThumb == WhichThumb.Start ? mThumbStart : mThumbEnd; 947 | 948 | if (thumb != null) { 949 | final float scale = mWhichThumb == WhichThumb.Start ? getScaleStart() : getScaleEnd(); 950 | setThumbPos(getWidth(), thumb, scale, mWhichThumb, Integer.MIN_VALUE); 951 | invalidate(thumb.getBounds()); 952 | } 953 | } 954 | 955 | onStartTrackingTouch(); 956 | trackTouchEvent(event); 957 | attemptClaimDrag(); 958 | } 959 | 960 | @Override 961 | public void setPressed(final boolean pressed) { 962 | logger.debug("setPressed(%b, %s)", pressed, mWhichThumb); 963 | 964 | if (!pressed) { 965 | mWhichThumb = WhichThumb.None; 966 | } 967 | 968 | super.setPressed(pressed); 969 | } 970 | 971 | private void setHotspot(float x, float y) { } 972 | 973 | private void trackTouchEvent(MotionEvent event) { 974 | if (null == mThumbStart || null == mThumbEnd) { 975 | return; 976 | } 977 | 978 | float x = event.getX(); 979 | float y = event.getY(); 980 | final int width = getWidth(); 981 | 982 | if (mWhichThumb == WhichThumb.End) { 983 | x -= getProgressOffset(); 984 | } 985 | 986 | final int thumbWidth = mThumbStart.getIntrinsicWidth(); 987 | final int availableWidth = width - mPaddingLeft - mPaddingRight - getProgressOffset() - thumbWidth + mThumbOffset * 2; 988 | 989 | x -= thumbWidth / 2f; 990 | x += mThumbOffset; 991 | 992 | final float scale; 993 | float progress = 0.0f; 994 | 995 | if (x < mPaddingLeft) { 996 | scale = 0.0f; 997 | } else if (x > width - mPaddingRight) { 998 | scale = 1.0f; 999 | } else { 1000 | scale = (x - mPaddingLeft) / (float) availableWidth; 1001 | progress = mTouchProgressOffset; 1002 | } 1003 | 1004 | final float max = getMax(); 1005 | progress += scale * max; 1006 | 1007 | setHotspot(x, y); 1008 | 1009 | if (mWhichThumb == WhichThumb.Start) { 1010 | progress = MathUtils.constrain(progress, 0, getProgressStartMaxValue()); 1011 | setProgressInternal(Math.round(progress), getProgressEnd(), true, false); 1012 | } else if (mWhichThumb == WhichThumb.End) { 1013 | progress = MathUtils.constrain(progress, getProgressEndMinValue(), getMax()); 1014 | setProgressInternal(getProgressStart(), Math.round(progress), true, false); 1015 | } 1016 | } 1017 | 1018 | @Override 1019 | synchronized boolean setProgressInternal( 1020 | int startValue, int endValue, final boolean fromUser, final boolean animate) { 1021 | 1022 | if (mStepSize > 1) { 1023 | final int remainderStart = startValue % mStepSize; 1024 | 1025 | if (remainderStart > 0) { 1026 | if ((float) remainderStart / mStepSize > 0.5) { 1027 | // value + (step-(value%step)) 1028 | startValue = startValue + (mStepSize - remainderStart); 1029 | } else { 1030 | // value - (value%step) 1031 | startValue = startValue - remainderStart; 1032 | } 1033 | } 1034 | 1035 | int remainderEnd = endValue % mStepSize; 1036 | 1037 | if (remainderEnd > 0) { 1038 | if ((float) remainderEnd / mStepSize > 0.5) { 1039 | endValue = endValue + (mStepSize - remainderEnd); 1040 | } else { 1041 | endValue = endValue - remainderEnd; 1042 | } 1043 | } 1044 | } 1045 | 1046 | if (mProgressStartMaxValue != -1 || mProgressEndMinValue != -1) { 1047 | if (mProgressStartMaxValue != -1) { 1048 | startValue = MathUtils.constrain(startValue, 0, mProgressStartMaxValue); 1049 | 1050 | } 1051 | if (mProgressEndMinValue != -1) { 1052 | endValue = MathUtils.constrain(endValue, mProgressEndMinValue, getMax()); 1053 | } 1054 | } else if (mMinMaxStepSize != 0) { 1055 | if (endValue - startValue < mMinMaxStepSize) { 1056 | endValue = startValue + getMinMapStepSize(); 1057 | } 1058 | } 1059 | 1060 | // startValue = MathUtils.constrain(startValue, 0, getProgressStartMaxValue()); 1061 | // endValue = MathUtils.constrain(endValue, getProgressEndMinValue(), getMax()); 1062 | 1063 | return super.setProgressInternal(startValue, endValue, fromUser, animate); 1064 | } 1065 | 1066 | private void attemptClaimDrag() { 1067 | if (getParent() != null) { 1068 | getParent().requestDisallowInterceptTouchEvent(true); 1069 | } 1070 | } 1071 | 1072 | void onStartTrackingTouch() { 1073 | mIsDragging = true; 1074 | if (mOnRangeSeekBarChangeListener != null) { 1075 | mOnRangeSeekBarChangeListener.onStartTrackingTouch(this); 1076 | } 1077 | } 1078 | 1079 | void onStopTrackingTouch() { 1080 | mIsDragging = false; 1081 | if (mOnRangeSeekBarChangeListener != null) { 1082 | mOnRangeSeekBarChangeListener.onStopTrackingTouch(this); 1083 | } 1084 | } 1085 | 1086 | void onKeyChange() { 1087 | } 1088 | 1089 | @Override 1090 | public boolean onKeyDown(int keyCode, KeyEvent event) { 1091 | if (isEnabled()) { 1092 | int increment = mKeyProgressIncrement; 1093 | switch (keyCode) { 1094 | case KeyEvent.KEYCODE_DPAD_LEFT: 1095 | case KeyEvent.KEYCODE_MINUS: 1096 | increment = -increment; 1097 | // fallthrough 1098 | case KeyEvent.KEYCODE_DPAD_RIGHT: 1099 | case KeyEvent.KEYCODE_PLUS: 1100 | case KeyEvent.KEYCODE_EQUALS: 1101 | if (setProgressInternal(getProgressStart() - increment, getProgressEnd() + increment, true, true)) { 1102 | onKeyChange(); 1103 | return true; 1104 | } 1105 | break; 1106 | } 1107 | } 1108 | 1109 | return super.onKeyDown(keyCode, event); 1110 | } 1111 | 1112 | @Override 1113 | public CharSequence getAccessibilityClassName() { 1114 | return RangeSeekBar.class.getName(); 1115 | } 1116 | 1117 | @Override 1118 | public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 1119 | super.onInitializeAccessibilityNodeInfo(info); 1120 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 1121 | info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_PROGRESS); 1122 | } 1123 | } 1124 | 1125 | @Override 1126 | public boolean performAccessibilityAction(int action, Bundle arguments) { 1127 | return super.performAccessibilityAction(action, arguments); 1128 | // TODO: to be implemented 1129 | } 1130 | } 1131 | -------------------------------------------------------------------------------- /rangeseekbar-library/src/main/java/it/sephiroth/android/library/rangeseekbar/SephirothViewCompat.java: -------------------------------------------------------------------------------- 1 | package it.sephiroth.android.library.rangeseekbar; 2 | 3 | import android.view.View; 4 | import android.view.ViewGroup; 5 | import android.view.ViewParent; 6 | 7 | /** 8 | * Created by sephiroth on 2/9/17. 9 | */ 10 | 11 | class SephirothViewCompat { 12 | static boolean isInScrollingContainer(final View view) { 13 | ViewParent p = view.getParent(); 14 | while (p instanceof ViewGroup) { 15 | if (((ViewGroup) p).shouldDelayChildPressedState()) { 16 | return true; 17 | } 18 | p = p.getParent(); 19 | } 20 | return false; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /rangeseekbar-library/src/main/java/it/sephiroth/android/library/rangeseekbar/ThemeUtils.java: -------------------------------------------------------------------------------- 1 | package it.sephiroth.android.library.rangeseekbar; 2 | 3 | import android.content.Context; 4 | import android.content.res.ColorStateList; 5 | import android.content.res.TypedArray; 6 | import android.graphics.Color; 7 | import android.util.TypedValue; 8 | 9 | import androidx.core.graphics.ColorUtils; 10 | 11 | /** 12 | * Created by crugnola on 2/10/17. 13 | * RangeSeekBar 14 | */ 15 | 16 | public class ThemeUtils { 17 | private static final ThreadLocal TL_TYPED_VALUE = new ThreadLocal<>(); 18 | 19 | static final int[] DISABLED_STATE_SET = new int[]{-android.R.attr.state_enabled}; 20 | static final int[] FOCUSED_STATE_SET = new int[]{android.R.attr.state_focused}; 21 | static final int[] ACTIVATED_STATE_SET = new int[]{android.R.attr.state_activated}; 22 | static final int[] PRESSED_STATE_SET = new int[]{android.R.attr.state_pressed}; 23 | static final int[] CHECKED_STATE_SET = new int[]{android.R.attr.state_checked}; 24 | static final int[] SELECTED_STATE_SET = new int[]{android.R.attr.state_selected}; 25 | static final int[] NOT_PRESSED_OR_FOCUSED_STATE_SET = new int[]{ 26 | -android.R.attr.state_pressed, -android.R.attr.state_focused 27 | }; 28 | static final int[] EMPTY_STATE_SET = new int[0]; 29 | 30 | private static final int[] TEMP_ARRAY = new int[1]; 31 | 32 | public static int getThemeAttrColor(Context context, int attr) { 33 | TEMP_ARRAY[0] = attr; 34 | final TypedArray a = context.obtainStyledAttributes(null, TEMP_ARRAY); 35 | try { 36 | return a.getColor(0, 0); 37 | } finally { 38 | a.recycle(); 39 | } 40 | } 41 | 42 | public static ColorStateList getThemeAttrColorStateList(Context context, int attr) { 43 | TEMP_ARRAY[0] = attr; 44 | final TypedArray a = context.obtainStyledAttributes(null, TEMP_ARRAY); 45 | try { 46 | return a.getColorStateList(0); 47 | } finally { 48 | a.recycle(); 49 | } 50 | } 51 | 52 | public static int getDisabledThemeAttrColor(Context context, int attr) { 53 | final ColorStateList csl = getThemeAttrColorStateList(context, attr); 54 | if (csl != null && csl.isStateful()) { 55 | return csl.getColorForState(DISABLED_STATE_SET, csl.getDefaultColor()); 56 | } else { 57 | final TypedValue tv = getTypedValue(); 58 | context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, tv, true); 59 | final float disabledAlpha = tv.getFloat(); 60 | 61 | return getThemeAttrColor(context, attr, disabledAlpha); 62 | } 63 | } 64 | 65 | private static TypedValue getTypedValue() { 66 | TypedValue typedValue = TL_TYPED_VALUE.get(); 67 | if (typedValue == null) { 68 | typedValue = new TypedValue(); 69 | TL_TYPED_VALUE.set(typedValue); 70 | } 71 | return typedValue; 72 | } 73 | 74 | static int getThemeAttrColor(Context context, int attr, float alpha) { 75 | final int color = getThemeAttrColor(context, attr); 76 | final int originalAlpha = Color.alpha(color); 77 | return ColorUtils.setAlphaComponent(color, Math.round(originalAlpha * alpha)); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /rangeseekbar-library/src/main/res/anim/sephiroth_rsb_seekbar_thumb_pressed_to_unpressed_thumb_animation.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 11 | 18 | 19 | -------------------------------------------------------------------------------- /rangeseekbar-library/src/main/res/anim/sephiroth_rsb_seekbar_thumb_unpressed_to_pressed_thumb_0_animation.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 11 | 18 | 19 | -------------------------------------------------------------------------------- /rangeseekbar-library/src/main/res/color/sephiroth_control_highlight_material.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /rangeseekbar-library/src/main/res/color/sephiroth_white_disabled_material.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /rangeseekbar-library/src/main/res/drawable/sephiroth_control_background_32dp_material.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /rangeseekbar-library/src/main/res/drawable/sephiroth_rsb_range_progress_material.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 22 | 23 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 38 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /rangeseekbar-library/src/main/res/drawable/sephiroth_rsb_range_progress_material_inverted.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /rangeseekbar-library/src/main/res/drawable/sephiroth_rsb_seekbar_thumb_material_anim.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 14 | 15 | 16 | 20 | 23 | 27 | 31 | 32 | -------------------------------------------------------------------------------- /rangeseekbar-library/src/main/res/drawable/sephiroth_rsb_seekbar_thumb_pressed_to_unpressed.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 16 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /rangeseekbar-library/src/main/res/drawable/sephiroth_rsb_seekbar_thumb_pressed_to_unpressed_animation.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /rangeseekbar-library/src/main/res/drawable/sephiroth_rsb_seekbar_thumb_unpressed_to_pressed.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 14 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /rangeseekbar-library/src/main/res/drawable/sephiroth_rsb_seekbar_thumb_unpressed_to_pressed_animation.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /rangeseekbar-library/src/main/res/drawable/sephiroth_rsb_tick_mark_material.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /rangeseekbar-library/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 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 51 | 52 | 53 | 54 | 55 | 58 | 59 | 60 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /rangeseekbar-library/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 2dp 4 | 3.5dp 5 | 3.5dp 6 | -------------------------------------------------------------------------------- /rangeseekbar-library/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 24 | 25 | 29 | 30 | 33 | 34 | 38 | 39 | -------------------------------------------------------------------------------- /rangeseekbar-library/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |