├── .gitignore ├── .idea ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── encodings.xml ├── gradle.xml ├── misc.xml ├── modules.xml └── runConfigurations.xml ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── tstb.gif └── tstb1.gif ├── settings.gradle ├── tristatetogglebutton_library ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── it │ │ └── beppi │ │ └── tristatetogglebutton_library │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ ├── com │ │ │ └── facebook │ │ │ │ └── rebound │ │ │ │ ├── AndroidSpringLooperFactory.java │ │ │ │ ├── BaseSpringSystem.java │ │ │ │ ├── OrigamiValueConverter.java │ │ │ │ ├── SimpleSpringListener.java │ │ │ │ ├── Spring.java │ │ │ │ ├── SpringConfig.java │ │ │ │ ├── SpringConfigRegistry.java │ │ │ │ ├── SpringListener.java │ │ │ │ ├── SpringLooper.java │ │ │ │ ├── SpringSystem.java │ │ │ │ ├── SpringSystemListener.java │ │ │ │ ├── SpringUtil.java │ │ │ │ ├── SteppingLooper.java │ │ │ │ ├── SynchronousLooper.java │ │ │ │ └── ui │ │ │ │ ├── SpringConfiguratorView.java │ │ │ │ └── Util.java │ │ └── it │ │ │ └── beppi │ │ │ └── tristatetogglebutton_library │ │ │ └── TriStateToggleButton.java │ └── res │ │ └── values │ │ ├── strings.xml │ │ └── toggle_button_attrs.xml │ └── test │ └── java │ └── it │ └── beppi │ └── tristatetogglebutton_library │ └── ExampleUnitTest.java └── tristatetogglebutton_sample ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src ├── androidTest └── java │ └── it │ └── beppi │ └── tristatetogglebutton │ └── ExampleInstrumentedTest.java ├── main ├── AndroidManifest.xml ├── java │ └── it │ │ └── beppi │ │ └── tristatetogglebuttonsample │ │ └── SampleActivity.java └── res │ ├── layout │ └── activity_sample.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml └── test └── java └── it └── beppi └── tristatetogglebutton └── ExampleUnitTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | Android 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 62 | 63 | 64 | 65 | 66 | 1.8 67 | 68 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TriState Toggle Button 2 | A fully customizable and super-easy tri-state toggle button (switch button if you prefer) for Android, based on iOS look and feel. 3 | Can act with three independent states, or with two states like a standard checkbox, or with two states plus one undefined. 4 | 5 | 6 |
7 | Android Arsenal 8 | 9 | * Out-of-the-box working 3-state toggle 10 | * Fully customizable and styleable 11 | * Can become a classic 2-state toggle returning booleans 12 | * Can become a 2.5-state toggle: on/off and an unselectable mid button 13 | * Can be enabled / disabled 14 | * Can be programmatically controlled 15 | * Works both with clicks and with swipes 16 | 17 |
18 | 19 | ### Setup (Gradle) 20 | In your project's build.gradle file: 21 | 22 | allprojects { 23 | repositories { 24 | ... 25 | maven { url "https://jitpack.io" } 26 | ... 27 | } 28 | } 29 | 30 | In your Application's or Module's build.gradle file: 31 | 32 | dependencies { 33 | ... 34 | compile 'com.github.BeppiMenozzi:TriStateToggleButton:1.1.4' 35 | ... 36 | } 37 | 38 | ### Setup (Eclipse) 39 | Whaaaaat? 40 | 41 | ### Minimal usage 42 | Layout: 43 | 44 | ... 45 | xmlns:app="http://schemas.android.com/apk/res-auto" 46 | ... 47 | 52 | 53 | Listener: 54 | 55 | ... 56 | TriStateToggleButton tstb_1 = (TriStateToggleButton) findViewById(R.id.tstb_1); 57 | tstb_1.setOnToggleChanged(new TriStateToggleButton.OnToggleChanged() { 58 | @Override 59 | public void onToggle(TriStateToggleButton.ToggleStatus toggleStatus, boolean booleanToggleStatus, int toggleIntValue) { 60 | switch (toggleStatus) { 61 | case off: break; 62 | case mid: break; 63 | case on: break; 64 | } 65 | } 66 | }); 67 | ... 68 | 69 | Inside onToggle() you can use the ToggleStatus type values, or limit yourself to use booleans or integers (0, 1, 2) if you want it easy. 70 | 71 | To have a two-states toggle button: 72 | 73 | 79 | 80 | To have a two-states toggle button, with an undefined starting value: 81 | 82 | 89 | 90 | Browse the full example here: 91 | Example 92 | 93 | ### Attributes description 94 | List of attributes with description: 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 |
tbBorderWidthWidth of the border of the widget
tdOffBorderColorColor of the width that appears with the button in state off. Used also for animations.
tbOffColorColor of the background of the toggle when off
tbMidColorColor of the background of the toggle with in mid position
tbOnColorColor of the background of the toggle when on
tbSpotColorColor of the handle of the toggle
tbAnimateTrue for animation
tbDefaultStatusStarting value for the toggle
tbIsMidSelectableIf false, the toggle becomes a standard two-states toggle, but can still assume the mid value if forced programmatically or set as default
tbSwipeSensitivityPixelsNumber of pixels a swipe must travel to fire a toggle event. Default is 200. If set to zero, swipes are disabled
107 | 108 | ### New in 1.1.3 109 | * Fixed: error drawing toggle in mid position when visibility passed from GONE to VISIBLE 110 | 111 | ### New in 1.1.0 112 | * Added swipe gesture management together with normal click 113 | * Gradle update 114 | * More documentation 115 | 116 | ### New in 1.0.5 117 | * Fixed: added super in setEnabled() 118 | 119 | ### New in 1.0.4 120 | * Fixed: setting a boolean value programmatically sometimes didn't update status 121 | 122 | ### New in 1.0.3 123 | * Toggle now can set and return integer values (0, 1, 2) 124 | * Warning: onToggle() changed to include integer values 125 | * Added static functions to convert from/to booleans and integers to/from toggleStatus 126 | 127 | Used in 128 | ------- 129 | * https://play.google.com/store/apps/details?id=it.beppi.bootwithlogo 130 | 131 | Credits 132 | ------- 133 | This project is strongly based on (and contains parts of code of) the very beautiful Toggle Button by zcweng. 134 | 135 | Author 136 | ------- 137 | * Beppi Menozzi 138 | 139 | License 140 | ------- 141 | The MIT License (MIT) 142 | 143 | Copyright (c) 2016 Beppi Menozzi 144 | 145 | Permission is hereby granted, free of charge, to any person obtaining a copy 146 | of this software and associated documentation files (the "Software"), to deal 147 | in the Software without restriction, including without limitation the rights 148 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 149 | copies of the Software, and to permit persons to whom the Software is 150 | furnished to do so, subject to the following conditions: 151 | 152 | The above copyright notice and this permission notice shall be included in all 153 | copies or substantial portions of the Software. 154 | 155 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 156 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 157 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 158 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 159 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 160 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 161 | SOFTWARE. 162 | 163 | 164 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:2.2.3' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | maven { url "https://jitpack.io" } 19 | } 20 | } 21 | 22 | task clean(type: Delete) { 23 | delete rootProject.buildDir 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 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BeppiMenozzi/TriStateToggleButton/3b21708073403039f8b3199805173a9b987bd95c/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Dec 28 10:00:20 PST 2015 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.14.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /images/tstb.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BeppiMenozzi/TriStateToggleButton/3b21708073403039f8b3199805173a9b987bd95c/images/tstb.gif -------------------------------------------------------------------------------- /images/tstb1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BeppiMenozzi/TriStateToggleButton/3b21708073403039f8b3199805173a9b987bd95c/images/tstb1.gif -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':tristatetogglebutton_sample', ':tristatetogglebutton_library' 2 | -------------------------------------------------------------------------------- /tristatetogglebutton_library/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /tristatetogglebutton_library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 25 5 | buildToolsVersion "25.0.0" 6 | 7 | defaultConfig { 8 | minSdkVersion 9 9 | targetSdkVersion 25 10 | versionCode 8 11 | versionName "1.1.4" 12 | 13 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 14 | 15 | } 16 | buildTypes { 17 | release { 18 | minifyEnabled false 19 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 20 | } 21 | } 22 | } 23 | 24 | dependencies { 25 | compile fileTree(dir: 'libs', include: ['*.jar']) 26 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 27 | exclude group: 'com.android.support', module: 'support-annotations' 28 | }) 29 | compile 'com.android.support:appcompat-v7:25.0.1' 30 | testCompile 'junit:junit:4.12' 31 | } 32 | -------------------------------------------------------------------------------- /tristatetogglebutton_library/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in F:\Android\android-sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /tristatetogglebutton_library/src/androidTest/java/it/beppi/tristatetogglebutton_library/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package it.beppi.tristatetogglebutton_library; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("it.beppi.tristatetogglebutton_library.test", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tristatetogglebutton_library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tristatetogglebutton_library/src/main/java/com/facebook/rebound/AndroidSpringLooperFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | */ 10 | 11 | package com.facebook.rebound; 12 | 13 | import android.annotation.TargetApi; 14 | import android.os.Build; 15 | import android.os.Handler; 16 | import android.os.SystemClock; 17 | import android.view.Choreographer; 18 | 19 | /** 20 | * Android version of the spring looper that uses the most appropriate frame callback mechanism 21 | * available. It uses Android's {@link Choreographer} when available, otherwise it uses a 22 | * {@link Handler}. 23 | */ 24 | abstract class AndroidSpringLooperFactory { 25 | 26 | /** 27 | * Create an Android {@link com.facebook.rebound.SpringLooper} for the detected Android platform. 28 | * @return a SpringLooper 29 | */ 30 | public static SpringLooper createSpringLooper() { 31 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { 32 | return ChoreographerAndroidSpringLooper.create(); 33 | } else { 34 | return LegacyAndroidSpringLooper.create(); 35 | } 36 | } 37 | 38 | /** 39 | * The base implementation of the Android spring looper, using a {@link Handler} for the 40 | * frame callbacks. 41 | */ 42 | private static class LegacyAndroidSpringLooper extends SpringLooper { 43 | 44 | private final Handler mHandler; 45 | private final Runnable mLooperRunnable; 46 | private boolean mStarted; 47 | private long mLastTime; 48 | 49 | /** 50 | * @return an Android spring looper using a new {@link Handler} instance 51 | */ 52 | public static SpringLooper create() { 53 | return new LegacyAndroidSpringLooper(new Handler()); 54 | } 55 | 56 | public LegacyAndroidSpringLooper(Handler handler) { 57 | mHandler = handler; 58 | mLooperRunnable = new Runnable() { 59 | @Override 60 | public void run() { 61 | if (!mStarted || mSpringSystem == null) { 62 | return; 63 | } 64 | long currentTime = SystemClock.uptimeMillis(); 65 | mSpringSystem.loop(currentTime - mLastTime); 66 | mHandler.post(mLooperRunnable); 67 | } 68 | }; 69 | } 70 | 71 | @Override 72 | public void start() { 73 | if (mStarted) { 74 | return; 75 | } 76 | mStarted = true; 77 | mLastTime = SystemClock.uptimeMillis(); 78 | mHandler.removeCallbacks(mLooperRunnable); 79 | mHandler.post(mLooperRunnable); 80 | } 81 | 82 | @Override 83 | public void stop() { 84 | mStarted = false; 85 | mHandler.removeCallbacks(mLooperRunnable); 86 | } 87 | } 88 | 89 | /** 90 | * The Jelly Bean and up implementation of the spring looper that uses Android's 91 | * {@link Choreographer} instead of a {@link Handler} 92 | */ 93 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN) 94 | private static class ChoreographerAndroidSpringLooper extends SpringLooper { 95 | 96 | private final Choreographer mChoreographer; 97 | private final Choreographer.FrameCallback mFrameCallback; 98 | private boolean mStarted; 99 | private long mLastTime; 100 | 101 | /** 102 | * @return an Android spring choreographer using the system {@link Choreographer} 103 | */ 104 | public static ChoreographerAndroidSpringLooper create() { 105 | return new ChoreographerAndroidSpringLooper(Choreographer.getInstance()); 106 | } 107 | 108 | public ChoreographerAndroidSpringLooper(Choreographer choreographer) { 109 | mChoreographer = choreographer; 110 | mFrameCallback = new Choreographer.FrameCallback() { 111 | @Override 112 | public void doFrame(long frameTimeNanos) { 113 | if (!mStarted || mSpringSystem == null) { 114 | return; 115 | } 116 | long currentTime = SystemClock.uptimeMillis(); 117 | mSpringSystem.loop(currentTime - mLastTime); 118 | mLastTime = currentTime; 119 | mChoreographer.postFrameCallback(mFrameCallback); 120 | } 121 | }; 122 | } 123 | 124 | @Override 125 | public void start() { 126 | if (mStarted) { 127 | return; 128 | } 129 | mStarted = true; 130 | mLastTime = SystemClock.uptimeMillis(); 131 | mChoreographer.removeFrameCallback(mFrameCallback); 132 | mChoreographer.postFrameCallback(mFrameCallback); 133 | } 134 | 135 | @Override 136 | public void stop() { 137 | mStarted = false; 138 | mChoreographer.removeFrameCallback(mFrameCallback); 139 | } 140 | } 141 | } -------------------------------------------------------------------------------- /tristatetogglebutton_library/src/main/java/com/facebook/rebound/BaseSpringSystem.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | */ 10 | 11 | package com.facebook.rebound; 12 | 13 | import java.util.ArrayList; 14 | import java.util.Collection; 15 | import java.util.Collections; 16 | import java.util.HashMap; 17 | import java.util.List; 18 | import java.util.Map; 19 | import java.util.Set; 20 | import java.util.concurrent.CopyOnWriteArraySet; 21 | 22 | /** 23 | * BaseSpringSystem maintains the set of springs within an Application context. It is responsible for 24 | * Running the spring integration loop and maintains a registry of all the Springs it solves for. 25 | * In addition to listening to physics events on the individual Springs in the system, listeners 26 | * can be added to the BaseSpringSystem itself to provide pre and post integration setup. 27 | */ 28 | public class BaseSpringSystem { 29 | 30 | private final Map mSpringRegistry = new HashMap(); 31 | private final Set mActiveSprings = new CopyOnWriteArraySet(); 32 | private final SpringLooper mSpringLooper; 33 | private final CopyOnWriteArraySet mListeners = new CopyOnWriteArraySet(); 34 | private boolean mIdle = true; 35 | 36 | /** 37 | * create a new BaseSpringSystem 38 | * @param springLooper parameterized springLooper to allow testability of the 39 | * physics loop 40 | */ 41 | public BaseSpringSystem(SpringLooper springLooper) { 42 | if (springLooper == null) { 43 | throw new IllegalArgumentException("springLooper is required"); 44 | } 45 | mSpringLooper = springLooper; 46 | mSpringLooper.setSpringSystem(this); 47 | } 48 | 49 | /** 50 | * check if the system is idle 51 | * @return is the system idle 52 | */ 53 | public boolean getIsIdle() { 54 | return mIdle; 55 | } 56 | 57 | /** 58 | * create a spring with a random uuid for its name. 59 | * @return the spring 60 | */ 61 | public Spring createSpring() { 62 | Spring spring = new Spring(this); 63 | registerSpring(spring); 64 | return spring; 65 | } 66 | 67 | /** 68 | * get a spring by name 69 | * @param id id of the spring to retrieve 70 | * @return Spring with the specified key 71 | */ 72 | public Spring getSpringById(String id) { 73 | if (id == null) { 74 | throw new IllegalArgumentException("id is required"); 75 | } 76 | return mSpringRegistry.get(id); 77 | } 78 | 79 | /** 80 | * return all the springs in the simulator 81 | * @return all the springs 82 | */ 83 | public List getAllSprings() { 84 | Collection collection = mSpringRegistry.values(); 85 | List list; 86 | if (collection instanceof List) { 87 | list = (List)collection; 88 | } else { 89 | list = new ArrayList(collection); 90 | } 91 | return Collections.unmodifiableList(list); 92 | } 93 | 94 | /** 95 | * Registers a Spring to this BaseSpringSystem so it can be iterated if active. 96 | * @param spring the Spring to register 97 | */ 98 | void registerSpring(Spring spring) { 99 | if (spring == null) { 100 | throw new IllegalArgumentException("spring is required"); 101 | } 102 | if (mSpringRegistry.containsKey(spring.getId())) { 103 | throw new IllegalArgumentException("spring is already registered"); } 104 | mSpringRegistry.put(spring.getId(), spring); 105 | } 106 | 107 | /** 108 | * Deregisters a Spring from this BaseSpringSystem, so it won't be iterated anymore. The Spring should 109 | * not be used anymore after doing this. 110 | * 111 | * @param spring the Spring to deregister 112 | */ 113 | void deregisterSpring(Spring spring) { 114 | if (spring == null) { 115 | throw new IllegalArgumentException("spring is required"); 116 | } 117 | mActiveSprings.remove(spring); 118 | mSpringRegistry.remove(spring.getId()); 119 | } 120 | 121 | /** 122 | * update the springs in the system 123 | * @param deltaTime delta since last update in millis 124 | */ 125 | void advance(double deltaTime) { 126 | for (Spring spring : mActiveSprings) { 127 | // advance time in seconds 128 | if (spring.systemShouldAdvance()) { 129 | spring.advance(deltaTime / 1000.0); 130 | } else { 131 | mActiveSprings.remove(spring); 132 | } 133 | } 134 | } 135 | 136 | /** 137 | * loop the system until idle 138 | * @param ellapsedMillis = = 139 | */ 140 | public void loop(double ellapsedMillis) { 141 | for (SpringSystemListener listener : mListeners) { 142 | listener.onBeforeIntegrate(this); 143 | } 144 | advance(ellapsedMillis); 145 | if (mActiveSprings.isEmpty()) { 146 | mIdle = true; 147 | } 148 | for (SpringSystemListener listener : mListeners) { 149 | listener.onAfterIntegrate(this); 150 | } 151 | if (mIdle) { 152 | mSpringLooper.stop(); 153 | } 154 | } 155 | 156 | /** 157 | * This is used internally by the {@link Spring}s created by this {@link BaseSpringSystem} to notify 158 | * it has reached a state where it needs to be iterated. This will add the spring to the list of 159 | * active springs on this system and start the iteration if the system was idle before this call. 160 | * @param springId the id of the Spring to be activated 161 | */ 162 | void activateSpring(String springId) { 163 | Spring spring = mSpringRegistry.get(springId); 164 | if (spring == null) { 165 | throw new IllegalArgumentException("springId " + springId + " does not reference a registered spring"); 166 | } 167 | mActiveSprings.add(spring); 168 | if (getIsIdle()) { 169 | mIdle = false; 170 | mSpringLooper.start(); 171 | } 172 | } 173 | 174 | /** listeners 175 | * @param newListener = = 176 | **/ 177 | public void addListener(SpringSystemListener newListener) { 178 | if (newListener == null) { 179 | throw new IllegalArgumentException("newListener is required"); 180 | } 181 | mListeners.add(newListener); 182 | } 183 | 184 | public void removeListener(SpringSystemListener listenerToRemove) { 185 | if (listenerToRemove == null) { 186 | throw new IllegalArgumentException("listenerToRemove is required"); 187 | } 188 | mListeners.remove(listenerToRemove); 189 | } 190 | 191 | public void removeAllListeners() { 192 | mListeners.clear(); 193 | } 194 | } 195 | 196 | 197 | -------------------------------------------------------------------------------- /tristatetogglebutton_library/src/main/java/com/facebook/rebound/OrigamiValueConverter.java: -------------------------------------------------------------------------------- 1 | package com.facebook.rebound; 2 | 3 | public class OrigamiValueConverter { 4 | 5 | public static double tensionFromOrigamiValue(double oValue) { 6 | return oValue == 0 ? 0 : (oValue - 30.0) * 3.62 + 194.0; 7 | } 8 | 9 | public static double origamiValueFromTension(double tension) { 10 | return tension == 0 ? 0 : (tension - 194.0) / 3.62 + 30.0; 11 | } 12 | 13 | public static double frictionFromOrigamiValue(double oValue) { 14 | return oValue == 0 ? 0 : (oValue - 8.0) * 3.0 + 25.0; 15 | } 16 | 17 | public static double origamiValueFromFriction(double friction) { 18 | return friction == 0 ? 0 : (friction - 25.0) / 3.0 + 8.0; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /tristatetogglebutton_library/src/main/java/com/facebook/rebound/SimpleSpringListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | */ 10 | 11 | package com.facebook.rebound; 12 | 13 | public class SimpleSpringListener implements SpringListener { 14 | @Override 15 | public void onSpringUpdate(Spring spring) { 16 | } 17 | 18 | @Override 19 | public void onSpringAtRest(Spring spring) { 20 | } 21 | 22 | @Override 23 | public void onSpringActivate(Spring spring) { 24 | } 25 | 26 | @Override 27 | public void onSpringEndStateChange(Spring spring) { 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tristatetogglebutton_library/src/main/java/com/facebook/rebound/Spring.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | */ 10 | 11 | package com.facebook.rebound; 12 | 13 | import java.util.concurrent.CopyOnWriteArraySet; 14 | 15 | /** 16 | * Classical spring implementing Hooke's law with configurable friction and tension. 17 | */ 18 | public class Spring { 19 | 20 | // unique incrementer id for springs 21 | private static int ID = 0; 22 | 23 | // maximum amount of time to simulate per physics iteration in seconds (4 frames at 60 FPS) 24 | private static final double MAX_DELTA_TIME_SEC = 0.064; 25 | // fixed timestep to use in the physics solver in seconds 26 | private static final double SOLVER_TIMESTEP_SEC = 0.001; 27 | private SpringConfig mSpringConfig; 28 | private boolean mOvershootClampingEnabled; 29 | 30 | // storage for the current and prior physics state while integration is occurring 31 | private static class PhysicsState { 32 | double position; 33 | double velocity; 34 | } 35 | 36 | // unique id for the spring in the system 37 | private final String mId; 38 | // all physics simulation objects are final and reused in each processing pass 39 | private final PhysicsState mCurrentState = new PhysicsState(); 40 | private final PhysicsState mPreviousState = new PhysicsState(); 41 | private final PhysicsState mTempState = new PhysicsState(); 42 | private double mStartValue; 43 | private double mEndValue; 44 | private boolean mWasAtRest = true; 45 | // thresholds for determining when the spring is at rest 46 | private double mRestSpeedThreshold = 0.005; 47 | private double mDisplacementFromRestThreshold = 0.005; 48 | private CopyOnWriteArraySet mListeners = new CopyOnWriteArraySet(); 49 | private double mTimeAccumulator = 0; 50 | 51 | private final BaseSpringSystem mSpringSystem; 52 | 53 | /** 54 | * create a new spring 55 | */ 56 | Spring(BaseSpringSystem springSystem) { 57 | if (springSystem == null) { 58 | throw new IllegalArgumentException("Spring cannot be created outside of a BaseSpringSystem"); 59 | } 60 | mSpringSystem = springSystem; 61 | mId = "spring:" + ID++; 62 | setSpringConfig(SpringConfig.defaultConfig); 63 | } 64 | 65 | /** 66 | * Destroys this Spring, meaning that it will be deregistered from its BaseSpringSystem so it won't be 67 | * iterated anymore and will clear its set of listeners. Do not use the Spring after calling this, 68 | * doing so may just cause an exception to be thrown. 69 | */ 70 | public void destroy() { 71 | mListeners.clear(); 72 | mSpringSystem.deregisterSpring(this); 73 | } 74 | 75 | /** 76 | * get the unique id for this spring 77 | * @return the unique id 78 | */ 79 | public String getId() { 80 | return mId; 81 | } 82 | 83 | /** 84 | * set the config class 85 | * @param springConfig config class for the spring 86 | * @return this Spring instance for chaining 87 | */ 88 | public Spring setSpringConfig(SpringConfig springConfig) { 89 | if (springConfig == null) { 90 | throw new IllegalArgumentException("springConfig is required"); 91 | } 92 | mSpringConfig = springConfig; 93 | return this; 94 | } 95 | 96 | /** 97 | * retrieve the spring config for this spring 98 | * @return the SpringConfig applied to this spring 99 | */ 100 | public SpringConfig getSpringConfig() { 101 | return mSpringConfig; 102 | } 103 | 104 | /** 105 | * Set the displaced value to determine the displacement for the spring from the rest value. 106 | * This value is retained and used to calculate the displacement ratio. 107 | * This also updates the start value of the Spring. 108 | * @param currentValue the new start and current value for the spring 109 | * @return the spring for chaining 110 | */ 111 | public Spring setCurrentValue(double currentValue) { 112 | mStartValue = currentValue; 113 | mCurrentState.position = currentValue; 114 | mSpringSystem.activateSpring(this.getId()); 115 | for (SpringListener listener : mListeners) { 116 | listener.onSpringUpdate(this); 117 | } 118 | return this; 119 | } 120 | 121 | /** 122 | * Get the displacement value from the last time setCurrentValue was called. 123 | * @return displacement value 124 | */ 125 | public double getStartValue() { 126 | return mStartValue; 127 | } 128 | 129 | /** 130 | * Get the current 131 | * @return current value 132 | */ 133 | public double getCurrentValue() { 134 | return mCurrentState.position; 135 | } 136 | 137 | /** 138 | * get the displacement of the springs current value from its rest value. 139 | * @return the distance displaced by 140 | */ 141 | public double getCurrentDisplacementDistance() { 142 | return getDisplacementDistanceForState(mCurrentState); 143 | } 144 | 145 | /** 146 | * get the displacement from rest for a given physics state 147 | * @param state the state to measure from 148 | * @return the distance displaced by 149 | */ 150 | private double getDisplacementDistanceForState(PhysicsState state) { 151 | return Math.abs(mEndValue - state.position); 152 | } 153 | 154 | /** 155 | * set the rest value to determine the displacement for the spring 156 | * @param endValue the endValue for the spring 157 | * @return the spring for chaining 158 | */ 159 | public Spring setEndValue(double endValue) { 160 | if (mEndValue == endValue && isAtRest()) { 161 | return this; 162 | } 163 | mStartValue = getCurrentValue(); 164 | mEndValue = endValue; 165 | mSpringSystem.activateSpring(this.getId()); 166 | for (SpringListener listener : mListeners) { 167 | listener.onSpringEndStateChange(this); 168 | } 169 | return this; 170 | } 171 | 172 | /** 173 | * get the rest value used for determining the displacement of the spring 174 | * @return the rest value for the spring 175 | */ 176 | public double getEndValue() { 177 | return mEndValue; 178 | } 179 | 180 | /** 181 | * set the velocity on the spring in pixels per second 182 | * @param velocity = = 183 | * @return the spring for chaining 184 | */ 185 | public Spring setVelocity(double velocity) { 186 | mCurrentState.velocity = velocity; 187 | mSpringSystem.activateSpring(this.getId()); 188 | return this; 189 | } 190 | 191 | /** 192 | * get the velocity of the spring 193 | * @return the current velocity 194 | */ 195 | public double getVelocity() { 196 | return mCurrentState.velocity; 197 | } 198 | 199 | /** 200 | * Sets the speed at which the spring should be considered at rest. 201 | * @param restSpeedThreshold speed pixels per second 202 | * @return the spring for chaining 203 | */ 204 | public Spring setRestSpeedThreshold(double restSpeedThreshold) { 205 | mRestSpeedThreshold = restSpeedThreshold; 206 | return this; 207 | } 208 | 209 | /** 210 | * Returns the speed at which the spring should be considered at rest in pixels per second 211 | * @return speed in pixels per second 212 | */ 213 | public double getRestSpeedThreshold() { 214 | return mRestSpeedThreshold; 215 | } 216 | 217 | /** 218 | * set the threshold of displacement from rest below which the spring should be considered at rest 219 | * @param displacementFromRestThreshold displacement to consider resting below 220 | * @return the spring for chaining 221 | */ 222 | public Spring setRestDisplacementThreshold(double displacementFromRestThreshold) { 223 | mDisplacementFromRestThreshold = displacementFromRestThreshold; 224 | return this; 225 | } 226 | 227 | /** 228 | * get the threshold of displacement from rest below which the spring should be considered at rest 229 | * @return displacement to consider resting below 230 | */ 231 | public double getRestDisplacementThreshold() { 232 | return mDisplacementFromRestThreshold; 233 | } 234 | 235 | /** 236 | * Force the spring to clamp at its end value to avoid overshooting the target value. 237 | * @param overshootClampingEnabled whether or not to enable overshoot clamping 238 | * @return the spring for chaining 239 | */ 240 | public Spring setOvershootClampingEnabled(boolean overshootClampingEnabled) { 241 | mOvershootClampingEnabled = overshootClampingEnabled; 242 | return this; 243 | } 244 | 245 | /** 246 | * Check if overshoot clamping is enabled. 247 | * @return is overshoot clamping enabled 248 | */ 249 | public boolean isOvershootClampingEnabled() { 250 | return mOvershootClampingEnabled; 251 | } 252 | 253 | /** 254 | * Check if the spring is overshooting beyond its target. 255 | * @return true if the spring is overshooting its target 256 | */ 257 | public boolean isOvershooting() { 258 | return (mStartValue < mEndValue && getCurrentValue() > mEndValue) || 259 | (mStartValue > mEndValue && getCurrentValue() < mEndValue); 260 | } 261 | 262 | /** 263 | * advance the physics simulation in SOLVER_TIMESTEP_SEC sized chunks to fulfill the required 264 | * realTimeDelta. 265 | * The math is inlined inside the loop since it made a huge performance impact when there are 266 | * several springs being advanced. 267 | * @param time clock time 268 | * @param realDeltaTime clock drift 269 | */ 270 | void advance(double realDeltaTime) { 271 | 272 | boolean isAtRest = isAtRest(); 273 | 274 | if (isAtRest && mWasAtRest) { 275 | /* begin debug 276 | Log.d(TAG, "bailing out because we are at rest:" + getName()); 277 | end debug */ 278 | return; 279 | } 280 | 281 | // clamp the amount of realTime to simulate to avoid stuttering in the UI. We should be able 282 | // to catch up in a subsequent advance if necessary. 283 | double adjustedDeltaTime = realDeltaTime; 284 | if (realDeltaTime > MAX_DELTA_TIME_SEC) { 285 | adjustedDeltaTime = MAX_DELTA_TIME_SEC; 286 | } 287 | 288 | /* begin debug 289 | long startTime = System.currentTimeMillis(); 290 | int iterations = 0; 291 | end debug */ 292 | 293 | mTimeAccumulator += adjustedDeltaTime; 294 | 295 | double tension = mSpringConfig.tension; 296 | double friction = mSpringConfig.friction; 297 | 298 | double position = mCurrentState.position; 299 | double velocity = mCurrentState.velocity; 300 | double tempPosition = mTempState.position; 301 | double tempVelocity = mTempState.velocity; 302 | 303 | double aVelocity, aAcceleration; 304 | double bVelocity, bAcceleration; 305 | double cVelocity, cAcceleration; 306 | double dVelocity, dAcceleration; 307 | 308 | double dxdt, dvdt; 309 | 310 | // iterate over the true time 311 | while (mTimeAccumulator >= SOLVER_TIMESTEP_SEC) { 312 | /* begin debug 313 | iterations++; 314 | end debug */ 315 | mTimeAccumulator -= SOLVER_TIMESTEP_SEC; 316 | 317 | if (mTimeAccumulator < SOLVER_TIMESTEP_SEC) { 318 | // This will be the last iteration. Remember the previous state in case we need to 319 | // interpolate 320 | mPreviousState.position = position; 321 | mPreviousState.velocity = velocity; 322 | } 323 | 324 | // Perform an RK4 integration to provide better detection of the acceleration curve via 325 | // sampling of Euler integrations at 4 intervals feeding each derivative into the calculation 326 | // of the next and taking a weighted sum of the 4 derivatives as the final output. 327 | 328 | // This math was inlined since it made for big performance improvements when advancing several 329 | // springs in one pass of the BaseSpringSystem. 330 | 331 | // The initial derivative is based on the current velocity and the calculated acceleration 332 | aVelocity = velocity; 333 | aAcceleration = (tension * (mEndValue - tempPosition)) - friction * velocity; 334 | 335 | // Calculate the next derivatives starting with the last derivative and integrating over the 336 | // timestep 337 | tempPosition = position + aVelocity * SOLVER_TIMESTEP_SEC * 0.5; 338 | tempVelocity = velocity + aAcceleration * SOLVER_TIMESTEP_SEC * 0.5; 339 | bVelocity = tempVelocity; 340 | bAcceleration = (tension * (mEndValue - tempPosition)) - friction * tempVelocity; 341 | 342 | tempPosition = position + bVelocity * SOLVER_TIMESTEP_SEC * 0.5; 343 | tempVelocity = velocity + bAcceleration * SOLVER_TIMESTEP_SEC * 0.5; 344 | cVelocity = tempVelocity; 345 | cAcceleration = (tension * (mEndValue - tempPosition)) - friction * tempVelocity; 346 | 347 | tempPosition = position + cVelocity * SOLVER_TIMESTEP_SEC; 348 | tempVelocity = velocity + cAcceleration * SOLVER_TIMESTEP_SEC; 349 | dVelocity = tempVelocity; 350 | dAcceleration = (tension * (mEndValue - tempPosition)) - friction * tempVelocity; 351 | 352 | // Take the weighted sum of the 4 derivatives as the final output. 353 | dxdt = 1.0/6.0 * (aVelocity + 2.0 * (bVelocity + cVelocity) + dVelocity); 354 | dvdt = 1.0/6.0 * (aAcceleration + 2.0 * (bAcceleration + cAcceleration) + dAcceleration); 355 | 356 | position += dxdt * SOLVER_TIMESTEP_SEC; 357 | velocity += dvdt * SOLVER_TIMESTEP_SEC; 358 | } 359 | 360 | mTempState.position = tempPosition; 361 | mTempState.velocity = tempVelocity; 362 | 363 | mCurrentState.position = position; 364 | mCurrentState.velocity = velocity; 365 | 366 | if (mTimeAccumulator > 0) { 367 | interpolate(mTimeAccumulator / SOLVER_TIMESTEP_SEC); 368 | } 369 | 370 | // End the spring immediately if it is overshooting and overshoot clamping is enabled. 371 | // Also make sure that if the spring was considered within a resting threshold that it's now 372 | // snapped to its end value. 373 | if (isAtRest() || (mOvershootClampingEnabled && isOvershooting())) { 374 | // Don't call setCurrentValue because that forces a call to onSpringUpdate 375 | mStartValue = mEndValue; 376 | mCurrentState.position = mEndValue; 377 | setVelocity(0); 378 | isAtRest = true; 379 | } 380 | 381 | /* begin debug 382 | long endTime = System.currentTimeMillis(); 383 | long elapsedMillis = endTime - startTime; 384 | Log.d(TAG, 385 | "iterations:" + iterations + 386 | " iterationTime:" + elapsedMillis + 387 | " position:" + mCurrentState.position + 388 | " velocity:" + mCurrentState.velocity + 389 | " realDeltaTime:" + realDeltaTime + 390 | " adjustedDeltaTime:" + adjustedDeltaTime + 391 | " isAtRest:" + isAtRest + 392 | " wasAtRest:" + mWasAtRest); 393 | end debug */ 394 | 395 | // NB: do these checks outside the loop so all listeners are properly notified of the state 396 | // transition 397 | boolean notifyActivate = false; 398 | if (mWasAtRest) { 399 | mWasAtRest = false; 400 | notifyActivate = true; 401 | } 402 | boolean notifyAtRest = false; 403 | if (isAtRest) { 404 | mWasAtRest = true; 405 | notifyAtRest = true; 406 | } 407 | for (SpringListener listener : mListeners) { 408 | // starting to move 409 | if (notifyActivate) { 410 | listener.onSpringActivate(this); 411 | } 412 | 413 | // updated 414 | listener.onSpringUpdate(this); 415 | 416 | // coming to rest 417 | if (notifyAtRest) { 418 | listener.onSpringAtRest(this); 419 | } 420 | } 421 | } 422 | 423 | /** 424 | * Check if this spring should be advanced by the system. * The rule is if the spring is 425 | * currently at rest and it was at rest in the previous advance, the system can skip this spring 426 | * @return should the system process this spring 427 | */ 428 | public boolean systemShouldAdvance() { 429 | return !isAtRest() || !wasAtRest(); 430 | } 431 | 432 | /** 433 | * Check if the spring was at rest in the prior iteration. This is used for ensuring the ending 434 | * callbacks are fired as the spring comes to a rest. 435 | * @return true if the spring was at rest in the prior iteration 436 | */ 437 | public boolean wasAtRest() { 438 | return mWasAtRest; 439 | } 440 | 441 | /** 442 | * check if the current state is at rest 443 | * @return is the spring at rest 444 | */ 445 | public boolean isAtRest() { 446 | return Math.abs(mCurrentState.velocity) <= mRestSpeedThreshold && 447 | getDisplacementDistanceForState(mCurrentState) <= mDisplacementFromRestThreshold; 448 | } 449 | 450 | /** 451 | * Set the spring to be at rest by making its end value equal to its current value and setting 452 | * velocity to 0. 453 | * @return = = 454 | */ 455 | public Spring setAtRest() { 456 | mEndValue = mCurrentState.position; 457 | mTempState.position = mCurrentState.position; 458 | mCurrentState.velocity = 0; 459 | return this; 460 | } 461 | 462 | /** 463 | * linear interpolation between the previous and current physics state based on the amount of 464 | * timestep remaining after processing the rendering delta time in timestep sized chunks. 465 | * @param alpha from 0 to 1, where 0 is the previous state, 1 is the current state 466 | */ 467 | private void interpolate(double alpha) { 468 | mCurrentState.position = mCurrentState.position * alpha + mPreviousState.position *(1-alpha); 469 | mCurrentState.velocity = mCurrentState.velocity * alpha + mPreviousState.velocity *(1-alpha); 470 | } 471 | 472 | /** listeners **/ 473 | 474 | /** 475 | * add a listener 476 | * @param newListener to add 477 | * @return the spring for chaining 478 | */ 479 | public Spring addListener(SpringListener newListener) { 480 | if (newListener == null) { 481 | throw new IllegalArgumentException("newListener is required"); 482 | } 483 | mListeners.add(newListener); 484 | return this; 485 | } 486 | 487 | /** 488 | * remove a listener 489 | * @param listenerToRemove to remove 490 | * @return the spring for chaining 491 | */ 492 | public Spring removeListener(SpringListener listenerToRemove) { 493 | if (listenerToRemove == null) { 494 | throw new IllegalArgumentException("listenerToRemove is required"); 495 | } 496 | mListeners.remove(listenerToRemove); 497 | return this; 498 | } 499 | 500 | /** 501 | * remove all of the listeners 502 | * @return the spring for chaining 503 | */ 504 | public Spring removeAllListeners() { 505 | mListeners.clear(); 506 | return this; 507 | } 508 | 509 | /** 510 | * This method checks to see that the current spring displacement value is equal to the input, 511 | * accounting for the spring's rest displacement threshold. 512 | * @param value The value to compare the spring value to 513 | * @return Whether the displacement value from the spring is within the bounds of the compare 514 | * value, accounting for threshold 515 | */ 516 | public boolean currentValueIsApproximately(double value) { 517 | return Math.abs(getCurrentValue() - value) <= getRestDisplacementThreshold(); 518 | } 519 | 520 | } 521 | 522 | -------------------------------------------------------------------------------- /tristatetogglebutton_library/src/main/java/com/facebook/rebound/SpringConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | */ 10 | 11 | package com.facebook.rebound; 12 | 13 | /** 14 | * Data structure for storing spring configuration. 15 | */ 16 | public class SpringConfig { 17 | public double friction; 18 | public double tension; 19 | 20 | public static SpringConfig defaultConfig = SpringConfig.fromOrigamiTensionAndFriction(40, 7); 21 | 22 | /** 23 | * constructor for the SpringConfig 24 | * @param tension tension value for the SpringConfig 25 | * @param friction friction value for the SpringConfig 26 | */ 27 | public SpringConfig(double tension, double friction) { 28 | this.tension = tension; 29 | this.friction = friction; 30 | } 31 | 32 | /** 33 | * A helper to make creating a SpringConfig easier with values mapping to the Origami values. 34 | * @param qcTension tension as defined in the Quartz Composition 35 | * @param qcFriction friction as defined in the Quartz Composition 36 | * @return a SpringConfig that maps to these values 37 | */ 38 | public static SpringConfig fromOrigamiTensionAndFriction(double qcTension, double qcFriction) { 39 | return new SpringConfig( 40 | OrigamiValueConverter.tensionFromOrigamiValue(qcTension), 41 | OrigamiValueConverter.frictionFromOrigamiValue(qcFriction) 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tristatetogglebutton_library/src/main/java/com/facebook/rebound/SpringConfigRegistry.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | */ 10 | 11 | package com.facebook.rebound; 12 | 13 | import java.util.Collections; 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | 17 | /** 18 | * class for maintaining a registry of all spring configs 19 | */ 20 | public class SpringConfigRegistry { 21 | 22 | private static final SpringConfigRegistry INSTANCE = new SpringConfigRegistry(true); 23 | 24 | public static SpringConfigRegistry getInstance() { 25 | return INSTANCE; 26 | } 27 | 28 | private final Map mSpringConfigMap; 29 | 30 | /** 31 | * constructor for the SpringConfigRegistry 32 | */ 33 | SpringConfigRegistry(boolean includeDefaultEntry) { 34 | mSpringConfigMap = new HashMap(); 35 | if (includeDefaultEntry) { 36 | addSpringConfig(SpringConfig.defaultConfig, "default config"); 37 | } 38 | } 39 | 40 | /** 41 | * add a SpringConfig to the registry 42 | * 43 | * @param springConfig SpringConfig to add to the registry 44 | * @param configName name to give the SpringConfig in the registry 45 | * @return true if the SpringConfig was added, false if a config with that name is already 46 | * present. 47 | */ 48 | public boolean addSpringConfig(SpringConfig springConfig, String configName) { 49 | if (springConfig == null) { 50 | throw new IllegalArgumentException("springConfig is required"); 51 | } 52 | if (configName == null) { 53 | throw new IllegalArgumentException("configName is required"); 54 | } 55 | if (mSpringConfigMap.containsKey(springConfig)) { 56 | return false; 57 | } 58 | mSpringConfigMap.put(springConfig, configName); 59 | return true; 60 | } 61 | 62 | /** 63 | * remove a specific SpringConfig from the registry 64 | * @param springConfig the of the SpringConfig to remove 65 | * @return true if the SpringConfig was removed, false if it was not present. 66 | */ 67 | public boolean removeSpringConfig(SpringConfig springConfig) { 68 | if (springConfig == null) { 69 | throw new IllegalArgumentException("springConfig is required"); 70 | } 71 | return mSpringConfigMap.remove(springConfig) != null; 72 | } 73 | 74 | /** 75 | * retrieve all SpringConfig in the registry 76 | * @return a list of all SpringConfig 77 | */ 78 | public Map getAllSpringConfig() { 79 | return Collections.unmodifiableMap(mSpringConfigMap); 80 | } 81 | 82 | /** 83 | * clear all SpringConfig in the registry 84 | */ 85 | public void removeAllSpringConfig() { 86 | mSpringConfigMap.clear(); 87 | } 88 | } 89 | 90 | -------------------------------------------------------------------------------- /tristatetogglebutton_library/src/main/java/com/facebook/rebound/SpringListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | */ 10 | 11 | package com.facebook.rebound; 12 | 13 | public interface SpringListener { 14 | 15 | /** 16 | * called whenever the spring is updated 17 | * @param spring the Spring sending the update 18 | */ 19 | void onSpringUpdate(Spring spring); 20 | 21 | /** 22 | * called whenever the spring achieves a resting state 23 | * @param spring the spring that's now resting 24 | */ 25 | void onSpringAtRest(Spring spring); 26 | 27 | /** 28 | * called whenever the spring leaves its resting state 29 | * @param spring the spring that has left its resting state 30 | */ 31 | void onSpringActivate(Spring spring); 32 | 33 | /** 34 | * called whenever the spring notifies of displacement state changes 35 | * @param spring the spring whose end state has changed 36 | */ 37 | void onSpringEndStateChange(Spring spring); 38 | } 39 | 40 | -------------------------------------------------------------------------------- /tristatetogglebutton_library/src/main/java/com/facebook/rebound/SpringLooper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | */ 10 | 11 | package com.facebook.rebound; 12 | 13 | /** 14 | * The spring looper is an interface for implementing platform-dependent run loops. 15 | */ 16 | public abstract class SpringLooper { 17 | 18 | protected BaseSpringSystem mSpringSystem; 19 | 20 | /** 21 | * Set the BaseSpringSystem that the SpringLooper will call back to. 22 | * @param springSystem the spring system to call loop on. 23 | */ 24 | public void setSpringSystem(BaseSpringSystem springSystem) { 25 | mSpringSystem = springSystem; 26 | } 27 | 28 | /** 29 | * The BaseSpringSystem has requested that the looper begins running this {@link Runnable} 30 | * on every frame. The {@link Runnable} will continue running on every frame until 31 | * {@link #stop()} is called. 32 | * If an existing {@link Runnable} had been started on this looper, it will be cancelled. 33 | */ 34 | public abstract void start(); 35 | 36 | /** 37 | * The looper will no longer run the {@link Runnable}. 38 | */ 39 | public abstract void stop(); 40 | } -------------------------------------------------------------------------------- /tristatetogglebutton_library/src/main/java/com/facebook/rebound/SpringSystem.java: -------------------------------------------------------------------------------- 1 | package com.facebook.rebound; 2 | 3 | /** 4 | * This is a wrapper for BaseSpringSystem that provides the convenience of automatically providing 5 | * the AndroidSpringLooper dependency in {@link SpringSystem#create}. 6 | */ 7 | public class SpringSystem extends BaseSpringSystem { 8 | 9 | /** 10 | * Create a new SpringSystem providing the appropriate constructor parameters to work properly 11 | * in an Android environment. 12 | * @return the SpringSystem 13 | */ 14 | public static SpringSystem create() { 15 | return new SpringSystem(AndroidSpringLooperFactory.createSpringLooper()); 16 | } 17 | 18 | private SpringSystem(SpringLooper springLooper) { 19 | super(springLooper); 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /tristatetogglebutton_library/src/main/java/com/facebook/rebound/SpringSystemListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | */ 10 | 11 | package com.facebook.rebound; 12 | 13 | /** 14 | * SpringSystemListener provides an interface for listening to events before and after each Physics 15 | * solving loop the BaseSpringSystem runs. 16 | */ 17 | public interface SpringSystemListener { 18 | 19 | /** 20 | * Runs before each pass through the physics integration loop providing an opportunity to do any 21 | * setup or alterations to the Physics state before integrating. 22 | * @param springSystem the BaseSpringSystem listened to 23 | */ 24 | void onBeforeIntegrate(BaseSpringSystem springSystem); 25 | 26 | /** 27 | * Runs after each pass through the physics integration loop providing an opportunity to do any 28 | * setup or alterations to the Physics state after integrating. 29 | * @param springSystem the BaseSpringSystem listened to 30 | */ 31 | void onAfterIntegrate(BaseSpringSystem springSystem); 32 | } 33 | 34 | -------------------------------------------------------------------------------- /tristatetogglebutton_library/src/main/java/com/facebook/rebound/SpringUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | */ 10 | 11 | package com.facebook.rebound; 12 | 13 | public class SpringUtil { 14 | 15 | /** 16 | * Map a value within a given range to another range. 17 | * @param value the value to map 18 | * @param fromLow the low end of the range the value is within 19 | * @param fromHigh the high end of the range the value is within 20 | * @param toLow the low end of the range to map to 21 | * @param toHigh the high end of the range to map to 22 | * @return the mapped value 23 | */ 24 | public static double mapValueFromRangeToRange( 25 | double value, 26 | double fromLow, 27 | double fromHigh, 28 | double toLow, 29 | double toHigh) { 30 | double fromRangeSize = fromHigh - fromLow; 31 | double toRangeSize = toHigh - toLow; 32 | double valueScale = (value - fromLow) / fromRangeSize; 33 | return toLow + (valueScale * toRangeSize); 34 | } 35 | 36 | /** 37 | * Clamp a value to be within the provided range. 38 | * @param value the value to clamp 39 | * @param low the low end of the range 40 | * @param high the high end of the range 41 | * @return the clamped value 42 | */ 43 | public static double clamp(double value, double low, double high) { 44 | return Math.min(Math.max(value, low), high); 45 | } 46 | 47 | public static int clamp(int value, int low, int high) { 48 | return Math.min(Math.max(value, low), high); 49 | } 50 | 51 | 52 | } 53 | 54 | -------------------------------------------------------------------------------- /tristatetogglebutton_library/src/main/java/com/facebook/rebound/SteppingLooper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.facebook.rebound; 11 | 12 | public class SteppingLooper extends SpringLooper { 13 | 14 | private boolean mStarted; 15 | private long mLastTime; 16 | 17 | @Override 18 | public void start() { 19 | mStarted = true; 20 | mLastTime = 0; 21 | } 22 | 23 | public boolean step(long interval) { 24 | if (mSpringSystem == null || !mStarted) { 25 | return false; 26 | } 27 | long currentTime = mLastTime + interval; 28 | mSpringSystem.loop(currentTime); 29 | mLastTime = currentTime; 30 | return mSpringSystem.getIsIdle(); 31 | } 32 | 33 | @Override 34 | public void stop() { 35 | mStarted = false; 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /tristatetogglebutton_library/src/main/java/com/facebook/rebound/SynchronousLooper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.facebook.rebound; 11 | 12 | public class SynchronousLooper extends SpringLooper { 13 | 14 | public static double SIXTY_FPS = 16.6667; 15 | private double mTimeStep; 16 | private boolean mRunning; 17 | 18 | public SynchronousLooper() { 19 | mTimeStep = SIXTY_FPS; 20 | } 21 | 22 | public double getTimeStep() { 23 | return mTimeStep; 24 | } 25 | 26 | public void setTimeStep(double timeStep) { 27 | mTimeStep = timeStep; 28 | } 29 | 30 | @Override 31 | public void start() { 32 | mRunning = true; 33 | while (!mSpringSystem.getIsIdle()) { 34 | if (mRunning == false) { 35 | break; 36 | } 37 | mSpringSystem.loop(mTimeStep); 38 | } 39 | } 40 | 41 | @Override 42 | public void stop() { 43 | mRunning = false; 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /tristatetogglebutton_library/src/main/java/com/facebook/rebound/ui/SpringConfiguratorView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | */ 10 | 11 | package com.facebook.rebound.ui; 12 | 13 | import android.annotation.TargetApi; 14 | import android.content.Context; 15 | import android.content.res.Resources; 16 | import android.graphics.Color; 17 | import android.os.Build; 18 | import android.util.AttributeSet; 19 | import android.view.Gravity; 20 | import android.view.MotionEvent; 21 | import android.view.View; 22 | import android.view.ViewGroup; 23 | import android.widget.AbsListView; 24 | import android.widget.AdapterView; 25 | import android.widget.BaseAdapter; 26 | import android.widget.FrameLayout; 27 | import android.widget.LinearLayout; 28 | import android.widget.SeekBar; 29 | import android.widget.Spinner; 30 | import android.widget.TableLayout; 31 | import android.widget.TextView; 32 | 33 | import com.facebook.rebound.OrigamiValueConverter; 34 | import com.facebook.rebound.Spring; 35 | import com.facebook.rebound.SpringConfig; 36 | import com.facebook.rebound.SpringConfigRegistry; 37 | import com.facebook.rebound.SpringListener; 38 | import com.facebook.rebound.SpringSystem; 39 | 40 | import java.text.DecimalFormat; 41 | import java.util.ArrayList; 42 | import java.util.List; 43 | import java.util.Map; 44 | 45 | import static com.facebook.rebound.ui.Util.*; 46 | 47 | /** 48 | * The SpringConfiguratorView provides a reusable view for live-editing all registered springs 49 | * within an Application. Each registered Spring can be accessed by its id and its tension and 50 | * friction properties can be edited while the user tests the effected UI live. 51 | */ 52 | public class SpringConfiguratorView extends FrameLayout { 53 | 54 | private static final int MAX_SEEKBAR_VAL = 100000; 55 | private static final float MIN_TENSION = 0; 56 | private static final float MAX_TENSION = 200; 57 | private static final float MIN_FRICTION = 0; 58 | private static final float MAX_FRICTION = 50; 59 | private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.#"); 60 | 61 | private final SpinnerAdapter spinnerAdapter; 62 | private final List mSpringConfigs = new ArrayList(); 63 | private final Spring mRevealerSpring; 64 | private final float mStashPx; 65 | private final float mRevealPx; 66 | private final SpringConfigRegistry springConfigRegistry; 67 | private final int mTextColor = Color.argb(255, 225, 225, 225); 68 | private SeekBar mTensionSeekBar; 69 | private SeekBar mFrictionSeekBar; 70 | private Spinner mSpringSelectorSpinner; 71 | private TextView mFrictionLabel; 72 | private TextView mTensionLabel; 73 | private SpringConfig mSelectedSpringConfig; 74 | 75 | public SpringConfiguratorView(Context context) { 76 | this(context, null); 77 | } 78 | 79 | public SpringConfiguratorView(Context context, AttributeSet attrs) { 80 | this(context, attrs, 0); 81 | } 82 | 83 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 84 | public SpringConfiguratorView(Context context, AttributeSet attrs, int defStyle) { 85 | super(context, attrs, defStyle); 86 | 87 | SpringSystem springSystem = SpringSystem.create(); 88 | springConfigRegistry = SpringConfigRegistry.getInstance(); 89 | spinnerAdapter = new SpinnerAdapter(context); 90 | 91 | Resources resources = getResources(); 92 | mRevealPx = dpToPx(40, resources); 93 | mStashPx = dpToPx(280, resources); 94 | 95 | mRevealerSpring = springSystem.createSpring(); 96 | SpringListener revealerSpringListener = new RevealerSpringListener(); 97 | mRevealerSpring 98 | .setCurrentValue(1) 99 | .setEndValue(1) 100 | .addListener(revealerSpringListener); 101 | 102 | addView(generateHierarchy(context)); 103 | 104 | SeekbarListener seekbarListener = new SeekbarListener(); 105 | mTensionSeekBar.setMax(MAX_SEEKBAR_VAL); 106 | mTensionSeekBar.setOnSeekBarChangeListener(seekbarListener); 107 | 108 | mFrictionSeekBar.setMax(MAX_SEEKBAR_VAL); 109 | mFrictionSeekBar.setOnSeekBarChangeListener(seekbarListener); 110 | 111 | mSpringSelectorSpinner.setAdapter(spinnerAdapter); 112 | mSpringSelectorSpinner.setOnItemSelectedListener(new SpringSelectedListener()); 113 | refreshSpringConfigurations(); 114 | 115 | this.setTranslationY(mStashPx); 116 | } 117 | 118 | /** 119 | * Programmatically build up the view hierarchy to avoid the need for resources. 120 | * @return View hierarchy 121 | */ 122 | private View generateHierarchy(Context context) { 123 | Resources resources = getResources(); 124 | 125 | FrameLayout.LayoutParams params; 126 | int fivePx = dpToPx(5, resources); 127 | int tenPx = dpToPx(10, resources); 128 | int twentyPx = dpToPx(20, resources); 129 | TableLayout.LayoutParams tableLayoutParams = new TableLayout.LayoutParams( 130 | 0, 131 | ViewGroup.LayoutParams.WRAP_CONTENT, 132 | 1f); 133 | tableLayoutParams.setMargins(0, 0, fivePx, 0); 134 | LinearLayout seekWrapper; 135 | 136 | FrameLayout root = new FrameLayout(context); 137 | params = createLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dpToPx(300, resources)); 138 | root.setLayoutParams(params); 139 | 140 | FrameLayout container = new FrameLayout(context); 141 | params = createMatchParams(); 142 | params.setMargins(0, twentyPx, 0, 0); 143 | container.setLayoutParams(params); 144 | container.setBackgroundColor(Color.argb(100, 0, 0, 0)); 145 | root.addView(container); 146 | 147 | mSpringSelectorSpinner = new Spinner(context, Spinner.MODE_DIALOG); 148 | params = createMatchWrapParams(); 149 | params.gravity = Gravity.TOP; 150 | params.setMargins(tenPx, tenPx, tenPx, 0); 151 | mSpringSelectorSpinner.setLayoutParams(params); 152 | container.addView(mSpringSelectorSpinner); 153 | 154 | LinearLayout linearLayout = new LinearLayout(context); 155 | params = createMatchWrapParams(); 156 | params.setMargins(0, 0, 0, dpToPx(80, resources)); 157 | params.gravity = Gravity.BOTTOM; 158 | linearLayout.setLayoutParams(params); 159 | linearLayout.setOrientation(LinearLayout.VERTICAL); 160 | container.addView(linearLayout); 161 | 162 | seekWrapper = new LinearLayout(context); 163 | params = createMatchWrapParams(); 164 | params.setMargins(tenPx, tenPx, tenPx, twentyPx); 165 | seekWrapper.setPadding(tenPx, tenPx, tenPx, tenPx); 166 | seekWrapper.setLayoutParams(params); 167 | seekWrapper.setOrientation(LinearLayout.HORIZONTAL); 168 | linearLayout.addView(seekWrapper); 169 | 170 | mTensionSeekBar = new SeekBar(context); 171 | mTensionSeekBar.setLayoutParams(tableLayoutParams); 172 | seekWrapper.addView(mTensionSeekBar); 173 | 174 | mTensionLabel = new TextView(getContext()); 175 | mTensionLabel.setTextColor(mTextColor); 176 | params = createLayoutParams( 177 | dpToPx(50, resources), 178 | ViewGroup.LayoutParams.MATCH_PARENT); 179 | mTensionLabel.setGravity(Gravity.CENTER_VERTICAL | Gravity.LEFT); 180 | mTensionLabel.setLayoutParams(params); 181 | mTensionLabel.setMaxLines(1); 182 | seekWrapper.addView(mTensionLabel); 183 | 184 | seekWrapper = new LinearLayout(context); 185 | params = createMatchWrapParams(); 186 | params.setMargins(tenPx, tenPx, tenPx, twentyPx); 187 | seekWrapper.setPadding(tenPx, tenPx, tenPx, tenPx); 188 | seekWrapper.setLayoutParams(params); 189 | seekWrapper.setOrientation(LinearLayout.HORIZONTAL); 190 | linearLayout.addView(seekWrapper); 191 | 192 | mFrictionSeekBar = new SeekBar(context); 193 | mFrictionSeekBar.setLayoutParams(tableLayoutParams); 194 | seekWrapper.addView(mFrictionSeekBar); 195 | 196 | mFrictionLabel = new TextView(getContext()); 197 | mFrictionLabel.setTextColor(mTextColor); 198 | params = createLayoutParams(dpToPx(50, resources), ViewGroup.LayoutParams.MATCH_PARENT); 199 | mFrictionLabel.setGravity(Gravity.CENTER_VERTICAL | Gravity.LEFT); 200 | mFrictionLabel.setLayoutParams(params); 201 | mFrictionLabel.setMaxLines(1); 202 | seekWrapper.addView(mFrictionLabel); 203 | 204 | View nub = new View(context); 205 | params = createLayoutParams(dpToPx(60, resources), dpToPx(40, resources)); 206 | params.gravity = Gravity.TOP | Gravity.CENTER; 207 | nub.setLayoutParams(params); 208 | nub.setOnTouchListener(new OnNubTouchListener()); 209 | nub.setBackgroundColor(Color.argb(255, 0, 164, 209)); 210 | root.addView(nub); 211 | 212 | return root; 213 | } 214 | 215 | /** 216 | * remove the configurator from its parent and clean up springs and listeners 217 | */ 218 | public void destroy() { 219 | ViewGroup parent = (ViewGroup) getParent(); 220 | if (parent != null) { 221 | parent.removeView(this); 222 | } 223 | mRevealerSpring.destroy(); 224 | } 225 | 226 | /** 227 | * reload the springs from the registry and update the UI 228 | */ 229 | public void refreshSpringConfigurations() { 230 | Map springConfigMap = springConfigRegistry.getAllSpringConfig(); 231 | 232 | spinnerAdapter.clear(); 233 | mSpringConfigs.clear(); 234 | 235 | for (Map.Entry entry : springConfigMap.entrySet()) { 236 | if (entry.getKey() == SpringConfig.defaultConfig) { 237 | continue; 238 | } 239 | mSpringConfigs.add(entry.getKey()); 240 | spinnerAdapter.add(entry.getValue()); 241 | } 242 | // Add the default config in last. 243 | mSpringConfigs.add(SpringConfig.defaultConfig); 244 | spinnerAdapter.add(springConfigMap.get(SpringConfig.defaultConfig)); 245 | spinnerAdapter.notifyDataSetChanged(); 246 | if (mSpringConfigs.size() > 0) { 247 | mSpringSelectorSpinner.setSelection(0); 248 | } 249 | } 250 | 251 | private class SpringSelectedListener implements AdapterView.OnItemSelectedListener { 252 | 253 | @Override 254 | public void onItemSelected(AdapterView adapterView, View view, int i, long l) { 255 | mSelectedSpringConfig = mSpringConfigs.get(i); 256 | updateSeekBarsForSpringConfig(mSelectedSpringConfig); 257 | } 258 | 259 | @Override 260 | public void onNothingSelected(AdapterView adapterView) { 261 | } 262 | } 263 | 264 | /** 265 | * listen to events on seekbars and update registered springs accordingly 266 | */ 267 | private class SeekbarListener implements SeekBar.OnSeekBarChangeListener { 268 | 269 | @Override 270 | public void onProgressChanged(SeekBar seekBar, int val, boolean b) { 271 | float tensionRange = MAX_TENSION - MIN_TENSION; 272 | float frictionRange = MAX_FRICTION - MIN_FRICTION; 273 | 274 | if (seekBar == mTensionSeekBar) { 275 | float scaledTension = ((val) * tensionRange) / MAX_SEEKBAR_VAL + MIN_TENSION; 276 | mSelectedSpringConfig.tension = 277 | OrigamiValueConverter.tensionFromOrigamiValue(scaledTension); 278 | String roundedTensionLabel = DECIMAL_FORMAT.format(scaledTension); 279 | mTensionLabel.setText("T:" + roundedTensionLabel); 280 | } 281 | 282 | if (seekBar == mFrictionSeekBar) { 283 | float scaledFriction = ((val) * frictionRange) / MAX_SEEKBAR_VAL + MIN_FRICTION; 284 | mSelectedSpringConfig.friction = 285 | OrigamiValueConverter.frictionFromOrigamiValue(scaledFriction); 286 | String roundedFrictionLabel = DECIMAL_FORMAT.format(scaledFriction); 287 | mFrictionLabel.setText("F:" + roundedFrictionLabel); 288 | } 289 | } 290 | 291 | @Override 292 | public void onStartTrackingTouch(SeekBar seekBar) { 293 | } 294 | 295 | @Override 296 | public void onStopTrackingTouch(SeekBar seekBar) { 297 | } 298 | } 299 | 300 | /** 301 | * update the position of the seekbars based on the spring value; 302 | * @param springConfig current editing spring 303 | */ 304 | private void updateSeekBarsForSpringConfig(SpringConfig springConfig) { 305 | float tension = (float) OrigamiValueConverter.origamiValueFromTension(springConfig.tension); 306 | float tensionRange = MAX_TENSION - MIN_TENSION; 307 | int scaledTension = Math.round(((tension - MIN_TENSION) * MAX_SEEKBAR_VAL) / tensionRange); 308 | 309 | float friction = (float) OrigamiValueConverter.origamiValueFromFriction(springConfig.friction); 310 | float frictionRange = MAX_FRICTION - MIN_FRICTION; 311 | int scaledFriction = Math.round(((friction - MIN_FRICTION) * MAX_SEEKBAR_VAL) / frictionRange); 312 | 313 | mTensionSeekBar.setProgress(scaledTension); 314 | mFrictionSeekBar.setProgress(scaledFriction); 315 | } 316 | 317 | /** 318 | * toggle visibility when the nub is tapped. 319 | */ 320 | private class OnNubTouchListener implements View.OnTouchListener { 321 | @Override 322 | public boolean onTouch(View view, MotionEvent motionEvent) { 323 | if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) { 324 | togglePosition(); 325 | } 326 | return true; 327 | } 328 | } 329 | 330 | private void togglePosition() { 331 | double currentValue = mRevealerSpring.getEndValue(); 332 | mRevealerSpring 333 | .setEndValue(currentValue == 1 ? 0 : 1); 334 | } 335 | 336 | private class RevealerSpringListener implements SpringListener { 337 | 338 | @Override 339 | public void onSpringUpdate(Spring spring) { 340 | float val = (float) spring.getCurrentValue(); 341 | float minTranslate = mRevealPx; 342 | float maxTranslate = mStashPx; 343 | float range = maxTranslate - minTranslate; 344 | float yTranslate = (val * range) + minTranslate; 345 | SpringConfiguratorView.this.setTranslationY(yTranslate); 346 | } 347 | 348 | @Override 349 | public void onSpringAtRest(Spring spring) { 350 | } 351 | 352 | @Override 353 | public void onSpringActivate(Spring spring) { 354 | } 355 | 356 | @Override 357 | public void onSpringEndStateChange(Spring spring) { 358 | } 359 | } 360 | 361 | private class SpinnerAdapter extends BaseAdapter { 362 | 363 | private final Context mContext; 364 | private final List mStrings; 365 | 366 | public SpinnerAdapter(Context context) { 367 | mContext = context; 368 | mStrings = new ArrayList(); 369 | } 370 | 371 | @Override 372 | public int getCount() { 373 | return mStrings.size(); 374 | } 375 | 376 | @Override 377 | public Object getItem(int position) { 378 | return mStrings.get(position); 379 | } 380 | 381 | @Override 382 | public long getItemId(int position) { 383 | return position; 384 | } 385 | 386 | public void add(String string) { 387 | mStrings.add(string); 388 | notifyDataSetChanged(); 389 | } 390 | 391 | /** 392 | * Remove all elements from the list. 393 | */ 394 | public void clear() { 395 | mStrings.clear(); 396 | notifyDataSetChanged(); 397 | } 398 | 399 | @Override 400 | public View getView(int position, View convertView, ViewGroup parent) { 401 | TextView textView; 402 | if (convertView == null) { 403 | textView = new TextView(mContext); 404 | AbsListView.LayoutParams params = new AbsListView.LayoutParams( 405 | ViewGroup.LayoutParams.MATCH_PARENT, 406 | ViewGroup.LayoutParams.MATCH_PARENT); 407 | textView.setLayoutParams(params); 408 | int twelvePx = dpToPx(12, getResources()); 409 | textView.setPadding(twelvePx, twelvePx, twelvePx, twelvePx); 410 | textView.setTextColor(mTextColor); 411 | } else { 412 | textView = (TextView) convertView; 413 | } 414 | textView.setText(mStrings.get(position)); 415 | return textView; 416 | } 417 | } 418 | } 419 | 420 | -------------------------------------------------------------------------------- /tristatetogglebutton_library/src/main/java/com/facebook/rebound/ui/Util.java: -------------------------------------------------------------------------------- 1 | package com.facebook.rebound.ui; 2 | 3 | import android.content.res.Resources; 4 | import android.util.TypedValue; 5 | import android.view.ViewGroup; 6 | import android.widget.FrameLayout; 7 | import android.widget.RelativeLayout; 8 | 9 | /** 10 | * Utilities for generating view hierarchies without using resources. 11 | */ 12 | public abstract class Util { 13 | 14 | public static final int dpToPx(float dp, Resources res) { 15 | return (int) TypedValue.applyDimension( 16 | TypedValue.COMPLEX_UNIT_DIP, 17 | dp, 18 | res.getDisplayMetrics()); 19 | } 20 | 21 | public static final FrameLayout.LayoutParams createLayoutParams(int width, int height) { 22 | return new FrameLayout.LayoutParams(width, height); 23 | } 24 | 25 | public static final FrameLayout.LayoutParams createMatchParams() { 26 | return createLayoutParams( 27 | ViewGroup.LayoutParams.MATCH_PARENT, 28 | ViewGroup.LayoutParams.MATCH_PARENT); 29 | } 30 | 31 | public static final FrameLayout.LayoutParams createWrapParams() { 32 | return createLayoutParams( 33 | ViewGroup.LayoutParams.WRAP_CONTENT, 34 | ViewGroup.LayoutParams.WRAP_CONTENT); 35 | } 36 | 37 | public static final FrameLayout.LayoutParams createWrapMatchParams() { 38 | return createLayoutParams( 39 | ViewGroup.LayoutParams.WRAP_CONTENT, 40 | ViewGroup.LayoutParams.MATCH_PARENT); 41 | } 42 | 43 | public static final FrameLayout.LayoutParams createMatchWrapParams() { 44 | return createLayoutParams( 45 | ViewGroup.LayoutParams.MATCH_PARENT, 46 | ViewGroup.LayoutParams.WRAP_CONTENT); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /tristatetogglebutton_library/src/main/java/it/beppi/tristatetogglebutton_library/TriStateToggleButton.java: -------------------------------------------------------------------------------- 1 | package it.beppi.tristatetogglebutton_library; 2 | 3 | import android.content.Context; 4 | import android.content.res.Resources; 5 | import android.content.res.TypedArray; 6 | import android.graphics.Canvas; 7 | import android.graphics.Color; 8 | import android.graphics.Paint; 9 | import android.graphics.Paint.Cap; 10 | import android.graphics.Paint.Style; 11 | import android.graphics.RectF; 12 | import android.util.AttributeSet; 13 | import android.util.TypedValue; 14 | import android.view.MotionEvent; 15 | import android.view.View; 16 | 17 | import com.facebook.rebound.SimpleSpringListener; 18 | import com.facebook.rebound.Spring; 19 | import com.facebook.rebound.SpringConfig; 20 | import com.facebook.rebound.SpringSystem; 21 | import com.facebook.rebound.SpringUtil; 22 | 23 | import static it.beppi.tristatetogglebutton_library.TriStateToggleButton.ToggleStatus.mid; 24 | import static it.beppi.tristatetogglebutton_library.TriStateToggleButton.ToggleStatus.off; 25 | import static it.beppi.tristatetogglebutton_library.TriStateToggleButton.ToggleStatus.on; 26 | 27 | 28 | /** 29 | * @author ThinkPad 30 | * @author modified by BeppiMenozzi 31 | */ 32 | public class TriStateToggleButton extends View{ 33 | // Beppi: added a three state enumerator 34 | public enum ToggleStatus { 35 | on, mid, off 36 | } 37 | // Beppi: convert from the string values defined in the xml attributes to an usable value 38 | private ToggleStatus attrToStatus(String attr) { 39 | if (attr == null) return off; 40 | if (attr.equals("0")) return off; 41 | else if (attr.equals("1")) return mid; 42 | else return on; 43 | } 44 | 45 | // Beppi: static shortcuts for handling boolean values with 2-state. mid = false. 46 | public static boolean toggleStatusToBoolean(ToggleStatus toggleStatus) { 47 | if (toggleStatus == on) return true; 48 | else return false; 49 | } 50 | public static ToggleStatus booleanToToggleStatus(boolean toggleStatus) { 51 | if (toggleStatus) return on; 52 | else return off; 53 | } 54 | // Beppi: same with integers 55 | public static int toggleStatusToInt(ToggleStatus toggleStatus) { 56 | switch (toggleStatus) { 57 | case off: return 0; 58 | case mid: return 1; 59 | case on: 60 | default: return 2; 61 | } 62 | } 63 | public static ToggleStatus intToToggleStatus(int toggleIntValue) { 64 | if (toggleIntValue == 0) return off; 65 | else if (toggleIntValue == 1) return mid; 66 | else return on; 67 | } 68 | 69 | 70 | private SpringSystem springSystem; 71 | private Spring spring ; 72 | /** */ 73 | private float radius; 74 | /** 开启颜色*/ // Turn on color 75 | // Beppi: Modified color to match material design 76 | // private int onColor = Color.parseColor("#4ebb7f"); 77 | private int onColor = Color.parseColor("#42bd41"); // green 300 78 | /** 关闭颜色*/ // Turn off color 79 | // Beppi: Modified color to match material design 80 | // private int offBorderColor = Color.parseColor("#dadbda"); 81 | private int offBorderColor = Color.parseColor("#bdbdbd"); // grey 400 82 | /** 灰色带颜色*/ // Gray color 83 | private int offColor = Color.parseColor("#ffffff"); 84 | /** 手柄颜色*/ // Handle color 85 | // Beppi: Added third mid color 86 | private int midColor = Color.parseColor("#ffca28"); // amber 400 87 | private int spotColor = Color.parseColor("#ffffff"); 88 | /** 边框颜色*/ // Border color 89 | private int borderColor = offBorderColor; 90 | /** 画笔*/ // brush 91 | private Paint paint ; 92 | /** 开关状态*/ // switch status 93 | // Beppi: changed the type of toggleOn from boolean to ToggleStatus 94 | // Beppi: refactored the variable name from toggleOn to toggleStatus 95 | // private boolean toggleOn = false; 96 | private ToggleStatus toggleStatus = off; 97 | // Beppi: added previousToggleStatus to manage transitions correctly 98 | private ToggleStatus previousToggleStatus = off; 99 | /** 边框大小*/ // Border size 100 | private int borderWidth = 2; 101 | /** 垂直中心*/ // Vertical center 102 | private float centerY; 103 | /** 按钮的开始和结束位置*/ // The start and end positions of the button 104 | // Beppi: added midX position 105 | // private float startX, endX; 106 | private float startX, midX, endX; 107 | /** 手柄X位置的最小和最大值*/ // The minimum and maximum values for the X position of the handle 108 | // Beppi: added spotMidX 109 | // private float spotMinX, spotMaxX; 110 | private float spotMinX, spotMidX, spotMaxX; 111 | /**手柄大小 */ // Handle size 112 | private int spotSize ; 113 | /** 手柄X位置*/ // Handle X position 114 | private float spotX; 115 | /** 关闭时内部灰色带高度*/ // Off Internal gray band height 116 | private float offLineWidth; 117 | /** */ 118 | private RectF rect = new RectF(); 119 | /** 默认使用动画*/ // Animation is used by default 120 | private boolean defaultAnimate = true; 121 | // Beppi: added midSelectable 122 | private boolean midSelectable = true; 123 | 124 | // Beppi: swipe management 125 | private int swipeSensitivityPixels = 200; 126 | private int swipeX = 0; 127 | 128 | /** 是否默认处于打开状态*/ // Whether it is on by default 129 | // Beppi: changed the type of isDefaultOn from boolean to ToggleStatus 130 | // Beppi: refactored the variable name from isDefaultOn to defaultStatus 131 | // private boolean isDefaultOn = false; 132 | private ToggleStatus defaultStatus = off; 133 | // Beppi: enabled && disabledColor 134 | private boolean enabled = true; 135 | private int disabledColor = Color.parseColor("#bdbdbd"); // grey 400 136 | 137 | private boolean swipeing = false; 138 | 139 | private OnToggleChanged listener; 140 | 141 | private TriStateToggleButton(Context context) { 142 | super(context); 143 | } 144 | public TriStateToggleButton(Context context, AttributeSet attrs, int defStyleAttr) { 145 | super(context, attrs, defStyleAttr); 146 | setup(attrs); 147 | } 148 | public TriStateToggleButton(Context context, AttributeSet attrs) { 149 | super(context, attrs); 150 | setup(attrs); 151 | } 152 | 153 | @Override 154 | protected void onDetachedFromWindow() { 155 | super.onDetachedFromWindow(); 156 | spring.removeListener(springListener); 157 | } 158 | 159 | public void onAttachedToWindow() { 160 | super.onAttachedToWindow(); 161 | spring.addListener(springListener); 162 | } 163 | 164 | public void setup(AttributeSet attrs) { 165 | paint = new Paint(Paint.ANTI_ALIAS_FLAG); 166 | paint.setStyle(Style.FILL); 167 | paint.setStrokeCap(Cap.ROUND); 168 | 169 | springSystem = SpringSystem.create(); 170 | spring = springSystem.createSpring(); 171 | spring.setSpringConfig(SpringConfig.fromOrigamiTensionAndFriction(50, 7)); 172 | 173 | this.setOnClickListener(new OnClickListener() { 174 | @Override 175 | public void onClick(View arg0) { 176 | toggle(defaultAnimate); 177 | } 178 | }); 179 | 180 | // Beppi: swipe management 181 | this.setOnTouchListener(new OnTouchListener() { 182 | @Override 183 | public boolean onTouch(View view, MotionEvent motionEvent) { 184 | int x = (int) motionEvent.getX(); 185 | int action = motionEvent.getAction(); 186 | if (action == MotionEvent.ACTION_DOWN) { 187 | swipeX = x; 188 | swipeing = false; 189 | } 190 | else if (action == MotionEvent.ACTION_MOVE) { 191 | if (swipeSensitivityPixels == 0) return false; 192 | else if (x - swipeX > swipeSensitivityPixels) { 193 | swipeX = x; 194 | swipeing = true; 195 | increaseValue(); 196 | return true; 197 | } 198 | else if (swipeX - x > swipeSensitivityPixels) { 199 | swipeX = x; 200 | swipeing = true; 201 | decreaseValue(); 202 | return true; 203 | } 204 | } 205 | else if (action == MotionEvent.ACTION_UP) { 206 | if (!swipeing) toggle(defaultAnimate); // here simple clicks are managed. 207 | return true; 208 | } 209 | return false; 210 | } 211 | }); 212 | 213 | TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.TriStateToggleButton); 214 | offBorderColor = typedArray.getColor(R.styleable.TriStateToggleButton_tbOffBorderColor, offBorderColor); 215 | onColor = typedArray.getColor(R.styleable.TriStateToggleButton_tbOnColor, onColor); 216 | spotColor = typedArray.getColor(R.styleable.TriStateToggleButton_tbSpotColor, spotColor); 217 | offColor = typedArray.getColor(R.styleable.TriStateToggleButton_tbOffColor, offColor); 218 | // Beppi: added midColor attribute 219 | midColor = typedArray.getColor(R.styleable.TriStateToggleButton_tbMidColor, midColor); 220 | borderWidth = typedArray.getDimensionPixelSize(R.styleable.TriStateToggleButton_tbBorderWidth, borderWidth); 221 | defaultAnimate = typedArray.getBoolean(R.styleable.TriStateToggleButton_tbAnimate, defaultAnimate); 222 | // Beppi: modified defaultStatus from boolean to DefaultStatus 223 | // defaultStatus = typedArray.getBoolean(R.styleable.ToggleButton_tbDefaultStatus, defaultStatus); 224 | defaultStatus = attrToStatus(typedArray.getString(R.styleable.TriStateToggleButton_tbDefaultStatus)); 225 | // Beppi: added tbIsMidSelectable 226 | midSelectable = typedArray.getBoolean(R.styleable.TriStateToggleButton_tbIsMidSelectable, midSelectable); 227 | // Beppi: added enabled 228 | enabled = typedArray.getBoolean(R.styleable.TriStateToggleButton_enabled, enabled); 229 | 230 | // Beppi: swipe 231 | swipeSensitivityPixels = typedArray.getInt(R.styleable.TriStateToggleButton_tbSwipeSensitivityPixels, swipeSensitivityPixels); 232 | // 0 == off 233 | 234 | typedArray.recycle(); 235 | 236 | borderColor = offBorderColor; 237 | 238 | // Beppi: changed the usage of defaultStatus to match ToggleStatus type 239 | // if (defaultStatus) { toggleOn(); } 240 | switch (defaultStatus) { 241 | case off: toggleOff(); break; // actually not needed, added for clearness 242 | case mid: toggleMid(); break; 243 | case on: toggleOn(); break; 244 | } 245 | } 246 | 247 | public void toggle() { 248 | toggle(true); 249 | } 250 | 251 | // Beppi: modified to iterate on the 3 values instead of switching between two 252 | public void toggle(boolean animate) { 253 | // toggleStatus = !toggleStatus; 254 | if (midSelectable) 255 | switch (toggleStatus) { 256 | case off: putValueInToggleStatus(mid); break; 257 | case mid: putValueInToggleStatus(on); break; 258 | case on: putValueInToggleStatus(off); break; 259 | } 260 | else 261 | switch (toggleStatus) { 262 | case off: 263 | case mid: putValueInToggleStatus(on); break; 264 | case on: putValueInToggleStatus(off); break; 265 | } 266 | takeEffect(animate); 267 | 268 | if(listener != null){ 269 | listener.onToggle(toggleStatus, toggleStatusToBoolean(toggleStatus), toggleStatusToInt(toggleStatus)); 270 | } 271 | } 272 | 273 | public void toggleOn() { 274 | setToggleOn(); 275 | if(listener != null){ 276 | listener.onToggle(toggleStatus, toggleStatusToBoolean(toggleStatus), toggleStatusToInt(toggleStatus)); 277 | } 278 | } 279 | 280 | public void toggleOff() { 281 | setToggleOff(); 282 | if(listener != null){ 283 | listener.onToggle(toggleStatus, toggleStatusToBoolean(toggleStatus), toggleStatusToInt(toggleStatus)); 284 | } 285 | } 286 | 287 | // Beppi: added method to handle the mid value 288 | public void toggleMid() { 289 | setToggleMid(); 290 | if(listener != null){ 291 | listener.onToggle(toggleStatus, toggleStatusToBoolean(toggleStatus), toggleStatusToInt(toggleStatus)); 292 | } 293 | } 294 | 295 | private void putValueInToggleStatus(ToggleStatus value) { 296 | if (!enabled) return; 297 | previousToggleStatus = toggleStatus; 298 | toggleStatus = value; 299 | } 300 | 301 | /** 302 | * 设置显示成打开样式,不会触发toggle事件 // Setting the display to open style does not fire the toggle event 303 | */ 304 | public void setToggleOn() { 305 | setToggleOn(true); 306 | } 307 | 308 | /** 309 | * @param animate asd 310 | */ 311 | public void setToggleOn(boolean animate){ 312 | // Beppi: changed toggleStatus value from true to on 313 | // toggleStatus = true; 314 | putValueInToggleStatus(on); 315 | takeEffect(animate); 316 | } 317 | 318 | /** 319 | * 设置显示成关闭样式,不会触发toggle事件 // Settings are shown as off styles, and the toggle event is not fired 320 | */ 321 | public void setToggleOff() { 322 | setToggleOff(true); 323 | } 324 | 325 | public void setToggleOff(boolean animate) { 326 | // Beppi: changed toggleStatus value from false to off 327 | // toggleStatus = false; 328 | putValueInToggleStatus(off); 329 | takeEffect(animate); 330 | } 331 | 332 | // Beppi: added method for Mid value management 333 | public void setToggleMid(boolean animate) { 334 | putValueInToggleStatus(mid); 335 | takeEffect(animate); 336 | } 337 | public void setToggleMid() { 338 | setToggleMid(true); 339 | } 340 | 341 | //Beppi: added setToggleStatus() method, that imho was missing and needed 342 | public void setToggleStatus(ToggleStatus toggleStatus, boolean animate) { 343 | putValueInToggleStatus(toggleStatus); 344 | takeEffect(animate); 345 | } 346 | public void setToggleStatus(ToggleStatus toggleStatus) { 347 | setToggleStatus(toggleStatus, true); 348 | } 349 | public void setToggleStatus(boolean toggleStatus) { 350 | setToggleStatus(toggleStatus, true); 351 | } 352 | public void setToggleStatus(boolean toggleStatus, boolean animate) { 353 | if (toggleStatus) putValueInToggleStatus(on); 354 | else putValueInToggleStatus(off); 355 | takeEffect(animate); 356 | } 357 | public void setToggleStatus(int toggleIntValue) { 358 | setToggleStatus(toggleIntValue, true); 359 | } 360 | public void setToggleStatus(int toggleIntValue, boolean animate) { 361 | setToggleStatus(intToToggleStatus(toggleIntValue), animate); 362 | } 363 | 364 | public void increaseValue(boolean animate) { // same as toggle, but after on does not rewind to off 365 | switch (toggleStatus) { 366 | case off: if (midSelectable) putValueInToggleStatus(mid); else putValueInToggleStatus(on); break; 367 | case mid: putValueInToggleStatus(on); break; 368 | case on: break; 369 | } 370 | takeEffect(animate); 371 | if(listener != null){ 372 | listener.onToggle(toggleStatus, toggleStatusToBoolean(toggleStatus), toggleStatusToInt(toggleStatus)); 373 | } 374 | } 375 | public void increaseValue() { 376 | increaseValue(true); 377 | } 378 | public void decreaseValue(boolean animate) { 379 | switch (toggleStatus) { 380 | case on: if (midSelectable) putValueInToggleStatus(mid); else putValueInToggleStatus(off); break; 381 | case mid: putValueInToggleStatus(off); break; 382 | case off: break; 383 | } 384 | takeEffect(animate); 385 | if(listener != null){ 386 | listener.onToggle(toggleStatus, toggleStatusToBoolean(toggleStatus), toggleStatusToInt(toggleStatus)); 387 | } 388 | } 389 | public void decreaseValue() { 390 | decreaseValue(true); 391 | } 392 | 393 | // Beppi: rewritten takeEffect() method to manage 3 states 394 | /* 395 | private void takeEffect(boolean animate) { 396 | if(animate){ 397 | spring.setEndValue(toggleStatus ? 1 : 0); 398 | }else{ 399 | //这里没有调用spring,所以spring里的当前值没有变更,这里要设置一下,同步两边的当前值 400 | // There is no call spring, so the current value of the spring has not changed, here to set it, the current value on both sides of synchronization 401 | spring.setCurrentValue(toggleStatus ? 1 : 0); 402 | calculateEffect(toggleStatus ? 1 : 0); 403 | } 404 | } 405 | */ 406 | private void takeEffect(boolean animate) { 407 | if(animate){ 408 | spring.setEndValue(toggleStatus == on ? 1 : toggleStatus == off ? 0 : 0.5); 409 | }else{ 410 | //这里没有调用spring,所以spring里的当前值没有变更,这里要设置一下,同步两边的当前值 411 | // There is no call spring, so the current value of the spring has not changed, here to set it, the current value on both sides of synchronization 412 | spring.setCurrentValue(toggleStatus == on ? 1 : toggleStatus == off ? 0 : 0.5); 413 | if (toggleStatus == on) calculateEffect(1); 414 | else if (toggleStatus == mid) calculateEffect(0.5); 415 | else calculateEffect(0); 416 | 417 | // calculateEffect(toggleStatus == on ? 1 : toggleStatus == off ? 0 : 0.5); 418 | } 419 | } 420 | 421 | 422 | @Override 423 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 424 | final int widthMode = MeasureSpec.getMode(widthMeasureSpec); 425 | final int heightMode = MeasureSpec.getMode(heightMeasureSpec); 426 | 427 | int widthSize = MeasureSpec.getSize(widthMeasureSpec); 428 | int heightSize = MeasureSpec.getSize(heightMeasureSpec); 429 | 430 | Resources r = Resources.getSystem(); 431 | if(widthMode == MeasureSpec.UNSPECIFIED || widthMode == MeasureSpec.AT_MOST){ 432 | widthSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50, r.getDisplayMetrics()); 433 | widthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); 434 | } 435 | 436 | if(heightMode == MeasureSpec.UNSPECIFIED || heightSize == MeasureSpec.AT_MOST){ 437 | heightSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 30, r.getDisplayMetrics()); 438 | heightMeasureSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY); 439 | } 440 | 441 | 442 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 443 | } 444 | 445 | 446 | @Override 447 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 448 | super.onLayout(changed, left, top, right, bottom); 449 | 450 | final int width = getWidth(); 451 | final int height = getHeight(); 452 | 453 | radius = Math.min(width, height) * 0.5f; 454 | centerY = radius; 455 | startX = radius; 456 | endX = width - radius; 457 | spotMinX = startX + borderWidth; 458 | spotMaxX = endX - borderWidth; 459 | spotMidX = (startX + endX) / 2; 460 | spotSize = height - 4 * borderWidth; 461 | // Beppi: changed management of the position according to 3 states 462 | // spotX = toggleStatus ? spotMaxX : spotMinX; 463 | spotX = toggleStatus == on ? spotMaxX : toggleStatus == off ? spotMinX : spotMidX; 464 | offLineWidth = 0; 465 | } 466 | 467 | 468 | SimpleSpringListener springListener = new SimpleSpringListener(){ 469 | @Override 470 | public void onSpringUpdate(Spring spring) { 471 | final double value = spring.getCurrentValue(); 472 | calculateEffect(value); 473 | } 474 | }; 475 | 476 | private int clamp(int value, int low, int high) { 477 | return Math.min(Math.max(value, low), high); 478 | } 479 | 480 | 481 | @Override 482 | public void draw(Canvas canvas) { 483 | rect.set(0, 0, getWidth(), getHeight()); 484 | paint.setColor(borderColor); 485 | canvas.drawRoundRect(rect, radius, radius, paint); 486 | 487 | if(offLineWidth > 0){ 488 | final float cy = offLineWidth * 0.5f; 489 | rect.set(spotX - cy, centerY - cy, endX + cy, centerY + cy); 490 | paint.setColor(enabled ? (toggleStatus == mid ? midColor : offColor) : disabledColor); 491 | canvas.drawRoundRect(rect, cy, cy, paint); 492 | } 493 | 494 | rect.set(spotX - 1 - radius, centerY - radius, spotX + 1.1f + radius, centerY + radius); 495 | paint.setColor(enabled ? borderColor : disabledColor); 496 | canvas.drawRoundRect(rect, radius, radius, paint); 497 | 498 | final float spotR = spotSize * 0.5f; 499 | rect.set(spotX - spotR, centerY - spotR, spotX + spotR, centerY + spotR); 500 | paint.setColor(enabled ? spotColor : disabledColor); 501 | canvas.drawRoundRect(rect, spotR, spotR, paint); 502 | 503 | } 504 | 505 | /** 506 | * @param value 507 | */ 508 | /* 509 | private void calculateEffect(final double value) { 510 | final float mapToggleX = (float) SpringUtil.mapValueFromRangeToRange(value, 0, 1, spotMinX, spotMaxX); 511 | spotX = mapToggleX; 512 | 513 | float mapOffLineWidth = (float) SpringUtil.mapValueFromRangeToRange(1 - value, 0, 1, 10, spotSize); 514 | 515 | offLineWidth = mapOffLineWidth; 516 | 517 | final int fromB = Color.blue(onColor); 518 | final int fromR = Color.red(onColor); 519 | final int fromG = Color.green(onColor); 520 | 521 | final int toB = Color.blue(offBorderColor); 522 | final int toR = Color.red(offBorderColor); 523 | final int toG = Color.green(offBorderColor); 524 | 525 | int springB = (int) SpringUtil.mapValueFromRangeToRange(1 - value, 0, 1, fromB, toB); 526 | int springR = (int) SpringUtil.mapValueFromRangeToRange(1 - value, 0, 1, fromR, toR); 527 | int springG = (int) SpringUtil.mapValueFromRangeToRange(1 - value, 0, 1, fromG, toG); 528 | 529 | springB = clamp(springB, 0, 255); 530 | springR = clamp(springR, 0, 255); 531 | springG = clamp(springG, 0, 255); 532 | 533 | borderColor = Color.rgb(springR, springG, springB); 534 | 535 | postInvalidate(); 536 | } 537 | */ 538 | private void calculateEffect(final double value) { 539 | final float mapToggleX = (float) SpringUtil.mapValueFromRangeToRange(value, 0, 1, spotMinX, spotMaxX); 540 | spotX = mapToggleX; 541 | double min = 0, max = 0; 542 | int fromColor, toColor; 543 | if (previousToggleStatus == off && toggleStatus == mid) { 544 | toColor = offBorderColor; fromColor = midColor; 545 | } else 546 | if (previousToggleStatus == off && toggleStatus == on) { 547 | toColor = offBorderColor; fromColor = onColor; 548 | } else 549 | if (previousToggleStatus == mid && toggleStatus == on) { 550 | toColor = midColor; fromColor = onColor; 551 | } else 552 | if (previousToggleStatus == on && toggleStatus == off) { 553 | toColor = offBorderColor; fromColor = onColor; 554 | } else 555 | if (previousToggleStatus == on && toggleStatus == mid) { 556 | toColor = midColor; fromColor = onColor; 557 | } else 558 | { 559 | toColor = offBorderColor; fromColor = onColor; 560 | } 561 | 562 | if (previousToggleStatus == off) min = 0; 563 | else if (previousToggleStatus == mid) min = 0.5; 564 | else min = 1; 565 | if (toggleStatus == off) max = 0; 566 | else if (toggleStatus == mid) max = 0.5; 567 | else max = 1; 568 | 569 | if (min == max) { min = 0; max = 1; } 570 | else if (min > max) { double temp = min; min = max; max = temp; } 571 | 572 | offLineWidth = (float) SpringUtil.mapValueFromRangeToRange(min + max - value, min, max, 0, spotSize); 573 | 574 | final int fromB = Color.blue(fromColor); 575 | final int fromR = Color.red(fromColor); 576 | final int fromG = Color.green(fromColor); 577 | final int toB = Color.blue(toColor); 578 | final int toR = Color.red(toColor); 579 | final int toG = Color.green(toColor); 580 | 581 | int springB = (int) SpringUtil.mapValueFromRangeToRange(min + max - value, min, max, fromB, toB); 582 | int springR = (int) SpringUtil.mapValueFromRangeToRange(min + max - value, min, max, fromR, toR); 583 | int springG = (int) SpringUtil.mapValueFromRangeToRange(min + max - value, min, max, fromG, toG); 584 | 585 | springB = clamp(springB, 0, 255); 586 | springR = clamp(springR, 0, 255); 587 | springG = clamp(springG, 0, 255); 588 | 589 | borderColor = Color.rgb(springR, springG, springB); 590 | 591 | postInvalidate(); 592 | } 593 | 594 | /** 595 | * @author ThinkPad 596 | * 597 | */ 598 | public interface OnToggleChanged{ 599 | /** 600 | * @param toggleStatus = = 601 | */ 602 | // Beppi: changed according to 3 states value 603 | // public void onToggle(boolean on); 604 | public void onToggle(ToggleStatus toggleStatus, boolean booleanToggleStatus, int toggleIntValue); 605 | } 606 | 607 | public void setOnToggleChanged(OnToggleChanged onToggleChanged) { 608 | listener = onToggleChanged; 609 | } 610 | 611 | public boolean isAnimate() { 612 | return defaultAnimate; 613 | } 614 | public void setAnimate(boolean animate) { 615 | this.defaultAnimate = animate; 616 | } 617 | 618 | // Beppi: added all next methods, getters and setters 619 | 620 | 621 | public int getOnColor() { 622 | return onColor; 623 | } 624 | 625 | public void setOnColor(int onColor) { 626 | this.onColor = onColor; 627 | postInvalidate(); 628 | } 629 | 630 | public int getOffBorderColor() { 631 | return offBorderColor; 632 | } 633 | 634 | public void setOffBorderColor(int offBorderColor) { 635 | this.offBorderColor = offBorderColor; 636 | postInvalidate(); 637 | } 638 | 639 | public int getOffColor() { 640 | return offColor; 641 | } 642 | 643 | public void setOffColor(int offColor) { 644 | this.offColor = offColor; 645 | postInvalidate(); 646 | } 647 | 648 | public int getMidColor() { 649 | return midColor; 650 | } 651 | 652 | public void setMidColor(int midColor) { 653 | this.midColor = midColor; 654 | postInvalidate(); 655 | } 656 | 657 | public int getSpotColor() { 658 | return spotColor; 659 | } 660 | 661 | public void setSpotColor(int spotColor) { 662 | this.spotColor = spotColor; 663 | postInvalidate(); 664 | } 665 | 666 | public int getBorderColor() { 667 | return borderColor; 668 | } 669 | 670 | public void setBorderColor(int borderColor) { 671 | this.borderColor = borderColor; 672 | postInvalidate(); 673 | } 674 | 675 | public ToggleStatus getToggleStatus() { 676 | return toggleStatus; 677 | } 678 | 679 | public int getBorderWidth() { 680 | return borderWidth; 681 | } 682 | 683 | public void setBorderWidth(int borderWidth) { 684 | this.borderWidth = borderWidth; 685 | postInvalidate(); 686 | } 687 | 688 | public int getSpotSize() { 689 | return spotSize; 690 | } 691 | 692 | public void setSpotSize(int spotSize) { 693 | this.spotSize = spotSize; 694 | postInvalidate(); 695 | } 696 | 697 | public boolean isMidSelectable() { 698 | return midSelectable; 699 | } 700 | 701 | public void setMidSelectable(boolean midSelectable) { 702 | this.midSelectable = midSelectable; 703 | } 704 | 705 | @Override 706 | public boolean isEnabled() { 707 | return enabled; 708 | } 709 | 710 | @Override 711 | public void setEnabled(boolean enabled) { 712 | this.enabled = enabled; 713 | postInvalidate(); 714 | super.setEnabled(enabled); 715 | } 716 | } 717 | -------------------------------------------------------------------------------- /tristatetogglebutton_library/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | TriStateToggleButton 3 | 4 | -------------------------------------------------------------------------------- /tristatetogglebutton_library/src/main/res/values/toggle_button_attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /tristatetogglebutton_library/src/test/java/it/beppi/tristatetogglebutton_library/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package it.beppi.tristatetogglebutton_library; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() throws Exception { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /tristatetogglebutton_sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /tristatetogglebutton_sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 25 5 | buildToolsVersion "25.0.0" 6 | defaultConfig { 7 | applicationId "it.beppi.tristatetogglebutton" 8 | minSdkVersion 9 9 | targetSdkVersion 25 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 25 | exclude group: 'com.android.support', module: 'support-annotations' 26 | }) 27 | compile 'com.android.support:appcompat-v7:25.0.1' 28 | testCompile 'junit:junit:4.12' 29 | compile project(':tristatetogglebutton_library') 30 | } 31 | -------------------------------------------------------------------------------- /tristatetogglebutton_sample/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in F:\Android\android-sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /tristatetogglebutton_sample/src/androidTest/java/it/beppi/tristatetogglebutton/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package it.beppi.tristatetogglebutton; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("it.beppi.tristatetogglebutton", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tristatetogglebutton_sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tristatetogglebutton_sample/src/main/java/it/beppi/tristatetogglebuttonsample/SampleActivity.java: -------------------------------------------------------------------------------- 1 | package it.beppi.tristatetogglebuttonsample; 2 | 3 | import android.graphics.Color; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.os.Bundle; 6 | import android.view.View; 7 | import android.widget.Button; 8 | import android.widget.TextView; 9 | 10 | import java.util.Random; 11 | 12 | import it.beppi.tristatetogglebutton_library.TriStateToggleButton; 13 | 14 | public class SampleActivity extends AppCompatActivity { 15 | 16 | @Override 17 | protected void onCreate(Bundle savedInstanceState) { 18 | super.onCreate(savedInstanceState); 19 | setContentView(R.layout.activity_sample); 20 | 21 | final TriStateToggleButton tstb_1 = (TriStateToggleButton) findViewById(R.id.tstb_1); 22 | final TextView tstb_1_text = (TextView) findViewById(R.id.tstb_1_text); 23 | 24 | final TriStateToggleButton tstb_2 = (TriStateToggleButton) findViewById(R.id.tstb_2); 25 | final TextView tstb_2_text = (TextView) findViewById(R.id.tstb_2_text); 26 | 27 | final TriStateToggleButton tstb_3 = (TriStateToggleButton) findViewById(R.id.tstb_3); 28 | final TextView tstb_3_text = (TextView) findViewById(R.id.tstb_3_text); 29 | 30 | final TriStateToggleButton tstb_4 = (TriStateToggleButton) findViewById(R.id.tstb_4); 31 | 32 | // Example 1: a default tristate toggle without any customization 33 | 34 | tstb_1_text.setText("Off"); 35 | tstb_1.setOnToggleChanged(new TriStateToggleButton.OnToggleChanged() { 36 | @Override 37 | public void onToggle(TriStateToggleButton.ToggleStatus toggleStatus, boolean booleanToggleStatus, int toggleIntValue) { 38 | switch (toggleStatus) { 39 | case off: tstb_1_text.setText("Off"); break; 40 | case mid: tstb_1_text.setText("Half way"); break; 41 | case on: tstb_1_text.setText("On"); break; 42 | } 43 | } 44 | }); 45 | 46 | // Example 2: a default tristate toggle that starts in the middle status and never gets back again to it 47 | 48 | tstb_2_text.setText("Almost..."); 49 | tstb_2.setOnToggleChanged(new TriStateToggleButton.OnToggleChanged() { 50 | @Override 51 | public void onToggle(TriStateToggleButton.ToggleStatus toggleStatus, boolean booleanToggleStatus, int toggleIntValue) { 52 | switch (toggleStatus) { 53 | case off: tstb_2_text.setText("Off"); break; 54 | case mid: tstb_2_text.setText("Almost..."); break; 55 | case on: tstb_2_text.setText("On"); break; 56 | } 57 | } 58 | }); 59 | 60 | // Example 3: a customized tristate toggle that is undefined in the middle and enables / disables another toggle 61 | 62 | tstb_3_text.setText("undefined"); 63 | tstb_3.setOnToggleChanged(new TriStateToggleButton.OnToggleChanged() { 64 | @Override 65 | public void onToggle(TriStateToggleButton.ToggleStatus toggleStatus, boolean booleanToggleStatus, int toggleIntValue) { 66 | switch (toggleStatus) { 67 | case off: tstb_3_text.setText("False"); break; 68 | case mid: tstb_3_text.setText("undefined"); break; 69 | case on: tstb_3_text.setText("True"); break; 70 | } 71 | } 72 | }); 73 | 74 | // Example 4: an out of the box classic 2-state toggle, using booleans, controls toggle 3 75 | 76 | tstb_4.setToggleStatus(true); 77 | tstb_4.setOnToggleChanged(new TriStateToggleButton.OnToggleChanged() { 78 | @Override 79 | public void onToggle(TriStateToggleButton.ToggleStatus toggleStatus, boolean booleanToggleStatus, int toggleIntValue) { 80 | tstb_3.setEnabled(booleanToggleStatus); 81 | } 82 | }); 83 | 84 | // Example 5: random restyle of toggle 3 85 | 86 | ((Button) findViewById(R.id.button_restyle)).setOnClickListener(new View.OnClickListener() { 87 | @Override 88 | public void onClick(View view) { 89 | restyle(tstb_3); 90 | } 91 | }); 92 | 93 | } 94 | 95 | void restyle(TriStateToggleButton toggleButton) { 96 | Random rnd = new Random(); 97 | for (int w = 0; w < 7; w++) { 98 | int randomColor = Color.argb(255, rnd.nextInt(256), rnd.nextInt(256), rnd.nextInt(256)); 99 | switch (w) { 100 | case 0: toggleButton.setMidColor(randomColor); break; 101 | case 1: toggleButton.setBorderColor(randomColor); break; 102 | case 2: toggleButton.setOffBorderColor(randomColor); break; 103 | case 3: toggleButton.setOffColor(randomColor); break; 104 | case 4: toggleButton.setOnColor(randomColor); break; 105 | case 5: toggleButton.setSpotColor(randomColor); break; 106 | case 6: toggleButton.setSpotSize(rnd.nextInt(20) + 30); break; 107 | } 108 | } 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /tristatetogglebutton_sample/src/main/res/layout/activity_sample.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 20 | 27 | 33 | 34 | 41 | 42 | 43 | 48 | 55 | 62 | 63 | 70 | 71 | 72 | 77 | 84 | 97 | 104 | 105 | 106 | 107 | 112 | 119 | 125 | 126 | 127 | 128 |