├── .gitignore ├── 21879.gif ├── LICENSE ├── README.md ├── SECURITY.md ├── build.gradle ├── device-capture.png ├── gradle-mvn-push.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── library ├── .gitignore ├── build.gradle ├── gradle.properties └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── suke │ │ └── widget │ │ └── SwitchButton.kt │ └── res │ └── values │ └── switch_button_attrs.xml ├── sample ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── suke │ │ └── widget │ │ └── sample │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── suke │ │ │ └── widget │ │ │ └── sample │ │ │ └── MainActivity.kt │ └── res │ │ ├── layout │ │ └── activity_main.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 │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── suke │ └── widget │ └── sample │ └── ExampleUnitTest.java └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .DS_Store 3 | /build 4 | /captures 5 | .externalNativeBuild 6 | 7 | 8 | .project 9 | .classpath 10 | .settings/ 11 | 12 | # vim 13 | *~ 14 | *.sw* 15 | 16 | # git 17 | local.properties 18 | 19 | #Idea 20 | *.iml 21 | /.idea 22 | -------------------------------------------------------------------------------- /21879.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcweng/SwitchButton/24ee0e0265dcafe524c8bb3898e6f06910b8b952/21879.gif -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 https://github.com/zcweng/SwitchButton 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwitchButton 2 | SwitchButton.An *beautiful+lightweight+custom-style-easy* switch widget for Android,minSdkVersion >= 14
3 | issues welcome~
4 | ![](21879.gif)
5 | 6 | Features 7 | ------- 8 | -depend without third-part library
9 | -without raw files(pictures/drawables etc...), only one java and style.xml file
10 | -drag switch supported
11 | 12 | 13 | UseAge 14 | ------- 15 | gradle: 16 | ```grovvy 17 | repositories { 18 | mavenCentral() 19 | jcenter() 20 | } 21 | 22 | ... 23 | 24 | dependencies { 25 | implementation 'com.github.zcweng:switch-button:0.0.3@aar' 26 | } 27 | ``` 28 | 29 | layout.xml: 30 | ```xml 31 | 36 | 37 | 41 | 42 | 43 | ``` 44 | 45 | Java: 46 | ```java 47 | SwitchButton switchButton = (com.suke.widget.SwitchButton) 48 | findViewById(R.id.switch_button); 49 | 50 | switchButton.setChecked(true); 51 | switchButton.isChecked(); 52 | switchButton.toggle(); //switch state 53 | switchButton.toggle(false);//switch without animation 54 | switchButton.setShadowEffect(true);//disable shadow effect 55 | switchButton.setEnabled(false);//disable button 56 | switchButton.setEnableEffect(false);//disable the switch animation 57 | switchButton.setOnCheckedChangeListener(new SwitchButton.OnCheckedChangeListener() { 58 | @Override 59 | public void onCheckedChanged(SwitchButton view, boolean isChecked) { 60 | //TODO do your job 61 | } 62 | }); 63 | 64 | 65 | ``` 66 | 67 | Kotlin: 68 | ```kotlin 69 | val switchButton = findViewById(R.id.switch_button) as SwitchButton 70 | 71 | switchButton.isChecked = true 72 | switchButton.isChecked 73 | switchButton.toggle() //switch state 74 | switchButton.toggle(false) //switch without animation 75 | switchButton.setShadowEffect(true) //disable shadow effect 76 | switchButton.isEnabled = false //disable button 77 | switchButton.setEnableEffect(false) //disable the switch animation 78 | switchButton.setOnCheckedChangeListener(object : SwitchButton.OnCheckedChangeListener { 79 | override fun onCheckedChanged(view: SwitchButton?, isChecked: Boolean) { 80 | //TODO do your job 81 | } 82 | }) 83 | 84 | 85 | ``` 86 | 87 | More Style: 88 | ```xml 89 | 阴影半径 90 | 阴影偏移 91 | 阴影颜色 92 | 关闭颜色 93 | 开启颜色 94 | 边框宽度 95 | 开启指示器颜色 96 | 开启指示器线宽 97 | 关闭指示器颜色 98 | 关闭指示器线宽 99 | 关闭指示器半径 100 | 是否选中 101 | 是否启用阴影 102 | 动画时间,默认300ms 103 | 按钮颜色 104 | 是否显示指示器,默认true:显示 105 | 背景色,默认白色 106 | 是否启用特效,默认true 107 | ``` 108 | 109 | 110 | ScreenShot 111 | ------- 112 | Sample Apk:
113 | ![](http://qr.api.cli.im/qr?data=https%253A%252F%252Fgithub.com%252Fzcweng%252FSwitchButton%252Fblob%252Fmaster%252Fsample%252Fsample-debug.apk&level=H&transparent=false&bgcolor=%23ffffff&forecolor=%23000000&blockpixel=12&marginblock=1&logourl=&size=280&kid=cliim&key=8144f9f150d38d7d364c923d0b9c87cf)
114 | 115 | 116 | License 117 | ------- 118 | MIT, See the [LICENSE] file for details. 119 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext.kotlin_version = '1.6.10' 5 | repositories { 6 | google() 7 | mavenCentral() 8 | maven { 9 | url 'https://maven.google.com' 10 | } 11 | } 12 | dependencies { 13 | classpath 'com.android.tools.build:gradle:3.6.0' 14 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | mavenCentral() 21 | google() 22 | maven { 23 | url 'https://maven.google.com' 24 | } 25 | } 26 | } 27 | 28 | task clean(type: Delete) { 29 | delete rootProject.buildDir 30 | } 31 | -------------------------------------------------------------------------------- /device-capture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcweng/SwitchButton/24ee0e0265dcafe524c8bb3898e6f06910b8b952/device-capture.png -------------------------------------------------------------------------------- /gradle-mvn-push.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'maven' 2 | apply plugin: 'signing' 3 | 4 | def isReleaseBuild() { 5 | return VERSION_NAME.contains("SNAPSHOT") == false 6 | } 7 | 8 | def getReleaseRepositoryUrl() { 9 | return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL 10 | : "https://oss.sonatype.org/service/local/staging/deploy/maven2/" 11 | } 12 | 13 | def getSnapshotRepositoryUrl() { 14 | return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL 15 | : "https://oss.sonatype.org/content/repositories/snapshots/" 16 | } 17 | 18 | def getRepositoryUsername() { 19 | return hasProperty('NEXUS_USERNAME') ? NEXUS_USERNAME : "" 20 | } 21 | 22 | def getRepositoryPassword() { 23 | return hasProperty('NEXUS_PASSWORD') ? NEXUS_PASSWORD : "" 24 | } 25 | 26 | afterEvaluate { project -> 27 | uploadArchives { 28 | repositories { 29 | mavenDeployer { 30 | beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } 31 | 32 | pom.groupId = GROUP 33 | pom.artifactId = POM_ARTIFACT_ID 34 | pom.version = VERSION_NAME 35 | 36 | repository(url: getReleaseRepositoryUrl()) { 37 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) 38 | } 39 | snapshotRepository(url: getSnapshotRepositoryUrl()) { 40 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) 41 | } 42 | 43 | pom.project { 44 | name POM_NAME 45 | packaging POM_PACKAGING 46 | description POM_DESCRIPTION 47 | url POM_URL 48 | 49 | scm { 50 | url POM_SCM_URL 51 | connection POM_SCM_CONNECTION 52 | developerConnection POM_SCM_DEV_CONNECTION 53 | } 54 | 55 | licenses { 56 | license { 57 | name POM_LICENCE_NAME 58 | url POM_LICENCE_URL 59 | distribution POM_LICENCE_DIST 60 | } 61 | } 62 | 63 | developers { 64 | developer { 65 | id POM_DEVELOPER_ID 66 | name POM_DEVELOPER_NAME 67 | } 68 | } 69 | } 70 | } 71 | } 72 | } 73 | 74 | signing { 75 | required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") } 76 | sign configurations.archives 77 | } 78 | 79 | task androidJavadocs(type: Javadoc) { 80 | source = android.sourceSets.main.java.srcDirs 81 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 82 | } 83 | 84 | task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) { 85 | classifier = 'javadoc' 86 | from androidJavadocs.destinationDir 87 | } 88 | 89 | task androidSourcesJar(type: Jar) { 90 | classifier = 'sources' 91 | from android.sourceSets.main.java.sourceFiles 92 | } 93 | 94 | artifacts { 95 | archives androidSourcesJar 96 | archives androidJavadocsJar 97 | } 98 | } -------------------------------------------------------------------------------- /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 | 19 | android.useAndroidX=true 20 | 21 | 22 | 23 | VERSION_NAME=0.0.3 24 | VERSION_CODE=3 25 | GROUP=com.github.zcweng 26 | 27 | POM_DESCRIPTION=SwitchButton.An *beautiful+lightweight+custom-style-easy* switch widget for Android 28 | POM_URL=https://github.com/zcweng/SwitchButton 29 | POM_SCM_URL=https://github.com/zcweng/SwitchButton 30 | POM_SCM_CONNECTION=scm:git@github.com:zcweng/SwitchButton.git 31 | POM_SCM_DEV_CONNECTION=scm:git@github.com:zcweng/SwitchButton.git 32 | POM_LICENCE_NAME=MIT 33 | POM_LICENCE_URL=https://github.com/zcweng/SwitchButton/LICENSE 34 | POM_LICENCE_DIST=repo 35 | POM_DEVELOPER_ID=suke 36 | POM_DEVELOPER_NAME=suke -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcweng/SwitchButton/24ee0e0265dcafe524c8bb3898e6f06910b8b952/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Apr 27 21:45:57 EEST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.1-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 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 | -------------------------------------------------------------------------------- /library/.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | 4 | apply from: '../gradle-mvn-push.gradle' 5 | 6 | def VERSION_CODE = 3 7 | def VERSION_NAME = "0.0.3" 8 | 9 | android { 10 | compileSdkVersion 33 11 | 12 | defaultConfig { 13 | minSdkVersion 14 14 | targetSdkVersion 33 15 | } 16 | 17 | lintOptions { 18 | abortOnError false 19 | } 20 | } 21 | 22 | 23 | dependencies { 24 | implementation "androidx.core:core-ktx:1.6.0" 25 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 26 | } -------------------------------------------------------------------------------- /library/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=SwitchButton 2 | POM_ARTIFACT_ID=switch-button 3 | POM_PACKAGING=aar -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /library/src/main/java/com/suke/widget/SwitchButton.kt: -------------------------------------------------------------------------------- 1 | package com.suke.widget 2 | 3 | import android.animation.Animator 4 | import android.widget.Checkable 5 | import android.annotation.TargetApi 6 | import android.os.Build 7 | import android.content.res.TypedArray 8 | import com.suke.widget.R 9 | import com.suke.widget.SwitchButton 10 | import com.suke.widget.SwitchButton.ViewState 11 | import android.animation.ValueAnimator 12 | import android.view.View.MeasureSpec 13 | import android.view.MotionEvent 14 | import android.view.View.OnLongClickListener 15 | import android.graphics.RectF 16 | import android.animation.ValueAnimator.AnimatorUpdateListener 17 | import android.animation.Animator.AnimatorListener 18 | import android.animation.ArgbEvaluator 19 | import android.content.Context 20 | import android.content.res.Resources 21 | import android.graphics.Canvas 22 | import android.graphics.Color 23 | import android.graphics.Paint 24 | import android.util.AttributeSet 25 | import android.util.TypedValue 26 | import android.view.View 27 | import java.lang.RuntimeException 28 | import kotlin.math.max 29 | import kotlin.math.min 30 | 31 | /** 32 | * SwitchButton. 33 | */ 34 | class SwitchButton : View, Checkable { 35 | /** 36 | * 动画状态: 37 | * 1.静止 38 | * 2.进入拖动 39 | * 3.处于拖动 40 | * 4.拖动-复位 41 | * 5.拖动-切换 42 | * 6.点击切换 43 | */ 44 | private val ANIMATE_STATE_NONE = 0 45 | private val ANIMATE_STATE_PENDING_DRAG = 1 46 | private val ANIMATE_STATE_DRAGING = 2 47 | private val ANIMATE_STATE_PENDING_RESET = 3 48 | private val ANIMATE_STATE_PENDING_SETTLE = 4 49 | private val ANIMATE_STATE_SWITCH = 5 50 | 51 | constructor(context: Context) : super(context) { 52 | init(context, null) 53 | } 54 | 55 | constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { 56 | init(context, attrs) 57 | } 58 | 59 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( 60 | context, 61 | attrs, 62 | defStyleAttr 63 | ) { 64 | init(context, attrs) 65 | } 66 | 67 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 68 | constructor( 69 | context: Context, 70 | attrs: AttributeSet?, 71 | defStyleAttr: Int, 72 | defStyleRes: Int 73 | ) : super(context, attrs, defStyleAttr, defStyleRes) { 74 | init(context, attrs) 75 | } 76 | 77 | override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) { 78 | super.setPadding(0, 0, 0, 0) 79 | } 80 | 81 | /** 82 | * 初始化参数 83 | */ 84 | private fun init(context: Context, attrs: AttributeSet?) { 85 | var typedArray: TypedArray? = null 86 | if (attrs != null) { 87 | typedArray = context.obtainStyledAttributes(attrs, R.styleable.SwitchButton) 88 | } 89 | shadowEffect = optBoolean( 90 | typedArray, 91 | R.styleable.SwitchButton_sb_shadow_effect, 92 | true 93 | ) 94 | uncheckCircleColor = optColor( 95 | typedArray, 96 | R.styleable.SwitchButton_sb_uncheckcircle_color, 97 | -0x555556 98 | ) //0XffAAAAAA; 99 | uncheckCircleWidth = optPixelSize( 100 | typedArray, 101 | R.styleable.SwitchButton_sb_uncheckcircle_width, 102 | dp2pxInt(1.5f) 103 | ) //dp2pxInt(1.5f); 104 | uncheckCircleOffsetX = dp2px(10f) 105 | uncheckCircleRadius = optPixelSize( 106 | typedArray, 107 | R.styleable.SwitchButton_sb_uncheckcircle_radius, 108 | dp2px(4f) 109 | ) //dp2px(4); 110 | checkedLineOffsetX = dp2px(4f) 111 | checkedLineOffsetY = dp2px(4f) 112 | shadowRadius = optPixelSize( 113 | typedArray, 114 | R.styleable.SwitchButton_sb_shadow_radius, 115 | dp2pxInt(2.5f) 116 | ) //dp2pxInt(2.5f); 117 | shadowOffset = optPixelSize( 118 | typedArray, 119 | R.styleable.SwitchButton_sb_shadow_offset, 120 | dp2pxInt(1.5f) 121 | ) //dp2pxInt(1.5f); 122 | shadowColor = optColor( 123 | typedArray, 124 | R.styleable.SwitchButton_sb_shadow_color, 125 | 0X33000000 126 | ) //0X33000000; 127 | uncheckColor = optColor( 128 | typedArray, 129 | R.styleable.SwitchButton_sb_uncheck_color, 130 | -0x222223 131 | ) //0XffDDDDDD; 132 | checkedColor = optColor( 133 | typedArray, 134 | R.styleable.SwitchButton_sb_checked_color, 135 | -0xae2c99 136 | ) //0Xff51d367; 137 | borderWidth = optPixelSize( 138 | typedArray, 139 | R.styleable.SwitchButton_sb_border_width, 140 | dp2pxInt(1f) 141 | ) //dp2pxInt(1); 142 | checkLineColor = optColor( 143 | typedArray, 144 | R.styleable.SwitchButton_sb_checkline_color, 145 | Color.WHITE 146 | ) //Color.WHITE; 147 | checkLineWidth = optPixelSize( 148 | typedArray, 149 | R.styleable.SwitchButton_sb_checkline_width, 150 | dp2pxInt(1f) 151 | ) //dp2pxInt(1.0f); 152 | checkLineLength = dp2px(6f) 153 | val buttonColor = optColor( 154 | typedArray, 155 | R.styleable.SwitchButton_sb_button_color, 156 | Color.WHITE 157 | ) //Color.WHITE; 158 | uncheckButtonColor = optColor( 159 | typedArray, 160 | R.styleable.SwitchButton_sb_uncheckbutton_color, 161 | buttonColor 162 | ) 163 | checkedButtonColor = optColor( 164 | typedArray, 165 | R.styleable.SwitchButton_sb_checkedbutton_color, 166 | buttonColor 167 | ) 168 | val effectDuration = optInt( 169 | typedArray, 170 | R.styleable.SwitchButton_sb_effect_duration, 171 | 300 172 | ) //300; 173 | isChecked = optBoolean( 174 | typedArray, 175 | R.styleable.SwitchButton_sb_checked, 176 | false 177 | ) 178 | showIndicator = optBoolean( 179 | typedArray, 180 | R.styleable.SwitchButton_sb_show_indicator, 181 | true 182 | ) 183 | background = optColor( 184 | typedArray, 185 | R.styleable.SwitchButton_sb_background, 186 | Color.WHITE 187 | ) //Color.WHITE; 188 | enableEffect = optBoolean( 189 | typedArray, 190 | R.styleable.SwitchButton_sb_enable_effect, 191 | true 192 | ) 193 | typedArray?.recycle() 194 | paint = Paint(Paint.ANTI_ALIAS_FLAG) 195 | buttonPaint = Paint(Paint.ANTI_ALIAS_FLAG) 196 | buttonPaint!!.color = buttonColor 197 | if (shadowEffect) { 198 | buttonPaint!!.setShadowLayer( 199 | shadowRadius.toFloat(), 0f, shadowOffset.toFloat(), 200 | shadowColor 201 | ) 202 | } 203 | viewState = ViewState() 204 | beforeState = ViewState() 205 | afterState = ViewState() 206 | valueAnimator = ValueAnimator.ofFloat(0f, 1f) 207 | valueAnimator?.duration = effectDuration.toLong() 208 | valueAnimator?.repeatCount = 0 209 | valueAnimator?.addUpdateListener(animatorUpdateListener) 210 | valueAnimator?.addListener(animatorListener) 211 | super.setClickable(true) 212 | setPadding(0, 0, 0, 0) 213 | setLayerType(LAYER_TYPE_SOFTWARE, null) 214 | 215 | } 216 | 217 | override fun onMeasure(widthMeasureSpecParam: Int, heightMeasureSpecParam: Int) { 218 | var widthMeasureSpec = widthMeasureSpecParam 219 | var heightMeasureSpec = heightMeasureSpecParam 220 | val widthMode = MeasureSpec.getMode(widthMeasureSpec) 221 | val heightMode = MeasureSpec.getMode(heightMeasureSpec) 222 | if (widthMode == MeasureSpec.UNSPECIFIED 223 | || widthMode == MeasureSpec.AT_MOST 224 | ) { 225 | widthMeasureSpec = MeasureSpec.makeMeasureSpec(DEFAULT_WIDTH, MeasureSpec.EXACTLY) 226 | } 227 | if (heightMode == MeasureSpec.UNSPECIFIED 228 | || heightMode == MeasureSpec.AT_MOST 229 | ) { 230 | heightMeasureSpec = MeasureSpec.makeMeasureSpec(DEFAULT_HEIGHT, MeasureSpec.EXACTLY) 231 | } 232 | super.onMeasure(widthMeasureSpec, heightMeasureSpec) 233 | } 234 | 235 | override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { 236 | super.onSizeChanged(w, h, oldw, oldh) 237 | val viewPadding = max(shadowRadius + shadowOffset, borderWidth).toFloat() 238 | height = h - viewPadding - viewPadding 239 | width = w - viewPadding - viewPadding 240 | viewRadius = height * .5f 241 | buttonRadius = viewRadius - borderWidth 242 | left = viewPadding 243 | top = viewPadding 244 | right = w - viewPadding 245 | bottom = h - viewPadding 246 | centerX = (left + right) * .5f 247 | centerY = (top + bottom) * .5f 248 | buttonMinX = left + viewRadius 249 | buttonMaxX = right - viewRadius 250 | if (isChecked()) { 251 | setCheckedViewState(viewState) 252 | } else { 253 | setUncheckViewState(viewState) 254 | } 255 | isUiInited = true 256 | postInvalidate() 257 | } 258 | 259 | /** 260 | * @param viewState 261 | */ 262 | private fun setUncheckViewState(viewState: ViewState?) { 263 | viewState?.radius = 0f 264 | viewState?.checkStateColor = uncheckColor 265 | viewState?.checkedLineColor = Color.TRANSPARENT 266 | viewState?.buttonX = buttonMinX 267 | buttonPaint?.color = uncheckButtonColor 268 | } 269 | 270 | /** 271 | * @param viewState 272 | */ 273 | private fun setCheckedViewState(viewState: ViewState?) { 274 | viewState?.radius = viewRadius 275 | viewState?.checkStateColor = checkedColor 276 | viewState?.checkedLineColor = checkLineColor 277 | viewState?.buttonX = buttonMaxX 278 | buttonPaint?.color = checkedButtonColor 279 | } 280 | 281 | override fun onDraw(canvas: Canvas) { 282 | super.onDraw(canvas) 283 | paint?.strokeWidth = borderWidth.toFloat() 284 | paint?.style = Paint.Style.FILL 285 | //绘制白色背景 286 | paint?.color = background 287 | drawRoundRect( 288 | canvas, 289 | left, top, right, bottom, 290 | viewRadius, paint 291 | ) 292 | //绘制关闭状态的边框 293 | paint?.style = Paint.Style.STROKE 294 | paint?.color = uncheckColor 295 | drawRoundRect( 296 | canvas, 297 | left, top, right, bottom, 298 | viewRadius, paint 299 | ) 300 | 301 | //绘制小圆圈 302 | if (showIndicator) { 303 | drawUncheckIndicator(canvas) 304 | } 305 | 306 | //绘制开启背景色 307 | val des = viewState!!.radius * .5f //[0-backgroundRadius*0.5f] 308 | paint?.style = Paint.Style.STROKE 309 | paint?.color = viewState?.checkStateColor ?: Color.TRANSPARENT 310 | paint?.strokeWidth = borderWidth + des * 2f 311 | drawRoundRect(canvas, left + des, top + des, right - des, bottom - des, viewRadius, paint) 312 | 313 | //绘制按钮左边绿色长条遮挡 314 | paint?.style = Paint.Style.FILL 315 | paint?.strokeWidth = 1f 316 | drawArc(canvas, left, top, left + 2 * viewRadius, top + 2 * viewRadius, 90f, 180f, paint) 317 | paint?.let { canvas.drawRect(left + viewRadius, top, viewState?.buttonX ?: 0f, top + 2 * viewRadius, it) } 318 | 319 | //绘制小线条 320 | if (showIndicator) { 321 | drawCheckedIndicator(canvas) 322 | } 323 | 324 | //绘制按钮 325 | viewState?.buttonX?.let { drawButton(canvas, it, centerY) } 326 | } 327 | /** 328 | * 绘制选中状态指示器 329 | * @param canvas 330 | * @param color 331 | * @param lineWidth 332 | * @param sx 333 | * @param sy 334 | * @param ex 335 | * @param ey 336 | * @param paint 337 | */ 338 | /** 339 | * 绘制选中状态指示器 340 | * @param canvas 341 | */ 342 | protected fun drawCheckedIndicator( 343 | canvas: Canvas, 344 | color: Int = viewState?.checkedLineColor ?: Color.TRANSPARENT, 345 | lineWidth: Float = checkLineWidth.toFloat(), 346 | sx: Float = left + viewRadius - checkedLineOffsetX, 347 | sy: Float = centerY - checkLineLength, 348 | ex: Float = left + viewRadius - checkedLineOffsetY, 349 | ey: Float = centerY + checkLineLength, 350 | paint: Paint? = this.paint 351 | ) { 352 | paint?.style = Paint.Style.STROKE 353 | paint?.color = color 354 | paint?.strokeWidth = lineWidth 355 | paint?.let { canvas.drawLine(sx, sy, ex, ey, it) } 356 | } 357 | 358 | /** 359 | * 绘制关闭状态指示器 360 | * @param canvas 361 | */ 362 | private fun drawUncheckIndicator(canvas: Canvas) { 363 | drawUncheckIndicator( 364 | canvas, 365 | uncheckCircleColor, 366 | uncheckCircleWidth.toFloat(), 367 | right - uncheckCircleOffsetX, centerY, 368 | uncheckCircleRadius, 369 | paint 370 | ) 371 | } 372 | 373 | /** 374 | * 绘制关闭状态指示器 375 | * @param canvas 376 | * @param color 377 | * @param lineWidth 378 | * @param centerX 379 | * @param centerY 380 | * @param radius 381 | * @param paint 382 | */ 383 | protected fun drawUncheckIndicator( 384 | canvas: Canvas, 385 | color: Int, 386 | lineWidth: Float, 387 | centerX: Float, centerY: Float, 388 | radius: Float, 389 | paint: Paint? 390 | ) { 391 | paint?.style = Paint.Style.STROKE 392 | paint?.color = color 393 | paint?.strokeWidth = lineWidth 394 | if (paint != null) { 395 | canvas.drawCircle(centerX, centerY, radius, paint) 396 | } 397 | } 398 | 399 | /** 400 | * @param canvas 401 | * @param left 402 | * @param top 403 | * @param right 404 | * @param bottom 405 | * @param startAngle 406 | * @param sweepAngle 407 | * @param paint 408 | */ 409 | private fun drawArc( 410 | canvas: Canvas, 411 | left: Float, top: Float, 412 | right: Float, bottom: Float, 413 | startAngle: Float, 414 | sweepAngle: Float, 415 | paint: Paint? 416 | ) { 417 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 418 | canvas.drawArc( 419 | left, top, right, bottom, 420 | startAngle, sweepAngle, true, paint!! 421 | ) 422 | } else { 423 | rect[left, top, right] = bottom 424 | canvas.drawArc( 425 | rect, 426 | startAngle, sweepAngle, true, paint!! 427 | ) 428 | } 429 | } 430 | 431 | /** 432 | * @param canvas 433 | * @param left 434 | * @param top 435 | * @param right 436 | * @param bottom 437 | * @param backgroundRadius 438 | * @param paint 439 | */ 440 | private fun drawRoundRect( 441 | canvas: Canvas, 442 | left: Float, top: Float, 443 | right: Float, bottom: Float, 444 | backgroundRadius: Float, 445 | paint: Paint? 446 | ) { 447 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 448 | canvas.drawRoundRect( 449 | left, top, right, bottom, 450 | backgroundRadius, backgroundRadius, paint!! 451 | ) 452 | } else { 453 | rect[left, top, right] = bottom 454 | canvas.drawRoundRect( 455 | rect, 456 | backgroundRadius, backgroundRadius, paint!! 457 | ) 458 | } 459 | } 460 | 461 | /** 462 | * @param canvas 463 | * @param x px 464 | * @param y px 465 | */ 466 | private fun drawButton(canvas: Canvas, x: Float, y: Float) { 467 | buttonPaint?.let { canvas.drawCircle(x, y, buttonRadius, it) } 468 | paint?.style = Paint.Style.STROKE 469 | paint?.strokeWidth = 1f 470 | paint?.color = -0x222223 471 | paint?.let { canvas.drawCircle(x, y, buttonRadius, it) } 472 | } 473 | 474 | override fun setChecked(checked: Boolean) { 475 | if (checked == isChecked()) { 476 | postInvalidate() 477 | return 478 | } 479 | toggle(enableEffect, false) 480 | } 481 | 482 | override fun isChecked(): Boolean { 483 | return isChecked 484 | } 485 | 486 | override fun toggle() { 487 | toggle(true) 488 | } 489 | 490 | /** 491 | * 切换状态 492 | * @param animate 493 | */ 494 | fun toggle(animate: Boolean) { 495 | toggle(animate, true) 496 | } 497 | 498 | private fun toggle(animate: Boolean, broadcast: Boolean) { 499 | if (!isEnabled) { 500 | return 501 | } 502 | if (isEventBroadcast) { 503 | throw RuntimeException("should NOT switch the state in method: [onCheckedChanged]!") 504 | } 505 | if (!isUiInited) { 506 | isChecked = !isChecked 507 | if (broadcast) { 508 | broadcastEvent() 509 | } 510 | return 511 | } 512 | if (valueAnimator!!.isRunning) { 513 | valueAnimator!!.cancel() 514 | } 515 | if (!enableEffect || !animate) { 516 | isChecked = !isChecked 517 | if (isChecked()) { 518 | setCheckedViewState(viewState) 519 | } else { 520 | setUncheckViewState(viewState) 521 | } 522 | postInvalidate() 523 | if (broadcast) { 524 | broadcastEvent() 525 | } 526 | return 527 | } 528 | animateState = ANIMATE_STATE_SWITCH 529 | viewState?.let { beforeState?.copy(it) } 530 | if (isChecked()) { 531 | //切换到unchecked 532 | setUncheckViewState(afterState) 533 | } else { 534 | setCheckedViewState(afterState) 535 | } 536 | valueAnimator!!.start() 537 | } 538 | 539 | /** 540 | * 541 | */ 542 | private fun broadcastEvent() { 543 | if (onCheckedChangeListener != null) { 544 | isEventBroadcast = true 545 | onCheckedChangeListener?.onCheckedChanged(this, isChecked()) 546 | } 547 | isEventBroadcast = false 548 | } 549 | 550 | override fun onTouchEvent(event: MotionEvent): Boolean { 551 | if (!isEnabled) { 552 | return false 553 | } 554 | when (event.actionMasked) { 555 | MotionEvent.ACTION_DOWN -> { 556 | isTouchingDown = true 557 | touchDownTime = System.currentTimeMillis() 558 | //取消准备进入拖动状态 559 | removeCallbacks(postPendingDrag) 560 | //预设100ms进入拖动状态 561 | postDelayed(postPendingDrag, 100) 562 | } 563 | MotionEvent.ACTION_MOVE -> { 564 | val eventX = event.x 565 | if (isPendingDragState) { 566 | //在准备进入拖动状态过程中,可以拖动按钮位置 567 | var fraction = eventX / getWidth() 568 | fraction = max(0f, min(1f, fraction)) 569 | viewState?.buttonX = (buttonMinX 570 | + (buttonMaxX - buttonMinX) 571 | * fraction) 572 | } else if (isDragState) { 573 | //拖动按钮位置,同时改变对应的背景颜色 574 | var fraction = eventX / getWidth() 575 | fraction = max(0f, min(1f, fraction)) 576 | viewState?.buttonX = (buttonMinX 577 | + (buttonMaxX - buttonMinX) 578 | * fraction) 579 | viewState?.checkStateColor = argbEvaluator.evaluate( 580 | fraction, 581 | uncheckColor, 582 | checkedColor 583 | ) as Int 584 | postInvalidate() 585 | } 586 | } 587 | MotionEvent.ACTION_UP -> { 588 | isTouchingDown = false 589 | //取消准备进入拖动状态 590 | removeCallbacks(postPendingDrag) 591 | if (System.currentTimeMillis() - touchDownTime <= 300) { 592 | //点击时间小于300ms,认为是点击操作 593 | toggle() 594 | } else if (isDragState) { 595 | //在拖动状态,计算按钮位置,设置是否切换状态 596 | val eventX = event.x 597 | var fraction = eventX / getWidth() 598 | fraction = Math.max(0f, Math.min(1f, fraction)) 599 | val newCheck = fraction > .5f 600 | if (newCheck == isChecked()) { 601 | pendingCancelDragState() 602 | } else { 603 | isChecked = newCheck 604 | pendingSettleState() 605 | } 606 | } else if (isPendingDragState) { 607 | //在准备进入拖动状态过程中,取消之,复位 608 | pendingCancelDragState() 609 | } 610 | } 611 | MotionEvent.ACTION_CANCEL -> { 612 | isTouchingDown = false 613 | removeCallbacks(postPendingDrag) 614 | if (isPendingDragState 615 | || isDragState 616 | ) { 617 | //复位 618 | pendingCancelDragState() 619 | } 620 | } 621 | } 622 | return true 623 | } 624 | 625 | /** 626 | * 是否在动画状态 627 | * @return 628 | */ 629 | private val isInAnimating: Boolean 630 | get() = animateState != ANIMATE_STATE_NONE 631 | 632 | /** 633 | * 是否在进入拖动或离开拖动状态 634 | * @return 635 | */ 636 | private val isPendingDragState: Boolean 637 | get() = (animateState == ANIMATE_STATE_PENDING_DRAG 638 | || animateState == ANIMATE_STATE_PENDING_RESET) 639 | 640 | /** 641 | * 是否在手指拖动状态 642 | * @return 643 | */ 644 | private val isDragState: Boolean 645 | get() = animateState == ANIMATE_STATE_DRAGING 646 | 647 | /** 648 | * 设置是否启用阴影效果 649 | * @param shadowEffect true.启用 650 | */ 651 | fun setShadowEffect(shadowEffect: Boolean) { 652 | if (this.shadowEffect == shadowEffect) { 653 | return 654 | } 655 | this.shadowEffect = shadowEffect 656 | if (this.shadowEffect) { 657 | buttonPaint!!.setShadowLayer( 658 | shadowRadius.toFloat(), 0f, shadowOffset.toFloat(), 659 | shadowColor 660 | ) 661 | } else { 662 | buttonPaint!!.setShadowLayer( 663 | 0f, 0f, 0f, 664 | 0 665 | ) 666 | } 667 | } 668 | 669 | fun setEnableEffect(enable: Boolean) { 670 | enableEffect = enable 671 | } 672 | 673 | /** 674 | * 开始进入拖动状态 675 | */ 676 | private fun pendingDragState() { 677 | if (isInAnimating) { 678 | return 679 | } 680 | if (!isTouchingDown) { 681 | return 682 | } 683 | if (valueAnimator!!.isRunning) { 684 | valueAnimator!!.cancel() 685 | } 686 | animateState = ANIMATE_STATE_PENDING_DRAG 687 | viewState?.let { beforeState?.copy(it) } 688 | viewState?.let { afterState?.copy(it) } 689 | if (isChecked()) { 690 | afterState?.checkStateColor = checkedColor 691 | afterState?.buttonX = buttonMaxX 692 | afterState?.checkedLineColor = checkedColor 693 | } else { 694 | afterState?.checkStateColor = uncheckColor 695 | afterState?.buttonX = buttonMinX 696 | afterState?.radius = viewRadius 697 | } 698 | valueAnimator?.start() 699 | } 700 | 701 | /** 702 | * 取消拖动状态 703 | */ 704 | private fun pendingCancelDragState() { 705 | if (isDragState || isPendingDragState) { 706 | if (valueAnimator?.isRunning == true) { 707 | valueAnimator?.cancel() 708 | } 709 | animateState = ANIMATE_STATE_PENDING_RESET 710 | viewState?.let { beforeState?.copy(it) } 711 | if (isChecked()) { 712 | setCheckedViewState(afterState) 713 | } else { 714 | setUncheckViewState(afterState) 715 | } 716 | valueAnimator?.start() 717 | } 718 | } 719 | 720 | /** 721 | * 动画-设置新的状态 722 | */ 723 | private fun pendingSettleState() { 724 | if (valueAnimator?.isRunning == true) { 725 | valueAnimator?.cancel() 726 | } 727 | animateState = ANIMATE_STATE_PENDING_SETTLE 728 | viewState?.let { beforeState?.copy(it) } 729 | if (isChecked()) { 730 | setCheckedViewState(afterState) 731 | } else { 732 | setUncheckViewState(afterState) 733 | } 734 | valueAnimator?.start() 735 | } 736 | 737 | override fun setOnClickListener(l: OnClickListener?) {} 738 | override fun setOnLongClickListener(l: OnLongClickListener?) {} 739 | fun setOnCheckedChangeListener(l: OnCheckedChangeListener?) { 740 | onCheckedChangeListener = l 741 | } 742 | 743 | interface OnCheckedChangeListener { 744 | fun onCheckedChanged(view: SwitchButton?, isChecked: Boolean) 745 | } 746 | /** */ 747 | /** 748 | * 阴影半径 749 | */ 750 | private var shadowRadius = 0 751 | 752 | /** 753 | * 阴影Y偏移px 754 | */ 755 | private var shadowOffset = 0 756 | 757 | /** 758 | * 阴影颜色 759 | */ 760 | private var shadowColor = 0 761 | 762 | /** 763 | * 背景半径 764 | */ 765 | private var viewRadius = 0f 766 | 767 | /** 768 | * 按钮半径 769 | */ 770 | private var buttonRadius = 0f 771 | 772 | /** 773 | * 背景高 774 | */ 775 | private var height = 0f 776 | 777 | /** 778 | * 背景宽 779 | */ 780 | private var width = 0f 781 | 782 | /** 783 | * 背景位置 784 | */ 785 | private var left = 0f 786 | private var top = 0f 787 | private var right = 0f 788 | private var bottom = 0f 789 | private var centerX = 0f 790 | private var centerY = 0f 791 | 792 | /** 793 | * 背景底色 794 | */ 795 | private var background = 0 796 | 797 | /** 798 | * 背景关闭颜色 799 | */ 800 | private var uncheckColor = 0 801 | 802 | /** 803 | * 背景打开颜色 804 | */ 805 | private var checkedColor = 0 806 | 807 | /** 808 | * 边框宽度px 809 | */ 810 | private var borderWidth = 0 811 | 812 | /** 813 | * 打开指示线颜色 814 | */ 815 | private var checkLineColor = 0 816 | 817 | /** 818 | * 打开指示线宽 819 | */ 820 | private var checkLineWidth = 0 821 | 822 | /** 823 | * 打开指示线长 824 | */ 825 | private var checkLineLength = 0f 826 | 827 | /** 828 | * 关闭圆圈颜色 829 | */ 830 | private var uncheckCircleColor = 0 831 | 832 | /** 833 | * 关闭圆圈线宽 834 | */ 835 | private var uncheckCircleWidth = 0 836 | 837 | /** 838 | * 关闭圆圈位移X 839 | */ 840 | private var uncheckCircleOffsetX = 0f 841 | 842 | /** 843 | * 关闭圆圈半径 844 | */ 845 | private var uncheckCircleRadius = 0f 846 | 847 | /** 848 | * 打开指示线位移X 849 | */ 850 | private var checkedLineOffsetX = 0f 851 | 852 | /** 853 | * 打开指示线位移Y 854 | */ 855 | private var checkedLineOffsetY = 0f 856 | 857 | /** 858 | * Color for button when it's uncheck 859 | */ 860 | private var uncheckButtonColor = 0 861 | 862 | /** 863 | * Color for button when it's check 864 | */ 865 | private var checkedButtonColor = 0 866 | 867 | /** 868 | * 按钮最左边 869 | */ 870 | private var buttonMinX = 0f 871 | 872 | /** 873 | * 按钮最右边 874 | */ 875 | private var buttonMaxX = 0f 876 | 877 | /** 878 | * 按钮画笔 879 | */ 880 | private var buttonPaint: Paint? = null 881 | 882 | /** 883 | * 背景画笔 884 | */ 885 | private var paint: Paint? = null 886 | 887 | /** 888 | * 当前状态 889 | */ 890 | private var viewState: ViewState? = null 891 | private var beforeState: ViewState? = null 892 | private var afterState: ViewState? = null 893 | private val rect = RectF() 894 | 895 | /** 896 | * 动画状态 897 | */ 898 | private var animateState = ANIMATE_STATE_NONE 899 | 900 | /** 901 | * 902 | */ 903 | private var valueAnimator: ValueAnimator? = null 904 | private val argbEvaluator = ArgbEvaluator() 905 | 906 | /** 907 | * 是否选中 908 | */ 909 | private var isChecked = false 910 | 911 | /** 912 | * 是否启用动画 913 | */ 914 | private var enableEffect = false 915 | 916 | /** 917 | * 是否启用阴影效果 918 | */ 919 | private var shadowEffect = false 920 | 921 | /** 922 | * 是否显示指示器 923 | */ 924 | private var showIndicator = false 925 | 926 | /** 927 | * 收拾是否按下 928 | */ 929 | private var isTouchingDown = false 930 | 931 | /** 932 | * 933 | */ 934 | private var isUiInited = false 935 | 936 | /** 937 | * 938 | */ 939 | private var isEventBroadcast = false 940 | private var onCheckedChangeListener: OnCheckedChangeListener? = null 941 | 942 | /** 943 | * 手势按下的时刻 944 | */ 945 | private var touchDownTime: Long = 0 946 | private val postPendingDrag = Runnable { 947 | if (!isInAnimating) { 948 | pendingDragState() 949 | } 950 | } 951 | private val animatorUpdateListener: AnimatorUpdateListener = object : AnimatorUpdateListener { 952 | override fun onAnimationUpdate(animation: ValueAnimator) { 953 | val value = animation.animatedValue as Float 954 | when (animateState) { 955 | ANIMATE_STATE_PENDING_SETTLE -> { 956 | run {} 957 | run {} 958 | run { 959 | viewState!!.checkedLineColor = argbEvaluator.evaluate( 960 | value, 961 | beforeState!!.checkedLineColor, 962 | afterState!!.checkedLineColor 963 | ) as Int 964 | viewState!!.radius = (beforeState!!.radius 965 | + (afterState!!.radius - beforeState!!.radius) * value) 966 | if (animateState != ANIMATE_STATE_PENDING_DRAG) { 967 | viewState!!.buttonX = (beforeState!!.buttonX 968 | + (afterState!!.buttonX - beforeState!!.buttonX) * value) 969 | } 970 | viewState!!.checkStateColor = argbEvaluator.evaluate( 971 | value, 972 | beforeState!!.checkStateColor, 973 | afterState!!.checkStateColor 974 | ) as Int 975 | } 976 | } 977 | ANIMATE_STATE_PENDING_RESET -> { 978 | run {} 979 | run { 980 | viewState!!.checkedLineColor = argbEvaluator.evaluate( 981 | value, 982 | beforeState!!.checkedLineColor, 983 | afterState!!.checkedLineColor 984 | ) as Int 985 | viewState!!.radius = (beforeState!!.radius 986 | + (afterState!!.radius - beforeState!!.radius) * value) 987 | if (animateState != ANIMATE_STATE_PENDING_DRAG) { 988 | viewState!!.buttonX = (beforeState!!.buttonX 989 | + (afterState!!.buttonX - beforeState!!.buttonX) * value) 990 | } 991 | viewState!!.checkStateColor = argbEvaluator.evaluate( 992 | value, 993 | beforeState!!.checkStateColor, 994 | afterState!!.checkStateColor 995 | ) as Int 996 | } 997 | } 998 | ANIMATE_STATE_PENDING_DRAG -> { 999 | viewState!!.checkedLineColor = argbEvaluator.evaluate( 1000 | value, 1001 | beforeState!!.checkedLineColor, 1002 | afterState!!.checkedLineColor 1003 | ) as Int 1004 | viewState!!.radius = (beforeState!!.radius 1005 | + (afterState!!.radius - beforeState!!.radius) * value) 1006 | if (animateState != ANIMATE_STATE_PENDING_DRAG) { 1007 | viewState!!.buttonX = (beforeState!!.buttonX 1008 | + (afterState!!.buttonX - beforeState!!.buttonX) * value) 1009 | } 1010 | viewState!!.checkStateColor = argbEvaluator.evaluate( 1011 | value, 1012 | beforeState!!.checkStateColor, 1013 | afterState!!.checkStateColor 1014 | ) as Int 1015 | } 1016 | ANIMATE_STATE_SWITCH -> { 1017 | viewState?.buttonX = (beforeState!!.buttonX 1018 | + (afterState!!.buttonX - beforeState!!.buttonX) * value) 1019 | val fraction = (viewState!!.buttonX - buttonMinX) / (buttonMaxX - buttonMinX) 1020 | viewState?.checkStateColor = argbEvaluator.evaluate( 1021 | fraction, 1022 | uncheckColor, 1023 | checkedColor 1024 | ) as Int 1025 | viewState?.radius = fraction * viewRadius 1026 | viewState?.checkedLineColor = argbEvaluator.evaluate( 1027 | fraction, 1028 | Color.TRANSPARENT, 1029 | checkLineColor 1030 | ) as Int 1031 | } 1032 | ANIMATE_STATE_DRAGING -> { 1033 | run {} 1034 | run {} 1035 | } 1036 | ANIMATE_STATE_NONE -> {} 1037 | else -> { 1038 | run {} 1039 | run {} 1040 | } 1041 | } 1042 | postInvalidate() 1043 | } 1044 | } 1045 | private val animatorListener: AnimatorListener = object : AnimatorListener { 1046 | override fun onAnimationStart(animation: Animator) {} 1047 | override fun onAnimationEnd(animation: Animator) { 1048 | when (animateState) { 1049 | ANIMATE_STATE_DRAGING -> {} 1050 | ANIMATE_STATE_PENDING_DRAG -> { 1051 | animateState = ANIMATE_STATE_DRAGING 1052 | viewState?.checkedLineColor = Color.TRANSPARENT 1053 | viewState?.radius = viewRadius 1054 | postInvalidate() 1055 | } 1056 | ANIMATE_STATE_PENDING_RESET -> { 1057 | animateState = ANIMATE_STATE_NONE 1058 | postInvalidate() 1059 | } 1060 | ANIMATE_STATE_PENDING_SETTLE -> { 1061 | animateState = ANIMATE_STATE_NONE 1062 | postInvalidate() 1063 | broadcastEvent() 1064 | } 1065 | ANIMATE_STATE_SWITCH -> { 1066 | isChecked = !isChecked 1067 | animateState = ANIMATE_STATE_NONE 1068 | postInvalidate() 1069 | broadcastEvent() 1070 | } 1071 | ANIMATE_STATE_NONE -> {} 1072 | else -> {} 1073 | } 1074 | } 1075 | 1076 | override fun onAnimationCancel(animation: Animator) {} 1077 | override fun onAnimationRepeat(animation: Animator) {} 1078 | } 1079 | /** */ 1080 | /** 1081 | * 保存动画状态 1082 | */ 1083 | private class ViewState internal constructor() { 1084 | /** 1085 | * 按钮x位置[buttonMinX-buttonMaxX] 1086 | */ 1087 | var buttonX = 0f 1088 | 1089 | /** 1090 | * 状态背景颜色 1091 | */ 1092 | var checkStateColor = 0 1093 | 1094 | /** 1095 | * 选中线的颜色 1096 | */ 1097 | var checkedLineColor = 0 1098 | 1099 | /** 1100 | * 状态背景的半径 1101 | */ 1102 | var radius = 0f 1103 | fun copy(source: ViewState) { 1104 | buttonX = source.buttonX 1105 | checkStateColor = source.checkStateColor 1106 | checkedLineColor = source.checkedLineColor 1107 | radius = source.radius 1108 | } 1109 | } 1110 | 1111 | companion object { 1112 | private val DEFAULT_WIDTH = dp2pxInt(58f) 1113 | private val DEFAULT_HEIGHT = dp2pxInt(36f) 1114 | 1115 | /** */ 1116 | private fun dp2px(dp: Float): Float { 1117 | val r = Resources.getSystem() 1118 | return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, r.displayMetrics) 1119 | } 1120 | 1121 | private fun dp2pxInt(dp: Float): Int { 1122 | return dp2px(dp).toInt() 1123 | } 1124 | 1125 | private fun optInt( 1126 | typedArray: TypedArray?, 1127 | index: Int, 1128 | def: Int 1129 | ): Int { 1130 | return typedArray?.getInt(index, def) ?: def 1131 | } 1132 | 1133 | private fun optPixelSize( 1134 | typedArray: TypedArray?, 1135 | index: Int, 1136 | def: Float 1137 | ): Float { 1138 | return typedArray?.getDimension(index, def) ?: def 1139 | } 1140 | 1141 | private fun optPixelSize( 1142 | typedArray: TypedArray?, 1143 | index: Int, 1144 | def: Int 1145 | ): Int { 1146 | return typedArray?.getDimensionPixelOffset(index, def) ?: def 1147 | } 1148 | 1149 | private fun optColor( 1150 | typedArray: TypedArray?, 1151 | index: Int, 1152 | def: Int 1153 | ): Int { 1154 | return typedArray?.getColor(index, def) ?: def 1155 | } 1156 | 1157 | private fun optBoolean( 1158 | typedArray: TypedArray?, 1159 | index: Int, 1160 | def: Boolean 1161 | ): Boolean { 1162 | return typedArray?.getBoolean(index, def) ?: def 1163 | } 1164 | } 1165 | } -------------------------------------------------------------------------------- /library/src/main/res/values/switch_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 | -------------------------------------------------------------------------------- /sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-kapt' 4 | 5 | android { 6 | compileSdkVersion 33 7 | 8 | defaultConfig { 9 | applicationId "com.suke.widget.sample" 10 | minSdkVersion 14 11 | targetSdkVersion 33 12 | versionCode 1 13 | versionName "1.0" 14 | 15 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 16 | 17 | } 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | compileOptions { 25 | sourceCompatibility JavaVersion.VERSION_1_8 26 | targetCompatibility JavaVersion.VERSION_1_8 27 | //coreLibraryDesugaringEnabled true 28 | } 29 | } 30 | 31 | dependencies { 32 | implementation fileTree(include: ['*.jar'], dir: 'libs') 33 | implementation project(':library') 34 | androidTestImplementation('com.android.support.test.espresso:espresso-core:3.0.2', { 35 | exclude group: 'com.android.support', module: 'support-annotations' 36 | }) 37 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 38 | implementation "androidx.core:core-ktx:1.6.0" 39 | 40 | implementation "androidx.appcompat:appcompat:1.6.1" 41 | implementation "androidx.constraintlayout:constraintlayout:2.1.4" 42 | testImplementation 'junit:junit:4.13.2' 43 | } 44 | -------------------------------------------------------------------------------- /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 /Users/suke/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /sample/src/androidTest/java/com/suke/widget/sample/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.suke.widget.sample; 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("com.suke.widget.sample", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /sample/src/main/java/com/suke/widget/sample/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.suke.widget.sample 2 | 3 | import android.app.Activity 4 | import android.os.Bundle 5 | import android.view.View 6 | import com.suke.widget.SwitchButton 7 | 8 | class MainActivity : Activity() { 9 | override fun onCreate(savedInstanceState: Bundle?) { 10 | super.onCreate(savedInstanceState) 11 | setContentView(R.layout.activity_main) 12 | val switchButton = findViewById(R.id.switch_button) as SwitchButton 13 | 14 | //switchButton.isChecked = true 15 | switchButton.toggle() //switch state 16 | switchButton.toggle(false) //switch without animation 17 | switchButton.setShadowEffect(true) //disable shadow effect 18 | switchButton.isEnabled = false //disable button 19 | switchButton.setEnableEffect(false) //disable the switch animation 20 | switchButton.setOnCheckedChangeListener(object : SwitchButton.OnCheckedChangeListener { 21 | override fun onCheckedChanged(view: SwitchButton?, isChecked: Boolean) { 22 | //TODO do your job 23 | } 24 | }) 25 | } 26 | } -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 16 | 17 | 26 | 27 | 35 | 36 | 45 | 46 | 55 | 56 | 65 | 66 | 75 | 76 | 85 | 86 | 101 | 102 | 111 | 112 | 120 | 121 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcweng/SwitchButton/24ee0e0265dcafe524c8bb3898e6f06910b8b952/sample/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcweng/SwitchButton/24ee0e0265dcafe524c8bb3898e6f06910b8b952/sample/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcweng/SwitchButton/24ee0e0265dcafe524c8bb3898e6f06910b8b952/sample/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcweng/SwitchButton/24ee0e0265dcafe524c8bb3898e6f06910b8b952/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcweng/SwitchButton/24ee0e0265dcafe524c8bb3898e6f06910b8b952/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Sample 3 | 4 | disable style + programing settings 5 | smooth swipe with indicator 6 | smooth swipe without indicator 7 | changed background color 8 | changed all colors style 9 | style without smooth swipe 10 | 11 | -------------------------------------------------------------------------------- /sample/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /sample/src/test/java/com/suke/widget/sample/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.suke.widget.sample; 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 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':sample', ':library' 2 | --------------------------------------------------------------------------------