├── .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 | 
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 | 
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 |
--------------------------------------------------------------------------------