├── sample ├── .gitignore ├── src │ └── main │ │ ├── res │ │ ├── drawable-hdpi │ │ │ └── ic_launcher.png │ │ ├── drawable-mdpi │ │ │ └── ic_launcher.png │ │ ├── drawable-xhdpi │ │ │ └── ic_launcher.png │ │ ├── drawable-xxhdpi │ │ │ └── ic_launcher.png │ │ ├── values-v11 │ │ │ └── styles.xml │ │ ├── values-v21 │ │ │ └── styles.xml │ │ ├── values │ │ │ ├── dimens.xml │ │ │ ├── styles.xml │ │ │ └── strings.xml │ │ ├── color │ │ │ ├── my_floater_color.xml │ │ │ └── my_progress_color.xml │ │ ├── values-w820dp │ │ │ └── dimens.xml │ │ └── layout │ │ │ └── activity_main.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── org │ │ └── adw │ │ └── samples │ │ └── discreteseekbar │ │ └── MainActivity.java └── build.gradle ├── library ├── .gitignore ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ ├── res │ │ ├── color │ │ │ ├── dsb_track_color_list.xml │ │ │ ├── dsb_progress_color_list.xml │ │ │ └── dsb_ripple_color_list.xml │ │ └── values │ │ │ ├── styles.xml │ │ │ └── attrs.xml │ │ └── java │ │ └── org │ │ └── adw │ │ └── library │ │ └── widgets │ │ └── discreteseekbar │ │ ├── internal │ │ ├── drawable │ │ │ ├── TrackRectDrawable.java │ │ │ ├── TrackOvalDrawable.java │ │ │ ├── ThumbDrawable.java │ │ │ ├── StateDrawable.java │ │ │ ├── AlmostRippleDrawable.java │ │ │ └── MarkerDrawable.java │ │ ├── compat │ │ │ ├── AnimatorCompatV11.java │ │ │ ├── SeekBarCompatDontCrash.java │ │ │ ├── AnimatorCompat.java │ │ │ └── SeekBarCompat.java │ │ ├── Marker.java │ │ └── PopupIndicator.java │ │ └── DiscreteSeekBar.java └── build.gradle ├── settings.gradle ├── .gitignore ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew.bat ├── gradlew ├── README.md └── LICENSE /sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /library/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':library', ':sample' 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | bintray.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | *.iml 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnderWeb/discreteSeekBar/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnderWeb/discreteSeekBar/HEAD/sample/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnderWeb/discreteSeekBar/HEAD/sample/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnderWeb/discreteSeekBar/HEAD/sample/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnderWeb/discreteSeekBar/HEAD/sample/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/values-v11/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /sample/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /sample/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /sample/src/main/res/color/my_floater_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sample/src/main/res/color/my_progress_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Nov 26 21:03:52 CET 2014 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-2.2.1-all.zip 7 | -------------------------------------------------------------------------------- /sample/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 22 5 | buildToolsVersion '22.0.1' 6 | 7 | defaultConfig { 8 | applicationId "org.adw.samples.discreteseekbar" 9 | minSdkVersion 4 10 | targetSdkVersion 22 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | 15 | } 16 | 17 | dependencies { 18 | compile project (':library') 19 | } 20 | -------------------------------------------------------------------------------- /sample/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /sample/src/main/java/org/adw/samples/discreteseekbar/MainActivity.java: -------------------------------------------------------------------------------- 1 | package org.adw.samples.discreteseekbar; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import android.view.View; 6 | 7 | import org.adw.library.widgets.discreteseekbar.DiscreteSeekBar; 8 | 9 | public class MainActivity extends Activity { 10 | 11 | @Override 12 | protected void onCreate(Bundle savedInstanceState) { 13 | super.onCreate(savedInstanceState); 14 | setContentView(R.layout.activity_main); 15 | DiscreteSeekBar discreteSeekBar1 = (DiscreteSeekBar) findViewById(R.id.discrete1); 16 | discreteSeekBar1.setNumericTransformer(new DiscreteSeekBar.NumericTransformer() { 17 | @Override 18 | public int transform(int value) { 19 | return value * 100; 20 | } 21 | }); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /library/src/main/res/color/dsb_track_color_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /library/src/main/res/color/dsb_progress_color_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /library/src/main/res/color/dsb_ripple_color_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /library/src/main/java/org/adw/library/widgets/discreteseekbar/internal/drawable/TrackRectDrawable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Gustavo Claramunt (AnderWeb) 2014. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.adw.library.widgets.discreteseekbar.internal.drawable; 18 | 19 | import android.content.res.ColorStateList; 20 | import android.graphics.Canvas; 21 | import android.graphics.Paint; 22 | import android.support.annotation.NonNull; 23 | 24 | /** 25 | * Simple {@link org.adw.library.widgets.discreteseekbar.internal.drawable.StateDrawable} implementation 26 | * to draw rectangles 27 | * 28 | * @hide 29 | */ 30 | public class TrackRectDrawable extends StateDrawable { 31 | public TrackRectDrawable(@NonNull ColorStateList tintStateList) { 32 | super(tintStateList); 33 | } 34 | 35 | @Override 36 | void doDraw(Canvas canvas, Paint paint) { 37 | canvas.drawRect(getBounds(), paint); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /library/src/main/java/org/adw/library/widgets/discreteseekbar/internal/drawable/TrackOvalDrawable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Gustavo Claramunt (AnderWeb) 2014. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.adw.library.widgets.discreteseekbar.internal.drawable; 18 | 19 | import android.content.res.ColorStateList; 20 | import android.graphics.Canvas; 21 | import android.graphics.Paint; 22 | import android.graphics.RectF; 23 | import android.support.annotation.NonNull; 24 | 25 | /** 26 | * Simple {@link org.adw.library.widgets.discreteseekbar.internal.drawable.StateDrawable} implementation 27 | * to draw circles/ovals 28 | * 29 | * @hide 30 | */ 31 | public class TrackOvalDrawable extends StateDrawable { 32 | private RectF mRectF = new RectF(); 33 | 34 | public TrackOvalDrawable(@NonNull ColorStateList tintStateList) { 35 | super(tintStateList); 36 | } 37 | 38 | @Override 39 | void doDraw(Canvas canvas, Paint paint) { 40 | mRectF.set(getBounds()); 41 | canvas.drawOval(mRectF, paint); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /library/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | #ff009688 19 | #ff939393 20 | #66939393 21 | #77939393 22 | #99999999 23 | 24 | 33 | 34 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | DiscreteSeekBar Sample 5 | Bacon ipsum dolor amet chuck alcatra tenderloin brisket filet mignon. Cupim capicola ground round sirloin. Meatball prosciutto pork bresaola biltong, shankle cow drumstick picanha short loin beef ribs pastrami cupim tail. Shank pastrami turkey cow swine tail ham hamburger pork loin pork belly tri-tip cupim. 6 | Brisket pig filet mignon porchetta short ribs, ball tip t-bone meatball alcatra prosciutto. Pork loin pastrami bacon, kevin meatloaf landjaeger ball tip andouille short ribs kielbasa cupim rump. Shoulder alcatra pork t-bone, sausage bacon filet mignon. Chuck pastrami tongue chicken brisket pork ball tip kevin tail frankfurter meatball prosciutto bacon. Venison short loin fatback pig picanha cow kevin tenderloin. Pork cupim leberkas, andouille frankfurter tenderloin meatloaf alcatra biltong.\nMeatloaf tenderloin ground round corned beef shoulder prosciutto biltong tail alcatra landjaeger frankfurter spare ribs. Rump swine capicola, shankle salami chuck sirloin sausage. Tenderloin pig tri-tip brisket chicken, ham ribeye short ribs shankle. T-bone turkey short loin jerky, porchetta turducken tail sausage biltong pork pork belly. Strip steak brisket pork loin salami, tail alcatra turkey kevin prosciutto jerky cupim ground round. Pork belly fatback bresaola porchetta andouille beef alcatra turkey picanha pork chop t-bone swine short loin ham. Short ribs pork swine, shoulder ham frankfurter tri-tip.\nDrumstick swine sausage tri-tip pork bacon. Kevin meatloaf andouille pork, shoulder short ribs capicola. Salami beef shank, shankle spare ribs cow doner leberkas tongue meatball hamburger shoulder. Pork chop ribeye prosciutto ground round brisket. Sirloin pig brisket short ribs corned beef, venison shoulder landjaeger cupim alcatra. Shankle andouille prosciutto, flank turkey venison kielbasa pork chop pork loin. Bresaola tri-tip turducken filet mignon, pork pig cow capicola brisket short ribs doner. 7 | 8 | -------------------------------------------------------------------------------- /library/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /library/src/main/java/org/adw/library/widgets/discreteseekbar/internal/compat/AnimatorCompatV11.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Gustavo Claramunt (AnderWeb) 2014. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.adw.library.widgets.discreteseekbar.internal.compat; 18 | 19 | import android.animation.ValueAnimator; 20 | import android.annotation.TargetApi; 21 | import android.os.Build; 22 | 23 | /** 24 | * Class to wrap a {@link android.animation.ValueAnimator} 25 | * for use with AnimatorCompat 26 | * 27 | * @hide 28 | * @see {@link org.adw.library.widgets.discreteseekbar.internal.compat.AnimatorCompat} 29 | */ 30 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 31 | public class AnimatorCompatV11 extends AnimatorCompat { 32 | 33 | ValueAnimator animator; 34 | 35 | public AnimatorCompatV11(float start, float end, final AnimationFrameUpdateListener listener) { 36 | super(); 37 | animator = ValueAnimator.ofFloat(start, end); 38 | animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 39 | @Override 40 | public void onAnimationUpdate(ValueAnimator animation) { 41 | listener.onAnimationFrame((Float) animation.getAnimatedValue()); 42 | } 43 | }); 44 | } 45 | 46 | @Override 47 | public void cancel() { 48 | animator.cancel(); 49 | } 50 | 51 | @Override 52 | public boolean isRunning() { 53 | return animator.isRunning(); 54 | } 55 | 56 | @Override 57 | public void setDuration(int duration) { 58 | animator.setDuration(duration); 59 | } 60 | 61 | @Override 62 | public void start() { 63 | animator.start(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Gustavo Claramunt (AnderWeb) 2014. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | apply plugin: 'com.android.library' 18 | apply plugin: 'bintray-release' // must be applied after your artifact generating plugin (eg. java / com.android.library) 19 | 20 | buildscript { 21 | repositories { 22 | jcenter() 23 | } 24 | dependencies { 25 | classpath 'com.novoda:bintray-release:0.2.10' 26 | } 27 | } 28 | 29 | /** 30 | * Properties for versioning 31 | */ 32 | def libVersionCode = 2 33 | def libVersion = '1.0.1' 34 | 35 | 36 | android { 37 | compileSdkVersion 22 38 | buildToolsVersion '22.0.1' 39 | 40 | defaultConfig { 41 | minSdkVersion 4 42 | targetSdkVersion 22 43 | versionCode libVersionCode 44 | versionName libVersion 45 | } 46 | } 47 | 48 | publish { 49 | /** 50 | * Properties for BinTray upload 51 | */ 52 | Properties properties = new Properties() 53 | File bintrayPropertiesFile = project.file('bintray.properties'); 54 | if (bintrayPropertiesFile.exists()) { 55 | properties.load(bintrayPropertiesFile.newDataInputStream()) 56 | } 57 | def btrayUser = properties.getProperty("bintray.user", "") 58 | def btrayKey = properties.getProperty("bintray.apikey", "") 59 | 60 | bintrayUser = btrayUser 61 | bintrayKey = btrayKey 62 | userOrg = btrayUser 63 | groupId = 'org.adw.library' 64 | artifactId = 'discrete-seekbar' 65 | licences = "['Apache-2.0']" 66 | publishVersion = libVersion 67 | description = 'DiscreteSeekbar is my poor attempt to develop an android implementation of the Discrete Slider component from the Google Material Design Guidelines' 68 | website = 'https://github.com/AnderWeb/discreteSeekBar' 69 | issueTracker = 'https://github.com/AnderWeb/discreteSeekBar/issues' 70 | repository = 'https://github.com/AnderWeb/discreteSeekBar.git' 71 | autoPublish = false 72 | dryRun = false 73 | } 74 | 75 | dependencies { 76 | compile 'com.android.support:support-v4:22.1.1' 77 | } 78 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /library/src/main/java/org/adw/library/widgets/discreteseekbar/internal/compat/SeekBarCompatDontCrash.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Gustavo Claramunt (AnderWeb) 2014. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.adw.library.widgets.discreteseekbar.internal.compat; 18 | 19 | import android.annotation.TargetApi; 20 | import android.content.res.ColorStateList; 21 | import android.graphics.Outline; 22 | import android.graphics.drawable.Drawable; 23 | import android.graphics.drawable.RippleDrawable; 24 | import android.view.View; 25 | import android.view.ViewGroup; 26 | import android.view.ViewOutlineProvider; 27 | import android.view.ViewParent; 28 | import android.widget.TextView; 29 | 30 | import org.adw.library.widgets.discreteseekbar.internal.drawable.MarkerDrawable; 31 | 32 | /** 33 | * Wrapper compatibility class to call some API-Specific methods 34 | * And offer alternate procedures when possible 35 | * 36 | * @hide 37 | */ 38 | @TargetApi(21) 39 | class SeekBarCompatDontCrash { 40 | public static void setOutlineProvider(View marker, final MarkerDrawable markerDrawable) { 41 | marker.setOutlineProvider(new ViewOutlineProvider() { 42 | @Override 43 | public void getOutline(View view, Outline outline) { 44 | outline.setConvexPath(markerDrawable.getPath()); 45 | } 46 | }); 47 | } 48 | 49 | public static Drawable getRipple(ColorStateList colorStateList) { 50 | return new RippleDrawable(colorStateList, null, null); 51 | } 52 | 53 | public static void setBackground(View view, Drawable background) { 54 | view.setBackground(background); 55 | } 56 | 57 | public static void setTextDirection(TextView number, int textDirection) { 58 | number.setTextDirection(textDirection); 59 | } 60 | 61 | public static boolean isInScrollingContainer(ViewParent p) { 62 | while (p != null && p instanceof ViewGroup) { 63 | if (((ViewGroup) p).shouldDelayChildPressedState()) { 64 | return true; 65 | } 66 | p = p.getParent(); 67 | } 68 | return false; 69 | } 70 | 71 | public static boolean isHardwareAccelerated(View view) { 72 | return view.isHardwareAccelerated(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | 21 | 29 | 30 | 40 | 41 | 44 | 48 | 52 | 59 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /library/src/main/java/org/adw/library/widgets/discreteseekbar/internal/compat/AnimatorCompat.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Gustavo Claramunt (AnderWeb) 2014. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.adw.library.widgets.discreteseekbar.internal.compat; 18 | 19 | import android.os.Build; 20 | 21 | /** 22 | * Currently, there's no {@link android.animation.ValueAnimator} compatibility version 23 | * and as we didn't want to throw in external dependencies, we made this small class. 24 | *

25 | *

26 | * This will work like {@link android.support.v4.view.ViewPropertyAnimatorCompat}, that is, 27 | * not doing anything on API<11 and using the default {@link android.animation.ValueAnimator} 28 | * on API>=11 29 | *

30 | *

31 | * This class is used to provide animation to the {@link org.adw.library.widgets.discreteseekbar.DiscreteSeekBar} 32 | * when navigating with the Keypad 33 | *

34 | * 35 | * @hide 36 | */ 37 | public abstract class AnimatorCompat { 38 | public interface AnimationFrameUpdateListener { 39 | public void onAnimationFrame(float currentValue); 40 | } 41 | 42 | AnimatorCompat() { 43 | 44 | } 45 | 46 | public abstract void cancel(); 47 | 48 | public abstract boolean isRunning(); 49 | 50 | public abstract void setDuration(int progressAnimationDuration); 51 | 52 | public abstract void start(); 53 | 54 | public static final AnimatorCompat create(float start, float end, AnimationFrameUpdateListener listener) { 55 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 56 | return new AnimatorCompatV11(start, end, listener); 57 | } else { 58 | return new AnimatorCompatBase(start, end, listener); 59 | } 60 | } 61 | 62 | private static class AnimatorCompatBase extends AnimatorCompat { 63 | 64 | private final AnimationFrameUpdateListener mListener; 65 | private final float mEndValue; 66 | 67 | public AnimatorCompatBase(float start, float end, AnimationFrameUpdateListener listener) { 68 | mListener = listener; 69 | mEndValue = end; 70 | } 71 | 72 | @Override 73 | public void cancel() { 74 | 75 | } 76 | 77 | @Override 78 | public boolean isRunning() { 79 | return false; 80 | } 81 | 82 | @Override 83 | public void setDuration(int progressAnimationDuration) { 84 | 85 | } 86 | 87 | @Override 88 | public void start() { 89 | mListener.onAnimationFrame(mEndValue); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /library/src/main/java/org/adw/library/widgets/discreteseekbar/internal/drawable/ThumbDrawable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Gustavo Claramunt (AnderWeb) 2014. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.adw.library.widgets.discreteseekbar.internal.drawable; 18 | 19 | import android.content.res.ColorStateList; 20 | import android.graphics.Canvas; 21 | import android.graphics.Paint; 22 | import android.graphics.Rect; 23 | import android.graphics.drawable.Animatable; 24 | import android.os.SystemClock; 25 | import android.support.annotation.NonNull; 26 | 27 | /** 28 | *

HACK

29 | *

30 | * Special {@link org.adw.library.widgets.discreteseekbar.internal.drawable.StateDrawable} implementation 31 | * to draw the Thumb circle. 32 | *

33 | *

34 | * It's special because it will stop drawing once the state is pressed/focused BUT only after a small delay. 35 | *

36 | *

37 | * This special delay is meant to help avoiding frame glitches while the {@link org.adw.library.widgets.discreteseekbar.internal.Marker} is added to the Window 38 | *

39 | * 40 | * @hide 41 | */ 42 | public class ThumbDrawable extends StateDrawable implements Animatable { 43 | //The current size for this drawable. Must be converted to real DPs 44 | public static final int DEFAULT_SIZE_DP = 12; 45 | private final int mSize; 46 | private boolean mOpen; 47 | private boolean mRunning; 48 | 49 | public ThumbDrawable(@NonNull ColorStateList tintStateList, int size) { 50 | super(tintStateList); 51 | mSize = size; 52 | } 53 | 54 | @Override 55 | public int getIntrinsicWidth() { 56 | return mSize; 57 | } 58 | 59 | @Override 60 | public int getIntrinsicHeight() { 61 | return mSize; 62 | } 63 | 64 | @Override 65 | public void doDraw(Canvas canvas, Paint paint) { 66 | if (!mOpen) { 67 | Rect bounds = getBounds(); 68 | float radius = (mSize / 2); 69 | canvas.drawCircle(bounds.centerX(), bounds.centerY(), radius, paint); 70 | } 71 | } 72 | 73 | public void animateToPressed() { 74 | scheduleSelf(opener, SystemClock.uptimeMillis() + 100); 75 | mRunning = true; 76 | } 77 | 78 | public void animateToNormal() { 79 | mOpen = false; 80 | mRunning = false; 81 | unscheduleSelf(opener); 82 | invalidateSelf(); 83 | } 84 | 85 | private Runnable opener = new Runnable() { 86 | @Override 87 | public void run() { 88 | mOpen = true; 89 | invalidateSelf(); 90 | mRunning = false; 91 | } 92 | }; 93 | 94 | @Override 95 | public void start() { 96 | //NOOP 97 | } 98 | 99 | @Override 100 | public void stop() { 101 | animateToNormal(); 102 | } 103 | 104 | @Override 105 | public boolean isRunning() { 106 | return mRunning; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /library/src/main/java/org/adw/library/widgets/discreteseekbar/internal/drawable/StateDrawable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Gustavo Claramunt (AnderWeb) 2014. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.adw.library.widgets.discreteseekbar.internal.drawable; 18 | 19 | import android.content.res.ColorStateList; 20 | import android.graphics.Canvas; 21 | import android.graphics.Color; 22 | import android.graphics.ColorFilter; 23 | import android.graphics.Paint; 24 | import android.graphics.PixelFormat; 25 | import android.graphics.drawable.Drawable; 26 | import android.support.annotation.NonNull; 27 | 28 | /** 29 | * A drawable that changes it's Paint color depending on the Drawable State 30 | *

31 | * Subclasses should implement {@link #doDraw(android.graphics.Canvas, android.graphics.Paint)} 32 | *

33 | * 34 | * @hide 35 | */ 36 | public abstract class StateDrawable extends Drawable { 37 | private ColorStateList mTintStateList; 38 | private final Paint mPaint; 39 | private int mCurrentColor; 40 | private int mAlpha = 255; 41 | 42 | public StateDrawable(@NonNull ColorStateList tintStateList) { 43 | super(); 44 | setColorStateList(tintStateList); 45 | mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 46 | } 47 | 48 | @Override 49 | public boolean isStateful() { 50 | return (mTintStateList.isStateful()) || super.isStateful(); 51 | } 52 | 53 | @Override 54 | public boolean setState(int[] stateSet) { 55 | boolean handled = super.setState(stateSet); 56 | handled = updateTint(stateSet) || handled; 57 | return handled; 58 | } 59 | 60 | @Override 61 | public int getOpacity() { 62 | return PixelFormat.TRANSLUCENT; 63 | } 64 | 65 | private boolean updateTint(int[] state) { 66 | final int color = mTintStateList.getColorForState(state, mCurrentColor); 67 | if (color != mCurrentColor) { 68 | mCurrentColor = color; 69 | //We've changed states 70 | invalidateSelf(); 71 | return true; 72 | } 73 | return false; 74 | } 75 | 76 | @Override 77 | public void draw(Canvas canvas) { 78 | mPaint.setColor(mCurrentColor); 79 | int alpha = modulateAlpha(Color.alpha(mCurrentColor)); 80 | mPaint.setAlpha(alpha); 81 | doDraw(canvas, mPaint); 82 | } 83 | 84 | public void setColorStateList(@NonNull ColorStateList tintStateList) { 85 | mTintStateList = tintStateList; 86 | mCurrentColor = tintStateList.getDefaultColor(); 87 | } 88 | 89 | /** 90 | * Subclasses should implement this method to do the actual drawing 91 | * 92 | * @param canvas The current {@link android.graphics.Canvas} to draw into 93 | * @param paint The {@link android.graphics.Paint} preconfigurred with the current 94 | * {@link android.content.res.ColorStateList} color 95 | */ 96 | abstract void doDraw(Canvas canvas, Paint paint); 97 | 98 | @Override 99 | public void setAlpha(int alpha) { 100 | mAlpha = alpha; 101 | invalidateSelf(); 102 | } 103 | 104 | int modulateAlpha(int alpha) { 105 | int scale = mAlpha + (mAlpha >> 7); 106 | return alpha * scale >> 8; 107 | } 108 | 109 | @Override 110 | public int getAlpha() { 111 | return mAlpha; 112 | } 113 | 114 | @Override 115 | public void setColorFilter(ColorFilter cf) { 116 | mPaint.setColorFilter(cf); 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /library/src/main/java/org/adw/library/widgets/discreteseekbar/internal/compat/SeekBarCompat.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Gustavo Claramunt (AnderWeb) 2014. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.adw.library.widgets.discreteseekbar.internal.compat; 18 | 19 | import android.content.res.ColorStateList; 20 | import android.graphics.drawable.Drawable; 21 | import android.graphics.drawable.RippleDrawable; 22 | import android.os.Build; 23 | import android.support.annotation.NonNull; 24 | import android.support.v4.graphics.drawable.DrawableCompat; 25 | import android.view.View; 26 | import android.view.ViewParent; 27 | import android.widget.TextView; 28 | 29 | import org.adw.library.widgets.discreteseekbar.internal.drawable.AlmostRippleDrawable; 30 | import org.adw.library.widgets.discreteseekbar.internal.drawable.MarkerDrawable; 31 | 32 | /** 33 | * Wrapper compatibility class to call some API-Specific methods 34 | * And offer alternate procedures when possible 35 | * 36 | * @hide 37 | */ 38 | public class SeekBarCompat { 39 | 40 | /** 41 | * Sets the custom Outline provider on API>=21. 42 | * Does nothing on API<21 43 | * 44 | * @param view 45 | * @param markerDrawable 46 | */ 47 | public static void setOutlineProvider(View view, final MarkerDrawable markerDrawable) { 48 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 49 | SeekBarCompatDontCrash.setOutlineProvider(view, markerDrawable); 50 | } 51 | } 52 | 53 | /** 54 | * Our DiscreteSeekBar implementation uses a circular drawable on API < 21 55 | * because we don't set it as Background, but draw it ourselves 56 | * 57 | * @param colorStateList 58 | * @return 59 | */ 60 | public static Drawable getRipple(ColorStateList colorStateList) { 61 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 62 | return SeekBarCompatDontCrash.getRipple(colorStateList); 63 | } else { 64 | return new AlmostRippleDrawable(colorStateList); 65 | } 66 | } 67 | 68 | /** 69 | * Sets the color of the seekbar ripple 70 | * @param drawable 71 | * @param colorStateList The ColorStateList the track ripple will be changed to 72 | */ 73 | public static void setRippleColor(@NonNull Drawable drawable, ColorStateList colorStateList) { 74 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 75 | ((RippleDrawable) drawable).setColor(colorStateList); 76 | } else { 77 | ((AlmostRippleDrawable) drawable).setColor(colorStateList); 78 | } 79 | } 80 | 81 | /** 82 | * As our DiscreteSeekBar implementation uses a circular drawable on API < 21 83 | * we want to use the same method to set its bounds as the Ripple's hotspot bounds. 84 | * 85 | * @param drawable 86 | * @param left 87 | * @param top 88 | * @param right 89 | * @param bottom 90 | */ 91 | public static void setHotspotBounds(Drawable drawable, int left, int top, int right, int bottom) { 92 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 93 | //We don't want the full size rect, Lollipop ripple would be too big 94 | int size = (right - left) / 8; 95 | DrawableCompat.setHotspotBounds(drawable, left + size, top + size, right - size, bottom - size); 96 | } else { 97 | drawable.setBounds(left, top, right, bottom); 98 | } 99 | } 100 | 101 | /** 102 | * android.support.v4.view.ViewCompat SHOULD include this once and for all!! 103 | * But it doesn't... 104 | * 105 | * @param view 106 | * @param background 107 | */ 108 | @SuppressWarnings("deprecation") 109 | public static void setBackground(View view, Drawable background) { 110 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { 111 | SeekBarCompatDontCrash.setBackground(view, background); 112 | } else { 113 | view.setBackgroundDrawable(background); 114 | } 115 | } 116 | 117 | /** 118 | * Sets the TextView text direction attribute when possible 119 | * 120 | * @param textView 121 | * @param textDirection 122 | * @see android.widget.TextView#setTextDirection(int) 123 | */ 124 | public static void setTextDirection(TextView textView, int textDirection) { 125 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { 126 | SeekBarCompatDontCrash.setTextDirection(textView, textDirection); 127 | } 128 | } 129 | 130 | public static boolean isInScrollingContainer(ViewParent p) { 131 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 132 | return SeekBarCompatDontCrash.isInScrollingContainer(p); 133 | } 134 | return false; 135 | } 136 | 137 | public static boolean isHardwareAccelerated(View view) { 138 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 139 | return SeekBarCompatDontCrash.isHardwareAccelerated(view); 140 | } 141 | return false; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /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 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #DiscreteSeekBar 2 | 3 | ![screenshot](https://lh6.googleusercontent.com/-JjvxVMCm1ug/VHUPWVBfpbI/AAAAAAAAHtQ/TPtoOjHI5MA/w639-h356/seekbar2.gif) 4 | 5 | ![screenshot](https://lh3.googleusercontent.com/-7nbVPXxUhYk/VG-rO64pMWI/AAAAAAAAHsM/aMRglt2Vzrk/w639-h480/animation.gif) 6 | 7 | DiscreteSeekbar is my poor attempt to develop an android implementation of the [Discrete Slider] component from the Google Material Design Guidelines. 8 | 9 | ##Prologe 10 | I really hope Google provides developers with a better (and official) implementation ;) 11 | 12 | ##Warning 13 | After a bunch of hours trying to replicate the exact feel of the Material's Discrete Seekbar, with a beautiful stuff-that-morphs-into-other-stuff animation I was convinced about releasing the current code. 14 | 15 | ```java 16 | android.util.Log.wtf("WARNING!! HACKERY-DRAGONS!!"); 17 | ``` 18 | I've done a few bit of hacky cede and a bunch of things I'm not completely proud of, so use under your sole responsibility (or help me improve it via pull-requests!) 19 | 20 | ##Implementation details 21 | This thing runs on minSDK=7 (well, technically could run 4 but can't test since AVDs for api 4 are deprecated and just don't boot). 22 | Obviously some of the subtle animations (navigating with the Keyboard, the Ripple effect, text fade ins/fade outs, etc) are not going to work on APIS lower than 11, but the bubble thing does. And I haven't found a way of improving this with 11-21 APIs, so... 23 | 24 | The base SeekBar is pretty simple. Just 3 drawables for the track, progress and thumb. Some touch event logic to drag, some key event logic to move, and that's all. 25 | 26 | It supports custom ranges (custom min/max), even for negative values. 27 | 28 | The bubble thing **DOESN'T USE** [VectorDrawableMagic] . I was not really needed for such a simple morph. It uses instead an [Animatable Drawable] for the animation with a lot of hackery for callbacks, drawing and a bunch of old simple math. 29 | 30 | >For this to work (and sync with events, etc) I've written a fair amount of shit questionable code... 31 | 32 | The material-floating-thing is composed into the WindowManager (like the typical overflow menus) to be able to show it over other Views without needing to set the SeekBar big enough to account for the (variable) size of he floating thing. 33 | 34 | >For this I'm not sure about the amounts of things I've copied from [PopupWindow] and the possible issues. 35 | 36 | ##Dependencies 37 | It uses **com.android.support:support-v4** as the only dependency. 38 | 39 | ##Usage 40 | This is published in jCenter so you need to use the appropiate repo: 41 | 42 | ```groovy 43 | repositories { 44 | jcenter() 45 | } 46 | 47 | dependencies { 48 | compile 'org.adw.library:discrete-seekbar:1.0.1' 49 | } 50 | ``` 51 | 52 | Once imported into your project, you just need to put them into your layous like: 53 | 54 | ```xml 55 | 61 | ``` 62 | 63 | ####Parameters 64 | You can tweak a few things of the DiscreteSeekbar: 65 | 66 | * **dsb_min**: minimum value 67 | * **dsb_max**: maximum value 68 | * **dsb_value**: current value 69 | * **dsb_mirrorForRtl**: reverse the DiscreteSeekBar for RTL locales 70 | * **dsb_allowTrackClickToDrag**: allows clicking outside the thumb circle to initiate drag. Default TRUE 71 | * **dsb_indicatorFormatter**: a string [Format] to apply to the value inside the bubble indicator. 72 | * **dsb_indicatorPopupEnabled**: choose if the bubble indicator will be shown. Default TRUE 73 | 74 | ####Design 75 | 76 | * **dsb_progressColor**: color/colorStateList for the progress bar and thumb drawable 77 | * **dsb_trackColor**: color/colorStateList for the track drawable 78 | * **dsb_indicatorTextAppearance**: TextAppearance for the bubble indicator 79 | * **dsb_indicatorColor**: color/colorStateList for the bubble shaped drawable 80 | * **dsb_indicatorElevation**: related to android:elevation. Will only be used on API level 21+ 81 | * **dsb_rippleColor**: color/colorStateList for the ripple drawable seen when pressing the thumb. (Yes, it does a kind of "ripple" on API levels lower than 21 and a real RippleDrawable for 21+. 82 | * **dsb_trackHeight**: dimension for the height of the track drawable. 83 | * **dsb_scrubberHeight**: dimension for the height of the scrubber (selected area) drawable. 84 | * **dsb_thumbSize**: dimension for the size of the thumb drawable. 85 | * **dsb_indicatorSeparation**: dimension for the vertical distance from the thumb to the indicator. 86 | 87 | You can also use the attribute **discreteSeekBarStyle** on your themes with a custom Style to be applied to all the DiscreteSeekBars on your app/activity/fragment/whatever. 88 | 89 | ##License 90 | ``` 91 | Copyright 2014 Gustavo Claramunt (Ander Webbs) 92 | 93 | Licensed under the Apache License, Version 2.0 (the "License"); 94 | you may not use this file except in compliance with the License. 95 | You may obtain a copy of the License at 96 | 97 | http://www.apache.org/licenses/LICENSE-2.0 98 | 99 | Unless required by applicable law or agreed to in writing, software 100 | distributed under the License is distributed on an "AS IS" BASIS, 101 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 102 | See the License for the specific language governing permissions and 103 | limitations under the License. 104 | ``` 105 | 106 | [Discrete Slider]:http://www.google.com/design/spec/components/sliders.html#sliders-discrete-slider 107 | [VectorDrawableMagic]:https://developer.android.com/reference/android/graphics/drawable/AnimatedVectorDrawable.html 108 | [Animatable Drawable]:https://developer.android.com/reference/android/graphics/drawable/Animatable.html 109 | [PopupWindow]:https://developer.android.com/reference/android/widget/PopupWindow.html 110 | [Format]:https://developer.android.com/reference/java/util/Formatter.html 111 | 112 | -------------------------------------------------------------------------------- /library/src/main/java/org/adw/library/widgets/discreteseekbar/internal/drawable/AlmostRippleDrawable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Gustavo Claramunt (AnderWeb) 2014. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.adw.library.widgets.discreteseekbar.internal.drawable; 18 | 19 | import android.content.res.ColorStateList; 20 | import android.graphics.Canvas; 21 | import android.graphics.Color; 22 | import android.graphics.Paint; 23 | import android.graphics.Rect; 24 | import android.graphics.drawable.Animatable; 25 | import android.os.SystemClock; 26 | import android.support.annotation.NonNull; 27 | import android.view.animation.AccelerateDecelerateInterpolator; 28 | import android.view.animation.Interpolator; 29 | 30 | public class AlmostRippleDrawable extends StateDrawable implements Animatable { 31 | private static final long FRAME_DURATION = 1000 / 60; 32 | private static final int ANIMATION_DURATION = 250; 33 | 34 | private static final float INACTIVE_SCALE = 0f; 35 | private static final float ACTIVE_SCALE = 1f; 36 | private float mCurrentScale = INACTIVE_SCALE; 37 | private Interpolator mInterpolator; 38 | private long mStartTime; 39 | private boolean mReverse = false; 40 | private boolean mRunning = false; 41 | private int mDuration = ANIMATION_DURATION; 42 | private float mAnimationInitialValue; 43 | //We don't use colors just with our drawable state because of animations 44 | private int mPressedColor; 45 | private int mFocusedColor; 46 | private int mDisabledColor; 47 | private int mRippleColor; 48 | private int mRippleBgColor; 49 | 50 | public AlmostRippleDrawable(@NonNull ColorStateList tintStateList) { 51 | super(tintStateList); 52 | mInterpolator = new AccelerateDecelerateInterpolator(); 53 | setColor(tintStateList); 54 | } 55 | 56 | public void setColor(@NonNull ColorStateList tintStateList) { 57 | int defaultColor = tintStateList.getDefaultColor(); 58 | mFocusedColor = tintStateList.getColorForState(new int[]{android.R.attr.state_enabled, android.R.attr.state_focused}, defaultColor); 59 | mPressedColor = tintStateList.getColorForState(new int[]{android.R.attr.state_enabled, android.R.attr.state_pressed}, defaultColor); 60 | mDisabledColor = tintStateList.getColorForState(new int[]{-android.R.attr.state_enabled}, defaultColor); 61 | 62 | //The ripple should be partially transparent 63 | mFocusedColor = getModulatedAlphaColor(130, mFocusedColor); 64 | mPressedColor = getModulatedAlphaColor(130, mPressedColor); 65 | mDisabledColor = getModulatedAlphaColor(130, mDisabledColor); 66 | } 67 | 68 | private static int getModulatedAlphaColor(int alphaValue, int originalColor) { 69 | int alpha = Color.alpha(originalColor); 70 | int scale = alphaValue + (alphaValue >> 7); 71 | alpha = alpha * scale >> 8; 72 | return Color.argb(alpha, Color.red(originalColor), Color.green(originalColor), Color.blue(originalColor)); 73 | } 74 | 75 | @Override 76 | public void doDraw(Canvas canvas, Paint paint) { 77 | Rect bounds = getBounds(); 78 | int size = Math.min(bounds.width(), bounds.height()); 79 | float scale = mCurrentScale; 80 | int rippleColor = mRippleColor; 81 | int bgColor = mRippleBgColor; 82 | float radius = (size / 2); 83 | float radiusAnimated = radius * scale; 84 | if (scale > INACTIVE_SCALE) { 85 | if (bgColor != 0) { 86 | paint.setColor(bgColor); 87 | paint.setAlpha(decreasedAlpha(Color.alpha(bgColor))); 88 | canvas.drawCircle(bounds.centerX(), bounds.centerY(), radius, paint); 89 | } 90 | if (rippleColor != 0) { 91 | paint.setColor(rippleColor); 92 | paint.setAlpha(modulateAlpha(Color.alpha(rippleColor))); 93 | canvas.drawCircle(bounds.centerX(), bounds.centerY(), radiusAnimated, paint); 94 | } 95 | } 96 | } 97 | 98 | private int decreasedAlpha(int alpha) { 99 | int scale = 100 + (100 >> 7); 100 | return alpha * scale >> 8; 101 | } 102 | 103 | @Override 104 | public boolean setState(int[] stateSet) { 105 | int[] oldState = getState(); 106 | boolean oldPressed = false; 107 | for (int i : oldState) { 108 | if (i == android.R.attr.state_pressed) { 109 | oldPressed = true; 110 | } 111 | } 112 | super.setState(stateSet); 113 | boolean focused = false; 114 | boolean pressed = false; 115 | boolean disabled = true; 116 | for (int i : stateSet) { 117 | if (i == android.R.attr.state_focused) { 118 | focused = true; 119 | } else if (i == android.R.attr.state_pressed) { 120 | pressed = true; 121 | } else if (i == android.R.attr.state_enabled) { 122 | disabled = false; 123 | } 124 | } 125 | 126 | if (disabled) { 127 | unscheduleSelf(mUpdater); 128 | mRippleColor = mDisabledColor; 129 | mRippleBgColor = 0; 130 | mCurrentScale = ACTIVE_SCALE / 2; 131 | invalidateSelf(); 132 | } else { 133 | if (pressed) { 134 | animateToPressed(); 135 | mRippleColor = mRippleBgColor = mPressedColor; 136 | } else if (oldPressed) { 137 | mRippleColor = mRippleBgColor = mPressedColor; 138 | animateToNormal(); 139 | } else if (focused) { 140 | mRippleColor = mFocusedColor; 141 | mRippleBgColor = 0; 142 | mCurrentScale = ACTIVE_SCALE; 143 | invalidateSelf(); 144 | } else { 145 | mRippleColor = 0; 146 | mRippleBgColor = 0; 147 | mCurrentScale = INACTIVE_SCALE; 148 | invalidateSelf(); 149 | } 150 | } 151 | return true; 152 | } 153 | 154 | public void animateToPressed() { 155 | unscheduleSelf(mUpdater); 156 | if (mCurrentScale < ACTIVE_SCALE) { 157 | mReverse = false; 158 | mRunning = true; 159 | mAnimationInitialValue = mCurrentScale; 160 | float durationFactor = 1f - ((mAnimationInitialValue - INACTIVE_SCALE) / (ACTIVE_SCALE - INACTIVE_SCALE)); 161 | mDuration = (int) (ANIMATION_DURATION * durationFactor); 162 | mStartTime = SystemClock.uptimeMillis(); 163 | scheduleSelf(mUpdater, mStartTime + FRAME_DURATION); 164 | } 165 | } 166 | 167 | public void animateToNormal() { 168 | unscheduleSelf(mUpdater); 169 | if (mCurrentScale > INACTIVE_SCALE) { 170 | mReverse = true; 171 | mRunning = true; 172 | mAnimationInitialValue = mCurrentScale; 173 | float durationFactor = 1f - ((mAnimationInitialValue - ACTIVE_SCALE) / (INACTIVE_SCALE - ACTIVE_SCALE)); 174 | mDuration = (int) (ANIMATION_DURATION * durationFactor); 175 | mStartTime = SystemClock.uptimeMillis(); 176 | scheduleSelf(mUpdater, mStartTime + FRAME_DURATION); 177 | } 178 | } 179 | 180 | private void updateAnimation(float factor) { 181 | float initial = mAnimationInitialValue; 182 | float destination = mReverse ? INACTIVE_SCALE : ACTIVE_SCALE; 183 | mCurrentScale = initial + (destination - initial) * factor; 184 | invalidateSelf(); 185 | } 186 | 187 | private final Runnable mUpdater = new Runnable() { 188 | 189 | @Override 190 | public void run() { 191 | 192 | long currentTime = SystemClock.uptimeMillis(); 193 | long diff = currentTime - mStartTime; 194 | if (diff < mDuration) { 195 | float interpolation = mInterpolator.getInterpolation((float) diff / (float) mDuration); 196 | scheduleSelf(mUpdater, currentTime + FRAME_DURATION); 197 | updateAnimation(interpolation); 198 | } else { 199 | unscheduleSelf(mUpdater); 200 | mRunning = false; 201 | updateAnimation(1f); 202 | } 203 | } 204 | }; 205 | 206 | @Override 207 | public void start() { 208 | //No-Op. We control our own animation 209 | } 210 | 211 | @Override 212 | public void stop() { 213 | //No-Op. We control our own animation 214 | } 215 | 216 | @Override 217 | public boolean isRunning() { 218 | return mRunning; 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /library/src/main/java/org/adw/library/widgets/discreteseekbar/internal/Marker.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Gustavo Claramunt (AnderWeb) 2014. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.adw.library.widgets.discreteseekbar.internal; 18 | 19 | import android.content.Context; 20 | import android.content.res.ColorStateList; 21 | import android.content.res.TypedArray; 22 | import android.graphics.Canvas; 23 | import android.graphics.drawable.Drawable; 24 | import android.os.Build; 25 | import android.support.v4.view.ViewCompat; 26 | import android.util.AttributeSet; 27 | import android.util.DisplayMetrics; 28 | import android.view.Gravity; 29 | import android.view.View; 30 | import android.view.ViewGroup; 31 | import android.widget.FrameLayout; 32 | import android.widget.TextView; 33 | 34 | import org.adw.library.widgets.discreteseekbar.R; 35 | import org.adw.library.widgets.discreteseekbar.internal.compat.SeekBarCompat; 36 | import org.adw.library.widgets.discreteseekbar.internal.drawable.MarkerDrawable; 37 | 38 | /** 39 | * {@link android.view.ViewGroup} to be used as the real indicator. 40 | *

41 | * I've used this to be able to accommodate the TextView 42 | * and the {@link org.adw.library.widgets.discreteseekbar.internal.drawable.MarkerDrawable} 43 | * with the required positions and offsets 44 | *

45 | * 46 | * @hide 47 | */ 48 | public class Marker extends ViewGroup implements MarkerDrawable.MarkerAnimationListener { 49 | private static final int PADDING_DP = 4; 50 | private static final int ELEVATION_DP = 8; 51 | //The TextView to show the info 52 | private TextView mNumber; 53 | //The max width of this View 54 | private int mWidth; 55 | //some distance between the thumb and our bubble marker. 56 | //This will be added to our measured height 57 | private int mSeparation; 58 | MarkerDrawable mMarkerDrawable; 59 | 60 | public Marker(Context context, AttributeSet attrs, int defStyleAttr, String maxValue, int thumbSize, int separation) { 61 | super(context, attrs, defStyleAttr); 62 | //as we're reading the parent DiscreteSeekBar attributes, it may wrongly set this view's visibility. 63 | setVisibility(View.VISIBLE); 64 | 65 | DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); 66 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DiscreteSeekBar, 67 | R.attr.discreteSeekBarStyle, R.style.Widget_DiscreteSeekBar); 68 | 69 | int padding = (int) (PADDING_DP * displayMetrics.density) * 2; 70 | int textAppearanceId = a.getResourceId(R.styleable.DiscreteSeekBar_dsb_indicatorTextAppearance, 71 | R.style.Widget_DiscreteIndicatorTextAppearance); 72 | mNumber = new TextView(context); 73 | //Add some padding to this textView so the bubble has some space to breath 74 | mNumber.setPadding(padding, 0, padding, 0); 75 | mNumber.setTextAppearance(context, textAppearanceId); 76 | mNumber.setGravity(Gravity.CENTER); 77 | mNumber.setText(maxValue); 78 | mNumber.setMaxLines(1); 79 | mNumber.setSingleLine(true); 80 | SeekBarCompat.setTextDirection(mNumber, TEXT_DIRECTION_LOCALE); 81 | mNumber.setVisibility(View.INVISIBLE); 82 | 83 | //add some padding for the elevation shadow not to be clipped 84 | //I'm sure there are better ways of doing this... 85 | setPadding(padding, padding, padding, padding); 86 | 87 | resetSizes(maxValue); 88 | 89 | mSeparation = separation; 90 | ColorStateList color = a.getColorStateList(R.styleable.DiscreteSeekBar_dsb_indicatorColor); 91 | mMarkerDrawable = new MarkerDrawable(color, thumbSize); 92 | mMarkerDrawable.setCallback(this); 93 | mMarkerDrawable.setMarkerListener(this); 94 | mMarkerDrawable.setExternalOffset(padding); 95 | 96 | //Elevation for anroid 5+ 97 | float elevation = a.getDimension(R.styleable.DiscreteSeekBar_dsb_indicatorElevation, ELEVATION_DP * displayMetrics.density); 98 | ViewCompat.setElevation(this, elevation); 99 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 100 | SeekBarCompat.setOutlineProvider(this, mMarkerDrawable); 101 | } 102 | a.recycle(); 103 | } 104 | 105 | public void resetSizes(String maxValue) { 106 | DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); 107 | //Account for negative numbers... is there any proper way of getting the biggest string between our range???? 108 | mNumber.setText("-" + maxValue); 109 | //Do a first forced measure call for the TextView (with the biggest text content), 110 | //to calculate the max width and use always the same. 111 | //this avoids the TextView from shrinking and growing when the text content changes 112 | int wSpec = MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels, MeasureSpec.AT_MOST); 113 | int hSpec = MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels, MeasureSpec.AT_MOST); 114 | mNumber.measure(wSpec, hSpec); 115 | mWidth = Math.max(mNumber.getMeasuredWidth(), mNumber.getMeasuredHeight()); 116 | removeView(mNumber); 117 | addView(mNumber, new FrameLayout.LayoutParams(mWidth, mWidth, Gravity.LEFT | Gravity.TOP)); 118 | } 119 | 120 | @Override 121 | protected void dispatchDraw(Canvas canvas) { 122 | mMarkerDrawable.draw(canvas); 123 | super.dispatchDraw(canvas); 124 | } 125 | 126 | @Override 127 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 128 | measureChildren(widthMeasureSpec, heightMeasureSpec); 129 | int widthSize = mWidth + getPaddingLeft() + getPaddingRight(); 130 | int heightSize = mWidth + getPaddingTop() + getPaddingBottom(); 131 | //This diff is the basic calculation of the difference between 132 | //a square side size and its diagonal 133 | //this helps us account for the visual offset created by MarkerDrawable 134 | //when leaving one of the corners un-rounded 135 | int diff = (int) ((1.41f * mWidth) - mWidth) / 2; 136 | setMeasuredDimension(widthSize, heightSize + diff + mSeparation); 137 | } 138 | 139 | @Override 140 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 141 | int left = getPaddingLeft(); 142 | int top = getPaddingTop(); 143 | int right = getWidth() - getPaddingRight(); 144 | int bottom = getHeight() - getPaddingBottom(); 145 | //the TetView is always layout at the top 146 | mNumber.layout(left, top, left + mWidth, top + mWidth); 147 | //the MarkerDrawable uses the whole view, it will adapt itself... 148 | // or it seems so... 149 | mMarkerDrawable.setBounds(left, top, right, bottom); 150 | } 151 | 152 | @Override 153 | protected boolean verifyDrawable(Drawable who) { 154 | return who == mMarkerDrawable || super.verifyDrawable(who); 155 | } 156 | 157 | @Override 158 | protected void onAttachedToWindow() { 159 | super.onAttachedToWindow(); 160 | //HACK: Sometimes, the animateOpen() call is made before the View is attached 161 | //so the drawable cannot schedule itself to run the animation 162 | //I think we can call it here safely. 163 | //I've seen it happen in android 2.3.7 164 | animateOpen(); 165 | } 166 | 167 | public void setValue(CharSequence value) { 168 | mNumber.setText(value); 169 | } 170 | 171 | public CharSequence getValue() { 172 | return mNumber.getText(); 173 | } 174 | 175 | public void animateOpen() { 176 | mMarkerDrawable.stop(); 177 | mMarkerDrawable.animateToPressed(); 178 | } 179 | 180 | public void animateClose() { 181 | mMarkerDrawable.stop(); 182 | mNumber.setVisibility(View.INVISIBLE); 183 | mMarkerDrawable.animateToNormal(); 184 | } 185 | 186 | @Override 187 | public void onOpeningComplete() { 188 | mNumber.setVisibility(View.VISIBLE); 189 | if (getParent() instanceof MarkerDrawable.MarkerAnimationListener) { 190 | ((MarkerDrawable.MarkerAnimationListener) getParent()).onOpeningComplete(); 191 | } 192 | } 193 | 194 | @Override 195 | public void onClosingComplete() { 196 | if (getParent() instanceof MarkerDrawable.MarkerAnimationListener) { 197 | ((MarkerDrawable.MarkerAnimationListener) getParent()).onClosingComplete(); 198 | } 199 | } 200 | 201 | @Override 202 | protected void onDetachedFromWindow() { 203 | super.onDetachedFromWindow(); 204 | mMarkerDrawable.stop(); 205 | } 206 | 207 | public void setColors(int startColor, int endColor) { 208 | mMarkerDrawable.setColors(startColor, endColor); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /library/src/main/java/org/adw/library/widgets/discreteseekbar/internal/drawable/MarkerDrawable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Gustavo Claramunt (AnderWeb) 2014. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.adw.library.widgets.discreteseekbar.internal.drawable; 18 | 19 | import android.content.res.ColorStateList; 20 | import android.graphics.Canvas; 21 | import android.graphics.Color; 22 | import android.graphics.Matrix; 23 | import android.graphics.Paint; 24 | import android.graphics.Path; 25 | import android.graphics.Rect; 26 | import android.graphics.RectF; 27 | import android.graphics.drawable.Animatable; 28 | import android.os.SystemClock; 29 | import android.support.annotation.NonNull; 30 | import android.view.animation.AccelerateDecelerateInterpolator; 31 | import android.view.animation.Interpolator; 32 | 33 | /** 34 | * Implementation of {@link StateDrawable} to draw a morphing marker symbol. 35 | *

36 | * It's basically an implementation of an {@link android.graphics.drawable.Animatable} Drawable with the following details: 37 | *

38 | * 43 | * 44 | * @hide 45 | */ 46 | public class MarkerDrawable extends StateDrawable implements Animatable { 47 | private static final long FRAME_DURATION = 1000 / 60; 48 | private static final int ANIMATION_DURATION = 250; 49 | 50 | private float mCurrentScale = 0f; 51 | private Interpolator mInterpolator; 52 | private long mStartTime; 53 | private boolean mReverse = false; 54 | private boolean mRunning = false; 55 | private int mDuration = ANIMATION_DURATION; 56 | //size of the actual thumb drawable to use as circle state size 57 | private float mClosedStateSize; 58 | //value to store que current scale when starting an animation and interpolate from it 59 | private float mAnimationInitialValue; 60 | //extra offset directed from the View to account 61 | //for its internal padding between circle state and marker state 62 | private int mExternalOffset; 63 | //colors for interpolation 64 | private int mStartColor;//Color when the Marker is OPEN 65 | private int mEndColor;//Color when the arker is CLOSED 66 | 67 | Path mPath = new Path(); 68 | RectF mRect = new RectF(); 69 | Matrix mMatrix = new Matrix(); 70 | private MarkerAnimationListener mMarkerListener; 71 | 72 | public MarkerDrawable(@NonNull ColorStateList tintList, int closedSize) { 73 | super(tintList); 74 | mInterpolator = new AccelerateDecelerateInterpolator(); 75 | mClosedStateSize = closedSize; 76 | mStartColor = tintList.getColorForState(new int[]{android.R.attr.state_enabled, android.R.attr.state_pressed}, tintList.getDefaultColor()); 77 | mEndColor = tintList.getDefaultColor(); 78 | 79 | } 80 | 81 | public void setExternalOffset(int offset) { 82 | mExternalOffset = offset; 83 | } 84 | 85 | /** 86 | * The two colors that will be used for the seek thumb. 87 | * 88 | * @param startColor Color used for the seek thumb 89 | * @param endColor Color used for popup indicator 90 | */ 91 | public void setColors(int startColor, int endColor) { 92 | mStartColor = startColor; 93 | mEndColor = endColor; 94 | } 95 | 96 | @Override 97 | void doDraw(Canvas canvas, Paint paint) { 98 | if (!mPath.isEmpty()) { 99 | paint.setStyle(Paint.Style.FILL); 100 | int color = blendColors(mStartColor, mEndColor, mCurrentScale); 101 | paint.setColor(color); 102 | canvas.drawPath(mPath, paint); 103 | } 104 | } 105 | 106 | public Path getPath() { 107 | return mPath; 108 | } 109 | 110 | @Override 111 | protected void onBoundsChange(Rect bounds) { 112 | super.onBoundsChange(bounds); 113 | computePath(bounds); 114 | } 115 | 116 | private void computePath(Rect bounds) { 117 | final float currentScale = mCurrentScale; 118 | final Path path = mPath; 119 | final RectF rect = mRect; 120 | final Matrix matrix = mMatrix; 121 | 122 | path.reset(); 123 | int totalSize = Math.min(bounds.width(), bounds.height()); 124 | 125 | float initial = mClosedStateSize; 126 | float destination = totalSize; 127 | float currentSize = initial + (destination - initial) * currentScale; 128 | 129 | float halfSize = currentSize / 2f; 130 | float inverseScale = 1f - currentScale; 131 | float cornerSize = halfSize * inverseScale; 132 | float[] corners = new float[]{halfSize, halfSize, halfSize, halfSize, halfSize, halfSize, cornerSize, cornerSize}; 133 | rect.set(bounds.left, bounds.top, bounds.left + currentSize, bounds.top + currentSize); 134 | path.addRoundRect(rect, corners, Path.Direction.CCW); 135 | matrix.reset(); 136 | matrix.postRotate(-45, bounds.left + halfSize, bounds.top + halfSize); 137 | matrix.postTranslate((bounds.width() - currentSize) / 2, 0); 138 | float hDiff = (bounds.bottom - currentSize - mExternalOffset) * inverseScale; 139 | matrix.postTranslate(0, hDiff); 140 | path.transform(matrix); 141 | } 142 | 143 | private void updateAnimation(float factor) { 144 | float initial = mAnimationInitialValue; 145 | float destination = mReverse ? 0f : 1f; 146 | mCurrentScale = initial + (destination - initial) * factor; 147 | computePath(getBounds()); 148 | invalidateSelf(); 149 | } 150 | 151 | public void animateToPressed() { 152 | unscheduleSelf(mUpdater); 153 | mReverse = false; 154 | if (mCurrentScale < 1) { 155 | mRunning = true; 156 | mAnimationInitialValue = mCurrentScale; 157 | float durationFactor = 1f - mCurrentScale; 158 | mDuration = (int) (ANIMATION_DURATION * durationFactor); 159 | mStartTime = SystemClock.uptimeMillis(); 160 | scheduleSelf(mUpdater, mStartTime + FRAME_DURATION); 161 | } else { 162 | notifyFinishedToListener(); 163 | } 164 | } 165 | 166 | public void animateToNormal() { 167 | mReverse = true; 168 | unscheduleSelf(mUpdater); 169 | if (mCurrentScale > 0) { 170 | mRunning = true; 171 | mAnimationInitialValue = mCurrentScale; 172 | float durationFactor = 1f - mCurrentScale; 173 | mDuration = ANIMATION_DURATION - (int) (ANIMATION_DURATION * durationFactor); 174 | mStartTime = SystemClock.uptimeMillis(); 175 | scheduleSelf(mUpdater, mStartTime + FRAME_DURATION); 176 | } else { 177 | notifyFinishedToListener(); 178 | } 179 | } 180 | 181 | private final Runnable mUpdater = new Runnable() { 182 | 183 | @Override 184 | public void run() { 185 | 186 | long currentTime = SystemClock.uptimeMillis(); 187 | long diff = currentTime - mStartTime; 188 | if (diff < mDuration) { 189 | float interpolation = mInterpolator.getInterpolation((float) diff / (float) mDuration); 190 | scheduleSelf(mUpdater, currentTime + FRAME_DURATION); 191 | updateAnimation(interpolation); 192 | } else { 193 | unscheduleSelf(mUpdater); 194 | mRunning = false; 195 | updateAnimation(1f); 196 | notifyFinishedToListener(); 197 | } 198 | } 199 | }; 200 | 201 | public void setMarkerListener(MarkerAnimationListener listener) { 202 | mMarkerListener = listener; 203 | } 204 | 205 | private void notifyFinishedToListener() { 206 | if (mMarkerListener != null) { 207 | if (mReverse) { 208 | mMarkerListener.onClosingComplete(); 209 | } else { 210 | mMarkerListener.onOpeningComplete(); 211 | } 212 | } 213 | } 214 | 215 | @Override 216 | public void start() { 217 | //No-Op. We control our own animation 218 | } 219 | 220 | @Override 221 | public void stop() { 222 | unscheduleSelf(mUpdater); 223 | } 224 | 225 | @Override 226 | public boolean isRunning() { 227 | return mRunning; 228 | } 229 | 230 | private static int blendColors(int color1, int color2, float factor) { 231 | final float inverseFactor = 1f - factor; 232 | float a = (Color.alpha(color1) * factor) + (Color.alpha(color2) * inverseFactor); 233 | float r = (Color.red(color1) * factor) + (Color.red(color2) * inverseFactor); 234 | float g = (Color.green(color1) * factor) + (Color.green(color2) * inverseFactor); 235 | float b = (Color.blue(color1) * factor) + (Color.blue(color2) * inverseFactor); 236 | return Color.argb((int) a, (int) r, (int) g, (int) b); 237 | } 238 | 239 | 240 | /** 241 | * A listener interface to porpagate animation events 242 | * This is the "poor's man" AnimatorListener for this Drawable 243 | */ 244 | public interface MarkerAnimationListener { 245 | public void onClosingComplete(); 246 | 247 | public void onOpeningComplete(); 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /library/src/main/java/org/adw/library/widgets/discreteseekbar/internal/PopupIndicator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Gustavo Claramunt (AnderWeb) 2014. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.adw.library.widgets.discreteseekbar.internal; 18 | 19 | import android.content.Context; 20 | import android.graphics.PixelFormat; 21 | import android.graphics.Point; 22 | import android.graphics.Rect; 23 | import android.os.IBinder; 24 | import android.support.v4.view.GravityCompat; 25 | import android.util.AttributeSet; 26 | import android.util.DisplayMetrics; 27 | import android.view.Gravity; 28 | import android.view.View; 29 | import android.view.ViewGroup; 30 | import android.view.WindowManager; 31 | import android.widget.FrameLayout; 32 | 33 | import org.adw.library.widgets.discreteseekbar.internal.compat.SeekBarCompat; 34 | import org.adw.library.widgets.discreteseekbar.internal.drawable.MarkerDrawable; 35 | 36 | /** 37 | * Class to manage the floating bubble thing, similar (but quite worse tested than {@link android.widget.PopupWindow} 38 | *

39 | *

40 | * This will attach a View to the Window (full-width, measured-height, positioned just under the thumb) 41 | *

42 | * 43 | * @hide 44 | * @see #showIndicator(android.view.View, android.graphics.Rect) 45 | * @see #dismiss() 46 | * @see #dismissComplete() 47 | * @see org.adw.library.widgets.discreteseekbar.internal.PopupIndicator.Floater 48 | */ 49 | public class PopupIndicator { 50 | 51 | private final WindowManager mWindowManager; 52 | private boolean mShowing; 53 | private Floater mPopupView; 54 | //Outside listener for the DiscreteSeekBar to get MarkerDrawable animation events. 55 | //The whole chain of events goes this way: 56 | //MarkerDrawable->Marker->Floater->mListener->DiscreteSeekBar.... 57 | //... phew! 58 | private MarkerDrawable.MarkerAnimationListener mListener; 59 | private int[] mDrawingLocation = new int[2]; 60 | Point screenSize = new Point(); 61 | 62 | public PopupIndicator(Context context, AttributeSet attrs, int defStyleAttr, String maxValue, int thumbSize, int separation) { 63 | mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 64 | mPopupView = new Floater(context, attrs, defStyleAttr, maxValue, thumbSize, separation); 65 | } 66 | 67 | public void updateSizes(String maxValue) { 68 | dismissComplete(); 69 | if (mPopupView != null) { 70 | mPopupView.mMarker.resetSizes(maxValue); 71 | } 72 | } 73 | 74 | public void setListener(MarkerDrawable.MarkerAnimationListener listener) { 75 | mListener = listener; 76 | } 77 | 78 | /** 79 | * We want the Floater to be full-width because the contents will be moved from side to side. 80 | * We may/should change this in the future to use just the PARENT View width and/or pass it in the constructor 81 | */ 82 | private void measureFloater() { 83 | int specWidth = View.MeasureSpec.makeMeasureSpec(screenSize.x, View.MeasureSpec.EXACTLY); 84 | int specHeight = View.MeasureSpec.makeMeasureSpec(screenSize.y, View.MeasureSpec.AT_MOST); 85 | mPopupView.measure(specWidth, specHeight); 86 | } 87 | 88 | public void setValue(CharSequence value) { 89 | mPopupView.mMarker.setValue(value); 90 | } 91 | 92 | public boolean isShowing() { 93 | return mShowing; 94 | } 95 | 96 | public void showIndicator(View parent, Rect touchBounds) { 97 | if (isShowing()) { 98 | mPopupView.mMarker.animateOpen(); 99 | return; 100 | } 101 | 102 | IBinder windowToken = parent.getWindowToken(); 103 | if (windowToken != null) { 104 | WindowManager.LayoutParams p = createPopupLayout(windowToken); 105 | 106 | p.gravity = Gravity.TOP | GravityCompat.START; 107 | updateLayoutParamsForPosiion(parent, p, touchBounds.bottom); 108 | mShowing = true; 109 | 110 | translateViewIntoPosition(touchBounds.centerX()); 111 | invokePopup(p); 112 | } 113 | } 114 | 115 | public void move(int x) { 116 | if (!isShowing()) { 117 | return; 118 | } 119 | translateViewIntoPosition(x); 120 | } 121 | 122 | public void setColors(int startColor, int endColor) { 123 | mPopupView.setColors(startColor, endColor); 124 | } 125 | 126 | /** 127 | * This will start the closing animation of the Marker and call onClosingComplete when finished 128 | */ 129 | public void dismiss() { 130 | mPopupView.mMarker.animateClose(); 131 | } 132 | 133 | /** 134 | * FORCE the popup window to be removed. 135 | * You typically calls this when the parent view is being removed from the window to avoid a Window Leak 136 | */ 137 | public void dismissComplete() { 138 | if (isShowing()) { 139 | mShowing = false; 140 | try { 141 | mWindowManager.removeViewImmediate(mPopupView); 142 | } finally { 143 | } 144 | } 145 | } 146 | 147 | private void updateLayoutParamsForPosiion(View anchor, WindowManager.LayoutParams p, int yOffset) { 148 | DisplayMetrics displayMetrics = anchor.getResources().getDisplayMetrics(); 149 | screenSize.set(displayMetrics.widthPixels, displayMetrics.heightPixels); 150 | 151 | measureFloater(); 152 | int measuredHeight = mPopupView.getMeasuredHeight(); 153 | int paddingBottom = mPopupView.mMarker.getPaddingBottom(); 154 | anchor.getLocationInWindow(mDrawingLocation); 155 | p.x = 0; 156 | p.y = mDrawingLocation[1] - measuredHeight + yOffset + paddingBottom; 157 | p.width = screenSize.x; 158 | p.height = measuredHeight; 159 | } 160 | 161 | private void translateViewIntoPosition(final int x) { 162 | mPopupView.setFloatOffset(x + mDrawingLocation[0]); 163 | } 164 | 165 | private void invokePopup(WindowManager.LayoutParams p) { 166 | mWindowManager.addView(mPopupView, p); 167 | mPopupView.mMarker.animateOpen(); 168 | } 169 | 170 | private WindowManager.LayoutParams createPopupLayout(IBinder token) { 171 | WindowManager.LayoutParams p = new WindowManager.LayoutParams(); 172 | p.gravity = Gravity.START | Gravity.TOP; 173 | p.width = ViewGroup.LayoutParams.MATCH_PARENT; 174 | p.height = ViewGroup.LayoutParams.MATCH_PARENT; 175 | p.format = PixelFormat.TRANSLUCENT; 176 | p.flags = computeFlags(p.flags); 177 | p.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; 178 | p.token = token; 179 | p.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN; 180 | p.setTitle("DiscreteSeekBar Indicator:" + Integer.toHexString(hashCode())); 181 | 182 | return p; 183 | } 184 | 185 | /** 186 | * I'm NOT completely sure how all this bitwise things work... 187 | * 188 | * @param curFlags 189 | * @return 190 | */ 191 | private int computeFlags(int curFlags) { 192 | curFlags &= ~( 193 | WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES | 194 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | 195 | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | 196 | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH | 197 | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS | 198 | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); 199 | curFlags |= WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES; 200 | curFlags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; 201 | curFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; 202 | curFlags |= WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; 203 | return curFlags; 204 | } 205 | 206 | /** 207 | * Small FrameLayout class to hold and move the bubble around when requested 208 | * I wanted to use the {@link Marker} directly 209 | * but doing so would make some things harder to implement 210 | * (like moving the marker around, having the Marker's outline to work, etc) 211 | */ 212 | private class Floater extends FrameLayout implements MarkerDrawable.MarkerAnimationListener { 213 | private Marker mMarker; 214 | private int mOffset; 215 | 216 | public Floater(Context context, AttributeSet attrs, int defStyleAttr, String maxValue, int thumbSize, int separation) { 217 | super(context); 218 | mMarker = new Marker(context, attrs, defStyleAttr, maxValue, thumbSize, separation); 219 | addView(mMarker, new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.LEFT | Gravity.TOP)); 220 | } 221 | 222 | @Override 223 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 224 | measureChildren(widthMeasureSpec, heightMeasureSpec); 225 | int widthSize = MeasureSpec.getSize(widthMeasureSpec); 226 | int heightSie = mMarker.getMeasuredHeight(); 227 | setMeasuredDimension(widthSize, heightSie); 228 | } 229 | 230 | @Override 231 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 232 | int centerDiffX = mMarker.getMeasuredWidth() / 2; 233 | int offset = (mOffset - centerDiffX); 234 | mMarker.layout(offset, 0, offset + mMarker.getMeasuredWidth(), mMarker.getMeasuredHeight()); 235 | } 236 | 237 | public void setFloatOffset(int x) { 238 | mOffset = x; 239 | int centerDiffX = mMarker.getMeasuredWidth() / 2; 240 | int offset = (x - centerDiffX); 241 | mMarker.offsetLeftAndRight(offset - mMarker.getLeft()); 242 | //Without hardware acceleration (or API levels<11), offsetting a view seems to NOT invalidate the proper area. 243 | //We should calc the proper invalidate Rect but this will be for now... 244 | if (!SeekBarCompat.isHardwareAccelerated(this)) { 245 | invalidate(); 246 | } 247 | } 248 | 249 | @Override 250 | public void onClosingComplete() { 251 | if (mListener != null) { 252 | mListener.onClosingComplete(); 253 | } 254 | dismissComplete(); 255 | } 256 | 257 | @Override 258 | public void onOpeningComplete() { 259 | if (mListener != null) { 260 | mListener.onOpeningComplete(); 261 | } 262 | } 263 | 264 | public void setColors(int startColor, int endColor) { 265 | mMarker.setColors(startColor, endColor); 266 | } 267 | } 268 | 269 | } 270 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /library/src/main/java/org/adw/library/widgets/discreteseekbar/DiscreteSeekBar.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Gustavo Claramunt (AnderWeb) 2014. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.adw.library.widgets.discreteseekbar; 18 | 19 | import android.content.Context; 20 | import android.content.res.ColorStateList; 21 | import android.content.res.TypedArray; 22 | import android.graphics.Canvas; 23 | import android.graphics.Color; 24 | import android.graphics.Rect; 25 | import android.graphics.drawable.Drawable; 26 | import android.os.Build; 27 | import android.os.Parcel; 28 | import android.os.Parcelable; 29 | import android.support.annotation.NonNull; 30 | import android.support.annotation.Nullable; 31 | import android.support.v4.graphics.drawable.DrawableCompat; 32 | import android.support.v4.view.MotionEventCompat; 33 | import android.support.v4.view.ViewCompat; 34 | import android.util.AttributeSet; 35 | import android.util.TypedValue; 36 | import android.view.KeyEvent; 37 | import android.view.MotionEvent; 38 | import android.view.View; 39 | import android.view.ViewConfiguration; 40 | import android.view.ViewParent; 41 | 42 | import org.adw.library.widgets.discreteseekbar.internal.PopupIndicator; 43 | import org.adw.library.widgets.discreteseekbar.internal.compat.AnimatorCompat; 44 | import org.adw.library.widgets.discreteseekbar.internal.compat.SeekBarCompat; 45 | import org.adw.library.widgets.discreteseekbar.internal.drawable.MarkerDrawable; 46 | import org.adw.library.widgets.discreteseekbar.internal.drawable.ThumbDrawable; 47 | import org.adw.library.widgets.discreteseekbar.internal.drawable.TrackRectDrawable; 48 | 49 | import java.util.Formatter; 50 | import java.util.Locale; 51 | 52 | public class DiscreteSeekBar extends View { 53 | 54 | /** 55 | * Interface to propagate seekbar change event 56 | */ 57 | public interface OnProgressChangeListener { 58 | /** 59 | * When the {@link DiscreteSeekBar} value changes 60 | * 61 | * @param seekBar The DiscreteSeekBar 62 | * @param value the new value 63 | * @param fromUser if the change was made from the user or not (i.e. the developer calling {@link #setProgress(int)} 64 | */ 65 | public void onProgressChanged(DiscreteSeekBar seekBar, int value, boolean fromUser); 66 | 67 | public void onStartTrackingTouch(DiscreteSeekBar seekBar); 68 | 69 | public void onStopTrackingTouch(DiscreteSeekBar seekBar); 70 | } 71 | 72 | /** 73 | * Interface to transform the current internal value of this DiscreteSeekBar to anther one for the visualization. 74 | *

75 | * This will be used on the floating bubble to display a different value if needed. 76 | *

77 | * Using this in conjunction with {@link #setIndicatorFormatter(String)} you will be able to manipulate the 78 | * value seen by the user 79 | * 80 | * @see #setIndicatorFormatter(String) 81 | * @see #setNumericTransformer(DiscreteSeekBar.NumericTransformer) 82 | */ 83 | public static abstract class NumericTransformer { 84 | /** 85 | * Return the desired value to be shown to the user. 86 | * This value will be formatted using the format specified by {@link #setIndicatorFormatter} before displaying it 87 | * 88 | * @param value The value to be transformed 89 | * @return The transformed int 90 | */ 91 | public abstract int transform(int value); 92 | 93 | /** 94 | * Return the desired value to be shown to the user. 95 | * This value will be displayed 'as is' without further formatting. 96 | * 97 | * @param value The value to be transformed 98 | * @return A formatted string 99 | */ 100 | public String transformToString(int value) { 101 | return String.valueOf(value); 102 | } 103 | 104 | /** 105 | * Used to indicate which transform will be used. If this method returns true, 106 | * {@link #transformToString(int)} will be used, otherwise {@link #transform(int)} 107 | * will be used 108 | */ 109 | public boolean useStringTransform() { 110 | return false; 111 | } 112 | } 113 | 114 | 115 | private static class DefaultNumericTransformer extends NumericTransformer { 116 | 117 | @Override 118 | public int transform(int value) { 119 | return value; 120 | } 121 | } 122 | 123 | 124 | private static final boolean isLollipopOrGreater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; 125 | //We want to always use a formatter so the indicator numbers are "translated" to specific locales. 126 | private static final String DEFAULT_FORMATTER = "%d"; 127 | 128 | private static final int PRESSED_STATE = android.R.attr.state_pressed; 129 | private static final int FOCUSED_STATE = android.R.attr.state_focused; 130 | private static final int PROGRESS_ANIMATION_DURATION = 250; 131 | private static final int INDICATOR_DELAY_FOR_TAPS = 150; 132 | private static final int DEFAULT_THUMB_COLOR = 0xff009688; 133 | private static final int SEPARATION_DP = 5; 134 | private ThumbDrawable mThumb; 135 | private TrackRectDrawable mTrack; 136 | private TrackRectDrawable mScrubber; 137 | private Drawable mRipple; 138 | 139 | private int mTrackHeight; 140 | private int mScrubberHeight; 141 | private int mAddedTouchBounds; 142 | 143 | private int mMax; 144 | private int mMin; 145 | private int mValue; 146 | private int mKeyProgressIncrement = 1; 147 | private boolean mMirrorForRtl = false; 148 | private boolean mAllowTrackClick = true; 149 | private boolean mIndicatorPopupEnabled = true; 150 | //We use our own Formatter to avoid creating new instances on every progress change 151 | Formatter mFormatter; 152 | private String mIndicatorFormatter; 153 | private NumericTransformer mNumericTransformer; 154 | private StringBuilder mFormatBuilder; 155 | private OnProgressChangeListener mPublicChangeListener; 156 | private boolean mIsDragging; 157 | private int mDragOffset; 158 | 159 | private Rect mInvalidateRect = new Rect(); 160 | private Rect mTempRect = new Rect(); 161 | private PopupIndicator mIndicator; 162 | private AnimatorCompat mPositionAnimator; 163 | private float mAnimationPosition; 164 | private int mAnimationTarget; 165 | private float mDownX; 166 | private float mTouchSlop; 167 | 168 | public DiscreteSeekBar(Context context) { 169 | this(context, null); 170 | } 171 | 172 | public DiscreteSeekBar(Context context, AttributeSet attrs) { 173 | this(context, attrs, R.attr.discreteSeekBarStyle); 174 | } 175 | 176 | public DiscreteSeekBar(Context context, AttributeSet attrs, int defStyleAttr) { 177 | super(context, attrs, defStyleAttr); 178 | setFocusable(true); 179 | setWillNotDraw(false); 180 | 181 | mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 182 | float density = context.getResources().getDisplayMetrics().density; 183 | 184 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DiscreteSeekBar, 185 | defStyleAttr, R.style.Widget_DiscreteSeekBar); 186 | 187 | int max = 100; 188 | int min = 0; 189 | int value = 0; 190 | mMirrorForRtl = a.getBoolean(R.styleable.DiscreteSeekBar_dsb_mirrorForRtl, mMirrorForRtl); 191 | mAllowTrackClick = a.getBoolean(R.styleable.DiscreteSeekBar_dsb_allowTrackClickToDrag, mAllowTrackClick); 192 | mIndicatorPopupEnabled = a.getBoolean(R.styleable.DiscreteSeekBar_dsb_indicatorPopupEnabled, mIndicatorPopupEnabled); 193 | mTrackHeight = a.getDimensionPixelSize(R.styleable.DiscreteSeekBar_dsb_trackHeight, (int) (1 * density)); 194 | mScrubberHeight = a.getDimensionPixelSize(R.styleable.DiscreteSeekBar_dsb_scrubberHeight, (int) (4 * density)); 195 | int thumbSize = a.getDimensionPixelSize(R.styleable.DiscreteSeekBar_dsb_thumbSize, (int) (density * ThumbDrawable.DEFAULT_SIZE_DP)); 196 | int separation = a.getDimensionPixelSize(R.styleable.DiscreteSeekBar_dsb_indicatorSeparation, 197 | (int) (SEPARATION_DP * density)); 198 | 199 | //Extra pixels for a minimum touch area of 32dp 200 | int touchBounds = (int) (density * 32); 201 | mAddedTouchBounds = Math.max(0, (touchBounds - thumbSize) / 2); 202 | 203 | int indexMax = R.styleable.DiscreteSeekBar_dsb_max; 204 | int indexMin = R.styleable.DiscreteSeekBar_dsb_min; 205 | int indexValue = R.styleable.DiscreteSeekBar_dsb_value; 206 | final TypedValue out = new TypedValue(); 207 | //Not sure why, but we wanted to be able to use dimensions here... 208 | if (a.getValue(indexMax, out)) { 209 | if (out.type == TypedValue.TYPE_DIMENSION) { 210 | max = a.getDimensionPixelSize(indexMax, max); 211 | } else { 212 | max = a.getInteger(indexMax, max); 213 | } 214 | } 215 | if (a.getValue(indexMin, out)) { 216 | if (out.type == TypedValue.TYPE_DIMENSION) { 217 | min = a.getDimensionPixelSize(indexMin, min); 218 | } else { 219 | min = a.getInteger(indexMin, min); 220 | } 221 | } 222 | if (a.getValue(indexValue, out)) { 223 | if (out.type == TypedValue.TYPE_DIMENSION) { 224 | value = a.getDimensionPixelSize(indexValue, value); 225 | } else { 226 | value = a.getInteger(indexValue, value); 227 | } 228 | } 229 | 230 | mMin = min; 231 | mMax = Math.max(min + 1, max); 232 | mValue = Math.max(min, Math.min(max, value)); 233 | updateKeyboardRange(); 234 | 235 | mIndicatorFormatter = a.getString(R.styleable.DiscreteSeekBar_dsb_indicatorFormatter); 236 | 237 | ColorStateList trackColor = a.getColorStateList(R.styleable.DiscreteSeekBar_dsb_trackColor); 238 | ColorStateList progressColor = a.getColorStateList(R.styleable.DiscreteSeekBar_dsb_progressColor); 239 | ColorStateList rippleColor = a.getColorStateList(R.styleable.DiscreteSeekBar_dsb_rippleColor); 240 | boolean editMode = isInEditMode(); 241 | if (editMode || rippleColor == null) { 242 | rippleColor = new ColorStateList(new int[][]{new int[]{}}, new int[]{Color.DKGRAY}); 243 | } 244 | if (editMode || trackColor == null) { 245 | trackColor = new ColorStateList(new int[][]{new int[]{}}, new int[]{Color.GRAY}); 246 | } 247 | if (editMode || progressColor == null) { 248 | progressColor = new ColorStateList(new int[][]{new int[]{}}, new int[]{DEFAULT_THUMB_COLOR}); 249 | } 250 | 251 | mRipple = SeekBarCompat.getRipple(rippleColor); 252 | if (isLollipopOrGreater) { 253 | SeekBarCompat.setBackground(this, mRipple); 254 | } else { 255 | mRipple.setCallback(this); 256 | } 257 | 258 | TrackRectDrawable shapeDrawable = new TrackRectDrawable(trackColor); 259 | mTrack = shapeDrawable; 260 | mTrack.setCallback(this); 261 | 262 | shapeDrawable = new TrackRectDrawable(progressColor); 263 | mScrubber = shapeDrawable; 264 | mScrubber.setCallback(this); 265 | 266 | mThumb = new ThumbDrawable(progressColor, thumbSize); 267 | mThumb.setCallback(this); 268 | mThumb.setBounds(0, 0, mThumb.getIntrinsicWidth(), mThumb.getIntrinsicHeight()); 269 | 270 | 271 | if (!editMode) { 272 | mIndicator = new PopupIndicator(context, attrs, defStyleAttr, convertValueToMessage(mMax), 273 | thumbSize, thumbSize + mAddedTouchBounds + separation); 274 | mIndicator.setListener(mFloaterListener); 275 | } 276 | a.recycle(); 277 | 278 | setNumericTransformer(new DefaultNumericTransformer()); 279 | 280 | } 281 | 282 | /** 283 | * Sets the current Indicator formatter string 284 | * 285 | * @param formatter 286 | * @see String#format(String, Object...) 287 | * @see #setNumericTransformer(DiscreteSeekBar.NumericTransformer) 288 | */ 289 | public void setIndicatorFormatter(@Nullable String formatter) { 290 | mIndicatorFormatter = formatter; 291 | updateProgressMessage(mValue); 292 | } 293 | 294 | /** 295 | * Sets the current {@link DiscreteSeekBar.NumericTransformer} 296 | * 297 | * @param transformer 298 | * @see #getNumericTransformer() 299 | */ 300 | public void setNumericTransformer(@Nullable NumericTransformer transformer) { 301 | mNumericTransformer = transformer != null ? transformer : new DefaultNumericTransformer(); 302 | //We need to refresh the PopupIndicator view 303 | updateIndicatorSizes(); 304 | updateProgressMessage(mValue); 305 | } 306 | 307 | /** 308 | * Retrieves the current {@link DiscreteSeekBar.NumericTransformer} 309 | * 310 | * @return NumericTransformer 311 | * @see #setNumericTransformer 312 | */ 313 | public NumericTransformer getNumericTransformer() { 314 | return mNumericTransformer; 315 | } 316 | 317 | /** 318 | * Sets the maximum value for this DiscreteSeekBar 319 | * if the supplied argument is smaller than the Current MIN value, 320 | * the MIN value will be set to MAX-1 321 | *

322 | *

323 | * Also if the current progress is out of the new range, it will be set to MIN 324 | *

325 | * 326 | * @param max 327 | * @see #setMin(int) 328 | * @see #setProgress(int) 329 | */ 330 | public void setMax(int max) { 331 | mMax = max; 332 | if (mMax < mMin) { 333 | setMin(mMax - 1); 334 | } 335 | updateKeyboardRange(); 336 | 337 | if (mValue < mMin || mValue > mMax) { 338 | setProgress(mMin); 339 | } 340 | //We need to refresh the PopupIndicator view 341 | updateIndicatorSizes(); 342 | } 343 | 344 | public int getMax() { 345 | return mMax; 346 | } 347 | 348 | /** 349 | * Sets the minimum value for this DiscreteSeekBar 350 | * if the supplied argument is bigger than the Current MAX value, 351 | * the MAX value will be set to MIN+1 352 | *

353 | * Also if the current progress is out of the new range, it will be set to MIN 354 | *

355 | * 356 | * @param min 357 | * @see #setMax(int) 358 | * @see #setProgress(int) 359 | */ 360 | public void setMin(int min) { 361 | mMin = min; 362 | if (mMin > mMax) { 363 | setMax(mMin + 1); 364 | } 365 | updateKeyboardRange(); 366 | 367 | if (mValue < mMin || mValue > mMax) { 368 | setProgress(mMin); 369 | } 370 | } 371 | 372 | public int getMin() { 373 | return mMin; 374 | } 375 | 376 | /** 377 | * Sets the current progress for this DiscreteSeekBar 378 | * The supplied argument will be capped to the current MIN-MAX range 379 | * 380 | * @param progress 381 | * @see #setMax(int) 382 | * @see #setMin(int) 383 | */ 384 | public void setProgress(int progress) { 385 | setProgress(progress, false); 386 | } 387 | 388 | private void setProgress(int value, boolean fromUser) { 389 | value = Math.max(mMin, Math.min(mMax, value)); 390 | if (isAnimationRunning()) { 391 | mPositionAnimator.cancel(); 392 | } 393 | 394 | if (mValue != value) { 395 | mValue = value; 396 | notifyProgress(value, fromUser); 397 | updateProgressMessage(value); 398 | updateThumbPosFromCurrentProgress(); 399 | } 400 | } 401 | 402 | /** 403 | * Get the current progress 404 | * 405 | * @return the current progress :-P 406 | */ 407 | public int getProgress() { 408 | return mValue; 409 | } 410 | 411 | /** 412 | * Sets a listener to receive notifications of changes to the DiscreteSeekBar's progress level. Also 413 | * provides notifications of when the DiscreteSeekBar shows/hides the bubble indicator. 414 | * 415 | * @param listener The seek bar notification listener 416 | * @see DiscreteSeekBar.OnProgressChangeListener 417 | */ 418 | public void setOnProgressChangeListener(@Nullable OnProgressChangeListener listener) { 419 | mPublicChangeListener = listener; 420 | } 421 | 422 | /** 423 | * Sets the color of the seek thumb, as well as the color of the popup indicator. 424 | * 425 | * @param thumbColor The color the seek thumb will be changed to 426 | * @param indicatorColor The color the popup indicator will be changed to 427 | * The indicator will animate from thumbColor to indicatorColor 428 | * when opening 429 | */ 430 | public void setThumbColor(int thumbColor, int indicatorColor) { 431 | mThumb.setColorStateList(ColorStateList.valueOf(thumbColor)); 432 | mIndicator.setColors(indicatorColor, thumbColor); 433 | } 434 | 435 | /** 436 | * Sets the color of the seek thumb, as well as the color of the popup indicator. 437 | * 438 | * @param thumbColorStateList The ColorStateList the seek thumb will be changed to 439 | * @param indicatorColor The color the popup indicator will be changed to 440 | * The indicator will animate from thumbColorStateList(pressed state) to indicatorColor 441 | * when opening 442 | */ 443 | public void setThumbColor(@NonNull ColorStateList thumbColorStateList, int indicatorColor) { 444 | mThumb.setColorStateList(thumbColorStateList); 445 | //we use the "pressed" color to morph the indicator from it to its own color 446 | int thumbColor = thumbColorStateList.getColorForState(new int[]{PRESSED_STATE}, thumbColorStateList.getDefaultColor()); 447 | mIndicator.setColors(indicatorColor, thumbColor); 448 | } 449 | 450 | /** 451 | * Sets the color of the seekbar scrubber 452 | * 453 | * @param color The color the track scrubber will be changed to 454 | */ 455 | public void setScrubberColor(int color) { 456 | mScrubber.setColorStateList(ColorStateList.valueOf(color)); 457 | } 458 | 459 | /** 460 | * Sets the color of the seekbar scrubber 461 | * 462 | * @param colorStateList The ColorStateList the track scrubber will be changed to 463 | */ 464 | public void setScrubberColor(@NonNull ColorStateList colorStateList) { 465 | mScrubber.setColorStateList(colorStateList); 466 | } 467 | 468 | /** 469 | * Sets the color of the seekbar ripple 470 | * 471 | * @param color The color the track ripple will be changed to 472 | */ 473 | public void setRippleColor(int color) { 474 | setRippleColor(new ColorStateList(new int[][]{new int[]{}}, new int[]{color})); 475 | } 476 | 477 | /** 478 | * Sets the color of the seekbar ripple 479 | * 480 | * @param colorStateList The ColorStateList the track ripple will be changed to 481 | */ 482 | public void setRippleColor(@NonNull ColorStateList colorStateList) { 483 | SeekBarCompat.setRippleColor(mRipple, colorStateList); 484 | } 485 | 486 | /** 487 | * Sets the color of the seekbar scrubber 488 | * 489 | * @param color The color the track will be changed to 490 | */ 491 | public void setTrackColor(int color) { 492 | mTrack.setColorStateList(ColorStateList.valueOf(color)); 493 | } 494 | 495 | /** 496 | * Sets the color of the seekbar scrubber 497 | * 498 | * @param colorStateList The ColorStateList the track will be changed to 499 | */ 500 | public void setTrackColor(@NonNull ColorStateList colorStateList) { 501 | mTrack.setColorStateList(colorStateList); 502 | } 503 | 504 | /** 505 | * If {@code enabled} is false the indicator won't appear. By default popup indicator is 506 | * enabled. 507 | */ 508 | public void setIndicatorPopupEnabled(boolean enabled) { 509 | this.mIndicatorPopupEnabled = enabled; 510 | } 511 | 512 | private void updateIndicatorSizes() { 513 | if (!isInEditMode()) { 514 | if (mNumericTransformer.useStringTransform()) { 515 | mIndicator.updateSizes(mNumericTransformer.transformToString(mMax)); 516 | } else { 517 | mIndicator.updateSizes(convertValueToMessage(mNumericTransformer.transform(mMax))); 518 | } 519 | } 520 | 521 | } 522 | 523 | private void notifyProgress(int value, boolean fromUser) { 524 | if (mPublicChangeListener != null) { 525 | mPublicChangeListener.onProgressChanged(DiscreteSeekBar.this, value, fromUser); 526 | } 527 | onValueChanged(value); 528 | } 529 | 530 | private void notifyBubble(boolean open) { 531 | if (open) { 532 | onShowBubble(); 533 | } else { 534 | onHideBubble(); 535 | } 536 | } 537 | 538 | /** 539 | * When the {@link DiscreteSeekBar} enters pressed or focused state 540 | * the bubble with the value will be shown, and this method called 541 | *

542 | * Subclasses may override this to add functionality around this event 543 | *

544 | */ 545 | protected void onShowBubble() { 546 | } 547 | 548 | /** 549 | * When the {@link DiscreteSeekBar} exits pressed or focused state 550 | * the bubble with the value will be hidden, and this method called 551 | *

552 | * Subclasses may override this to add functionality around this event 553 | *

554 | */ 555 | protected void onHideBubble() { 556 | } 557 | 558 | /** 559 | * When the {@link DiscreteSeekBar} value changes this method is called 560 | *

561 | * Subclasses may override this to add functionality around this event 562 | * without having to specify a {@link DiscreteSeekBar.OnProgressChangeListener} 563 | *

564 | */ 565 | protected void onValueChanged(int value) { 566 | } 567 | 568 | private void updateKeyboardRange() { 569 | int range = mMax - mMin; 570 | if ((mKeyProgressIncrement == 0) || (range / mKeyProgressIncrement > 20)) { 571 | // It will take the user too long to change this via keys, change it 572 | // to something more reasonable 573 | mKeyProgressIncrement = Math.max(1, Math.round((float) range / 20)); 574 | } 575 | } 576 | 577 | 578 | @Override 579 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 580 | int widthSize = MeasureSpec.getSize(widthMeasureSpec); 581 | int height = mThumb.getIntrinsicHeight() + getPaddingTop() + getPaddingBottom(); 582 | height += (mAddedTouchBounds * 2); 583 | setMeasuredDimension(widthSize, height); 584 | } 585 | 586 | @Override 587 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 588 | super.onLayout(changed, left, top, right, bottom); 589 | if (changed) { 590 | removeCallbacks(mShowIndicatorRunnable); 591 | if (!isInEditMode()) { 592 | mIndicator.dismissComplete(); 593 | } 594 | updateFromDrawableState(); 595 | } 596 | } 597 | 598 | @Override 599 | public void scheduleDrawable(Drawable who, Runnable what, long when) { 600 | super.scheduleDrawable(who, what, when); 601 | } 602 | 603 | @Override 604 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 605 | super.onSizeChanged(w, h, oldw, oldh); 606 | int thumbWidth = mThumb.getIntrinsicWidth(); 607 | int thumbHeight = mThumb.getIntrinsicHeight(); 608 | int addedThumb = mAddedTouchBounds; 609 | int halfThumb = thumbWidth / 2; 610 | int paddingLeft = getPaddingLeft() + addedThumb; 611 | int paddingRight = getPaddingRight(); 612 | int bottom = getHeight() - getPaddingBottom() - addedThumb; 613 | mThumb.setBounds(paddingLeft, bottom - thumbHeight, paddingLeft + thumbWidth, bottom); 614 | int trackHeight = Math.max(mTrackHeight / 2, 1); 615 | mTrack.setBounds(paddingLeft + halfThumb, bottom - halfThumb - trackHeight, 616 | getWidth() - halfThumb - paddingRight - addedThumb, bottom - halfThumb + trackHeight); 617 | int scrubberHeight = Math.max(mScrubberHeight / 2, 2); 618 | mScrubber.setBounds(paddingLeft + halfThumb, bottom - halfThumb - scrubberHeight, 619 | paddingLeft + halfThumb, bottom - halfThumb + scrubberHeight); 620 | 621 | //Update the thumb position after size changed 622 | updateThumbPosFromCurrentProgress(); 623 | } 624 | 625 | @Override 626 | protected synchronized void onDraw(Canvas canvas) { 627 | if (!isLollipopOrGreater) { 628 | mRipple.draw(canvas); 629 | } 630 | super.onDraw(canvas); 631 | mTrack.draw(canvas); 632 | mScrubber.draw(canvas); 633 | mThumb.draw(canvas); 634 | } 635 | 636 | @Override 637 | protected void drawableStateChanged() { 638 | super.drawableStateChanged(); 639 | updateFromDrawableState(); 640 | } 641 | 642 | private void updateFromDrawableState() { 643 | int[] state = getDrawableState(); 644 | boolean focused = false; 645 | boolean pressed = false; 646 | for (int i : state) { 647 | if (i == FOCUSED_STATE) { 648 | focused = true; 649 | } else if (i == PRESSED_STATE) { 650 | pressed = true; 651 | } 652 | } 653 | if (isEnabled() && (focused || pressed) && mIndicatorPopupEnabled) { 654 | //We want to add a small delay here to avoid 655 | //poping in/out on simple taps 656 | removeCallbacks(mShowIndicatorRunnable); 657 | postDelayed(mShowIndicatorRunnable, INDICATOR_DELAY_FOR_TAPS); 658 | } else { 659 | hideFloater(); 660 | } 661 | mThumb.setState(state); 662 | mTrack.setState(state); 663 | mScrubber.setState(state); 664 | mRipple.setState(state); 665 | } 666 | 667 | private void updateProgressMessage(int value) { 668 | if (!isInEditMode()) { 669 | if (mNumericTransformer.useStringTransform()) { 670 | mIndicator.setValue(mNumericTransformer.transformToString(value)); 671 | } else { 672 | mIndicator.setValue(convertValueToMessage(mNumericTransformer.transform(value))); 673 | } 674 | } 675 | } 676 | 677 | private String convertValueToMessage(int value) { 678 | String format = mIndicatorFormatter != null ? mIndicatorFormatter : DEFAULT_FORMATTER; 679 | //We're trying to re-use the Formatter here to avoid too much memory allocations 680 | //But I'm not completey sure if it's doing anything good... :( 681 | //Previously, this condition was wrong so the Formatter was always re-created 682 | //But as I fixed the condition, the formatter started outputting trash characters from previous 683 | //calls, so I mark the StringBuilder as empty before calling format again. 684 | 685 | //Anyways, I see the memory usage still go up on every call to this method 686 | //and I have no clue on how to fix that... damn Strings... 687 | if (mFormatter == null || !mFormatter.locale().equals(Locale.getDefault())) { 688 | int bufferSize = format.length() + String.valueOf(mMax).length(); 689 | if (mFormatBuilder == null) { 690 | mFormatBuilder = new StringBuilder(bufferSize); 691 | } else { 692 | mFormatBuilder.ensureCapacity(bufferSize); 693 | } 694 | mFormatter = new Formatter(mFormatBuilder, Locale.getDefault()); 695 | } else { 696 | mFormatBuilder.setLength(0); 697 | } 698 | return mFormatter.format(format, value).toString(); 699 | } 700 | 701 | @Override 702 | public boolean onTouchEvent(MotionEvent event) { 703 | if (!isEnabled()) { 704 | return false; 705 | } 706 | int actionMasked = MotionEventCompat.getActionMasked(event); 707 | switch (actionMasked) { 708 | case MotionEvent.ACTION_DOWN: 709 | mDownX = event.getX(); 710 | startDragging(event, isInScrollingContainer()); 711 | break; 712 | case MotionEvent.ACTION_MOVE: 713 | if (isDragging()) { 714 | updateDragging(event); 715 | } else { 716 | final float x = event.getX(); 717 | if (Math.abs(x - mDownX) > mTouchSlop) { 718 | startDragging(event, false); 719 | } 720 | } 721 | break; 722 | case MotionEvent.ACTION_UP: 723 | if (!isDragging() && mAllowTrackClick) { 724 | startDragging(event, false); 725 | updateDragging(event); 726 | } 727 | stopDragging(); 728 | break; 729 | case MotionEvent.ACTION_CANCEL: 730 | stopDragging(); 731 | break; 732 | } 733 | return true; 734 | } 735 | 736 | private boolean isInScrollingContainer() { 737 | return SeekBarCompat.isInScrollingContainer(getParent()); 738 | } 739 | 740 | private boolean startDragging(MotionEvent ev, boolean ignoreTrackIfInScrollContainer) { 741 | final Rect bounds = mTempRect; 742 | mThumb.copyBounds(bounds); 743 | //Grow the current thumb rect for a bigger touch area 744 | bounds.inset(-mAddedTouchBounds, -mAddedTouchBounds); 745 | mIsDragging = (bounds.contains((int) ev.getX(), (int) ev.getY())); 746 | if (!mIsDragging && mAllowTrackClick && !ignoreTrackIfInScrollContainer) { 747 | //If the user clicked outside the thumb, we compute the current position 748 | //and force an immediate drag to it. 749 | mIsDragging = true; 750 | mDragOffset = (bounds.width() / 2) - mAddedTouchBounds; 751 | updateDragging(ev); 752 | //As the thumb may have moved, get the bounds again 753 | mThumb.copyBounds(bounds); 754 | bounds.inset(-mAddedTouchBounds, -mAddedTouchBounds); 755 | } 756 | if (mIsDragging) { 757 | setPressed(true); 758 | attemptClaimDrag(); 759 | setHotspot(ev.getX(), ev.getY()); 760 | mDragOffset = (int) (ev.getX() - bounds.left - mAddedTouchBounds); 761 | if (mPublicChangeListener != null) { 762 | mPublicChangeListener.onStartTrackingTouch(this); 763 | } 764 | } 765 | return mIsDragging; 766 | } 767 | 768 | private boolean isDragging() { 769 | return mIsDragging; 770 | } 771 | 772 | private void stopDragging() { 773 | if (mPublicChangeListener != null) { 774 | mPublicChangeListener.onStopTrackingTouch(this); 775 | } 776 | mIsDragging = false; 777 | setPressed(false); 778 | } 779 | 780 | @Override 781 | public boolean onKeyDown(int keyCode, KeyEvent event) { 782 | //TODO: Should we reverse the keys for RTL? The framework's SeekBar does NOT.... 783 | boolean handled = false; 784 | if (isEnabled()) { 785 | int progress = getAnimatedProgress(); 786 | switch (keyCode) { 787 | case KeyEvent.KEYCODE_DPAD_LEFT: 788 | handled = true; 789 | if (progress <= mMin) break; 790 | animateSetProgress(progress - mKeyProgressIncrement); 791 | break; 792 | case KeyEvent.KEYCODE_DPAD_RIGHT: 793 | handled = true; 794 | if (progress >= mMax) break; 795 | animateSetProgress(progress + mKeyProgressIncrement); 796 | break; 797 | } 798 | } 799 | 800 | return handled || super.onKeyDown(keyCode, event); 801 | } 802 | 803 | private int getAnimatedProgress() { 804 | return isAnimationRunning() ? getAnimationTarget() : mValue; 805 | } 806 | 807 | 808 | boolean isAnimationRunning() { 809 | return mPositionAnimator != null && mPositionAnimator.isRunning(); 810 | } 811 | 812 | void animateSetProgress(int progress) { 813 | final float curProgress = isAnimationRunning() ? getAnimationPosition() : getProgress(); 814 | 815 | if (progress < mMin) { 816 | progress = mMin; 817 | } else if (progress > mMax) { 818 | progress = mMax; 819 | } 820 | //setProgressValueOnly(progress); 821 | 822 | if (mPositionAnimator != null) { 823 | mPositionAnimator.cancel(); 824 | } 825 | 826 | mAnimationTarget = progress; 827 | mPositionAnimator = AnimatorCompat.create(curProgress, 828 | progress, new AnimatorCompat.AnimationFrameUpdateListener() { 829 | @Override 830 | public void onAnimationFrame(float currentValue) { 831 | setAnimationPosition(currentValue); 832 | } 833 | }); 834 | mPositionAnimator.setDuration(PROGRESS_ANIMATION_DURATION); 835 | mPositionAnimator.start(); 836 | } 837 | 838 | private int getAnimationTarget() { 839 | return mAnimationTarget; 840 | } 841 | 842 | void setAnimationPosition(float position) { 843 | mAnimationPosition = position; 844 | float currentScale = (position - mMin) / (float) (mMax - mMin); 845 | updateProgressFromAnimation(currentScale); 846 | } 847 | 848 | float getAnimationPosition() { 849 | return mAnimationPosition; 850 | } 851 | 852 | 853 | private void updateDragging(MotionEvent ev) { 854 | setHotspot(ev.getX(), ev.getY()); 855 | int x = (int) ev.getX(); 856 | Rect oldBounds = mThumb.getBounds(); 857 | int halfThumb = oldBounds.width() / 2; 858 | int addedThumb = mAddedTouchBounds; 859 | int newX = x - mDragOffset + halfThumb; 860 | int left = getPaddingLeft() + halfThumb + addedThumb; 861 | int right = getWidth() - (getPaddingRight() + halfThumb + addedThumb); 862 | if (newX < left) { 863 | newX = left; 864 | } else if (newX > right) { 865 | newX = right; 866 | } 867 | 868 | int available = right - left; 869 | float scale = (float) (newX - left) / (float) available; 870 | if (isRtl()) { 871 | scale = 1f - scale; 872 | } 873 | int progress = Math.round((scale * (mMax - mMin)) + mMin); 874 | setProgress(progress, true); 875 | } 876 | 877 | private void updateProgressFromAnimation(float scale) { 878 | Rect bounds = mThumb.getBounds(); 879 | int halfThumb = bounds.width() / 2; 880 | int addedThumb = mAddedTouchBounds; 881 | int left = getPaddingLeft() + halfThumb + addedThumb; 882 | int right = getWidth() - (getPaddingRight() + halfThumb + addedThumb); 883 | int available = right - left; 884 | int progress = Math.round((scale * (mMax - mMin)) + mMin); 885 | //we don't want to just call setProgress here to avoid the animation being cancelled, 886 | //and this position is not bound to a real progress value but interpolated 887 | if (progress != getProgress()) { 888 | mValue = progress; 889 | notifyProgress(mValue, true); 890 | updateProgressMessage(progress); 891 | } 892 | final int thumbPos = (int) (scale * available + 0.5f); 893 | updateThumbPos(thumbPos); 894 | } 895 | 896 | private void updateThumbPosFromCurrentProgress() { 897 | int thumbWidth = mThumb.getIntrinsicWidth(); 898 | int addedThumb = mAddedTouchBounds; 899 | int halfThumb = thumbWidth / 2; 900 | float scaleDraw = (mValue - mMin) / (float) (mMax - mMin); 901 | 902 | //This doesn't matter if RTL, as we just need the "avaiable" area 903 | int left = getPaddingLeft() + halfThumb + addedThumb; 904 | int right = getWidth() - (getPaddingRight() + halfThumb + addedThumb); 905 | int available = right - left; 906 | 907 | final int thumbPos = (int) (scaleDraw * available + 0.5f); 908 | updateThumbPos(thumbPos); 909 | } 910 | 911 | private void updateThumbPos(int posX) { 912 | int thumbWidth = mThumb.getIntrinsicWidth(); 913 | int halfThumb = thumbWidth / 2; 914 | int start; 915 | if (isRtl()) { 916 | start = getWidth() - getPaddingRight() - mAddedTouchBounds; 917 | posX = start - posX - thumbWidth; 918 | } else { 919 | start = getPaddingLeft() + mAddedTouchBounds; 920 | posX = start + posX; 921 | } 922 | mThumb.copyBounds(mInvalidateRect); 923 | mThumb.setBounds(posX, mInvalidateRect.top, posX + thumbWidth, mInvalidateRect.bottom); 924 | if (isRtl()) { 925 | mScrubber.getBounds().right = start - halfThumb; 926 | mScrubber.getBounds().left = posX + halfThumb; 927 | } else { 928 | mScrubber.getBounds().left = start + halfThumb; 929 | mScrubber.getBounds().right = posX + halfThumb; 930 | } 931 | final Rect finalBounds = mTempRect; 932 | mThumb.copyBounds(finalBounds); 933 | if (!isInEditMode()) { 934 | mIndicator.move(finalBounds.centerX()); 935 | } 936 | 937 | mInvalidateRect.inset(-mAddedTouchBounds, -mAddedTouchBounds); 938 | finalBounds.inset(-mAddedTouchBounds, -mAddedTouchBounds); 939 | mInvalidateRect.union(finalBounds); 940 | SeekBarCompat.setHotspotBounds(mRipple, finalBounds.left, finalBounds.top, finalBounds.right, finalBounds.bottom); 941 | invalidate(mInvalidateRect); 942 | } 943 | 944 | 945 | private void setHotspot(float x, float y) { 946 | DrawableCompat.setHotspot(mRipple, x, y); 947 | } 948 | 949 | @Override 950 | protected boolean verifyDrawable(Drawable who) { 951 | return who == mThumb || who == mTrack || who == mScrubber || who == mRipple || super.verifyDrawable(who); 952 | } 953 | 954 | private void attemptClaimDrag() { 955 | ViewParent parent = getParent(); 956 | if (parent != null) { 957 | parent.requestDisallowInterceptTouchEvent(true); 958 | } 959 | } 960 | 961 | private Runnable mShowIndicatorRunnable = new Runnable() { 962 | @Override 963 | public void run() { 964 | showFloater(); 965 | } 966 | }; 967 | 968 | private void showFloater() { 969 | if (!isInEditMode()) { 970 | mThumb.animateToPressed(); 971 | mIndicator.showIndicator(this, mThumb.getBounds()); 972 | notifyBubble(true); 973 | } 974 | } 975 | 976 | private void hideFloater() { 977 | removeCallbacks(mShowIndicatorRunnable); 978 | if (!isInEditMode()) { 979 | mIndicator.dismiss(); 980 | notifyBubble(false); 981 | } 982 | } 983 | 984 | private MarkerDrawable.MarkerAnimationListener mFloaterListener = new MarkerDrawable.MarkerAnimationListener() { 985 | @Override 986 | public void onClosingComplete() { 987 | mThumb.animateToNormal(); 988 | } 989 | 990 | @Override 991 | public void onOpeningComplete() { 992 | 993 | } 994 | 995 | }; 996 | 997 | @Override 998 | protected void onDetachedFromWindow() { 999 | super.onDetachedFromWindow(); 1000 | removeCallbacks(mShowIndicatorRunnable); 1001 | if (!isInEditMode()) { 1002 | mIndicator.dismissComplete(); 1003 | } 1004 | } 1005 | 1006 | public boolean isRtl() { 1007 | return (ViewCompat.getLayoutDirection(this) == LAYOUT_DIRECTION_RTL) && mMirrorForRtl; 1008 | } 1009 | 1010 | @Override 1011 | protected Parcelable onSaveInstanceState() { 1012 | Parcelable superState = super.onSaveInstanceState(); 1013 | CustomState state = new CustomState(superState); 1014 | state.progress = getProgress(); 1015 | state.max = mMax; 1016 | state.min = mMin; 1017 | return state; 1018 | } 1019 | 1020 | @Override 1021 | protected void onRestoreInstanceState(Parcelable state) { 1022 | if (state == null || !state.getClass().equals(CustomState.class)) { 1023 | super.onRestoreInstanceState(state); 1024 | return; 1025 | } 1026 | 1027 | CustomState customState = (CustomState) state; 1028 | setMin(customState.min); 1029 | setMax(customState.max); 1030 | setProgress(customState.progress, false); 1031 | super.onRestoreInstanceState(customState.getSuperState()); 1032 | } 1033 | 1034 | static class CustomState extends BaseSavedState { 1035 | private int progress; 1036 | private int max; 1037 | private int min; 1038 | 1039 | public CustomState(Parcel source) { 1040 | super(source); 1041 | progress = source.readInt(); 1042 | max = source.readInt(); 1043 | min = source.readInt(); 1044 | } 1045 | 1046 | public CustomState(Parcelable superState) { 1047 | super(superState); 1048 | } 1049 | 1050 | @Override 1051 | public void writeToParcel(Parcel outcoming, int flags) { 1052 | super.writeToParcel(outcoming, flags); 1053 | outcoming.writeInt(progress); 1054 | outcoming.writeInt(max); 1055 | outcoming.writeInt(min); 1056 | } 1057 | 1058 | public static final Creator CREATOR = 1059 | new Creator() { 1060 | 1061 | @Override 1062 | public CustomState[] newArray(int size) { 1063 | return new CustomState[size]; 1064 | } 1065 | 1066 | @Override 1067 | public CustomState createFromParcel(Parcel incoming) { 1068 | return new CustomState(incoming); 1069 | } 1070 | }; 1071 | } 1072 | } 1073 | --------------------------------------------------------------------------------