diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c6cbe56 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..e679ad8 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +MDPreference \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..96cc43e --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100644 index 0000000..e7bedf3 --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..97626ba --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..fd89f26 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,27 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..fbb6828 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..0ad302c --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..5939a8c --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,27 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.2" + + defaultConfig { + applicationId "xhinliang.github.io.mdpreference" + minSdkVersion 16 + targetSdkVersion 23 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + testCompile 'junit:junit:4.12' + compile 'com.android.support:appcompat-v7:23.1.1' + compile project(':mdpreference') +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..7873ee5 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /opt/android-sdk-linux/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/app/src/androidTest/java/xhinliang/github/io/mdpreference/ApplicationTest.java b/app/src/androidTest/java/xhinliang/github/io/mdpreference/ApplicationTest.java new file mode 100644 index 0000000..af09ff3 --- /dev/null +++ b/app/src/androidTest/java/xhinliang/github/io/mdpreference/ApplicationTest.java @@ -0,0 +1,13 @@ +package xhinliang.github.io.mdpreference; + +import android.app.Application; +import android.test.ApplicationTestCase; + +/** + * Testing Fundamentals + */ +public class ApplicationTest extends ApplicationTestCase { + public ApplicationTest() { + super(Application.class); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..5d7fa25 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/java/xhinliang/github/io/mdpreference/SettingsActivity.java b/app/src/main/java/xhinliang/github/io/mdpreference/SettingsActivity.java new file mode 100644 index 0000000..45d03ac --- /dev/null +++ b/app/src/main/java/xhinliang/github/io/mdpreference/SettingsActivity.java @@ -0,0 +1,13 @@ +package xhinliang.github.io.mdpreference; + +import android.app.Fragment; +import android.support.v7.app.AppCompatActivity; +import android.os.Bundle; + +public class SettingsActivity extends SingleFragmentActivity { + + @Override + protected Fragment createFragment() { + return new SettingsFragment(); + } +} diff --git a/app/src/main/java/xhinliang/github/io/mdpreference/SettingsFragment.java b/app/src/main/java/xhinliang/github/io/mdpreference/SettingsFragment.java new file mode 100644 index 0000000..94cc911 --- /dev/null +++ b/app/src/main/java/xhinliang/github/io/mdpreference/SettingsFragment.java @@ -0,0 +1,21 @@ +package xhinliang.github.io.mdpreference; + +import android.os.Bundle; + +import io.github.xhinliang.mdpreference.PreferenceFragment; + + +/** + * Created by xhinliang on 16-2-3. + * xhinliang@gmail.com + */ +public class SettingsFragment extends PreferenceFragment { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getPreferenceManager().setSharedPreferencesName(getString(R.string.app_name)); + addPreferencesFromResource(R.xml.preference_settings); + } + +} \ No newline at end of file diff --git a/app/src/main/java/xhinliang/github/io/mdpreference/SingleFragmentActivity.java b/app/src/main/java/xhinliang/github/io/mdpreference/SingleFragmentActivity.java new file mode 100644 index 0000000..c4209b7 --- /dev/null +++ b/app/src/main/java/xhinliang/github/io/mdpreference/SingleFragmentActivity.java @@ -0,0 +1,50 @@ +package xhinliang.github.io.mdpreference; + +import android.app.Fragment; +import android.app.FragmentManager; +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.view.MenuItem; + + +/** + * Created by xhinliang on 15-8-30. + * xhinliang@gmail.com + * 托管单个Fragment的抽象Activity + */ +public abstract class SingleFragmentActivity extends AppCompatActivity { + + + protected abstract Fragment createFragment(); + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_single_fragment); + if (null != getSupportActionBar()) { + getSupportActionBar().setHomeButtonEnabled(true); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + FragmentManager manager = getFragmentManager(); + Fragment fragment = manager.findFragmentById(R.id.fragment_main); + if (fragment == null) { + fragment = createFragment(); + manager.beginTransaction() + .add(R.id.fragment_main, fragment) + .commit(); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + finish(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_single_fragment.xml b/app/src/main/res/layout/activity_single_fragment.xml new file mode 100644 index 0000000..e4385e6 --- /dev/null +++ b/app/src/main/res/layout/activity_single_fragment.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..cde69bc Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..c133a0c Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..bfa42f0 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..324e72c Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..aee44e1 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/values-w820dp/dimens.xml b/app/src/main/res/values-w820dp/dimens.xml new file mode 100644 index 0000000..63fc816 --- /dev/null +++ b/app/src/main/res/values-w820dp/dimens.xml @@ -0,0 +1,6 @@ + + + 64dp + diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml new file mode 100644 index 0000000..43f7bda --- /dev/null +++ b/app/src/main/res/values/arrays.xml @@ -0,0 +1,27 @@ + + + + + + 6:00 + 8:00 + 10:00 + 12:00 + 14:00 + 16:00 + 18:00 + 20:00 + 22:00 + + + 6 + 8 + 10 + 12 + 14 + 16 + 18 + 20 + 22 + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..3ab3e9c --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #3F51B5 + #303F9F + #FF4081 + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..47c8224 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,5 @@ + + + 16dp + 16dp + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..247e174 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + MDPreference + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..5885930 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/main/res/xml/preference_settings.xml b/app/src/main/res/xml/preference_settings.xml new file mode 100644 index 0000000..ffbeabf --- /dev/null +++ b/app/src/main/res/xml/preference_settings.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/test/java/xhinliang/github/io/mdpreference/ExampleUnitTest.java b/app/src/test/java/xhinliang/github/io/mdpreference/ExampleUnitTest.java new file mode 100644 index 0000000..09e2d3f --- /dev/null +++ b/app/src/test/java/xhinliang/github/io/mdpreference/ExampleUnitTest.java @@ -0,0 +1,15 @@ +package xhinliang.github.io.mdpreference; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * To work on unit tests, switch the Test Artifact in the Build Variants view. + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() throws Exception { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..481a2e2 --- /dev/null +++ b/build.gradle @@ -0,0 +1,23 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:2.0.0-beta5' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + jcenter() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..1d3591c --- /dev/null +++ b/gradle.properties @@ -0,0 +1,18 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx10248m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..13372ae Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..122a0dc --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Dec 28 10:00:20 PST 2015 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..9d82f78 --- /dev/null +++ b/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..8a0b282 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/material/.gitignore b/material/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/material/.gitignore @@ -0,0 +1 @@ +/build diff --git a/material/build.gradle b/material/build.gradle new file mode 100644 index 0000000..af07b92 --- /dev/null +++ b/material/build.gradle @@ -0,0 +1,29 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.2" + + defaultConfig { + minSdkVersion 16 + targetSdkVersion 23 + versionCode 21 + versionName "1.2.2" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(include: ['*.jar'], dir: 'libs') + testCompile 'junit:junit:4.12' + compile 'com.android.support:appcompat-v7:23.1.1' + compile 'com.android.support:cardview-v7:23.1.1' + compile 'com.android.support:recyclerview-v7:23.1.1' + compile files('libs/LunarCalendar-all-1.3.0.jar') +} + diff --git a/material/libs/LunarCalendar-all-1.3.0.jar b/material/libs/LunarCalendar-all-1.3.0.jar new file mode 100644 index 0000000..699aea7 Binary files /dev/null and b/material/libs/LunarCalendar-all-1.3.0.jar differ diff --git a/material/proguard-rules.pro b/material/proguard-rules.pro new file mode 100644 index 0000000..1eed81c --- /dev/null +++ b/material/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in D:\vietph\android\adt-bundle-windows-x86_64-20140702\sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/material/src/androidTest/java/com/rey/material/ApplicationTest.java b/material/src/androidTest/java/com/rey/material/ApplicationTest.java new file mode 100644 index 0000000..ebf59de --- /dev/null +++ b/material/src/androidTest/java/com/rey/material/ApplicationTest.java @@ -0,0 +1,13 @@ +package com.rey.material; + +import android.app.Application; +import android.test.ApplicationTestCase; + +/** + * Testing Fundamentals + */ +public class ApplicationTest extends ApplicationTestCase { + public ApplicationTest() { + super(Application.class); + } +} \ No newline at end of file diff --git a/material/src/main/AndroidManifest.xml b/material/src/main/AndroidManifest.xml new file mode 100644 index 0000000..13be4a6 --- /dev/null +++ b/material/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/material/src/main/java/com/rey/material/dialog/BottomSheetDialog.java b/material/src/main/java/com/rey/material/dialog/BottomSheetDialog.java new file mode 100644 index 0000000..4256730 --- /dev/null +++ b/material/src/main/java/com/rey/material/dialog/BottomSheetDialog.java @@ -0,0 +1,544 @@ +package com.rey.material.dialog; + +import android.app.Dialog; +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Handler; +import android.view.GestureDetector; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; +import android.view.animation.Transformation; +import android.widget.FrameLayout; + +import com.rey.material.R; +import com.rey.material.drawable.BlankDrawable; + +/** + * Created by Rey on 7/25/2015. + * dialog + */ +public class BottomSheetDialog extends Dialog { + + private boolean mCancelable = true; + private boolean mCanceledOnTouchOutside = true; + private int mLayoutHeight = ViewGroup.LayoutParams.WRAP_CONTENT; + private Interpolator mInInterpolator; + private int mInDuration; + private Interpolator mOutInterpolator; + private int mOutDuration; + + private ContainerFrameLayout mContainer; + private View mContentView; + + private GestureDetector mGestureDetector; + private int mMinFlingVelocity; + + private final Handler mHandler = new Handler(); + private final Runnable mDismissAction = new Runnable() { + public void run() { + //dirty fix for java.lang.IllegalArgumentException: View not attached to window manager + try { + BottomSheetDialog.super.dismiss(); + } catch (IllegalArgumentException ex) { + } + } + }; + + private boolean mRunShowAnimation = false; + private Animation mAnimation; + + public BottomSheetDialog(Context context) { + this(context, R.style.Material_App_BottomSheetDialog); + } + + public BottomSheetDialog(Context context, int style) { + super(context, style); + + //Override style to ensure not show window's title or background. + //TODO: find a way to ensure windowIsFloating attribute is false. + requestWindowFeature(Window.FEATURE_NO_TITLE); + getWindow().setBackgroundDrawable(BlankDrawable.getInstance()); + WindowManager.LayoutParams layout = getWindow().getAttributes(); + layout.width = ViewGroup.LayoutParams.MATCH_PARENT; + layout.height = ViewGroup.LayoutParams.MATCH_PARENT; + layout.windowAnimations = R.style.DialogNoAnimation; + getWindow().setAttributes(layout); + + init(context, style); + } + + private void init(Context context, int style) { + mContainer = new ContainerFrameLayout(context); + + cancelable(true); + canceledOnTouchOutside(true); + + onCreate(); + applyStyle(style); + + mMinFlingVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity() * 2; + + mGestureDetector = new GestureDetector(context, new GestureDetector.OnGestureListener() { + @Override + public boolean onDown(MotionEvent e) { + return false; + } + + @Override + public void onShowPress(MotionEvent e) { + + } + + @Override + public boolean onSingleTapUp(MotionEvent e) { + return false; + } + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + return false; + } + + @Override + public void onLongPress(MotionEvent e) { + + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + if (velocityY > mMinFlingVelocity) { + dismiss(); + return true; + } + return false; + } + }); + + super.setContentView(mContainer); + } + + protected void onCreate() { + } + + public BottomSheetDialog applyStyle(int styleId) { + Context context = getContext(); + TypedArray a = context.obtainStyledAttributes(styleId, R.styleable.BottomSheetDialog); + + for (int i = 0, count = a.getIndexCount(); i < count; i++) { + int attr = a.getIndex(i); + if (attr == R.styleable.BottomSheetDialog_android_layout_height) + heightParam(a.getLayoutDimension(attr, ViewGroup.LayoutParams.WRAP_CONTENT)); + else if (attr == R.styleable.BottomSheetDialog_bsd_cancelable) + cancelable(a.getBoolean(attr, true)); + else if (attr == R.styleable.BottomSheetDialog_bsd_canceledOnTouchOutside) + canceledOnTouchOutside(a.getBoolean(attr, true)); + else if (attr == R.styleable.BottomSheetDialog_bsd_dimAmount) + dimAmount(a.getFloat(attr, 0f)); + else if (attr == R.styleable.BottomSheetDialog_bsd_inDuration) + inDuration(a.getInteger(attr, 0)); + else if (attr == R.styleable.BottomSheetDialog_bsd_inInterpolator) { + int resId = a.getResourceId(attr, 0); + if (resId != 0) + inInterpolator(AnimationUtils.loadInterpolator(context, resId)); + } else if (attr == R.styleable.BottomSheetDialog_bsd_outDuration) + outDuration(a.getInteger(attr, 0)); + else if (attr == R.styleable.BottomSheetDialog_bsd_outInterpolator) { + int resId = a.getResourceId(attr, 0); + if (resId != 0) + outInterpolator(AnimationUtils.loadInterpolator(context, resId)); + } + } + + a.recycle(); + + if (mInInterpolator == null) + mInInterpolator = new DecelerateInterpolator(); + + if (mOutInterpolator == null) + mOutInterpolator = new AccelerateInterpolator(); + + return this; + } + + public BottomSheetDialog cancelable(boolean cancelable) { + super.setCancelable(cancelable); + mCancelable = cancelable; + return this; + } + + public BottomSheetDialog canceledOnTouchOutside(boolean cancel) { + super.setCanceledOnTouchOutside(cancel); + mCanceledOnTouchOutside = cancel; + return this; + } + + /** + * Set the dim amount of the region outside this BottomSheetDialog. + * + * @param amount The dim amount in [0..1]. + * @return The BottomSheetDialog for chaining methods. + */ + public BottomSheetDialog dimAmount(float amount) { + Window window = getWindow(); + if (amount > 0f) { + window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); + WindowManager.LayoutParams lp = window.getAttributes(); + lp.dimAmount = amount; + window.setAttributes(lp); + } else + window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); + return this; + } + + /** + * Set the content view of this BottomSheetDialog. + * + * @param v The content view. + * @return The BottomSheetDialog for chaining methods. + */ + public BottomSheetDialog contentView(View v) { + mContentView = v; + mContainer.removeAllViews(); + mContainer.addView(v); + return this; + } + + /** + * Set the content view of this BottomSheetDialog. + * + * @param layoutId The reourceId of layout. + * @return The BottomSheetDialog for chaining methods. + */ + public BottomSheetDialog contentView(int layoutId) { + if (layoutId == 0) + return this; + + View v = LayoutInflater.from(getContext()).inflate(layoutId, null); + return contentView(v); + } + + /** + * Set the height params of this BottomSheetDialog's content view. + * + * @param height The height param. Can be the size in pixels, or {@link ViewGroup.LayoutParams#WRAP_CONTENT} or {@link ViewGroup.LayoutParams#MATCH_PARENT}. + * @return The BottomSheetDialog for chaining methods. + */ + public BottomSheetDialog heightParam(int height) { + if (mLayoutHeight != height) { + mLayoutHeight = height; + + if (isShowing() && mContentView != null) { + mRunShowAnimation = true; + mContainer.forceLayout(); + mContainer.requestLayout(); + } + } + return this; + } + + /** + * Set the duration of in animation. + * + * @param duration The duration of animation. + * @return The BottomSheetDialog for chaining methods. + */ + public BottomSheetDialog inDuration(int duration) { + mInDuration = duration; + return this; + } + + /** + * Set the interpolator of in animation. + * + * @param interpolator The duration of animation. + * @return The BottomSheetDialog for chaining methods. + */ + public BottomSheetDialog inInterpolator(Interpolator interpolator) { + mInInterpolator = interpolator; + return this; + } + + /** + * Set the duration of out animation. + * + * @param duration The duration of animation. + * @return The BottomSheetDialog for chaining methods. + */ + public BottomSheetDialog outDuration(int duration) { + mOutDuration = duration; + return this; + } + + /** + * Set the interpolator of out animation. + * + * @param interpolator The duration of animation. + * @return The BottomSheetDialog for chaining methods. + */ + public BottomSheetDialog outInterpolator(Interpolator interpolator) { + mOutInterpolator = interpolator; + return this; + } + + @Override + protected void onStart() { + super.onStart(); + if (mContentView != null) { + mRunShowAnimation = true; + mContainer.forceLayout(); + } + } + + @Override + protected void onStop() { + super.onStop(); + mContainer = null; + mContentView = null; + mGestureDetector = null; + } + + @Override + public void setCancelable(boolean flag) { + cancelable(flag); + } + + @Override + public void setCanceledOnTouchOutside(boolean cancel) { + canceledOnTouchOutside(cancel); + } + + @Override + public void setContentView(View v) { + contentView(v); + } + + @Override + public void setContentView(int layoutId) { + contentView(layoutId); + } + + @Override + public void setContentView(View v, ViewGroup.LayoutParams params) { + contentView(v); + } + + @Override + public void addContentView(View view, ViewGroup.LayoutParams params) { + contentView(view); + } + + /** + * Dismiss Dialog immediately without showing out animation. + */ + public void dismissImmediately() { + super.dismiss(); + + if (mAnimation != null) + mAnimation.cancel(); + + if (mHandler != null) + mHandler.removeCallbacks(mDismissAction); + } + + @Override + public void dismiss() { + if (!isShowing()) + return; + + if (mContentView != null) { + mAnimation = new SlideAnimation(mContentView.getTop(), mContainer.getMeasuredHeight()); + mAnimation.setDuration(mOutDuration); + mAnimation.setInterpolator(mOutInterpolator); + mAnimation.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + + @Override + public void onAnimationEnd(Animation animation) { + mAnimation = null; + mHandler.post(mDismissAction); + } + }); + mContentView.startAnimation(mAnimation); + } else + mHandler.post(mDismissAction); + } + + protected int getContainerHeight() { + return mContainer == null ? 0 : mContainer.getHeight(); + } + + private class ContainerFrameLayout extends FrameLayout { + + private boolean mClickOutside = false; + private int mChildTop = -1; + + public ContainerFrameLayout(Context context) { + super(context); + } + + public void setChildTop(int top) { + mChildTop = top; + View child = getChildAt(0); + if (child != null) + child.offsetTopAndBottom(mChildTop - child.getTop()); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + View child = getChildAt(0); + if (child != null) { + switch (mLayoutHeight) { + case ViewGroup.LayoutParams.WRAP_CONTENT: + child.measure(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.AT_MOST)); + break; + case ViewGroup.LayoutParams.MATCH_PARENT: + child.measure(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY)); + break; + default: + child.measure(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(Math.min(mLayoutHeight, heightSize), MeasureSpec.EXACTLY)); + break; + } + } + setMeasuredDimension(widthSize, heightSize); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + View child = getChildAt(0); + + if (child != null) { + if (mChildTop < 0) + mChildTop = bottom - top; + + child.layout(0, mChildTop, child.getMeasuredWidth(), Math.max(bottom - top, mChildTop + child.getMeasuredHeight())); + + if (mRunShowAnimation) { + mRunShowAnimation = false; + + if (mAnimation != null) { + mAnimation.cancel(); + mAnimation = null; + } + + if (mContentView != null) { + int start = mChildTop < 0 ? getHeight() : child.getTop(); + int end = getMeasuredHeight() - mContentView.getMeasuredHeight(); + if (start != end) { + mAnimation = new SlideAnimation(start, end); + mAnimation.setDuration(mInDuration); + mAnimation.setInterpolator(mInInterpolator); + mAnimation.setAnimationListener(animationListener); + mContentView.startAnimation(mAnimation); + } + } + } + } + } + + private boolean isOutsideDialog(float x, float y) { + if (y < mChildTop) + return true; + + View child = getChildAt(0); + if (child != null && y > mChildTop + child.getMeasuredHeight()) + return true; + + return false; + } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + if (!super.dispatchTouchEvent(ev) && mGestureDetector != null) + mGestureDetector.onTouchEvent(ev); + return true; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + boolean handled = super.onTouchEvent(event); + + if (handled) + return true; + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + if (isOutsideDialog(event.getX(), event.getY())) { + mClickOutside = true; + return true; + } + return false; + case MotionEvent.ACTION_MOVE: + return mClickOutside; + case MotionEvent.ACTION_CANCEL: + mClickOutside = false; + return false; + case MotionEvent.ACTION_UP: + if (mClickOutside && isOutsideDialog(event.getX(), event.getY())) { + mClickOutside = false; + if (mCancelable && mCanceledOnTouchOutside) + dismiss(); + return true; + } + return false; + } + + return false; + } + + } + + Animation.AnimationListener animationListener = new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + + @Override + public void onAnimationEnd(Animation animation) { + mAnimation = null; + } + }; + + private class SlideAnimation extends Animation { + + int mStart; + int mEnd; + + public SlideAnimation(int start, int end) { + mStart = start; + mEnd = end; + } + + @Override + protected void applyTransformation(float interpolatedTime, Transformation t) { + int top = Math.round((mEnd - mStart) * interpolatedTime + mStart); + if (mContainer != null) + mContainer.setChildTop(top); + else + cancel(); + } + } + +} diff --git a/material/src/main/java/com/rey/material/dialog/DatePickerDialog.java b/material/src/main/java/com/rey/material/dialog/DatePickerDialog.java new file mode 100644 index 0000000..09dd405 --- /dev/null +++ b/material/src/main/java/com/rey/material/dialog/DatePickerDialog.java @@ -0,0 +1,852 @@ +package com.rey.material.dialog; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Rect; +import android.graphics.RectF; +import android.support.annotation.NonNull; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.widget.FrameLayout; + +import com.rey.material.R; +import com.rey.material.util.ThemeUtil; +import com.rey.material.widget.DatePicker; +import com.rey.material.widget.YearPicker; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; + +import io.github.xhinliang.lunarcalendar.LunarCalendar; + +/** + * Created by Rey on 12/30/2014. + * Dialog + */ +@SuppressWarnings("unused") +public class DatePickerDialog extends Dialog { + + private DatePickerLayout mDatePickerLayout; + private float mCornerRadius; + + /** + * Interface definition for a callback to be invoked when the selected date is changed. + */ + public interface OnDateChangedListener { + + /** + * Called when the selected date is changed. + * + * @param oldDay The day value of old date. + * @param oldMonth The month value of old date. + * @param oldYear The year value of old date. + * @param newDay The day value of new date. + * @param newMonth The month value of new date. + * @param newYear The year value of new date. + */ + void onDateChanged(int oldDay, int oldMonth, int oldYear, int newDay, int newMonth, int newYear); + } + + private OnDateChangedListener mOnDateChangedListener; + + public DatePickerDialog(Context context) { + super(context, R.style.Material_App_Dialog_DatePicker_Light); + } + + public DatePickerDialog(Context context, int style) { + super(context, style); + } + + @Override + protected void onCreate() { + mDatePickerLayout = new DatePickerLayout(getContext()); + contentView(mDatePickerLayout); + } + + @Override + public Dialog applyStyle(int resId) { + super.applyStyle(resId); + + if (resId == 0) + return this; + + mDatePickerLayout.applyStyle(resId); + layoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + return this; + } + + @Override + public Dialog layoutParams(int width, int height) { + return super.layoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + } + + @Override + public Dialog cornerRadius(float radius) { + mCornerRadius = radius; + return super.cornerRadius(radius); + } + + /** + * Set the range of selectable dates. + * + * @param minDay The day value of minimum date. + * @param minMonth The month value of minimum date. + * @param minYear The year value of minimum date. + * @param maxDay The day value of maximum date. + * @param maxMonth The month value of maximum date. + * @param maxYear The year value of maximum date. + * @return The DatePickerDialog for chaining methods. + */ + public DatePickerDialog dateRange(int minDay, int minMonth, int minYear, int maxDay, int maxMonth, int maxYear) { + mDatePickerLayout.setDateRange(minDay, minMonth, minYear, maxDay, maxMonth, maxYear); + return this; + } + + /** + * Set the range of selectable dates. + * + * @param minTime The minimum date in millis. + * @param maxTime The maximum date in millis + * @return The DatePickerDialog for chaining methods. + */ + public DatePickerDialog dateRange(long minTime, long maxTime) { + mDatePickerLayout.setDateRange(minTime, maxTime); + return this; + } + + /** + * Set the selected date of this DatePickerDialog. + * + * @param day The day value of selected date. + * @param month The month value of selected date. + * @param year The year value of selected date. + * @return The DatePickerDialog for chaining methods. + */ + public DatePickerDialog date(int day, int month, int year) { + mDatePickerLayout.setDate(day, month, year); + return this; + } + + /** + * Set the selected date of this DatePickerDialog. + * + * @param time The date in millis. + * @return The DatePickerDialog for chaining methods. + */ + public DatePickerDialog date(long time) { + mDatePickerLayout.setDate(time); + return this; + } + + /** + * Set the listener will be called when the selected date is changed. + * + * @param listener The {@link OnDateChangedListener} will be called. + * @return The DatePickerDialog for chaining methods. + */ + public DatePickerDialog onDateChangedListener(OnDateChangedListener listener) { + mOnDateChangedListener = listener; + return this; + } + + /** + * @return The day value of selected date. + */ + public int getDay() { + return mDatePickerLayout.getDay(); + } + + /** + * @return The month value of selected date. + */ + public int getMonth() { + return mDatePickerLayout.getMonth(); + } + + /** + * @return The year value of selected date. + */ + public int getYear() { + return mDatePickerLayout.getYear(); + } + + /** + * @return The selected date. + */ + public long getDate() { + Calendar cal = getCalendar(); + cal.set(Calendar.MILLISECOND, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.HOUR, 0); + cal.set(Calendar.DAY_OF_MONTH, getDay()); + cal.set(Calendar.MONTH, getMonth()); + cal.set(Calendar.YEAR, getYear()); + return cal.getTimeInMillis(); + } + + public Calendar getCalendar() { + return mDatePickerLayout.getCalendar(); + } + + /** + * Get the formatted string of selected date. + * + * @param formatter The DateFormat used to format the date. + * @return FormattedDate + */ + public String getFormattedDate(DateFormat formatter) { + return mDatePickerLayout.getFormattedDate(formatter); + } + + public LunarCalendar getLunarCalendar() { + return LunarCalendar.getInstance(mDatePickerLayout.getYear(), + mDatePickerLayout.getMonth() + 1, mDatePickerLayout.getDay()); + } + + private class DatePickerLayout extends FrameLayout implements DatePicker.OnDateChangedListener, YearPicker.OnYearChangedListener { + + private YearPicker mYearPicker; + private DatePicker mDatePicker; + + private int mHeaderPrimaryHeight; + private int mHeaderPrimaryColor; + private int mHeaderSecondaryHeight; + private int mHeaderSecondaryColor; + private int mHeaderPrimaryTextSize; + private int mHeaderSecondaryTextSize; + private int mTextHeaderColor = 0xFF000000; + + private Paint mPaint; + private int mHeaderPrimaryRealHeight; + private int mHeaderRealWidth; + private RectF mRect; + private Path mHeaderSecondaryBackground; + + private int mPadding; + + private boolean mDaySelectMode = true; + + private boolean mMonthFirst = true; + private boolean mLocationDirty = true; + + private String mWeekDay; + private String mMonth; + private String mDay; + private String mYear; + + private float mBaseX; + private float mWeekDayY; + private float mMonthY; + private float mDayY; + private float mYearY; + private float mFirstWidth; + private float mCenterY; + private float mSecondWidth; + + private static final String BASE_TEXT = "0"; + private static final String DAY_FORMAT = "%2d"; + private static final String YEAR_FORMAT = "%4d"; + + @SuppressLint("PrivateResource") + public DatePickerLayout(Context context) { + super(context); + + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mPaint.setStyle(Paint.Style.FILL); + mPaint.setTextAlign(Paint.Align.CENTER); + mRect = new RectF(); + mHeaderSecondaryBackground = new Path(); + mPadding = ThemeUtil.dpToPx(context, 8); + + mYearPicker = new YearPicker(context); + mDatePicker = new DatePicker(context); + mYearPicker.setPadding(mPadding, mPadding, mPadding, mPadding); + mYearPicker.setOnYearChangedListener(this); + mDatePicker.setContentPadding(mPadding, mPadding, mPadding, mPadding); + mDatePicker.setOnDateChangedListener(this); + + addView(mDatePicker); + addView(mYearPicker); + + mYearPicker.setVisibility(mDaySelectMode ? View.GONE : View.VISIBLE); + mDatePicker.setVisibility(mDaySelectMode ? View.VISIBLE : View.GONE); + + mMonthFirst = isMonthFirst(); + + setWillNotDraw(false); + + mHeaderPrimaryHeight = ThemeUtil.dpToPx(context, 144); + mHeaderSecondaryHeight = ThemeUtil.dpToPx(context, 32); + mHeaderPrimaryTextSize = context.getResources().getDimensionPixelOffset(R.dimen.abc_text_size_display_2_material); + mHeaderSecondaryTextSize = context.getResources().getDimensionPixelOffset(R.dimen.abc_text_size_headline_material); + } + + private boolean isMonthFirst() { + SimpleDateFormat format = (SimpleDateFormat) SimpleDateFormat.getDateInstance(SimpleDateFormat.FULL); + String pattern = format.toLocalizedPattern(); + + return pattern.indexOf("M") < pattern.indexOf("d"); + } + + public void setDateSelectMode(boolean enable) { + if (mDaySelectMode != enable) { + mDaySelectMode = enable; + + if (mDaySelectMode) { + mDatePicker.goTo(mDatePicker.getMonth(), mDatePicker.getYear()); + animOut(mYearPicker); + animIn(mDatePicker); + } else { + mYearPicker.goTo(mYearPicker.getYear()); + animOut(mDatePicker); + animIn(mYearPicker); + } + + invalidate(0, 0, mHeaderRealWidth, mHeaderPrimaryRealHeight + mHeaderSecondaryHeight); + } + } + + private void animOut(final View v) { + Animation anim = new AlphaAnimation(1f, 0f); + anim.setDuration(getContext().getResources().getInteger(android.R.integer.config_mediumAnimTime)); + anim.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + } + + @Override + public void onAnimationEnd(Animation animation) { + v.setVisibility(View.GONE); + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + }); + v.startAnimation(anim); + } + + private void animIn(final View v) { + Animation anim = new AlphaAnimation(0f, 1f); + anim.setDuration(getContext().getResources().getInteger(android.R.integer.config_mediumAnimTime)); + anim.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + v.setVisibility(View.VISIBLE); + } + + @Override + public void onAnimationEnd(Animation animation) { + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + }); + v.startAnimation(anim); + } + + public void applyStyle(int resId) { + mYearPicker.applyStyle(resId); + mDatePicker.applyStyle(resId); + + mHeaderPrimaryColor = mDatePicker.getSelectionColor(); + mHeaderSecondaryColor = mHeaderPrimaryColor; + + Context context = getContext(); + TypedArray a = context.obtainStyledAttributes(resId, R.styleable.DatePickerDialog); + + for (int i = 0, count = a.getIndexCount(); i < count; i++) { + int attr = a.getIndex(i); + + if (attr == R.styleable.DatePickerDialog_dp_headerPrimaryHeight) + mHeaderPrimaryHeight = a.getDimensionPixelSize(attr, 0); + else if (attr == R.styleable.DatePickerDialog_dp_headerSecondaryHeight) + mHeaderSecondaryHeight = a.getDimensionPixelSize(attr, 0); + else if (attr == R.styleable.DatePickerDialog_dp_headerPrimaryColor) + mHeaderPrimaryColor = a.getColor(attr, 0); + else if (attr == R.styleable.DatePickerDialog_dp_headerSecondaryColor) + mHeaderSecondaryColor = a.getColor(attr, 0); + else if (attr == R.styleable.DatePickerDialog_dp_headerPrimaryTextSize) + mHeaderPrimaryTextSize = a.getDimensionPixelSize(attr, 0); + else if (attr == R.styleable.DatePickerDialog_dp_headerSecondaryTextSize) + mHeaderSecondaryTextSize = a.getDimensionPixelSize(attr, 0); + else if (attr == R.styleable.DatePickerDialog_dp_textHeaderColor) + mTextHeaderColor = a.getColor(attr, 0); + } + + a.recycle(); + + mPaint.setTypeface(mDatePicker.getTypeface()); + } + + public void setDateRange(int minDay, int minMonth, int minYear, int maxDay, int maxMonth, int maxYear) { + mDatePicker.setDateRange(minDay, minMonth, minYear, maxDay, maxMonth, maxYear); + mYearPicker.setYearRange(minYear, maxYear); + } + + public void setDateRange(long minTime, long maxTime) { + Calendar cal = mDatePicker.getCalendar(); + cal.setTimeInMillis(minTime); + int minDay = cal.get(Calendar.DAY_OF_MONTH); + int minMonth = cal.get(Calendar.MONTH); + int minYear = cal.get(Calendar.YEAR); + cal.setTimeInMillis(maxTime); + int maxDay = cal.get(Calendar.DAY_OF_MONTH); + int maxMonth = cal.get(Calendar.MONTH); + int maxYear = cal.get(Calendar.YEAR); + + setDateRange(minDay, minMonth, minYear, maxDay, maxMonth, maxYear); + } + + public void setDate(int day, int month, int year) { + mDatePicker.setDate(day, month, year); + } + + public void setDate(long time) { + Calendar cal = mDatePicker.getCalendar(); + cal.setTimeInMillis(time); + int day = cal.get(Calendar.DAY_OF_MONTH); + int month = cal.get(Calendar.MONTH); + int year = cal.get(Calendar.YEAR); + mDatePicker.setDate(day, month, year); + } + + public int getDay() { + return mDatePicker.getDay(); + } + + public int getMonth() { + return mDatePicker.getMonth(); + } + + public int getYear() { + return mDatePicker.getYear(); + } + + public String getFormattedDate(DateFormat formatter) { + return mDatePicker.getFormattedDate(formatter); + } + + + public Calendar getCalendar() { + return mDatePicker.getCalendar(); + } + + @Override + public void onYearChanged(int oldYear, int newYear) { + if (!mDaySelectMode) + mDatePicker.setDate(mDatePicker.getDay(), mDatePicker.getMonth(), newYear); + } + + @Override + public void onDateChanged(int oldDay, int oldMonth, int oldYear, int newDay, int newMonth, int newYear) { + if (mDaySelectMode) + mYearPicker.setYear(newYear); + + if (newDay < 0 || newMonth < 0 || newYear < 0) { + mWeekDay = null; + mMonth = null; + mDay = null; + mYear = null; + } else { + Calendar cal = mDatePicker.getCalendar(); + cal.set(Calendar.YEAR, newYear); + cal.set(Calendar.MONTH, newMonth); + cal.set(Calendar.DAY_OF_MONTH, newDay); + + mWeekDay = cal.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.LONG, Locale.getDefault()); + mMonth = cal.getDisplayName(Calendar.MONTH, Calendar.SHORT, Locale.getDefault()); + mDay = String.format(DAY_FORMAT, newDay); + mYear = String.format(YEAR_FORMAT, newYear); + + if (oldMonth != newMonth || oldYear != newYear) + mDatePicker.goTo(newMonth, newYear); + } + + mLocationDirty = true; + invalidate(0, 0, mHeaderRealWidth, mHeaderPrimaryRealHeight + mHeaderSecondaryHeight); + + if (mOnDateChangedListener != null) + mOnDateChangedListener.onDateChanged(oldDay, oldMonth, oldYear, newDay, newMonth, newYear); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + + boolean isPortrait = getContext().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT; + if (isPortrait) { + if (heightMode == MeasureSpec.AT_MOST) { + int ws = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); + int hs = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + mDatePicker.measure(ws, hs); + mYearPicker.measure(ws, ws); + } else { + int height = Math.max(heightSize - mHeaderSecondaryHeight - mHeaderPrimaryHeight, mDatePicker.getMeasuredHeight()); + int ws = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); + mDatePicker.measure(ws, MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); + mYearPicker.measure(ws, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + if (mYearPicker.getMeasuredHeight() != height) + mYearPicker.measure(ws, MeasureSpec.makeMeasureSpec(Math.min(mYearPicker.getMeasuredHeight(), height), MeasureSpec.EXACTLY)); + } + + setMeasuredDimension(widthSize, heightSize); + } else { + if (heightMode == MeasureSpec.AT_MOST) { + int ws = MeasureSpec.makeMeasureSpec(widthSize / 2, MeasureSpec.EXACTLY); + int hs = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + mDatePicker.measure(ws, hs); + mYearPicker.measure(ws, ws); + } else { + int height = Math.max(heightSize, mDatePicker.getMeasuredHeight()); + int ws = MeasureSpec.makeMeasureSpec(widthSize / 2, MeasureSpec.EXACTLY); + mDatePicker.measure(ws, MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); + mYearPicker.measure(ws, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + if (mYearPicker.getMeasuredHeight() != height) + mYearPicker.measure(ws, MeasureSpec.makeMeasureSpec(Math.min(mYearPicker.getMeasuredHeight(), height), MeasureSpec.EXACTLY)); + } + + setMeasuredDimension(widthSize, heightSize); + } + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + boolean isPortrait = getContext().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT; + + if (isPortrait) { + mHeaderRealWidth = w; + mHeaderPrimaryRealHeight = h - mHeaderSecondaryHeight - mDatePicker.getMeasuredHeight(); + mHeaderSecondaryBackground.reset(); + if (mCornerRadius == 0) + mHeaderSecondaryBackground.addRect(0, 0, mHeaderRealWidth, mHeaderSecondaryHeight, Path.Direction.CW); + else { + mHeaderSecondaryBackground.moveTo(0, mHeaderSecondaryHeight); + mHeaderSecondaryBackground.lineTo(0, mCornerRadius); + mRect.set(0, 0, mCornerRadius * 2, mCornerRadius * 2); + mHeaderSecondaryBackground.arcTo(mRect, 180f, 90f, false); + mHeaderSecondaryBackground.lineTo(mHeaderRealWidth - mCornerRadius, 0); + mRect.set(mHeaderRealWidth - mCornerRadius * 2, 0, mHeaderRealWidth, mCornerRadius * 2); + mHeaderSecondaryBackground.arcTo(mRect, 270f, 90f, false); + mHeaderSecondaryBackground.lineTo(mHeaderRealWidth, mHeaderSecondaryHeight); + mHeaderSecondaryBackground.close(); + } + } else { + mHeaderRealWidth = w - mDatePicker.getMeasuredWidth(); + mHeaderPrimaryRealHeight = h - mHeaderSecondaryHeight; + mHeaderSecondaryBackground.reset(); + if (mCornerRadius == 0) + mHeaderSecondaryBackground.addRect(0, 0, mHeaderRealWidth, mHeaderSecondaryHeight, Path.Direction.CW); + else { + mHeaderSecondaryBackground.moveTo(0, mHeaderSecondaryHeight); + mHeaderSecondaryBackground.lineTo(0, mCornerRadius); + mRect.set(0, 0, mCornerRadius * 2, mCornerRadius * 2); + mHeaderSecondaryBackground.arcTo(mRect, 180f, 90f, false); + mHeaderSecondaryBackground.lineTo(mHeaderRealWidth, 0); + mHeaderSecondaryBackground.lineTo(mHeaderRealWidth, mHeaderSecondaryHeight); + mHeaderSecondaryBackground.close(); + } + } + + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int childLeft = 0; + int childTop = 0; + int childRight = right - left; + int childBottom = bottom - top; + + boolean isPortrait = getContext().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT; + + if (isPortrait) + childTop += mHeaderPrimaryRealHeight + mHeaderSecondaryHeight; + else + childLeft += mHeaderRealWidth; + + mDatePicker.layout(childLeft, childTop, childRight, childBottom); + + childTop = (childBottom + childTop - mYearPicker.getMeasuredHeight()) / 2; + mYearPicker.layout(childLeft, childTop, childRight, childTop + mYearPicker.getMeasuredHeight()); + } + + private void measureHeaderText() { + if (!mLocationDirty) + return; + + if (mWeekDay == null) { + mLocationDirty = false; + return; + } + + mBaseX = mHeaderRealWidth / 2f; + Rect bounds = new Rect(); + + mPaint.setTextSize(mDatePicker.getTextSize()); + mPaint.getTextBounds(BASE_TEXT, 0, BASE_TEXT.length(), bounds); + int height = bounds.height(); + mWeekDayY = (mHeaderSecondaryHeight + height) / 2f; + + mPaint.setTextSize(mHeaderPrimaryTextSize); + mPaint.getTextBounds(BASE_TEXT, 0, BASE_TEXT.length(), bounds); + int primaryTextHeight = bounds.height(); + if (mMonthFirst) + mFirstWidth = mPaint.measureText(mDay, 0, mDay.length()); + else + mFirstWidth = mPaint.measureText(mMonth, 0, mMonth.length()); + + mPaint.setTextSize(mHeaderSecondaryTextSize); + mPaint.getTextBounds(BASE_TEXT, 0, BASE_TEXT.length(), bounds); + int secondaryTextHeight = bounds.height(); + if (mMonthFirst) + mFirstWidth = Math.max(mFirstWidth, mPaint.measureText(mMonth, 0, mMonth.length())); + else + mFirstWidth = Math.max(mFirstWidth, mPaint.measureText(mDay, 0, mDay.length())); + mSecondWidth = mPaint.measureText(mYear, 0, mYear.length()); + + mCenterY = mHeaderSecondaryHeight + (mHeaderPrimaryRealHeight + primaryTextHeight) / 2f; + float y = ((mHeaderPrimaryRealHeight - primaryTextHeight) / 2f + secondaryTextHeight) / 2f; + float aboveY = mHeaderSecondaryHeight + y; + float belowY = mCenterY + y; + + if (mMonthFirst) { + mDayY = mCenterY; + mMonthY = aboveY; + } else { + mMonthY = mCenterY; + mDayY = aboveY; + } + + mYearY = belowY; + + mLocationDirty = false; + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + + mPaint.setColor(mHeaderSecondaryColor); + canvas.drawPath(mHeaderSecondaryBackground, mPaint); + mPaint.setColor(mHeaderPrimaryColor); + canvas.drawRect(0, mHeaderSecondaryHeight, mHeaderRealWidth, mHeaderPrimaryRealHeight + mHeaderSecondaryHeight, mPaint); + + measureHeaderText(); + + if (mWeekDay == null) + return; + + mPaint.setTextSize(mDatePicker.getTextSize()); + mPaint.setColor(mDatePicker.getTextHighlightColor()); + + canvas.drawText(mWeekDay, 0, mWeekDay.length(), mBaseX, mWeekDayY, mPaint); + + mPaint.setColor(mDaySelectMode ? mDatePicker.getTextHighlightColor() : mTextHeaderColor); + mPaint.setTextSize(mHeaderPrimaryTextSize); + if (mMonthFirst) + canvas.drawText(mDay, 0, mDay.length(), mBaseX, mDayY, mPaint); + else + canvas.drawText(mMonth, 0, mMonth.length(), mBaseX, mMonthY, mPaint); + + mPaint.setTextSize(mHeaderSecondaryTextSize); + if (mMonthFirst) + canvas.drawText(mMonth, 0, mMonth.length(), mBaseX, mMonthY, mPaint); + else + canvas.drawText(mDay, 0, mDay.length(), mBaseX, mDayY, mPaint); + + mPaint.setColor(mDaySelectMode ? mTextHeaderColor : mDatePicker.getTextHighlightColor()); + canvas.drawText(mYear, 0, mYear.length(), mBaseX, mYearY, mPaint); + } + + private boolean isTouched(float left, float top, float right, float bottom, float x, float y) { + return x >= left && x <= right && y >= top && y <= bottom; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + boolean handled = super.onTouchEvent(event); + + if (handled) + return true; + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + if (isTouched(mBaseX - mFirstWidth / 2f, mHeaderSecondaryHeight, mBaseX + mFirstWidth / 2f, mCenterY, event.getX(), event.getY())) + return !mDaySelectMode; + + if (isTouched(mBaseX - mSecondWidth / 2f, mCenterY, mBaseX + mSecondWidth / 2f, mHeaderSecondaryHeight + mHeaderPrimaryRealHeight, event.getX(), event.getY())) + return mDaySelectMode; + break; + case MotionEvent.ACTION_UP: + if (isTouched(mBaseX - mFirstWidth / 2f, mHeaderSecondaryHeight, mBaseX + mFirstWidth / 2f, mCenterY, event.getX(), event.getY())) { + setDateSelectMode(true); + return true; + } + + if (isTouched(mBaseX - mSecondWidth / 2f, mCenterY, mBaseX + mSecondWidth / 2f, mHeaderSecondaryHeight + mHeaderPrimaryRealHeight, event.getX(), event.getY())) { + setDateSelectMode(false); + return true; + } + break; + } + + return false; + } + + } + + public static class Builder extends Dialog.Builder { + + protected int mMinDay; + protected int mMinMonth; + protected int mMinYear; + protected int mMaxDay; + protected int mMaxMonth; + protected int mMaxYear; + protected int mDay; + protected int mMonth; + protected int mYear; + + private Calendar mCalendar; + + public Builder() { + this(R.style.Material_App_Dialog_DatePicker_Light); + } + + public Builder(int styleId) { + super(styleId); + Calendar cal = Calendar.getInstance(); + mDay = cal.get(Calendar.DAY_OF_MONTH); + mMonth = cal.get(Calendar.MONTH); + mYear = cal.get(Calendar.YEAR); + mMinDay = mDay; + mMinMonth = mMonth; + mMinYear = mYear - 12; + mMaxDay = mDay; + mMaxMonth = mMonth; + mMaxYear = mYear + 12; + } + + public Builder(int minDay, int minMonth, int minYear, int maxDay, int maxMonth, int maxYear, int day, int month, int year) { + this(R.style.Material_App_Dialog_DatePicker_Light, minDay, minMonth, minYear, maxDay, maxMonth, maxYear, day, month, year); + } + + public Builder(int styleId, int minDay, int minMonth, int minYear, int maxDay, int maxMonth, int maxYear, int day, int month, int year) { + super(styleId); + mMinDay = minDay; + mMinMonth = minMonth; + mMinYear = minYear; + mMaxDay = maxDay; + mMaxMonth = maxMonth; + mMaxYear = maxYear; + mDay = day; + mMonth = month; + mYear = year; + } + + public Builder initDate(int year, int month, int day) { + mDay = day; + mMonth = month; + mYear = year; + return this; + } + + public Builder initDate(Date date) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + mDay = calendar.get(Calendar.DAY_OF_MONTH); + mMonth = calendar.get(Calendar.MONTH); + mYear = calendar.get(Calendar.YEAR); + return this; + } + + public Builder dateRange(int minDay, int minMonth, int minYear, int maxDay, int maxMonth, int maxYear) { + mMinDay = minDay; + mMinMonth = minMonth; + mMinYear = minYear; + mMaxDay = maxDay; + mMaxMonth = maxMonth; + mMaxYear = maxYear; + return this; + } + + public Builder dateRange(long minTime, long maxTime) { + if (mCalendar == null) + mCalendar = Calendar.getInstance(); + mCalendar.setTimeInMillis(minTime); + int minDay = mCalendar.get(Calendar.DAY_OF_MONTH); + int minMonth = mCalendar.get(Calendar.MONTH); + int minYear = mCalendar.get(Calendar.YEAR); + mCalendar.setTimeInMillis(maxTime); + int maxDay = mCalendar.get(Calendar.DAY_OF_MONTH); + int maxMonth = mCalendar.get(Calendar.MONTH); + int maxYear = mCalendar.get(Calendar.YEAR); + return dateRange(minDay, minMonth, minYear, maxDay, maxMonth, maxYear); + } + + + public Builder date(int day, int month, int year) { + mDay = day; + mMonth = month; + mYear = year; + return this; + } + + public Builder date(long time) { + if (mCalendar == null) + mCalendar = Calendar.getInstance(); + mCalendar.setTimeInMillis(time); + int day = mCalendar.get(Calendar.DAY_OF_MONTH); + int month = mCalendar.get(Calendar.MONTH); + int year = mCalendar.get(Calendar.YEAR); + return date(day, month, year); + } + + @Override + public Builder contentView(int layoutId) { + return this; + } + + @Override + protected Dialog onBuild(Context context, int styleId) { + DatePickerDialog dialog = new DatePickerDialog(context, styleId); + OnDateChangedListener listener = getOnDateChangedListener(); + dialog.dateRange(mMinDay, mMinMonth, mMinYear, mMaxDay, mMaxMonth, mMaxYear) + .date(mDay, mMonth, mYear) + .onDateChangedListener(listener); + return dialog; + } + + @NonNull + private OnDateChangedListener getOnDateChangedListener() { + return new OnDateChangedListener() { + @Override + public void onDateChanged(int oldDay, int oldMonth, int oldYear, int newDay, int newMonth, int newYear) { + date(newDay, newMonth, newYear); + } + }; + } + + @Override + public DatePickerDialog build(Context context) { + return (DatePickerDialog) super.build(context); + } + } + +} diff --git a/material/src/main/java/com/rey/material/dialog/Dialog.java b/material/src/main/java/com/rey/material/dialog/Dialog.java new file mode 100644 index 0000000..9fd8cdd --- /dev/null +++ b/material/src/main/java/com/rey/material/dialog/Dialog.java @@ -0,0 +1,1616 @@ +package com.rey.material.dialog; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Handler; +import android.support.v4.view.ViewCompat; +import android.support.v7.widget.CardView; +import android.text.TextUtils; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.Window; +import android.view.WindowManager; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.FrameLayout; + +import com.rey.material.R; +import com.rey.material.drawable.BlankDrawable; +import com.rey.material.drawable.RippleDrawable; +import com.rey.material.util.ThemeUtil; +import com.rey.material.util.ViewUtil; +import com.rey.material.widget.Button; +import com.rey.material.widget.TextView; + +@SuppressWarnings("unused") +public class Dialog extends android.app.Dialog { + + private ContainerFrameLayout mContainer; + private int mLayoutWidth = ViewGroup.LayoutParams.WRAP_CONTENT; + private int mLayoutHeight = ViewGroup.LayoutParams.WRAP_CONTENT; + private int mMaxWidth; + private int mMaxHeight; + + protected TextView mTitle; + protected Button mPositiveAction; + protected Button mNegativeAction; + protected Button mNeutralAction; + private View mContent; + private DialogCardView mCardView; + + + protected int mContentPadding; + protected int mActionHeight; + protected int mActionOuterHeight; + protected int mActionOuterPadding; + protected int mActionMinWidth; + protected int mActionPadding; + protected int mDialogHorizontalPadding; + protected int mDialogVerticalPadding; + + protected int mInAnimationId; + protected int mOutAnimationId; + + private final Handler mHandler = new Handler(); + private final Runnable mDismissAction = new Runnable() { + public void run() { + //dirty fix for java.lang.IllegalArgumentException: View not attached to window manager + try { + Dialog.super.dismiss(); + } catch (IllegalArgumentException ignored) { + } + } + }; + + + private boolean mLayoutActionVertical = false; + + private boolean mCancelable = true; + private boolean mCanceledOnTouchOutside = true; + + /** + * The viewId of title view. + */ + public static final int TITLE = ViewUtil.generateViewId(); + /** + * The viewId of positive action button. + */ + public static final int ACTION_POSITIVE = ViewUtil.generateViewId(); + /** + * The viewId of negative action button. + */ + public static final int ACTION_NEGATIVE = ViewUtil.generateViewId(); + /** + * The viewId of neutral action button. + */ + public static final int ACTION_NEUTRAL = ViewUtil.generateViewId(); + + public Dialog(Context context) { + this(context, R.style.Material_App_Dialog_Light); + } + + + public Dialog(Context context, int style) { + super(context, style); + + //Override style to ensure not show window's title or background. + requestWindowFeature(Window.FEATURE_NO_TITLE); + getWindow().setBackgroundDrawable(BlankDrawable.getInstance()); + WindowManager.LayoutParams layout = getWindow().getAttributes(); + layout.width = ViewGroup.LayoutParams.MATCH_PARENT; + layout.height = ViewGroup.LayoutParams.MATCH_PARENT; + layout.windowAnimations = R.style.DialogNoAnimation; + getWindow().setAttributes(layout); + + init(context, style); + } + + @SuppressLint("PrivateResource") + private void init(Context context, int style) { + mContentPadding = ThemeUtil.dpToPx(context, 24); + mActionMinWidth = ThemeUtil.dpToPx(context, 64); + mActionHeight = ThemeUtil.dpToPx(context, 36); + mActionOuterHeight = ThemeUtil.dpToPx(context, 48); + mActionPadding = ThemeUtil.dpToPx(context, 4); + mActionOuterPadding = ThemeUtil.dpToPx(context, 16); + mDialogHorizontalPadding = ThemeUtil.dpToPx(context, 40); + mDialogVerticalPadding = ThemeUtil.dpToPx(context, 24); + + mCardView = new DialogCardView(context); + mContainer = new ContainerFrameLayout(context); + mTitle = new TextView(context); + mPositiveAction = new Button(context); + mNegativeAction = new Button(context); + mNeutralAction = new Button(context); + + mCardView.setPreventCornerOverlap(false); + mCardView.setUseCompatPadding(true); + + mTitle.setId(TITLE); + mTitle.setGravity(Gravity.START); + mTitle.setPadding(mContentPadding, mContentPadding, mContentPadding, mContentPadding - mActionPadding); + mPositiveAction.setId(ACTION_POSITIVE); + mPositiveAction.setPadding(mActionPadding, 0, mActionPadding, 0); + mPositiveAction.setBackgroundResource(0); + mNegativeAction.setId(ACTION_NEGATIVE); + mNegativeAction.setPadding(mActionPadding, 0, mActionPadding, 0); + mNegativeAction.setBackgroundResource(0); + mNeutralAction.setId(ACTION_NEUTRAL); + mNeutralAction.setPadding(mActionPadding, 0, mActionPadding, 0); + mNeutralAction.setBackgroundResource(0); + + mContainer.addView(mCardView); + mCardView.addView(mTitle); + mCardView.addView(mPositiveAction); + mCardView.addView(mNegativeAction); + mCardView.addView(mNeutralAction); + + backgroundColor(ThemeUtil.windowBackground(context, 0xFFFFFFFF)); + elevation(ThemeUtil.dpToPx(context, 4)); + cornerRadius(ThemeUtil.dpToPx(context, 2)); + dimAmount(0.5f); + layoutDirection(ViewCompat.LAYOUT_DIRECTION_LOCALE); + titleTextAppearance(R.style.TextAppearance_AppCompat_Title); + actionTextAppearance(R.style.TextAppearance_AppCompat_Button); + dividerColor(0x1E000000); + dividerHeight(ThemeUtil.dpToPx(context, 1)); + + cancelable(true); + canceledOnTouchOutside(true); + clearContent(); + onCreate(); + applyStyle(style); + + super.setContentView(mContainer); + } + + protected void onCreate() { + } + + @SuppressWarnings("ConstantConditions") + public Dialog applyStyle(int resId) { + Context context = getContext(); + TypedArray a = context.obtainStyledAttributes(resId, R.styleable.Dialog); + + int layout_width = mLayoutWidth; + int layout_height = mLayoutHeight; + boolean layoutParamsDefined = false; + int titleTextAppearance = 0; + int titleTextColor = 0; + boolean titleTextColorDefined = false; + int actionBackground = 0; + int actionRipple = 0; + int actionTextAppearance = 0; + ColorStateList actionTextColors = null; + int positiveActionBackground = 0; + int positiveActionRipple = 0; + int positiveActionTextAppearance = 0; + ColorStateList positiveActionTextColors = null; + int negativeActionBackground = 0; + int negativeActionRipple = 0; + int negativeActionTextAppearance = 0; + ColorStateList negativeActionTextColors = null; + int neutralActionBackground = 0; + int neutralActionRipple = 0; + int neutralActionTextAppearance = 0; + ColorStateList neutralActionTextColors = null; + + for (int i = 0, count = a.getIndexCount(); i < count; i++) { + int attr = a.getIndex(i); + + if (attr == R.styleable.Dialog_android_layout_width) { + layout_width = a.getLayoutDimension(attr, ViewGroup.LayoutParams.WRAP_CONTENT); + layoutParamsDefined = true; + } else if (attr == R.styleable.Dialog_android_layout_height) { + layout_height = a.getLayoutDimension(attr, ViewGroup.LayoutParams.WRAP_CONTENT); + layoutParamsDefined = true; + } else if (attr == R.styleable.Dialog_di_maxWidth) + maxWidth(a.getDimensionPixelOffset(attr, 0)); + else if (attr == R.styleable.Dialog_di_maxHeight) + maxHeight(a.getDimensionPixelOffset(attr, 0)); + else if (attr == R.styleable.Dialog_di_dimAmount) + dimAmount(a.getFloat(attr, 0)); + else if (attr == R.styleable.Dialog_di_backgroundColor) + backgroundColor(a.getColor(attr, 0)); + else if (attr == R.styleable.Dialog_di_maxElevation) + maxElevation(a.getDimensionPixelOffset(attr, 0)); + else if (attr == R.styleable.Dialog_di_elevation) + elevation(a.getDimensionPixelOffset(attr, 0)); + else if (attr == R.styleable.Dialog_di_cornerRadius) + cornerRadius(a.getDimensionPixelOffset(attr, 0)); + else if (attr == R.styleable.Dialog_di_layoutDirection) + layoutDirection(a.getInteger(attr, 0)); + else if (attr == R.styleable.Dialog_di_titleTextAppearance) + titleTextAppearance = a.getResourceId(attr, 0); + else if (attr == R.styleable.Dialog_di_titleTextColor) { + titleTextColor = a.getColor(attr, 0); + titleTextColorDefined = true; + } else if (attr == R.styleable.Dialog_di_actionBackground) + actionBackground = a.getResourceId(attr, 0); + else if (attr == R.styleable.Dialog_di_actionRipple) + actionRipple = a.getResourceId(attr, 0); + else if (attr == R.styleable.Dialog_di_actionTextAppearance) + actionTextAppearance = a.getResourceId(attr, 0); + else if (attr == R.styleable.Dialog_di_actionTextColor) + actionTextColors = a.getColorStateList(attr); + else if (attr == R.styleable.Dialog_di_positiveActionBackground) + positiveActionBackground = a.getResourceId(attr, 0); + else if (attr == R.styleable.Dialog_di_positiveActionRipple) + positiveActionRipple = a.getResourceId(attr, 0); + else if (attr == R.styleable.Dialog_di_positiveActionTextAppearance) + positiveActionTextAppearance = a.getResourceId(attr, 0); + else if (attr == R.styleable.Dialog_di_positiveActionTextColor) + positiveActionTextColors = a.getColorStateList(attr); + else if (attr == R.styleable.Dialog_di_negativeActionBackground) + negativeActionBackground = a.getResourceId(attr, 0); + else if (attr == R.styleable.Dialog_di_negativeActionRipple) + negativeActionRipple = a.getResourceId(attr, 0); + else if (attr == R.styleable.Dialog_di_negativeActionTextAppearance) + negativeActionTextAppearance = a.getResourceId(attr, 0); + else if (attr == R.styleable.Dialog_di_negativeActionTextColor) + negativeActionTextColors = a.getColorStateList(attr); + else if (attr == R.styleable.Dialog_di_neutralActionBackground) + neutralActionBackground = a.getResourceId(attr, 0); + else if (attr == R.styleable.Dialog_di_neutralActionRipple) + neutralActionRipple = a.getResourceId(attr, 0); + else if (attr == R.styleable.Dialog_di_neutralActionTextAppearance) + neutralActionTextAppearance = a.getResourceId(attr, 0); + else if (attr == R.styleable.Dialog_di_neutralActionTextColor) + neutralActionTextColors = a.getColorStateList(attr); + else if (attr == R.styleable.Dialog_di_dividerColor) + dividerColor(a.getColor(attr, 0)); + else if (attr == R.styleable.Dialog_di_dividerHeight) + dividerHeight(a.getDimensionPixelOffset(attr, 0)); + else if (attr == R.styleable.Dialog_di_cancelable) + cancelable(a.getBoolean(attr, true)); + else if (attr == R.styleable.Dialog_di_canceledOnTouchOutside) + canceledOnTouchOutside(a.getBoolean(attr, true)); + } + + a.recycle(); + + inAnimation(R.anim.anim_scale_in); + outAnimation(R.anim.anim_scale_out); + + if (layoutParamsDefined) + layoutParams(layout_width, layout_height); + + if (titleTextAppearance != 0) + titleTextAppearance(titleTextAppearance); + + if (titleTextColorDefined) + titleColor(titleTextColor); + + if (actionBackground != 0) + actionBackground(actionBackground); + + if (actionRipple != 0) + actionRipple(actionRipple); + + if (actionTextAppearance != 0) + actionTextAppearance(actionTextAppearance); + + if (actionTextColors != null) + actionTextColor(actionTextColors); + + if (positiveActionBackground != 0) + positiveActionBackground(positiveActionBackground); + + if (positiveActionRipple != 0) + positiveActionRipple(positiveActionRipple); + + if (positiveActionTextAppearance != 0) + positiveActionTextAppearance(positiveActionTextAppearance); + + if (positiveActionTextColors != null) + positiveActionTextColor(positiveActionTextColors); + + if (negativeActionBackground != 0) + negativeActionBackground(negativeActionBackground); + + if (negativeActionRipple != 0) + negativeActionRipple(negativeActionRipple); + + if (negativeActionTextAppearance != 0) + negativeActionTextAppearance(negativeActionTextAppearance); + + if (negativeActionTextColors != null) + negativeActionTextColor(negativeActionTextColors); + + if (neutralActionBackground != 0) + neutralActionBackground(neutralActionBackground); + + if (neutralActionRipple != 0) + neutralActionRipple(neutralActionRipple); + + if (neutralActionTextAppearance != 0) + neutralActionTextAppearance(neutralActionTextAppearance); + + if (neutralActionTextColors != null) + neutralActionTextColor(neutralActionTextColors); + + return this; + } + + /** + * Clear the content of this Dialog. + * + * @return The Dialog for chaining methods. + */ + public Dialog clearContent() { + title(0); + positiveAction(0, null); + positiveActionClickListener(null); + negativeAction(0, null); + negativeActionClickListener(null); + neutralAction(0, null); + neutralActionClickListener(null); + contentView(null); + return this; + } + + /** + * Set the params of this Dialog layout. + * + * @param width The width param. Can be the size in pixels, or {@link ViewGroup.LayoutParams#WRAP_CONTENT} or {@link ViewGroup.LayoutParams#MATCH_PARENT}. + * @param height The height param. Can be the size in pixels, or {@link ViewGroup.LayoutParams#WRAP_CONTENT} or {@link ViewGroup.LayoutParams#MATCH_PARENT}. + * @return The Dialog for chaining methods. + */ + public Dialog layoutParams(int width, int height) { + mLayoutWidth = width; + mLayoutHeight = height; + return this; + } + + /** + * Set the maximum width of this Dialog layout. + * + * @param width The maximum width in pixels. + * @return The Dialog for chaining methods. + */ + public Dialog maxWidth(int width) { + mMaxWidth = width; + return this; + } + + /** + * Set the maximum height of this Dialog layout. + * + * @param height The maximum height in pixels. + * @return The Dialog for chaining methods. + */ + public Dialog maxHeight(int height) { + mMaxHeight = height; + return this; + } + + /** + * Set the dim amount of the region outside this Dialog. + * + * @param amount The dim amount in [0..1]. + * @return The Dialog for chaining methods. + */ + public Dialog dimAmount(float amount) { + Window window = getWindow(); + if (amount > 0f) { + window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); + WindowManager.LayoutParams lp = window.getAttributes(); + lp.dimAmount = amount; + window.setAttributes(lp); + } else + window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); + return this; + } + + /** + * Set the background color of this Dialog + * + * @param color The color value. + * @return The Dialog for chaining methods. + */ + public Dialog backgroundColor(int color) { + mCardView.setCardBackgroundColor(color); + return this; + } + + /** + * Set the elevation value of this Dialog. + * + * @return The Dialog for chaining methods. + */ + public Dialog elevation(float elevation) { + if (mCardView.getMaxCardElevation() < elevation) + mCardView.setMaxCardElevation(elevation); + + mCardView.setCardElevation(elevation); + return this; + } + + /** + * Set the maximum elevation value of this Dialog. + * + * @return The Dialog for chaining methods. + */ + public Dialog maxElevation(float elevation) { + mCardView.setMaxCardElevation(elevation); + return this; + } + + /** + * Set the corner radius of this Dialog. + * + * @param radius The corner radius. + * @return The Dialog for chaining methods. + */ + public Dialog cornerRadius(float radius) { + mCardView.setRadius(radius); + return this; + } + + /** + * Set the divider's color of this Dialog. + * + * @param color The color value. + * @return The Dialog for chaining methods. + */ + public Dialog dividerColor(int color) { + mCardView.setDividerColor(color); + return this; + } + + /** + * Set the height of divider of this Dialog. + * + * @param width The size value in pixels. + * @return The Dialog for chaining methods. + */ + public Dialog dividerHeight(int width) { + mCardView.setDividerHeight(width); + return this; + } + + /** + * Set the title of this Dialog. + * + * @param title The title text. + * @return The Dialog for chaining methods. + */ + public Dialog title(CharSequence title) { + mTitle.setText(title); + mTitle.setVisibility(TextUtils.isEmpty(title) ? View.GONE : View.VISIBLE); + return this; + } + + /** + * Set the title of this Dialog. + * + * @param id The resourceId of text. + * @return The Dialog for chaining methods. + */ + public Dialog title(int id) { + return title(id == 0 ? null : getContext().getResources().getString(id)); + } + + @Override + public void setTitle(CharSequence title) { + title(title); + } + + @Override + public void setTitle(int titleId) { + title(titleId); + } + + /** + * Set the text's color of Dialog's title. + * + * @param color The color value. + * @return The Dialog for chaining methods. + */ + public Dialog titleColor(int color) { + mTitle.setTextColor(color); + return this; + } + + /** + * Sets the text color, size, style of the title view from the specified TextAppearance resource. + * + * @param resId The resourceId value. + * @return The Dialog for chaining methods. + */ + public Dialog titleTextAppearance(int resId) { + mTitle.setTextAppearance(resId); + return this; + } + + /** + * Set the background drawable of all action buttons. + * + * @param id The resourceId of drawable. + * @return The Dialog for chaining methods. + */ + public Dialog actionBackground(int id) { + positiveActionBackground(id); + negativeActionBackground(id); + neutralActionBackground(id); + return this; + } + + /** + * Set the background drawable of all action buttons. + * + * @param drawable The background drawable. + * @return The Dialog for chaining methods. + */ + public Dialog actionBackground(Drawable drawable) { + positiveActionBackground(drawable); + negativeActionBackground(drawable); + neutralActionBackground(drawable); + return this; + } + + /** + * Set the RippleEffect of all action buttons. + * + * @param resId The resourceId of style. + * @return The Dialog for chaining methods. + */ + public Dialog actionRipple(int resId) { + positiveActionRipple(resId); + negativeActionRipple(resId); + neutralActionRipple(resId); + return this; + } + + /** + * Sets the text color, size, style of all action buttons from the specified TextAppearance resource. + * + * @param resId The resourceId value. + * @return The Dialog for chaining methods. + */ + public Dialog actionTextAppearance(int resId) { + positiveActionTextAppearance(resId); + negativeActionTextAppearance(resId); + neutralActionTextAppearance(resId); + return this; + } + + /** + * Sets the text color of all action buttons. + * + * @return The Dialog for chaining methods. + */ + public Dialog actionTextColor(ColorStateList color) { + positiveActionTextColor(color); + negativeActionTextColor(color); + neutralActionTextColor(color); + return this; + } + + /** + * Sets the text color of all action buttons. + * + * @return The Dialog for chaining methods. + */ + public Dialog actionTextColor(int color) { + positiveActionTextColor(color); + negativeActionTextColor(color); + neutralActionTextColor(color); + return this; + } + + /** + * Set the text of positive action button. + * + * @return The Dialog for chaining methods. + */ + public Dialog positiveAction(CharSequence action, final Action1 callback) { + mPositiveAction.setText(action); + mPositiveAction.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (callback != null) + callback.onAction(Dialog.this); + if (autoDismiss) + Dialog.this.dismiss(); + } + }); + mPositiveAction.setVisibility(TextUtils.isEmpty(action) ? View.GONE : View.VISIBLE); + return this; + } + + private boolean autoDismiss = true; + + public Dialog setAutoDismiss(boolean autoDismiss) { + this.autoDismiss = autoDismiss; + return this; + } + + /** + * Set the text of positive action button. + * + * @param id The resourceId of text. + * @return The Dialog for chaining methods. + */ + public Dialog positiveAction(int id, Action1 callback) { + return positiveAction(id == 0 ? null : getContext().getResources().getString(id), callback); + } + + /** + * Set the background drawable of positive action button. + * + * @param drawable The background drawable. + * @return The Dialog for chaining methods. + */ + public Dialog positiveActionBackground(Drawable drawable) { + ViewUtil.setBackground(mPositiveAction, drawable); + return this; + } + + /** + * Set the background drawable of positive action button. + * + * @param id The resourceId of drawable. + * @return The Dialog for chaining methods. + */ + public Dialog positiveActionBackground(int id) { + return positiveActionBackground(id == 0 ? null : getDrawable(id)); + } + + private Drawable getDrawable(int id) { + return getContext().getResources().getDrawable(id); + } + + /** + * Set the RippleEffect of positive action button. + * + * @param resId The resourceId of style. + * @return The Dialog for chaining methods. + */ + public Dialog positiveActionRipple(int resId) { + RippleDrawable drawable = new RippleDrawable.Builder(getContext(), resId).build(); + return positiveActionBackground(drawable); + } + + /** + * Sets the text color, size, style of positive action button from the specified TextAppearance resource. + * + * @param resId The resourceId value. + * @return The Dialog for chaining methods. + */ + public Dialog positiveActionTextAppearance(int resId) { + mPositiveAction.setTextAppearance(resId); + return this; + } + + /** + * Sets the text color of positive action button. + * + * @return The Dialog for chaining methods. + */ + public Dialog positiveActionTextColor(ColorStateList color) { + mPositiveAction.setTextColor(color); + return this; + } + + /** + * Sets the text color of positive action button. + * + * @return The Dialog for chaining methods. + */ + public Dialog positiveActionTextColor(int color) { + mPositiveAction.setTextColor(color); + return this; + } + + /** + * Set a listener will be called when positive action button is clicked. + * + * @param listener The {@link View.OnClickListener} will be called. + * @return The Dialog for chaining methods. + */ + public Dialog positiveActionClickListener(View.OnClickListener listener) { + mPositiveAction.setOnClickListener(listener); + return this; + } + + /** + * Set the text of negative action button. + * + * @return The Dialog for chaining methods. + */ + public Dialog negativeAction(CharSequence action, final Action1 callback) { + mNegativeAction.setText(action); + mNegativeAction.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (callback != null) + callback.onAction(Dialog.this); + if (autoDismiss) + Dialog.this.dismiss(); + } + }); + mNegativeAction.setVisibility(TextUtils.isEmpty(action) ? View.GONE : View.VISIBLE); + return this; + } + + /** + * Set the text of negative action button. + * + * @param id The resourceId of text. + * @return The Dialog for chaining methods. + */ + public Dialog negativeAction(int id, Action1 callback) { + return negativeAction(id == 0 ? null : getContext().getResources().getString(id), callback); + } + + /** + * Set the background drawable of negative action button. + * + * @param drawable The background drawable. + * @return The Dialog for chaining methods. + */ + public Dialog negativeActionBackground(Drawable drawable) { + ViewUtil.setBackground(mNegativeAction, drawable); + return this; + } + + /** + * Set the background drawable of negative action button. + * + * @param id The resourceId of drawable. + * @return The Dialog for chaining methods. + */ + public Dialog negativeActionBackground(int id) { + return negativeActionBackground(id == 0 ? null : getDrawable(id)); + } + + /** + * Set the RippleEffect of negative action button. + * + * @param resId The resourceId of style. + * @return The Dialog for chaining methods. + */ + public Dialog negativeActionRipple(int resId) { + RippleDrawable drawable = new RippleDrawable.Builder(getContext(), resId).build(); + return negativeActionBackground(drawable); + } + + /** + * Sets the text color, size, style of negative action button from the specified TextAppearance resource. + * + * @param resId The resourceId value. + * @return The Dialog for chaining methods. + */ + public Dialog negativeActionTextAppearance(int resId) { + mNegativeAction.setTextAppearance(resId); + return this; + } + + /** + * Sets the text color of negative action button. + * + * @return The Dialog for chaining methods. + */ + public Dialog negativeActionTextColor(ColorStateList color) { + mNegativeAction.setTextColor(color); + return this; + } + + /** + * Sets the text color of negative action button. + * + * @return The Dialog for chaining methods. + */ + public Dialog negativeActionTextColor(int color) { + mNegativeAction.setTextColor(color); + return this; + } + + /** + * Set a listener will be called when negative action button is clicked. + * + * @param listener The {@link View.OnClickListener} will be called. + * @return The Dialog for chaining methods. + */ + public Dialog negativeActionClickListener(View.OnClickListener listener) { + mNegativeAction.setOnClickListener(listener); + return this; + } + + /** + * Set the text of neutral action button. + * + * @return The Dialog for chaining methods. + */ + public Dialog neutralAction(CharSequence action, final Action1 callback) { + mNeutralAction.setText(action); + mNeutralAction.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (callback != null) + callback.onAction(Dialog.this); + if (autoDismiss) + Dialog.this.dismiss(); + } + }); + mNeutralAction.setVisibility(TextUtils.isEmpty(action) ? View.GONE : View.VISIBLE); + return this; + } + + /** + * Set the text of neutral action button. + * + * @param id The resourceId of text. + * @return The Dialog for chaining methods. + */ + public Dialog neutralAction(int id, Action1 callback) { + return neutralAction(id == 0 ? null : getContext().getResources().getString(id), callback); + } + + /** + * Set the background drawable of neutral action button. + * + * @param drawable The background drawable. + * @return The Dialog for chaining methods. + */ + public Dialog neutralActionBackground(Drawable drawable) { + ViewUtil.setBackground(mNeutralAction, drawable); + return this; + } + + /** + * Set the background drawable of neutral action button. + * + * @param id The resourceId of drawable. + * @return The Dialog for chaining methods. + */ + public Dialog neutralActionBackground(int id) { + return neutralActionBackground(id == 0 ? null : getDrawable(id)); + } + + /** + * Set the RippleEffect of neutral action button. + * + * @param resId The resourceId of style. + * @return The Dialog for chaining methods. + */ + public Dialog neutralActionRipple(int resId) { + RippleDrawable drawable = new RippleDrawable.Builder(getContext(), resId).build(); + return neutralActionBackground(drawable); + } + + /** + * Sets the text color, size, style of neutral action button from the specified TextAppearance resource. + * + * @param resId The resourceId value. + * @return The Dialog for chaining methods. + */ + public Dialog neutralActionTextAppearance(int resId) { + mNeutralAction.setTextAppearance(resId); + return this; + } + + /** + * Sets the text color of neutral action button. + * + * @return The Dialog for chaining methods. + */ + public Dialog neutralActionTextColor(ColorStateList color) { + mNeutralAction.setTextColor(color); + return this; + } + + /** + * Sets the text color of neutral action button. + * + * @return The Dialog for chaining methods. + */ + public Dialog neutralActionTextColor(int color) { + mNeutralAction.setTextColor(color); + return this; + } + + /** + * Set a listener will be called when neutral action button is clicked. + * + * @param listener The {@link View.OnClickListener} will be called. + * @return The Dialog for chaining methods. + */ + public Dialog neutralActionClickListener(View.OnClickListener listener) { + mNeutralAction.setOnClickListener(listener); + return this; + } + + /** + * Set the layout direction of this Dialog + * + * @param direction The layout direction value. Can be {@link View#LAYOUT_DIRECTION_LTR}, {@link View#LAYOUT_DIRECTION_RTL} or {@link View#LAYOUT_DIRECTION_LOCALE} + * @return The Dialog for chaining methods. + */ + public Dialog layoutDirection(int direction) { + ViewCompat.setLayoutDirection(mCardView, direction); + return this; + } + + /** + * Set the animation when Dialog enter the screen. + * + * @param resId The resourceId of animation. + * @return The Dialog for chaining methods. + */ + public Dialog inAnimation(int resId) { + mInAnimationId = resId; + return this; + } + + /** + * Set the animation when Dialog exit the screen. + * + * @param resId The resourceId of animation. + * @return The Dialog for chaining methods. + */ + public Dialog outAnimation(int resId) { + mOutAnimationId = resId; + return this; + } + + /** + * Indicate that Dialog should show divider when the content is longer than container view. + * + * @return The Dialog for chaining methods. + */ + public Dialog showDivider(boolean show) { + mCardView.setShowDivider(show); + return this; + } + + /** + * Set the content view of this Dialog. + * + * @param v The content view. + * @return The Dialog for chaining methods. + */ + public Dialog contentView(View v) { + if (mContent != v) { + if (mContent != null) + mCardView.removeView(mContent); + + mContent = v; + } + + if (mContent != null) + mCardView.addView(mContent); + + return this; + } + + /** + * Set the content view of this Dialog. + * + * @param layoutId The reourceId of layout. + * @return The Dialog for chaining methods. + */ + public Dialog contentView(int layoutId) { + if (layoutId == 0) + return this; + + View v = LayoutInflater.from(getContext()).inflate(layoutId, null); + return contentView(v); + } + + /** + * Sets whether this dialog is cancelable with the + * {@link android.view.KeyEvent#KEYCODE_BACK BACK} key. + * + * @return The Dialog for chaining methods. + */ + public Dialog cancelable(boolean cancelable) { + super.setCancelable(cancelable); + mCancelable = cancelable; + return this; + } + + /** + * Sets whether this dialog is canceled when touched outside the window's + * bounds. If setting to true, the dialog is set to be cancelable if not + * already set. + * + * @param cancel Whether the dialog should be canceled when touched outside + * @return The Dialog for chaining methods. + */ + public Dialog canceledOnTouchOutside(boolean cancel) { + super.setCanceledOnTouchOutside(cancel); + mCanceledOnTouchOutside = cancel; + return this; + } + + /** + * Set the margin between content view and Dialog border. + * + * @param margin The margin size in pixels. + * @return The Dialog for chaining methods. + */ + public Dialog contentMargin(int margin) { + mCardView.setContentMargin(margin); + return this; + } + + /** + * Set the margin between content view and Dialog border. + * + * @param left The left margin size in pixels. + * @param top The top margin size in pixels. + * @param right The right margin size in pixels. + * @param bottom The bottom margin size in pixels. + * @return The Dialog for chaining methods. + */ + public Dialog contentMargin(int left, int top, int right, int bottom) { + mCardView.setContentMargin(left, top, right, bottom); + return this; + } + + @Override + public void setCancelable(boolean flag) { + cancelable(flag); + } + + @Override + public void setCanceledOnTouchOutside(boolean cancel) { + canceledOnTouchOutside(cancel); + } + + @Override + public void setContentView(View v) { + contentView(v); + } + + @Override + public void setContentView(int layoutId) { + contentView(layoutId); + } + + @Override + public void setContentView(View v, ViewGroup.LayoutParams params) { + contentView(v); + } + + @Override + public void addContentView(View view, ViewGroup.LayoutParams params) { + contentView(view); + } + + @Override + protected void onStart() { + super.onStart(); + mCardView.setVisibility(View.VISIBLE); + if (mInAnimationId != 0) + mCardView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + mCardView.getViewTreeObserver().removeOnPreDrawListener(this); + Animation anim = AnimationUtils.loadAnimation(mCardView.getContext(), mInAnimationId); + mCardView.startAnimation(anim); + return false; + } + }); + } + + /** + * Dismiss Dialog immediately without showing out animation. + */ + public void dismissImmediately() { + super.dismiss(); + + if (mHandler != null) + mHandler.removeCallbacks(mDismissAction); + } + + @Override + public void dismiss() { + if (!isShowing()) + return; + + if (mOutAnimationId != 0) { + Animation anim = AnimationUtils.loadAnimation(mContainer.getContext(), mOutAnimationId); + anim.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + + @Override + public void onAnimationEnd(Animation animation) { + mCardView.setVisibility(View.GONE); + mHandler.post(mDismissAction); + } + }); + mCardView.startAnimation(anim); + } else + mHandler.post(mDismissAction); + } + + private class ContainerFrameLayout extends FrameLayout { + + private boolean mClickOutside = false; + + public ContainerFrameLayout(Context context) { + super(context); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + mCardView.measure(widthMeasureSpec, heightMeasureSpec); + setMeasuredDimension(widthSize, heightSize); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int childLeft = (right - left - mCardView.getMeasuredWidth()) / 2; + int childTop = (bottom - top - mCardView.getMeasuredHeight()) / 2; + int childRight = childLeft + mCardView.getMeasuredWidth(); + int childBottom = childTop + mCardView.getMeasuredHeight(); + + mCardView.layout(childLeft, childTop, childRight, childBottom); + } + + private boolean isOutsideDialog(float x, float y) { + return x < mCardView.getLeft() + mCardView.getPaddingLeft() || x > mCardView.getRight() - mCardView.getPaddingRight() || y < mCardView.getTop() + mCardView.getPaddingTop() || y > mCardView.getBottom() - mCardView.getPaddingBottom(); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + boolean handled = super.onTouchEvent(event); + + if (handled) + return true; + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + if (isOutsideDialog(event.getX(), event.getY())) { + mClickOutside = true; + return true; + } + return false; + case MotionEvent.ACTION_MOVE: + return mClickOutside; + case MotionEvent.ACTION_CANCEL: + mClickOutside = false; + return false; + case MotionEvent.ACTION_UP: + if (mClickOutside && isOutsideDialog(event.getX(), event.getY())) { + mClickOutside = false; + if (mCancelable && mCanceledOnTouchOutside) + dismiss(); + return true; + } + return false; + } + + return false; + } + + } + + private class DialogCardView extends CardView { + + private Paint mDividerPaint; + private float mDividerPos = -1f; + private boolean mShowDivider = false; + + private int mContentMarginLeft; + private int mContentMarginTop; + private int mContentMarginRight; + private int mContentMarginBottom; + + private boolean mIsRtl = false; + + public DialogCardView(Context context) { + super(context); + + mDividerPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mDividerPaint.setStyle(Paint.Style.STROKE); + setWillNotDraw(false); + } + + public void setContentMargin(int margin) { + setContentMargin(margin, margin, margin, margin); + } + + public void setContentMargin(int left, int top, int right, int bottom) { + mContentMarginLeft = left; + mContentMarginTop = top; + mContentMarginRight = right; + mContentMarginBottom = bottom; + } + + public void setDividerColor(int color) { + mDividerPaint.setColor(color); + invalidate(); + } + + public void setDividerHeight(int width) { + mDividerPaint.setStrokeWidth(width); + invalidate(); + } + + public void setShowDivider(boolean show) { + if (mShowDivider != show) { + mShowDivider = show; + invalidate(); + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + @Override + public void onRtlPropertiesChanged(int layoutDirection) { + boolean rtl = layoutDirection == LAYOUT_DIRECTION_RTL; + if (mIsRtl != rtl) { + mIsRtl = rtl; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + int direction = mIsRtl ? TEXT_DIRECTION_RTL : TEXT_DIRECTION_LTR; + + mTitle.setTextDirection(direction); + mPositiveAction.setTextDirection(direction); + mNegativeAction.setTextDirection(direction); + mNeutralAction.setTextDirection(direction); + } + + requestLayout(); + } + } + + @SuppressWarnings("ConstantConditions") + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthSize = View.MeasureSpec.getSize(widthMeasureSpec); + int heightSize = View.MeasureSpec.getSize(heightMeasureSpec); + + int paddingLeft = Math.max(mDialogHorizontalPadding, mCardView.getPaddingLeft()); + int paddingRight = Math.max(mDialogHorizontalPadding, mCardView.getPaddingRight()); + int paddingTop = Math.max(mDialogVerticalPadding, mCardView.getPaddingTop()); + int paddingBottom = Math.max(mDialogVerticalPadding, mCardView.getPaddingBottom()); + + int maxWidth = widthSize - paddingLeft - paddingRight; + if (mMaxWidth > 0) + maxWidth = Math.min(maxWidth, mMaxWidth); + int maxHeight = heightSize - paddingTop - paddingBottom; + if (mMaxHeight > 0) + maxHeight = Math.min(maxHeight, mMaxHeight); + + int width = mLayoutWidth == ViewGroup.LayoutParams.MATCH_PARENT ? maxWidth : mLayoutWidth; + int height = mLayoutHeight == ViewGroup.LayoutParams.MATCH_PARENT ? maxHeight : mLayoutHeight; + + int widthMs; + int heightMs; + + int titleWidth = 0; + int titleHeight = 0; + + if (mTitle.getVisibility() == View.VISIBLE) { + widthMs = View.MeasureSpec.makeMeasureSpec(width == ViewGroup.LayoutParams.WRAP_CONTENT ? maxWidth : width, View.MeasureSpec.AT_MOST); + heightMs = View.MeasureSpec.makeMeasureSpec(maxHeight, View.MeasureSpec.AT_MOST); + mTitle.measure(widthMs, heightMs); + titleWidth = mTitle.getMeasuredWidth(); + titleHeight = mTitle.getMeasuredHeight(); + } + + int contentWidth = 0; + int contentHeight = 0; + + if (mContent != null) { + widthMs = View.MeasureSpec.makeMeasureSpec((width == ViewGroup.LayoutParams.WRAP_CONTENT ? maxWidth : width) - mContentMarginLeft - mContentMarginRight, View.MeasureSpec.AT_MOST); + heightMs = View.MeasureSpec.makeMeasureSpec(maxHeight - mContentMarginTop - mContentMarginBottom, View.MeasureSpec.AT_MOST); + mContent.measure(widthMs, heightMs); + contentWidth = mContent.getMeasuredWidth(); + contentHeight = mContent.getMeasuredHeight(); + } + + int visibleActions = 0; + int positiveActionWidth = 0; + + if (mPositiveAction.getVisibility() == View.VISIBLE) { + widthMs = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + heightMs = View.MeasureSpec.makeMeasureSpec(mActionHeight, View.MeasureSpec.EXACTLY); + mPositiveAction.measure(widthMs, heightMs); + + positiveActionWidth = mPositiveAction.getMeasuredWidth(); + + if (positiveActionWidth < mActionMinWidth) { + mPositiveAction.measure(View.MeasureSpec.makeMeasureSpec(mActionMinWidth, View.MeasureSpec.EXACTLY), heightMs); + positiveActionWidth = mActionMinWidth; + } + + visibleActions++; + } + + int negativeActionWidth = 0; + + if (mNegativeAction.getVisibility() == View.VISIBLE) { + widthMs = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + heightMs = View.MeasureSpec.makeMeasureSpec(mActionHeight, View.MeasureSpec.EXACTLY); + mNegativeAction.measure(widthMs, heightMs); + + negativeActionWidth = mNegativeAction.getMeasuredWidth(); + + if (negativeActionWidth < mActionMinWidth) { + mNegativeAction.measure(View.MeasureSpec.makeMeasureSpec(mActionMinWidth, View.MeasureSpec.EXACTLY), heightMs); + negativeActionWidth = mActionMinWidth; + } + + visibleActions++; + } + + int neutralActionWidth = 0; + + if (mNeutralAction.getVisibility() == View.VISIBLE) { + widthMs = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + heightMs = View.MeasureSpec.makeMeasureSpec(mActionHeight, View.MeasureSpec.EXACTLY); + mNeutralAction.measure(widthMs, heightMs); + + neutralActionWidth = mNeutralAction.getMeasuredWidth(); + + if (neutralActionWidth < mActionMinWidth) { + mNeutralAction.measure(View.MeasureSpec.makeMeasureSpec(mActionMinWidth, View.MeasureSpec.EXACTLY), heightMs); + neutralActionWidth = mActionMinWidth; + } + + visibleActions++; + } + + int actionBarWidth = positiveActionWidth + negativeActionWidth + neutralActionWidth + mActionOuterPadding * 2 + mActionPadding * Math.max(0, visibleActions - 1); + + if (width == ViewGroup.LayoutParams.WRAP_CONTENT) + width = Math.min(maxWidth, Math.max(titleWidth, Math.max(contentWidth + mContentMarginLeft + mContentMarginRight, actionBarWidth))); + + mLayoutActionVertical = actionBarWidth > width; + + int nonContentHeight = titleHeight + (visibleActions > 0 ? mActionPadding : 0) + mContentMarginTop + mContentMarginBottom; + if (mLayoutActionVertical) + nonContentHeight += mActionOuterHeight * visibleActions; + else + nonContentHeight += (visibleActions > 0) ? mActionOuterHeight : 0; + + if (height == ViewGroup.LayoutParams.WRAP_CONTENT) + height = Math.min(maxHeight, contentHeight + nonContentHeight); + + if (mContent != null) + mContent.measure(View.MeasureSpec.makeMeasureSpec(width - mContentMarginLeft - mContentMarginRight, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(height - nonContentHeight, View.MeasureSpec.EXACTLY)); + + setMeasuredDimension(width + getPaddingLeft() + getPaddingRight(), height + getPaddingTop() + getPaddingBottom()); + } + + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int childLeft = 0; + int childTop = 0; + int childRight = right - left; + int childBottom = bottom - top; + + childLeft += getPaddingLeft(); + childTop += getPaddingTop(); + childRight -= getPaddingRight(); + childBottom -= getPaddingBottom(); + + if (mTitle.getVisibility() == View.VISIBLE) { + if (mIsRtl) + mTitle.layout(childRight - mTitle.getMeasuredWidth(), childTop, childRight, childTop + mTitle.getMeasuredHeight()); + else + mTitle.layout(childLeft, childTop, childLeft + mTitle.getMeasuredWidth(), childTop + mTitle.getMeasuredHeight()); + childTop += mTitle.getMeasuredHeight(); + } + + boolean hasAction = mNeutralAction.getVisibility() == View.VISIBLE || mNegativeAction.getVisibility() == View.VISIBLE || mPositiveAction.getVisibility() == View.VISIBLE; + if (hasAction) + childBottom -= mActionPadding; + + int temp = (mActionOuterHeight - mActionHeight) / 2; + + if (hasAction) { + if (mLayoutActionVertical) { + if (mNeutralAction.getVisibility() == View.VISIBLE) { + mNeutralAction.layout(childRight - mActionOuterPadding - mNeutralAction.getMeasuredWidth(), childBottom - mActionOuterHeight + temp, childRight - mActionOuterPadding, childBottom - temp); + childBottom -= mActionOuterHeight; + } + + if (mNegativeAction.getVisibility() == View.VISIBLE) { + mNegativeAction.layout(childRight - mActionOuterPadding - mNegativeAction.getMeasuredWidth(), childBottom - mActionOuterHeight + temp, childRight - mActionOuterPadding, childBottom - temp); + childBottom -= mActionOuterHeight; + } + + if (mPositiveAction.getVisibility() == View.VISIBLE) { + mPositiveAction.layout(childRight - mActionOuterPadding - mPositiveAction.getMeasuredWidth(), childBottom - mActionOuterHeight + temp, childRight - mActionOuterPadding, childBottom - temp); + childBottom -= mActionOuterHeight; + } + } else { + int actionLeft = childLeft + mActionOuterPadding; + int actionRight = childRight - mActionOuterPadding; + int actionTop = childBottom - mActionOuterHeight + temp; + int actionBottom = childBottom - temp; + + if (mIsRtl) { + if (mPositiveAction.getVisibility() == View.VISIBLE) { + mPositiveAction.layout(actionLeft, actionTop, actionLeft + mPositiveAction.getMeasuredWidth(), actionBottom); + actionLeft += mPositiveAction.getMeasuredWidth() + mActionPadding; + } + + if (mNegativeAction.getVisibility() == View.VISIBLE) + mNegativeAction.layout(actionLeft, actionTop, actionLeft + mNegativeAction.getMeasuredWidth(), actionBottom); + + + if (mNeutralAction.getVisibility() == View.VISIBLE) + mNeutralAction.layout(actionRight - mNeutralAction.getMeasuredWidth(), actionTop, actionRight, actionBottom); + } else { + + if (mPositiveAction.getVisibility() == View.VISIBLE) { + mPositiveAction.layout(actionRight - mPositiveAction.getMeasuredWidth(), actionTop, actionRight, actionBottom); + actionRight -= mPositiveAction.getMeasuredWidth() + mActionPadding; + } + + if (mNegativeAction.getVisibility() == View.VISIBLE) + mNegativeAction.layout(actionRight - mNegativeAction.getMeasuredWidth(), actionTop, actionRight, actionBottom); + + if (mNeutralAction.getVisibility() == View.VISIBLE) + mNeutralAction.layout(actionLeft, actionTop, actionLeft + mNeutralAction.getMeasuredWidth(), actionBottom); + } + + childBottom -= mActionOuterHeight; + } + } + + mDividerPos = childBottom - mDividerPaint.getStrokeWidth() / 2f; + + if (mContent != null) + mContent.layout(childLeft + mContentMarginLeft, childTop + mContentMarginTop, childRight - mContentMarginRight, childBottom - mContentMarginBottom); + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + + if (mShowDivider && (mPositiveAction.getVisibility() == View.VISIBLE || mNegativeAction.getVisibility() == View.VISIBLE || mNeutralAction.getVisibility() == View.VISIBLE)) + canvas.drawLine(getPaddingLeft(), mDividerPos, getWidth() - getPaddingRight(), mDividerPos, mDividerPaint); + } + + } + + public static class Builder implements DialogFragment.Builder { + + protected int mStyleId; + protected int mContentViewId; + protected CharSequence mTitle, mPositiveText, mNegativeText, mNeutralText; + protected Action1 onPositive, onNegative, onNeutral; + protected Action1 onDismissCallback; + protected View mContentView; + private boolean autoDismiss = true; + + public Builder() { + this(R.style.Material_App_Dialog_Light); + } + + public Builder(int styleId) { + mStyleId = styleId; + } + + public Builder contentView(int layoutId) { + mContentViewId = layoutId; + mContentView = null; + return this; + } + + public Builder contentView(View view) { + mContentViewId = -0; + mContentView = view; + return this; + } + + // TODO 实现Parcelable接口 + public Builder title(CharSequence title) { + mTitle = title; + return this; + } + + public Builder positiveAction(CharSequence text, Action1 callback) { + mPositiveText = text; + onPositive = callback; + return this; + } + + public Builder negativeAction(CharSequence text, Action1 callback) { + mNegativeText = text; + onNegative = callback; + return this; + } + + public Builder neutralAction(CharSequence text, Action1 callback) { + mNeutralText = text; + onNeutral = callback; + return this; + } + + public Builder onDismiss(Action1 callback) { + onDismissCallback = callback; + return this; + } + + public Builder autoDismiss(boolean autoDismiss) { + this.autoDismiss = autoDismiss; + return this; + } + + public Dialog build(Context context) { + Dialog mDialog = onBuild(context, mStyleId); + + mDialog.title(mTitle) + .positiveAction(mPositiveText, onPositive) + .negativeAction(mNegativeText, onNegative) + .neutralAction(mNeutralText, onNeutral) + .setAutoDismiss(autoDismiss); + + if (onDismissCallback != null) + mDialog.setOnDismissListener(new OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + onDismissCallback.onAction((Dialog) dialog); + } + }); + + if (mContentViewId != 0) + mDialog.contentView(mContentViewId); + else if (mContentView != null) { + mDialog.contentView(mContentView); + } + + onBuildDone(mDialog); + + return mDialog; + } + + + /** + * Get a appropriate Dialog instance will be used for styling later. + * Child class should override this function to return appropriate Dialog instance. + * If you want to apply styling to dialog, or get content view, you should do it in {@link #onBuildDone(Dialog)} + * + * @param context A Context instance. + * @param styleId The resourceId of Dialog's style. + * @return A Dialog instance will be used for styling later. + */ + protected Dialog onBuild(Context context, int styleId) { + return new Dialog(context, styleId); + } + + /** + * This function will be called after Builder done apply styling to Dialog. + * + * @param dialog The Dialog instance. + */ + protected void onBuildDone(Dialog dialog) { + } + } + + public interface Action1 { + void onAction(Dialog dialog); + } + +} diff --git a/material/src/main/java/com/rey/material/dialog/DialogFragment.java b/material/src/main/java/com/rey/material/dialog/DialogFragment.java new file mode 100644 index 0000000..7d59a5f --- /dev/null +++ b/material/src/main/java/com/rey/material/dialog/DialogFragment.java @@ -0,0 +1,55 @@ +package com.rey.material.dialog; + +import android.content.Context; +import android.os.Bundle; +import android.os.Parcelable; +import android.support.annotation.NonNull; + +/** + * Created by Rey on 1/12/2015. + * Interface + */ +public class DialogFragment extends android.support.v4.app.DialogFragment{ + + + protected static final String ARG_BUILDER = "arg_builder"; + protected Builder mBuilder; + + public interface Builder{ + Dialog build(Context context); + } + + public static DialogFragment newInstance(Builder builder){ + DialogFragment fragment = new DialogFragment(); + fragment.mBuilder = builder; + return fragment; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + return mBuilder == null ? new Dialog(getActivity()) : mBuilder.build(getActivity()); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if(savedInstanceState != null && mBuilder == null) + mBuilder = savedInstanceState.getParcelable(ARG_BUILDER); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if(mBuilder != null && mBuilder instanceof Parcelable) + outState.putParcelable(ARG_BUILDER, (Parcelable)mBuilder); + } + + @Override + public void onDestroyView() { + android.app.Dialog dialog = getDialog(); + if(dialog != null && dialog instanceof Dialog) + ((Dialog)dialog).dismissImmediately(); + super.onDestroyView(); + } +} diff --git a/material/src/main/java/com/rey/material/dialog/SimpleDialog.java b/material/src/main/java/com/rey/material/dialog/SimpleDialog.java new file mode 100644 index 0000000..32c8422 --- /dev/null +++ b/material/src/main/java/com/rey/material/dialog/SimpleDialog.java @@ -0,0 +1,684 @@ +package com.rey.material.dialog; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Build; +import android.support.annotation.NonNull; +import android.support.v4.view.ViewCompat; +import android.text.TextUtils; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ScrollView; + +import com.rey.material.R; +import com.rey.material.drawable.BlankDrawable; +import com.rey.material.widget.CheckBox; +import com.rey.material.widget.CompoundButton; +import com.rey.material.widget.ListView; +import com.rey.material.widget.RadioButton; +import com.rey.material.widget.TextView; + +@SuppressWarnings("unused") +public class SimpleDialog extends Dialog { + + private TextView mMessage; + private InternalScrollView mScrollView; + private InternalListView mListView; + private InternalAdapter mAdapter; + + private int mMessageTextAppearanceId; + private int mMessageTextColor; + + private int mRadioButtonStyle; + private int mCheckBoxStyle; + private int mItemHeight; + private int mItemTextAppearance; + + private int mMode; + + protected static final int MODE_NONE = 0; + protected static final int MODE_MESSAGE = 1; + protected static final int MODE_ITEMS = 2; + protected static final int MODE_MULTI_ITEMS = 3; + protected static final int MODE_CUSTOM = 4; + + /** + * Interface definition for a callback to be invoked when the checked state of an item changed. + */ + public interface OnSelectionChangedListener { + /** + * Called when the checked state of an item changed. + * + * @param index The index of item. + * @param selected The checked state. + */ + void onSelectionChanged(int index, boolean selected); + } + + private OnSelectionChangedListener mOnSelectionChangedListener; + + public SimpleDialog(Context context) { + super(context, R.style.Material_App_Dialog_Simple_Light); + } + + public SimpleDialog(Context context, int style) { + super(context, style); + } + + @SuppressLint("PrivateResource") + @Override + protected void onCreate() { + messageTextAppearance(R.style.TextAppearance_AppCompat_Body1); + itemHeight(ViewGroup.LayoutParams.WRAP_CONTENT); + itemTextAppearance(R.style.TextAppearance_AppCompat_Body1); + } + + @Override + public Dialog applyStyle(int resId) { + super.applyStyle(resId); + + if (resId == 0) + return this; + + TypedArray a = getContext().obtainStyledAttributes(resId, R.styleable.SimpleDialog); + int textAppearance = 0; + int textColor = 0; + boolean textColorDefined = false; + + for (int i = 0, count = a.getIndexCount(); i < count; i++) { + int attr = a.getIndex(i); + + if (attr == R.styleable.SimpleDialog_di_messageTextAppearance) + textAppearance = a.getResourceId(attr, 0); + else if (attr == R.styleable.SimpleDialog_di_messageTextColor) { + textColor = a.getColor(attr, 0); + textColorDefined = true; + } else if (attr == R.styleable.SimpleDialog_di_radioButtonStyle) + radioButtonStyle(a.getResourceId(attr, 0)); + else if (attr == R.styleable.SimpleDialog_di_checkBoxStyle) + checkBoxStyle(a.getResourceId(attr, 0)); + else if (attr == R.styleable.SimpleDialog_di_itemHeight) + itemHeight(a.getDimensionPixelSize(attr, 0)); + else if (attr == R.styleable.SimpleDialog_di_itemTextAppearance) + itemTextAppearance(a.getResourceId(attr, 0)); + } + + a.recycle(); + + if (textAppearance != 0) + messageTextAppearance(textAppearance); + + if (textColorDefined) + messageTextColor(textColor); + + return this; + } + + @Override + public Dialog clearContent() { + super.clearContent(); + mMode = MODE_NONE; + return this; + } + + @Override + public Dialog title(CharSequence title) { + boolean titleVisible = !TextUtils.isEmpty(title); + contentMargin(mContentPadding, titleVisible ? 0 : mContentPadding, mContentPadding, 0); + return super.title(title); + } + + @Override + public Dialog contentView(View v) { + if (mScrollView == null) + initScrollView(); + + if (mScrollView.getChildAt(0) != v && v != null) { + mScrollView.removeAllViews(); + mScrollView.addView(v); + mMode = MODE_CUSTOM; + super.contentView(mScrollView); + } + + return this; + } + + private void initScrollView() { + mScrollView = new InternalScrollView(getContext()); + mScrollView.setPadding(0, 0, 0, mContentPadding - mActionPadding); + mScrollView.setClipToPadding(false); + mScrollView.setFillViewport(true); + mScrollView.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY); + ViewCompat.setLayoutDirection(mScrollView, ViewCompat.LAYOUT_DIRECTION_INHERIT); + } + + private void initMessageView() { + mMessage = new TextView(getContext()); + mMessage.setTextAppearance(mMessageTextAppearanceId); + mMessage.setTextColor(mMessageTextColor); + mMessage.setGravity(Gravity.START | Gravity.CENTER_VERTICAL); + } + + /** + * Set a message text to this SimpleDialog. + * + * @return The SimpleDialog for chaining methods. + */ + public SimpleDialog message(CharSequence message) { + if (mScrollView == null) + initScrollView(); + + if (mMessage == null) + initMessageView(); + + if (mScrollView.getChildAt(0) != mMessage) { + mScrollView.removeAllViews(); + mScrollView.addView(mMessage); + } + + mMessage.setText(message); + if (!TextUtils.isEmpty(message)) { + mMode = MODE_MESSAGE; + super.contentView(mScrollView); + } + return this; + } + + /** + * Set a message text to this SimpleDialog. + * + * @param id The resourceId of text. + * @return The SimpleDialog for chaining methods. + */ + public SimpleDialog message(int id) { + return message(id == 0 ? null : getContext().getResources().getString(id)); + } + + /** + * Sets the text color, size, style of the message view from the specified TextAppearance resource. + * + * @param resId The resourceId value. + * @return The SimpleDialog for chaining methods. + */ + public SimpleDialog messageTextAppearance(int resId) { + if (mMessageTextAppearanceId != resId) { + mMessageTextAppearanceId = resId; + if (mMessage != null) + mMessage.setTextAppearance(mMessageTextAppearanceId); + } + return this; + } + + /** + * Sets the text color of the message view. + * + * @param color The color value. + * @return The SimpleDialog for chaining methods. + */ + public SimpleDialog messageTextColor(int color) { + if (mMessageTextColor != color) { + mMessageTextColor = color; + if (mMessage != null) + mMessage.setTextColor(color); + } + return this; + } + + /** + * Sets the style of radio button. + * + * @param resId The resourceId of style. + * @return The SimpleDialog for chaining methods. + */ + public SimpleDialog radioButtonStyle(int resId) { + if (mRadioButtonStyle != resId) { + mRadioButtonStyle = resId; + if (mAdapter != null && mMode == MODE_ITEMS) + mAdapter.notifyDataSetChanged(); + } + return this; + } + + /** + * Sets the style of check box. + * + * @param resId The resourceId of style. + * @return The SimpleDialog for chaining methods. + */ + public SimpleDialog checkBoxStyle(int resId) { + if (mCheckBoxStyle != resId) { + mCheckBoxStyle = resId; + if (mAdapter != null && mMode == MODE_MULTI_ITEMS) + mAdapter.notifyDataSetChanged(); + } + return this; + } + + /** + * Sets the height of item + * + * @param height The size in pixels. + * @return The SimpleDialog for chaining methods. + */ + public SimpleDialog itemHeight(int height) { + if (mItemHeight != height) { + mItemHeight = height; + if (mAdapter != null) + mAdapter.notifyDataSetChanged(); + } + return this; + } + + /** + * Sets the text color, size, style of the item view from the specified TextAppearance resource. + * + * @param resId The resourceId value. + * @return The SimpleDialog for chaining methods. + */ + public SimpleDialog itemTextAppearance(int resId) { + if (mItemTextAppearance != resId) { + mItemTextAppearance = resId; + if (mAdapter != null) + mAdapter.notifyDataSetChanged(); + } + return this; + } + + private void initListView() { + mListView = new InternalListView(getContext()); + mListView.setDividerHeight(0); + mListView.setCacheColorHint(0x00000000); + mListView.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY); + mListView.setClipToPadding(false); + mListView.setSelector(BlankDrawable.getInstance()); + mListView.setPadding(0, 0, 0, mContentPadding - mActionPadding); + mListView.setVerticalFadingEdgeEnabled(false); + mListView.setOverScrollMode(ListView.OVER_SCROLL_NEVER); + ViewCompat.setLayoutDirection(mListView, ViewCompat.LAYOUT_DIRECTION_INHERIT); + + mAdapter = new InternalAdapter(); + mListView.setAdapter(mAdapter); + } + + /** + * Set the list of items in single-choice mode. + * + * @param items The list of items. + * @param selectedIndex The index of selected item. + * @return The SimpleDialog for chaining methods. + */ + public SimpleDialog items(CharSequence[] items, int selectedIndex) { + if (mListView == null) + initListView(); + + mMode = MODE_ITEMS; + mAdapter.setItems(items, selectedIndex); + super.contentView(mListView); + return this; + } + + /** + * Set the list of items in multi-choice mode. + * + * @param items The list of items. + * @param selectedIndexes The indexes of selected items. + * @return The SimpleDialog for chaining methods. + */ + public SimpleDialog multiChoiceItems(CharSequence[] items, int... selectedIndexes) { + if (mListView == null) + initListView(); + + mMode = MODE_MULTI_ITEMS; + mAdapter.setItems(items, selectedIndexes); + super.contentView(mListView); + return this; + } + + /** + * Set a listener will be called when the checked state of a item is changed. + * + * @param listener The {@link OnSelectionChangedListener} will be called. + * @return The SimpleDialog for chaining methods. + */ + public SimpleDialog onSelectionChangedListener(OnSelectionChangedListener listener) { + mOnSelectionChangedListener = listener; + return this; + } + + /** + * @return The list of index of all selected items. + */ + public int[] getSelectedIndexes() { + return mAdapter == null ? null : mAdapter.getSelectedIndexes(); + } + + /** + * @return The list of value of all selected items. + */ + public CharSequence[] getSelectedValues() { + return mAdapter.getSelectedValues(); + } + + /** + * @return The index of selected item. + */ + public int getSelectedIndex() { + return mAdapter == null ? -1 : mAdapter.getLastSelectedIndex(); + } + + /** + * @return The value of selected item. + */ + public CharSequence getSelectedValue() { + return mAdapter.getLastSelectedValue(); + } + + private class InternalScrollView extends ScrollView { + + private boolean mIsRtl = false; + + public InternalScrollView(Context context) { + super(context); + } + + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public void onRtlPropertiesChanged(int layoutDirection) { + boolean rtl = layoutDirection == LAYOUT_DIRECTION_RTL; + if (mIsRtl != rtl) { + mIsRtl = rtl; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + View v = getChildAt(0); + if (v != null && v == mMessage) + mMessage.setTextDirection(mIsRtl ? View.TEXT_DIRECTION_RTL : View.TEXT_DIRECTION_LTR); + } + requestLayout(); + } + } + + public boolean isLayoutRtl() { + return mIsRtl; + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + + View child = getChildAt(0); + showDivider(child != null && child.getMeasuredHeight() > getMeasuredHeight() - getPaddingTop() - getPaddingBottom()); + } + } + + private class InternalListView extends ListView { + + private boolean mIsRtl = false; + + public InternalListView(Context context) { + super(context); + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public void onRtlPropertiesChanged(int layoutDirection) { + boolean rtl = layoutDirection == LAYOUT_DIRECTION_RTL; + if (mIsRtl != rtl) { + mIsRtl = rtl; + requestLayout(); + } + } + + public boolean isLayoutRtl() { + return mIsRtl; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int heightMode = View.MeasureSpec.getMode(heightMeasureSpec); + if (heightMode == View.MeasureSpec.UNSPECIFIED) { + if (mItemHeight != ViewGroup.LayoutParams.WRAP_CONTENT) + heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(mItemHeight * getAdapter().getCount() + getPaddingTop() + getPaddingBottom(), View.MeasureSpec.EXACTLY); + } + + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + + int totalHeight = 0; + int childCount = getChildCount(); + + for (int i = 0; i < childCount; i++) + totalHeight += getChildAt(i).getMeasuredHeight(); + + showDivider(totalHeight > getMeasuredHeight() || (totalHeight == getMeasuredHeight() && getAdapter().getCount() > childCount)); + } + + } + + private class InternalAdapter extends BaseAdapter implements CompoundButton.OnCheckedChangeListener { + + private CharSequence[] mItems; + private boolean[] mSelected; + private int mLastSelectedIndex; + + public void setItems(CharSequence[] items, int... selectedIndexes) { + mItems = items; + + if (mSelected == null || mSelected.length != items.length) + mSelected = new boolean[items.length]; + + for (int i = 0; i < mSelected.length; i++) + mSelected[i] = false; + + if (selectedIndexes != null) + for (int index : selectedIndexes) + if (index >= 0 && index < mSelected.length) { + mSelected[index] = true; + mLastSelectedIndex = index; + } + + notifyDataSetChanged(); + } + + public int getLastSelectedIndex() { + return mLastSelectedIndex; + } + + public CharSequence getLastSelectedValue() { + return mItems[mLastSelectedIndex]; + } + + public int[] getSelectedIndexes() { + int count = 0; + for (boolean aMSelected : mSelected) + if (aMSelected) + count++; + + if (count == 0) + return null; + + int[] result = new int[count]; + count = 0; + for (int i = 0; i < mSelected.length; i++) + if (mSelected[i]) { + result[count] = i; + count++; + } + + return result; + } + + public CharSequence[] getSelectedValues() { + int count = 0; + for (boolean aMSelected : mSelected) + if (aMSelected) + count++; + + if (count == 0) + return null; + + CharSequence[] result = new CharSequence[count]; + count = 0; + for (int i = 0; i < mSelected.length; i++) + if (mSelected[i]) { + result[count] = mItems[i]; + count++; + } + + return result; + } + + @Override + public int getCount() { + return mItems == null ? 0 : mItems.length; + } + + @Override + public Object getItem(int position) { + return mItems == null ? 0 : mItems[position]; + } + + @Override + public long getItemId(int position) { + return 0; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + CompoundButton v = (CompoundButton) convertView; + if (v == null) { + if (mMode == MODE_MULTI_ITEMS) { + v = new CheckBox(parent.getContext()); + v.applyStyle(mCheckBoxStyle); + } else { + v = new RadioButton(parent.getContext()); + v.applyStyle(mRadioButtonStyle); + } + if (mItemHeight != ViewGroup.LayoutParams.WRAP_CONTENT) + v.setMinHeight(mItemHeight); + v.setGravity(Gravity.START | Gravity.CENTER_VERTICAL); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) + v.setTextDirection(((InternalListView) parent).isLayoutRtl() ? View.TEXT_DIRECTION_RTL : View.TEXT_DIRECTION_LTR); + v.setTextAppearance(mItemTextAppearance); + v.setCompoundDrawablePadding(mContentPadding); + } + v.setTag(position); + v.setText(mItems[position]); + if (v instanceof CheckBox) + ((CheckBox) v).setCheckedImmediately(mSelected[position]); + else + ((RadioButton) v).setCheckedImmediately(mSelected[position]); + v.setOnCheckedChangeListener(this); + return v; + } + + @Override + public void onCheckedChanged(android.widget.CompoundButton v, boolean isChecked) { + int position = (Integer) v.getTag(); + if (mSelected[position] != isChecked) { + mSelected[position] = isChecked; + if (mOnSelectionChangedListener != null) + mOnSelectionChangedListener.onSelectionChanged(position, mSelected[position]); + } + if (mMode == MODE_ITEMS && isChecked && mLastSelectedIndex != position) { + mSelected[mLastSelectedIndex] = false; + if (mOnSelectionChangedListener != null) + mOnSelectionChangedListener.onSelectionChanged(mLastSelectedIndex, false); + CompoundButton child = (CompoundButton) mListView.getChildAt(mLastSelectedIndex - mListView.getFirstVisiblePosition()); + if (child != null) + child.setChecked(false); + mLastSelectedIndex = position; + } + } + } + + public static class Builder extends Dialog.Builder { + + protected int mMode; + protected CharSequence mMessage; + protected CharSequence[] mItems; + protected int[] mSelectedIndexes; + + public Builder() { + super(R.style.Material_App_Dialog_Simple_Light); + } + + public Builder(int styleId) { + super(styleId); + } + + public Builder message(CharSequence message) { + mMode = MODE_MESSAGE; + mMessage = message; + return this; + } + + @Override + protected void onBuildDone(Dialog dialog) { + dialog.layoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + } + + public Builder items(CharSequence[] items, int selectedIndex) { + mMode = MODE_ITEMS; + mItems = items; + mSelectedIndexes = new int[]{selectedIndex}; + return this; + } + + public Builder multiChoiceItems(CharSequence[] items, int... selectedIndexes) { + mMode = MODE_MULTI_ITEMS; + mItems = items; + mSelectedIndexes = selectedIndexes; + return this; + } + + @Override + protected Dialog onBuild(Context context, int styleId) { + SimpleDialog dialog = new SimpleDialog(context, styleId); + OnSelectionChangedListener listener = getListener(dialog); + switch (mMode) { + case MODE_MESSAGE: + dialog.message(mMessage); + break; + case MODE_ITEMS: + dialog.items(mItems, mSelectedIndexes == null ? 0 : mSelectedIndexes[0]); + dialog.onSelectionChangedListener(listener); + break; + case MODE_MULTI_ITEMS: + dialog.multiChoiceItems(mItems, mSelectedIndexes); + dialog.onSelectionChangedListener(listener); + break; + } + return dialog; + } + + @NonNull + private OnSelectionChangedListener getListener(final SimpleDialog dialog) { + return new OnSelectionChangedListener() { + @Override + public void onSelectionChanged(int index, boolean selected) { + switch (mMode) { + case MODE_ITEMS: + if (selected) { + if (mSelectedIndexes == null) + mSelectedIndexes = new int[]{index}; + else + mSelectedIndexes[0] = index; + } + break; + case MODE_MULTI_ITEMS: + mSelectedIndexes = dialog.getSelectedIndexes(); + break; + } + } + }; + } + + + } +} diff --git a/material/src/main/java/com/rey/material/dialog/TimePickerDialog.java b/material/src/main/java/com/rey/material/dialog/TimePickerDialog.java new file mode 100644 index 0000000..41a0c9a --- /dev/null +++ b/material/src/main/java/com/rey/material/dialog/TimePickerDialog.java @@ -0,0 +1,663 @@ +package com.rey.material.dialog; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Configuration; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Rect; +import android.graphics.RectF; +import android.os.Build; +import android.support.annotation.NonNull; +import android.text.format.DateUtils; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; + +import com.rey.material.R; +import com.rey.material.util.ThemeUtil; +import com.rey.material.widget.CircleCheckedTextView; +import com.rey.material.widget.TimePicker; + +import java.text.DateFormat; +import java.util.Calendar; + +@SuppressWarnings("unused") +public class TimePickerDialog extends Dialog { + + private TimePickerLayout mTimePickerLayout; + private float mCornerRadius; + + /** + * Interface definition for a callback to be invoked when the selected time is changed. + */ + public interface OnTimeChangedListener { + + /** + * Called when the selected time is changed. + * + * @param oldHour The hour value of old time. + * @param oldMinute The minute value of old time. + * @param newHour The hour value of new time. + * @param newMinute The minute value of new time. + */ + void onTimeChanged(int oldHour, int oldMinute, int newHour, int newMinute); + + } + + public TimePickerDialog(Context context) { + super(context, R.style.Material_App_Dialog_TimePicker_Light); + } + + public TimePickerDialog(Context context, int style) { + super(context, style); + } + + @Override + protected void onCreate() { + mTimePickerLayout = new TimePickerLayout(getContext()); + contentView(mTimePickerLayout); + } + + @Override + public Dialog applyStyle(int resId) { + super.applyStyle(resId); + + if (resId == 0) + return this; + + mTimePickerLayout.applyStyle(resId); + layoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + return this; + } + + @Override + public Dialog layoutParams(int width, int height) { + return super.layoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + } + + @Override + public Dialog cornerRadius(float radius) { + mCornerRadius = radius; + return super.cornerRadius(radius); + } + + /** + * Set the selected hour value. + * + * @param hour The selected hour value. + * @return The TimePickerDialog for chaining methods. + */ + public TimePickerDialog hour(int hour) { + mTimePickerLayout.setHour(hour); + return this; + } + + /** + * Set the selected minute value. + * + * @param minute The selected minute value. + * @return The TimePickerDialog for chaining methods. + */ + public TimePickerDialog minute(int minute) { + mTimePickerLayout.setMinute(minute); + return this; + } + + /** + * Set a listener will be called when the selected time is changed. + * + * @param listener The {@link OnTimeChangedListener} will be called. + */ + public TimePickerDialog onTimeChangedListener(OnTimeChangedListener listener) { + mTimePickerLayout.setOnTimeChangedListener(listener); + return this; + } + + /** + * @return The selected hour value. + */ + public int getHour() { + return mTimePickerLayout.getHour(); + } + + /** + * @return The selected minute value. + */ + public int getMinute() { + return mTimePickerLayout.getMinute(); + } + + /** + * Get the formatted string of selected time. + * + * @param formatter The DateFormat used to format the time. + */ + + public String getFormattedTime(DateFormat formatter) { + return mTimePickerLayout.getFormattedTime(formatter); + } + + private class TimePickerLayout extends android.widget.FrameLayout implements View.OnClickListener, TimePicker.OnTimeChangedListener { + + private int mHeaderHeight; + private int mTextTimeColor = 0xFF000000; + private int mTextTimeSize; + private boolean mIsLeadingZero = false; + + private boolean mIsAm = true; + private int mCheckBoxSize; + + private int mHeaderRealWidth; + private int mHeaderRealHeight; + + private CircleCheckedTextView mAmView; + private CircleCheckedTextView mPmView; + private TimePicker mTimePicker; + + private Paint mPaint; + private Path mHeaderBackground; + private RectF mRect; + + private static final String TIME_DIVIDER = ":"; + private static final String BASE_TEXT = "0"; + private static final String FORMAT = "%02d"; + private static final String FORMAT_NO_LEADING_ZERO = "%d"; + + private boolean mLocationDirty = true; + private float mBaseY; + private float mHourX; + private float mDividerX; + private float mMinuteX; + private float mMiddayX; + private float mHourWidth; + private float mMinuteWidth; + private float mBaseHeight; + + private String mHour; + private String mMinute; + private String mMidday; + + private OnTimeChangedListener mOnTimeChangedListener; + + @SuppressLint("PrivateResource") + public TimePickerLayout(Context context) { + super(context); + + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mPaint.setTextAlign(Paint.Align.LEFT); + mHeaderBackground = new Path(); + mRect = new RectF(); + + mAmView = new CircleCheckedTextView(context); + mPmView = new CircleCheckedTextView(context); + mTimePicker = new TimePicker(context); + + mTimePicker.setPadding(mContentPadding, mContentPadding, mContentPadding, mContentPadding); + mTimePicker.setOnTimeChangedListener(this); + mAmView.setGravity(Gravity.CENTER); + mPmView.setGravity(Gravity.CENTER); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + mAmView.setTextAlignment(TEXT_ALIGNMENT_CENTER); + mPmView.setTextAlignment(TEXT_ALIGNMENT_CENTER); + } + mAmView.setCheckedImmediately(mIsAm); + mPmView.setCheckedImmediately(!mIsAm); + mAmView.setOnClickListener(this); + mPmView.setOnClickListener(this); + + addView(mTimePicker); + addView(mAmView); + addView(mPmView); + + setWillNotDraw(false); + + mCheckBoxSize = ThemeUtil.dpToPx(context, 48); + mHeaderHeight = ThemeUtil.dpToPx(context, 120); + mTextTimeSize = context.getResources().getDimensionPixelOffset(R.dimen.abc_text_size_headline_material); + } + + public void applyStyle(int resId) { + mTimePicker.applyStyle(resId); + + Context context = getContext(); + TypedArray a = context.obtainStyledAttributes(resId, R.styleable.TimePickerDialog); + + String am = null; + String pm = null; + + for (int i = 0, count = a.getIndexCount(); i < count; i++) { + int attr = a.getIndex(i); + + if (attr == R.styleable.TimePickerDialog_tp_headerHeight) + mHeaderHeight = a.getDimensionPixelSize(attr, 0); + else if (attr == R.styleable.TimePickerDialog_tp_textTimeColor) + mTextTimeColor = a.getColor(attr, 0); + else if (attr == R.styleable.TimePickerDialog_tp_textTimeSize) + mTextTimeSize = a.getDimensionPixelSize(attr, 0); + else if (attr == R.styleable.TimePickerDialog_tp_leadingZero) + mIsLeadingZero = a.getBoolean(attr, false); + else if (attr == R.styleable.TimePickerDialog_tp_am) + am = a.getString(attr); + else if (attr == R.styleable.TimePickerDialog_tp_pm) + pm = a.getString(attr); + } + + a.recycle(); + if (am == null) { + //noinspection deprecation + am = DateUtils.getAMPMString(Calendar.AM).toUpperCase(); + } + if (pm == null) + //noinspection deprecation + pm = DateUtils.getAMPMString(Calendar.PM).toUpperCase(); + + int[][] states = new int[][]{ + new int[]{-android.R.attr.state_checked}, + new int[]{android.R.attr.state_checked}, + }; + int[] colors = new int[]{ + mTimePicker.getTextColor(), + mTimePicker.getTextHighlightColor(), + }; + + mAmView.setBackgroundColor(mTimePicker.getSelectionColor()); + mAmView.setAnimDuration(mTimePicker.getAnimDuration()); + mAmView.setInterpolator(mTimePicker.getInInterpolator(), mTimePicker.getOutInterpolator()); + mAmView.setTypeface(mTimePicker.getTypeface()); + mAmView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTimePicker.getTextSize()); + mAmView.setTextColor(new ColorStateList(states, colors)); + mAmView.setText(am); + + mPmView.setBackgroundColor(mTimePicker.getSelectionColor()); + mPmView.setAnimDuration(mTimePicker.getAnimDuration()); + mPmView.setInterpolator(mTimePicker.getInInterpolator(), mTimePicker.getOutInterpolator()); + mPmView.setTypeface(mTimePicker.getTypeface()); + mPmView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTimePicker.getTextSize()); + mPmView.setTextColor(new ColorStateList(states, colors)); + mPmView.setText(pm); + + mPaint.setTypeface(mTimePicker.getTypeface()); + + mHour = String.format(mIsLeadingZero ? FORMAT : FORMAT_NO_LEADING_ZERO, !mTimePicker.is24Hour() && mTimePicker.getHour() == 0 ? 12 : mTimePicker.getHour()); + mMinute = String.format(FORMAT, mTimePicker.getMinute()); + + if (!mTimePicker.is24Hour()) + mMidday = mIsAm ? mAmView.getText().toString() : mPmView.getText().toString(); + + mLocationDirty = true; + invalidate(0, 0, mHeaderRealWidth, mHeaderRealHeight); + } + + public void setHour(int hour) { + if (!mTimePicker.is24Hour()) { + if (hour > 11 && hour < 24) + setAm(false, false); + else + setAm(true, false); + } + mTimePicker.setHour(hour); + } + + public int getHour() { + return mTimePicker.is24Hour() || mIsAm ? mTimePicker.getHour() : mTimePicker.getHour() + 12; + } + + public void setMinute(int minute) { + mTimePicker.setMinute(minute); + } + + public int getMinute() { + return mTimePicker.getMinute(); + } + + private void setAm(boolean am, boolean animation) { + if (mTimePicker.is24Hour()) + return; + + if (mIsAm != am) { + int oldHour = getHour(); + + mIsAm = am; + if (animation) { + mAmView.setChecked(mIsAm); + mPmView.setChecked(!mIsAm); + } else { + mAmView.setCheckedImmediately(mIsAm); + mPmView.setCheckedImmediately(!mIsAm); + } + mMidday = mIsAm ? mAmView.getText().toString() : mPmView.getText().toString(); + invalidate(0, 0, mHeaderRealWidth, mHeaderRealHeight); + + if (mOnTimeChangedListener != null) + mOnTimeChangedListener.onTimeChanged(oldHour, getMinute(), getHour(), getMinute()); + } + } + + public String getFormattedTime(DateFormat formatter) { + Calendar cal = Calendar.getInstance(); + cal.set(Calendar.HOUR_OF_DAY, getHour()); + cal.set(Calendar.MINUTE, getMinute()); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + + return formatter.format(cal.getTime()); + } + + public void setOnTimeChangedListener(OnTimeChangedListener listener) { + mOnTimeChangedListener = listener; + } + + @Override + public void onClick(View v) { + setAm(v == mAmView, true); + } + + @Override + public void onModeChanged(int mode) { + invalidate(0, 0, mHeaderRealWidth, mHeaderRealHeight); + } + + @Override + public void onHourChanged(int oldValue, int newValue) { + int oldHour = mTimePicker.is24Hour() || mIsAm ? oldValue : oldValue + 12; + + mHour = String.format(mIsLeadingZero ? FORMAT : FORMAT_NO_LEADING_ZERO, !mTimePicker.is24Hour() && newValue == 0 ? 12 : newValue); + mLocationDirty = true; + invalidate(0, 0, mHeaderRealWidth, mHeaderRealHeight); + + if (mOnTimeChangedListener != null) + mOnTimeChangedListener.onTimeChanged(oldHour, getMinute(), getHour(), getMinute()); + } + + @Override + public void onMinuteChanged(int oldValue, int newValue) { + mMinute = String.format(FORMAT, newValue); + mLocationDirty = true; + invalidate(0, 0, mHeaderRealWidth, mHeaderRealHeight); + + if (mOnTimeChangedListener != null) + mOnTimeChangedListener.onTimeChanged(getHour(), oldValue, getHour(), newValue); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + + boolean isPortrait = getContext().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT; + + int checkboxSize = mTimePicker.is24Hour() ? 0 : mCheckBoxSize; + + if (isPortrait) { + if (heightMode == MeasureSpec.AT_MOST) + heightSize = Math.min(heightSize, checkboxSize + widthSize + mHeaderHeight); + + if (checkboxSize > 0) { + int spec = MeasureSpec.makeMeasureSpec(mCheckBoxSize, MeasureSpec.EXACTLY); + mAmView.setVisibility(View.VISIBLE); + mPmView.setVisibility(View.VISIBLE); + mAmView.measure(spec, spec); + mPmView.measure(spec, spec); + } else { + mAmView.setVisibility(View.GONE); + mPmView.setVisibility(View.GONE); + } + + int spec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); + mTimePicker.measure(spec, spec); + + setMeasuredDimension(widthSize, heightSize); + } else { + int halfWidth = widthSize / 2; + + if (heightMode == MeasureSpec.AT_MOST) + heightSize = Math.min(heightSize, Math.max(checkboxSize > 0 ? checkboxSize + mHeaderHeight + mContentPadding : mHeaderHeight, halfWidth)); + + if (checkboxSize > 0) { + int spec = MeasureSpec.makeMeasureSpec(checkboxSize, MeasureSpec.EXACTLY); + mAmView.setVisibility(View.VISIBLE); + mPmView.setVisibility(View.VISIBLE); + mAmView.measure(spec, spec); + mPmView.measure(spec, spec); + } else { + mAmView.setVisibility(View.GONE); + mPmView.setVisibility(View.GONE); + } + + int spec = MeasureSpec.makeMeasureSpec(Math.min(halfWidth, heightSize), MeasureSpec.EXACTLY); + mTimePicker.measure(spec, spec); + + setMeasuredDimension(widthSize, heightSize); + } + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + boolean isPortrait = getContext().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT; + + mLocationDirty = true; + int checkboxSize = mTimePicker.is24Hour() ? 0 : mCheckBoxSize; + + if (isPortrait) { + mHeaderRealWidth = w; + mHeaderRealHeight = h - checkboxSize - w; + mHeaderBackground.reset(); + if (mCornerRadius == 0) + mHeaderBackground.addRect(0, 0, mHeaderRealWidth, mHeaderRealHeight, Path.Direction.CW); + else { + mHeaderBackground.moveTo(0, mHeaderRealHeight); + mHeaderBackground.lineTo(0, mCornerRadius); + mRect.set(0, 0, mCornerRadius * 2, mCornerRadius * 2); + mHeaderBackground.arcTo(mRect, 180f, 90f, false); + mHeaderBackground.lineTo(mHeaderRealWidth - mCornerRadius, 0); + mRect.set(mHeaderRealWidth - mCornerRadius * 2, 0, mHeaderRealWidth, mCornerRadius * 2); + mHeaderBackground.arcTo(mRect, 270f, 90f, false); + mHeaderBackground.lineTo(mHeaderRealWidth, mHeaderRealHeight); + mHeaderBackground.close(); + } + } else { + mHeaderRealWidth = w / 2; + mHeaderRealHeight = checkboxSize > 0 ? h - checkboxSize - mContentPadding : h; + mHeaderBackground.reset(); + if (mCornerRadius == 0) + mHeaderBackground.addRect(0, 0, mHeaderRealWidth, mHeaderRealHeight, Path.Direction.CW); + else { + mHeaderBackground.moveTo(0, mHeaderRealHeight); + mHeaderBackground.lineTo(0, mCornerRadius); + mRect.set(0, 0, mCornerRadius * 2, mCornerRadius * 2); + mHeaderBackground.arcTo(mRect, 180f, 90f, false); + mHeaderBackground.lineTo(mHeaderRealWidth, 0); + mHeaderBackground.lineTo(mHeaderRealWidth, mHeaderRealHeight); + mHeaderBackground.close(); + } + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int childLeft = 0; + int childTop = 0; + int childRight = right - left; + int childBottom = bottom - top; + + boolean isPortrait = getContext().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT; + int checkboxSize = mTimePicker.is24Hour() ? 0 : mCheckBoxSize; + + if (isPortrait) { + int paddingHorizontal = mContentPadding + mActionPadding; + int paddingVertical = mContentPadding - mActionPadding; + + if (checkboxSize > 0) { + mAmView.layout(childLeft + paddingHorizontal, childBottom - paddingVertical - checkboxSize, childLeft + paddingHorizontal + checkboxSize, childBottom - paddingVertical); + mPmView.layout(childRight - paddingHorizontal - checkboxSize, childBottom - paddingVertical - checkboxSize, childRight - paddingHorizontal, childBottom - paddingVertical); + } + + childTop += mHeaderRealHeight; + childBottom -= checkboxSize; + mTimePicker.layout(childLeft, childTop, childRight, childBottom); + } else { + int paddingHorizontal = (childRight / 2 - mTimePicker.getMeasuredWidth()) / 2; + int paddingVertical = (childBottom - mTimePicker.getMeasuredHeight()) / 2; + mTimePicker.layout(childRight - paddingHorizontal - mTimePicker.getMeasuredWidth(), childTop + paddingVertical, childRight - paddingHorizontal, childTop + paddingVertical + mTimePicker.getMeasuredHeight()); + + if (checkboxSize > 0) { + childRight = childRight / 2; + + paddingHorizontal = mContentPadding + mActionPadding; + paddingVertical = mContentPadding - mActionPadding; + mAmView.layout(childLeft + paddingHorizontal, childBottom - paddingVertical - checkboxSize, childLeft + paddingHorizontal + checkboxSize, childBottom - paddingVertical); + mPmView.layout(childRight - paddingHorizontal - checkboxSize, childBottom - paddingVertical - checkboxSize, childRight - paddingHorizontal, childBottom - paddingVertical); + } + } + } + + private void measureTimeText() { + if (!mLocationDirty) + return; + + mPaint.setTextSize(mTextTimeSize); + + Rect bounds = new Rect(); + mPaint.getTextBounds(BASE_TEXT, 0, BASE_TEXT.length(), bounds); + mBaseHeight = bounds.height(); + + mBaseY = (mHeaderRealHeight + mBaseHeight) / 2f; + + float dividerWidth = mPaint.measureText(TIME_DIVIDER, 0, TIME_DIVIDER.length()); + mHourWidth = mPaint.measureText(mHour, 0, mHour.length()); + mMinuteWidth = mPaint.measureText(mMinute, 0, mMinute.length()); + + mDividerX = (mHeaderRealWidth - dividerWidth) / 2f; + mHourX = mDividerX - mHourWidth; + mMinuteX = mDividerX + dividerWidth; + mMiddayX = mMinuteX + mMinuteWidth; + + mLocationDirty = false; + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + + mPaint.setStyle(Paint.Style.FILL); + mPaint.setColor(mTimePicker.getSelectionColor()); + canvas.drawPath(mHeaderBackground, mPaint); + + measureTimeText(); + + mPaint.setTextSize(mTextTimeSize); + mPaint.setColor(mTimePicker.getMode() == TimePicker.MODE_HOUR ? mTimePicker.getTextHighlightColor() : mTextTimeColor); + canvas.drawText(mHour, mHourX, mBaseY, mPaint); + + mPaint.setColor(mTextTimeColor); + canvas.drawText(TIME_DIVIDER, mDividerX, mBaseY, mPaint); + + mPaint.setColor(mTimePicker.getMode() == TimePicker.MODE_MINUTE ? mTimePicker.getTextHighlightColor() : mTextTimeColor); + canvas.drawText(mMinute, mMinuteX, mBaseY, mPaint); + + if (!mTimePicker.is24Hour()) { + mPaint.setTextSize(mTimePicker.getTextSize()); + mPaint.setColor(mTextTimeColor); + canvas.drawText(mMidday, mMiddayX, mBaseY, mPaint); + } + } + + private boolean isTouched(float left, float top, float right, float bottom, float x, float y) { + return x >= left && x <= right && y >= top && y <= bottom; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + boolean handled = super.onTouchEvent(event); + + if (handled) + return true; + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + if (isTouched(mHourX, mBaseY - mBaseHeight, mHourX + mHourWidth, mBaseY, event.getX(), event.getY())) + return mTimePicker.getMode() == TimePicker.MODE_MINUTE; + + if (isTouched(mMinuteX, mBaseY - mBaseHeight, mMinuteX + mMinuteWidth, mBaseY, event.getX(), event.getY())) + return mTimePicker.getMode() == TimePicker.MODE_HOUR; + break; + case MotionEvent.ACTION_UP: + if (isTouched(mHourX, mBaseY - mBaseHeight, mHourX + mHourWidth, mBaseY, event.getX(), event.getY())) + mTimePicker.setMode(TimePicker.MODE_HOUR, true); + + if (isTouched(mMinuteX, mBaseY - mBaseHeight, mMinuteX + mMinuteWidth, mBaseY, event.getX(), event.getY())) + mTimePicker.setMode(TimePicker.MODE_MINUTE, true); + break; + } + + return false; + } + } + + public static class Builder extends Dialog.Builder { + + protected int mHour; + protected int mMinute; + + public Builder() { + super(R.style.Material_App_Dialog_TimePicker_Light); + Calendar cal = Calendar.getInstance(); + mHour = cal.get(Calendar.HOUR_OF_DAY); + mMinute = cal.get(Calendar.MINUTE); + } + + public Builder(int hourOfDay, int minute) { + this(R.style.Material_App_Dialog_TimePicker_Light, hourOfDay, minute); + } + + public Builder(int styleId, int hourOfDay, int minute) { + super(styleId); + hour(hourOfDay); + minute(minute); + } + + public Builder hour(int hour) { + mHour = Math.min(Math.max(hour, 0), 24); + return this; + } + + public Builder minute(int minute) { + mMinute = minute; + return this; + } + + @Override + public Builder contentView(int layoutId) { + return this; + } + + @Override + protected Dialog onBuild(Context context, int styleId) { + TimePickerDialog dialog = new TimePickerDialog(context, styleId); + + OnTimeChangedListener listener = getOnTimeChangedListener(); + + dialog.hour(mHour) + .minute(mMinute) + .onTimeChangedListener(listener); + return dialog; + } + + @NonNull + private OnTimeChangedListener getOnTimeChangedListener() { + return new OnTimeChangedListener() { + @Override + public void onTimeChanged(int oldHour, int oldMinute, int newHour, int newMinute) { + hour(newHour).minute(newMinute); + } + }; + } + } +} diff --git a/material/src/main/java/com/rey/material/drawable/ArrowDrawable.java b/material/src/main/java/com/rey/material/drawable/ArrowDrawable.java new file mode 100644 index 0000000..4df7649 --- /dev/null +++ b/material/src/main/java/com/rey/material/drawable/ArrowDrawable.java @@ -0,0 +1,226 @@ +package com.rey.material.drawable; + +import android.content.res.ColorStateList; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; +import android.os.SystemClock; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + +import com.rey.material.util.ViewUtil; + +public class ArrowDrawable extends Drawable implements Animatable{ + + private boolean mRunning = false; + private long mStartTime; + private float mAnimProgress; + private int mAnimDuration; + + private Paint mPaint; + private ColorStateList mColorStateList; + private int mSize; + private int mCurColor; + private int mMode; + private Interpolator mInterpolator; + + private Path mPath; + + public static int MODE_DOWN = 0; + public static int MODE_UP = 1; + + private boolean mClockwise = true; + + public ArrowDrawable(int mode, int size, ColorStateList colorStateList, int animDuration, Interpolator interpolator, boolean clockwise){ + mSize = size; + mAnimDuration = animDuration; + mMode = mode; + mInterpolator = interpolator; + if(mInterpolator == null) + mInterpolator = new DecelerateInterpolator(); + mClockwise = clockwise; + + mPaint = new Paint(); + mPaint.setAntiAlias(true); + mPaint.setStyle(Paint.Style.FILL); + + mPath = new Path(); + + setColor(colorStateList); + } + + public void setColor(ColorStateList colorStateList){ + mColorStateList = colorStateList; + onStateChange(getState()); + } + + public void setAnimationDuration(int duration){ + mAnimDuration = duration; + } + + public void setInterpolator(Interpolator interpolator){ + mInterpolator = interpolator; + } + + public void setClockwise(boolean clockwise){ + mClockwise = clockwise; + } + + public void setArrowSize(int size){ + if(mSize != size){ + mSize = size; + invalidateSelf(); + } + } + + public void setMode(int mode, boolean animation){ + if(mMode != mode){ + mMode = mode; + if(animation && mAnimDuration > 0) + start(); + else + invalidateSelf(); + } + } + + public int getMode(){ + return mMode; + } + + @Override + protected void onBoundsChange(Rect bounds) { + float x = bounds.exactCenterX(); + float y = bounds.exactCenterY(); + + mPath.reset(); + mPath.moveTo(x, y + mSize / 2f); + mPath.lineTo(x - mSize, y - mSize / 2f); + mPath.lineTo(x + mSize, y - mSize / 2f); + mPath.close(); + } + + @Override + public void draw(Canvas canvas) { + int saveCount = canvas.save(); + Rect bounds = getBounds(); + + if(!isRunning()){ + if(mMode == MODE_UP) + canvas.rotate(180, bounds.exactCenterX(), bounds.exactCenterY()); + } + else{ + float value = mInterpolator.getInterpolation(mAnimProgress); + float degree; + + if(mClockwise){ + if(mMode == MODE_UP) // move down > up + degree = 180 * value; + else // move up > down + degree = 180 * (1 + value); + } + else{ + if(mMode == MODE_UP) // move down > up + degree = -180 * value; + else // move up > down + degree = -180 * (1 + value); + } + + canvas.rotate(degree, bounds.exactCenterX(), bounds.exactCenterY()); + } + + mPaint.setColor(mCurColor); + canvas.drawPath(mPath, mPaint); + + canvas.restoreToCount(saveCount); + } + + @Override + public void setAlpha(int alpha) { + mPaint.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter cf) { + mPaint.setColorFilter(cf); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + @Override + public boolean isStateful() { + return true; + } + + @Override + protected boolean onStateChange(int[] state) { + int color = mColorStateList.getColorForState(state, mCurColor); + + if(mCurColor != color){ + mCurColor = color; + return true; + } + + return false; + } + + private void resetAnimation(){ + mStartTime = SystemClock.uptimeMillis(); + mAnimProgress = 0f; + } + + @Override + public void start() { + resetAnimation(); + scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + invalidateSelf(); + } + + @Override + public void stop() { + mRunning = false; + unscheduleSelf(mUpdater); + invalidateSelf(); + } + + @Override + public boolean isRunning() { + return mRunning; + } + + @Override + public void scheduleSelf(Runnable what, long when) { + mRunning = true; + super.scheduleSelf(what, when); + } + + private final Runnable mUpdater = new Runnable() { + + @Override + public void run() { + update(); + } + + }; + + private void update(){ + long curTime = SystemClock.uptimeMillis(); + mAnimProgress = Math.min(1f, (float)(curTime - mStartTime) / mAnimDuration); + + if(mAnimProgress == 1f) + mRunning = false; + + if(isRunning()) + scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + + invalidateSelf(); + } + +} diff --git a/material/src/main/java/com/rey/material/drawable/BlankDrawable.java b/material/src/main/java/com/rey/material/drawable/BlankDrawable.java new file mode 100644 index 0000000..a2d4ecb --- /dev/null +++ b/material/src/main/java/com/rey/material/drawable/BlankDrawable.java @@ -0,0 +1,40 @@ +package com.rey.material.drawable; + +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.PixelFormat; +import android.graphics.drawable.Drawable; + +/** + * A drawable that draw nothing. + * @author Rey + */ +public class BlankDrawable extends Drawable { + + private static BlankDrawable mInstance; + + public static BlankDrawable getInstance(){ + if(mInstance == null) + synchronized (BlankDrawable.class) { + if(mInstance == null) + mInstance = new BlankDrawable(); + } + + return mInstance; + } + + @Override + public void draw(Canvas canvas) {} + + @Override + public void setAlpha(int alpha) {} + + @Override + public void setColorFilter(ColorFilter cf) {} + + @Override + public int getOpacity() { + return PixelFormat.TRANSPARENT; + } + +} diff --git a/material/src/main/java/com/rey/material/drawable/CheckBoxDrawable.java b/material/src/main/java/com/rey/material/drawable/CheckBoxDrawable.java new file mode 100644 index 0000000..4c7292a --- /dev/null +++ b/material/src/main/java/com/rey/material/drawable/CheckBoxDrawable.java @@ -0,0 +1,451 @@ +package com.rey.material.drawable; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; +import android.os.SystemClock; +import android.util.AttributeSet; + +import com.rey.material.R; +import com.rey.material.util.ColorUtil; +import com.rey.material.util.ThemeUtil; +import com.rey.material.util.ViewUtil; + +public class CheckBoxDrawable extends Drawable implements Animatable { + + private boolean mRunning = false; + + private Paint mPaint; + + private long mStartTime; + private float mAnimProgress; + private int mAnimDuration; + private int mStrokeSize; + private int mWidth; + private int mHeight; + private int mCornerRadius; + private int mBoxSize; + private int mTickColor; + private int mPrevColor; + private int mCurColor; + private ColorStateList mStrokeColor; + private RectF mBoxRect; + private Path mTickPath; + private float mTickPathProgress = -1f; + private boolean mChecked = false; + + private boolean mInEditMode = false; + private boolean mAnimEnable = true; + + private static final float[] TICK_DATA = new float[]{0f, 0.473f, 0.367f, 0.839f, 1f, 0.207f}; + private static final float FILL_TIME = 0.4f; + + private CheckBoxDrawable(int width, int height, int boxSize, int cornerRadius, int strokeSize, ColorStateList strokeColor, int tickColor, int animDuration){ + mWidth = width; + mHeight = height; + mBoxSize = boxSize; + mCornerRadius = cornerRadius; + mStrokeSize = strokeSize; + mStrokeColor = strokeColor; + mTickColor = tickColor; + mAnimDuration = animDuration; + + mPaint = new Paint(); + mPaint.setAntiAlias(true); + + mBoxRect = new RectF(); + mTickPath = new Path(); + } + + public void setInEditMode(boolean b){ + mInEditMode = b; + } + + public void setAnimEnable(boolean b){ + mAnimEnable = b; + } + + public boolean isAnimEnable(){ + return mAnimEnable; + } + + @Override + public int getIntrinsicWidth() { + return mWidth; + } + + @Override + public int getIntrinsicHeight() { + return mHeight; + } + + @Override + public int getMinimumWidth() { + return mWidth; + } + + @Override + public int getMinimumHeight() { + return mHeight; + } + + @Override + public boolean isStateful() { + return true; + } + + @Override + protected void onBoundsChange(Rect bounds) { + mBoxRect.set(bounds.exactCenterX() - mBoxSize / 2, bounds.exactCenterY() - mBoxSize / 2, bounds.exactCenterX() + mBoxSize / 2, bounds.exactCenterY() + mBoxSize / 2); + } + + @Override + public void draw(Canvas canvas) { + if(mChecked) + drawChecked(canvas); + else + drawUnchecked(canvas); + } + + private Path getTickPath(Path path, float x, float y, float size, float progress, boolean in){ + if(mTickPathProgress == progress) + return path; + + mTickPathProgress = progress; + + float x1 = x + size * TICK_DATA[0]; + float y1 = y + size * TICK_DATA[1]; + float x2 = x + size * TICK_DATA[2]; + float y2 = y + size * TICK_DATA[3]; + float x3 = x + size * TICK_DATA[4]; + float y3 = y + size * TICK_DATA[5]; + + float d1 = (float)Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2)); + float d2 = (float)Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2)); + float midProgress = d1 / (d1 + d2); + + path.reset(); + + if(in){ + path.moveTo(x1, y1); + + if(progress < midProgress){ + progress = progress / midProgress; + path.lineTo(x1 * (1 - progress) + x2 * progress, y1 * (1 - progress) + y2 * progress); + } + else{ + progress = (progress - midProgress) / (1f - midProgress); + path.lineTo(x2, y2); + path.lineTo(x2 * (1 - progress) + x3 * progress, y2 * (1 - progress) + y3 * progress); + } + } + else{ + path.moveTo(x3, y3); + + if(progress < midProgress){ + progress = progress / midProgress; + path.lineTo(x2, y2); + path.lineTo(x1 * (1 - progress) + x2 * progress, y1 * (1 - progress) + y2 * progress); + } + else{ + progress = (progress - midProgress) / (1f - midProgress); + path.lineTo(x2 * (1 - progress) + x3 * progress, y2 * (1 - progress) + y3 * progress); + } + } + + return path; + } + + private void drawChecked(Canvas canvas){ + float size = mBoxSize - mStrokeSize * 2; + float x = mBoxRect.left + mStrokeSize; + float y = mBoxRect.top + mStrokeSize; + + if(isRunning()){ + if(mAnimProgress < FILL_TIME){ + float progress = mAnimProgress / FILL_TIME; + float fillWidth = (mBoxSize - mStrokeSize) / 2f * progress; + float padding = mStrokeSize / 2f + fillWidth / 2f - 0.5f; + + mPaint.setColor(ColorUtil.getMiddleColor(mPrevColor, mCurColor, progress)); + mPaint.setStrokeWidth(fillWidth); + mPaint.setStyle(Paint.Style.STROKE); + canvas.drawRect(mBoxRect.left + padding, mBoxRect.top + padding, mBoxRect.right - padding, mBoxRect.bottom - padding, mPaint); + + mPaint.setStrokeWidth(mStrokeSize); + canvas.drawRoundRect(mBoxRect, mCornerRadius, mCornerRadius, mPaint); + } + else{ + float progress = (mAnimProgress - FILL_TIME) / (1f - FILL_TIME); + + mPaint.setColor(mCurColor); + mPaint.setStrokeWidth(mStrokeSize); + mPaint.setStyle(Paint.Style.FILL_AND_STROKE); + canvas.drawRoundRect(mBoxRect, mCornerRadius, mCornerRadius, mPaint); + + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setStrokeJoin(Paint.Join.MITER); + mPaint.setStrokeCap(Paint.Cap.BUTT); + mPaint.setColor(mTickColor); + + canvas.drawPath(getTickPath(mTickPath, x, y, size, progress, true), mPaint); + } + } + else{ + mPaint.setColor(mCurColor); + mPaint.setStrokeWidth(mStrokeSize); + mPaint.setStyle(Paint.Style.FILL_AND_STROKE); + canvas.drawRoundRect(mBoxRect, mCornerRadius, mCornerRadius, mPaint); + + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setStrokeJoin(Paint.Join.MITER); + mPaint.setStrokeCap(Paint.Cap.BUTT); + mPaint.setColor(mTickColor); + + canvas.drawPath(getTickPath(mTickPath, x, y, size, 1f, true), mPaint); + } + } + + private void drawUnchecked(Canvas canvas){ + if(isRunning()){ + if(mAnimProgress < 1f - FILL_TIME){ + float size = mBoxSize - mStrokeSize * 2; + float x = mBoxRect.left + mStrokeSize; + float y = mBoxRect.top + mStrokeSize; + float progress = mAnimProgress / (1f -FILL_TIME); + + mPaint.setColor(mPrevColor); + mPaint.setStrokeWidth(mStrokeSize); + mPaint.setStyle(Paint.Style.FILL_AND_STROKE); + canvas.drawRoundRect(mBoxRect, mCornerRadius, mCornerRadius, mPaint); + + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setStrokeJoin(Paint.Join.MITER); + mPaint.setStrokeCap(Paint.Cap.BUTT); + mPaint.setColor(mTickColor); + + canvas.drawPath(getTickPath(mTickPath, x, y, size, progress, false), mPaint); + } + else{ + float progress = (mAnimProgress + FILL_TIME - 1f) / FILL_TIME; + float fillWidth = (mBoxSize - mStrokeSize) / 2f * (1f - progress); + float padding = mStrokeSize / 2f + fillWidth / 2f - 0.5f; + + mPaint.setColor(ColorUtil.getMiddleColor(mPrevColor, mCurColor, progress)); + mPaint.setStrokeWidth(fillWidth); + mPaint.setStyle(Paint.Style.STROKE); + canvas.drawRect(mBoxRect.left + padding, mBoxRect.top + padding, mBoxRect.right - padding, mBoxRect.bottom - padding, mPaint); + + mPaint.setStrokeWidth(mStrokeSize); + canvas.drawRoundRect(mBoxRect, mCornerRadius, mCornerRadius, mPaint); + } + } + else{ + mPaint.setColor(mCurColor); + mPaint.setStrokeWidth(mStrokeSize); + mPaint.setStyle(Paint.Style.STROKE); + canvas.drawRoundRect(mBoxRect, mCornerRadius, mCornerRadius, mPaint); + } + } + + @Override + protected boolean onStateChange(int[] state) { + boolean checked = ViewUtil.hasState(state, android.R.attr.state_checked); + int color = mStrokeColor.getColorForState(state, mCurColor); + boolean needRedraw = false; + + if(mChecked != checked){ + mChecked = checked; + needRedraw = true; + if(!mInEditMode && mAnimEnable) + start(); + } + + if(mCurColor != color){ + mPrevColor = isRunning() ? mCurColor : color; + mCurColor = color; + needRedraw = true; + } + else if(!isRunning()) + mPrevColor = color; + + return needRedraw; + } + + @Override + public void setAlpha(int alpha) { + mPaint.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter cf) { + mPaint.setColorFilter(cf); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + //Animation: based on http://cyrilmottier.com/2012/11/27/actionbar-on-the-move/ + + private void resetAnimation(){ + mStartTime = SystemClock.uptimeMillis(); + mAnimProgress = 0f; + } + + @Override + public void start() { + resetAnimation(); + + scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + invalidateSelf(); + } + + @Override + public void stop() { + mRunning = false; + unscheduleSelf(mUpdater); + invalidateSelf(); + } + + @Override + public boolean isRunning() { + return mRunning; + } + + @Override + public void scheduleSelf(Runnable what, long when) { + mRunning = true; + super.scheduleSelf(what, when); + } + + private final Runnable mUpdater = new Runnable() { + + @Override + public void run() { + update(); + } + + }; + + private void update(){ + long curTime = SystemClock.uptimeMillis(); + mAnimProgress = Math.min(1f, (float)(curTime - mStartTime) / mAnimDuration); + + if(mAnimProgress == 1f) + mRunning = false; + + if(isRunning()) + scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + + invalidateSelf(); + } + + public static class Builder{ + + private int mAnimDuration = 400; + private int mStrokeSize = 4; + private int mWidth = 64; + private int mHeight = 64; + private ColorStateList mStrokeColor; + private int mCornerRadius = 8; + private int mBoxSize = 32; + private int mTickColor = 0xFFFFFFFF; + + public Builder(){} + + public Builder(Context context, int defStyleRes){ + this(context, null, 0, defStyleRes); + } + + public Builder(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CheckBoxDrawable, defStyleAttr, defStyleRes); + + width(a.getDimensionPixelSize(R.styleable.CheckBoxDrawable_cbd_width, ThemeUtil.dpToPx(context, 32))); + height(a.getDimensionPixelSize(R.styleable.CheckBoxDrawable_cbd_height, ThemeUtil.dpToPx(context, 32))); + boxSize(a.getDimensionPixelSize(R.styleable.CheckBoxDrawable_cbd_boxSize, ThemeUtil.dpToPx(context, 18))); + cornerRadius(a.getDimensionPixelSize(R.styleable.CheckBoxDrawable_cbd_cornerRadius, ThemeUtil.dpToPx(context, 2))); + strokeSize(a.getDimensionPixelSize(R.styleable.CheckBoxDrawable_cbd_strokeSize, ThemeUtil.dpToPx(context, 2))); + strokeColor(a.getColorStateList(R.styleable.CheckBoxDrawable_cbd_strokeColor)); + tickColor(a.getColor(R.styleable.CheckBoxDrawable_cbd_tickColor, 0xFFFFFFFF)); + animDuration(a.getInt(R.styleable.CheckBoxDrawable_cbd_animDuration, context.getResources().getInteger(android.R.integer.config_mediumAnimTime))); + + a.recycle(); + + if(mStrokeColor == null){ + int[][] states = new int[][]{ + new int[]{-android.R.attr.state_checked}, + new int[]{android.R.attr.state_checked}, + }; + int[] colors = new int[]{ + ThemeUtil.colorControlNormal(context, 0xFF000000), + ThemeUtil.colorControlActivated(context, 0xFF000000), + }; + strokeColor(new ColorStateList(states, colors)); + } + } + + public CheckBoxDrawable build(){ + if(mStrokeColor == null) + mStrokeColor = ColorStateList.valueOf(0xFF000000); + + return new CheckBoxDrawable(mWidth, mHeight, mBoxSize, mCornerRadius, mStrokeSize, mStrokeColor, mTickColor, mAnimDuration); + } + + public Builder width(int width){ + mWidth = width; + return this; + } + + public Builder height(int height){ + mHeight = height; + return this; + } + + public Builder strokeSize(int size){ + mStrokeSize = size; + return this; + } + + public Builder strokeColor(int color){ + mStrokeColor = ColorStateList.valueOf(color); + return this; + } + + public Builder strokeColor(ColorStateList color){ + mStrokeColor = color; + return this; + } + + public Builder tickColor(int color){ + mTickColor = color; + return this; + } + + public Builder cornerRadius(int radius){ + mCornerRadius = radius; + return this; + } + + public Builder boxSize(int size){ + mBoxSize = size; + return this; + } + + public Builder animDuration(int duration){ + mAnimDuration = duration; + return this; + } + } + +} \ No newline at end of file diff --git a/material/src/main/java/com/rey/material/drawable/CircleDrawable.java b/material/src/main/java/com/rey/material/drawable/CircleDrawable.java new file mode 100644 index 0000000..5e34534 --- /dev/null +++ b/material/src/main/java/com/rey/material/drawable/CircleDrawable.java @@ -0,0 +1,166 @@ +package com.rey.material.drawable; + +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; +import android.os.SystemClock; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + +import com.rey.material.util.ViewUtil; + +public class CircleDrawable extends Drawable implements Animatable { + + private boolean mRunning = false; + private long mStartTime; + private float mAnimProgress; + private int mAnimDuration = 1000; + private Interpolator mInInterpolator = new DecelerateInterpolator(); + private Interpolator mOutInterpolator = new DecelerateInterpolator(); + + private Paint mPaint; + + private float mX; + private float mY; + private float mRadius; + + private boolean mVisible; + private boolean mInEditMode = false; + private boolean mAnimEnable = true; + + public CircleDrawable() { + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mPaint.setStyle(Paint.Style.FILL); + } + + public void setInEditMode(boolean b) { + mInEditMode = b; + } + + public void setAnimEnable(boolean b) { + mAnimEnable = b; + } + + public void setColor(int color) { + mPaint.setColor(color); + invalidateSelf(); + } + + public void setAnimDuration(int duration) { + mAnimDuration = duration; + } + + public void setInterpolator(Interpolator in, Interpolator out) { + mInInterpolator = in; + mOutInterpolator = out; + } + + @Override + public boolean isStateful() { + return true; + } + + @Override + protected boolean onStateChange(int[] state) { + boolean visible = ViewUtil.hasState(state, android.R.attr.state_checked) || ViewUtil.hasState(state, android.R.attr.state_pressed); + + if (mVisible != visible) { + mVisible = visible; + if (!mInEditMode && mAnimEnable) + start(); + return true; + } + + return false; + } + + @Override + protected void onBoundsChange(Rect bounds) { + mX = bounds.exactCenterX(); + mY = bounds.exactCenterY(); + mRadius = Math.min(bounds.width(), bounds.height()) / 2f; + } + + @Override + public void draw(Canvas canvas) { + if (!mRunning) { + if (mVisible) + canvas.drawCircle(mX, mY, mRadius, mPaint); + } else { + float radius = mVisible ? mInInterpolator.getInterpolation(mAnimProgress) * mRadius : (1f - mOutInterpolator.getInterpolation(mAnimProgress)) * mRadius; + canvas.drawCircle(mX, mY, radius, mPaint); + } + } + + @Override + public void setAlpha(int alpha) { + mPaint.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter cf) { + mPaint.setColorFilter(cf); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + private void resetAnimation() { + mStartTime = SystemClock.uptimeMillis(); + mAnimProgress = 0f; + } + + @Override + public void start() { + resetAnimation(); + scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + invalidateSelf(); + } + + @Override + public void stop() { + mRunning = false; + unscheduleSelf(mUpdater); + invalidateSelf(); + } + + @Override + public boolean isRunning() { + return mRunning; + } + + @Override + public void scheduleSelf(Runnable what, long when) { + mRunning = true; + super.scheduleSelf(what, when); + } + + private final Runnable mUpdater = new Runnable() { + + @Override + public void run() { + update(); + } + + }; + + private void update() { + long curTime = SystemClock.uptimeMillis(); + mAnimProgress = Math.min(1f, (float) (curTime - mStartTime) / mAnimDuration); + + if (mAnimProgress == 1f) + mRunning = false; + + if (isRunning()) + scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + + invalidateSelf(); + } + +} diff --git a/material/src/main/java/com/rey/material/drawable/CircularProgressDrawable.java b/material/src/main/java/com/rey/material/drawable/CircularProgressDrawable.java new file mode 100644 index 0000000..a7d37cc --- /dev/null +++ b/material/src/main/java/com/rey/material/drawable/CircularProgressDrawable.java @@ -0,0 +1,786 @@ +package com.rey.material.drawable; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; +import android.os.SystemClock; +import android.util.AttributeSet; +import android.view.animation.AnimationUtils; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + +import com.rey.material.R; +import com.rey.material.util.ColorUtil; +import com.rey.material.util.ThemeUtil; +import com.rey.material.util.ViewUtil; +import com.rey.material.widget.ProgressView; + +public class CircularProgressDrawable extends Drawable implements Animatable { + + private long mLastUpdateTime; + private long mLastProgressStateTime; + private long mLastRunStateTime; + + private int mProgressState; + + private static final int PROGRESS_STATE_HIDE = -1; + private static final int PROGRESS_STATE_STRETCH = 0; + private static final int PROGRESS_STATE_KEEP_STRETCH = 1; + private static final int PROGRESS_STATE_SHRINK = 2; + private static final int PROGRESS_STATE_KEEP_SHRINK = 3; + + private int mRunState = RUN_STATE_STOPPED; + + private static final int RUN_STATE_STOPPED = 0; + private static final int RUN_STATE_STARTING = 1; + private static final int RUN_STATE_STARTED = 2; + private static final int RUN_STATE_RUNNING = 3; + private static final int RUN_STATE_STOPPING = 4; + + private Paint mPaint; + private RectF mRect; + private float mStartAngle; + private float mSweepAngle; + private int mStrokeColorIndex; + + private int mPadding; + private float mInitialAngle; + private float mProgressPercent; + private float mSecondaryProgressPercent; + private float mMaxSweepAngle; + private float mMinSweepAngle; + private int mStrokeSize; + private int[] mStrokeColors; + private int mStrokeSecondaryColor; + private boolean mReverse; + private int mRotateDuration; + private int mTransformDuration; + private int mKeepDuration; + private float mInStepPercent; + private int[] mInColors; + private int mInAnimationDuration; + private int mOutAnimationDuration; + private int mProgressMode; + private Interpolator mTransformInterpolator; + + private CircularProgressDrawable(int padding, float initialAngle, float progressPercent, float secondaryProgressPercent, float maxSweepAngle, float minSweepAngle, int strokeSize, int[] strokeColors, int strokeSecondaryColor, boolean reverse, int rotateDuration, int transformDuration, int keepDuration, Interpolator transformInterpolator, int progressMode, int inAnimDuration, float inStepPercent, int[] inStepColors, int outAnimDuration){ + mPadding = padding; + mInitialAngle = initialAngle; + setProgress(progressPercent); + setSecondaryProgress(secondaryProgressPercent); + mMaxSweepAngle = maxSweepAngle; + mMinSweepAngle = minSweepAngle; + mStrokeSize = strokeSize; + mStrokeColors = strokeColors; + mStrokeSecondaryColor = strokeSecondaryColor; + mReverse = reverse; + mRotateDuration = rotateDuration; + mTransformDuration = transformDuration; + mKeepDuration = keepDuration; + mTransformInterpolator = transformInterpolator; + mProgressMode = progressMode; + mInAnimationDuration = inAnimDuration; + mInStepPercent = inStepPercent; + mInColors = inStepColors; + mOutAnimationDuration = outAnimDuration; + + mPaint = new Paint(); + mPaint.setAntiAlias(true); + mPaint.setStrokeCap(Paint.Cap.ROUND); + mPaint.setStrokeJoin(Paint.Join.ROUND); + + mRect = new RectF(); + } + + public void applyStyle(Context context, int resId){ + TypedArray a = context.obtainStyledAttributes(resId, R.styleable.CircularProgressDrawable); + + int strokeColor = 0; + boolean strokeColorDefined = false; + int[] strokeColors = null; + + for(int i = 0, count = a.getIndexCount(); i < count; i++){ + int attr = a.getIndex(i); + + if(attr == R.styleable.CircularProgressDrawable_cpd_padding) + mPadding = a.getDimensionPixelSize(attr, 0); + else if(attr == R.styleable.CircularProgressDrawable_cpd_initialAngle) + mInitialAngle = a.getInteger(attr, 0); + else if(attr == R.styleable.CircularProgressDrawable_pv_progress) + setProgress(a.getFloat(attr, 0)); + else if(attr == R.styleable.CircularProgressDrawable_pv_secondaryProgress) + setSecondaryProgress(a.getFloat(attr, 0)); + else if(attr == R.styleable.CircularProgressDrawable_cpd_maxSweepAngle) + mMaxSweepAngle = a.getInteger(attr, 0); + else if(attr == R.styleable.CircularProgressDrawable_cpd_minSweepAngle) + mMinSweepAngle = a.getInteger(attr, 0); + else if(attr == R.styleable.CircularProgressDrawable_cpd_strokeSize) + mStrokeSize = a.getDimensionPixelSize(attr, 0); + else if(attr == R.styleable.CircularProgressDrawable_cpd_strokeColor) { + strokeColor = a.getColor(attr, 0); + strokeColorDefined = true; + } + else if(attr == R.styleable.CircularProgressDrawable_cpd_strokeColors){ + TypedArray ta = context.getResources().obtainTypedArray(a.getResourceId(attr, 0)); + strokeColors = new int[ta.length()]; + for(int j = 0; j < ta.length(); j++) + strokeColors[j] = ta.getColor(j, 0); + ta.recycle(); + } + else if(attr == R.styleable.CircularProgressDrawable_cpd_strokeSecondaryColor) + mStrokeSecondaryColor = a.getColor(attr, 0); + else if(attr == R.styleable.CircularProgressDrawable_cpd_reverse) + mReverse = a.getBoolean(attr, false); + else if(attr == R.styleable.CircularProgressDrawable_cpd_rotateDuration) + mRotateDuration = a.getInteger(attr, 0); + else if(attr == R.styleable.CircularProgressDrawable_cpd_transformDuration) + mTransformDuration = a.getInteger(attr, 0); + else if(attr == R.styleable.CircularProgressDrawable_cpd_keepDuration) + mKeepDuration = a.getInteger(attr, 0); + else if(attr == R.styleable.CircularProgressDrawable_cpd_transformInterpolator) + mTransformInterpolator = AnimationUtils.loadInterpolator(context, a.getResourceId(attr, 0)); + else if(attr == R.styleable.CircularProgressDrawable_pv_progressMode) + mProgressMode = a.getInteger(attr, 0); + else if(attr == R.styleable.CircularProgressDrawable_cpd_inAnimDuration) + mInAnimationDuration = a.getInteger(attr, 0); + else if(attr == R.styleable.CircularProgressDrawable_cpd_inStepColors){ + TypedArray ta = context.getResources().obtainTypedArray(a.getResourceId(attr, 0)); + mInColors = new int[ta.length()]; + for(int j = 0; j < ta.length(); j++) + mInColors[j] = ta.getColor(j, 0); + ta.recycle(); + } + else if(attr == R.styleable.CircularProgressDrawable_cpd_inStepPercent) + mInStepPercent = a.getFloat(attr, 0); + else if(attr == R.styleable.CircularProgressDrawable_cpd_outAnimDuration) + mOutAnimationDuration = a.getInteger(attr, 0); + } + + a.recycle(); + + if(strokeColors != null) + mStrokeColors = strokeColors; + else if(strokeColorDefined) + mStrokeColors = new int[]{strokeColor}; + + if(mStrokeColorIndex >= mStrokeColors.length) + mStrokeColorIndex = 0; + + invalidateSelf(); + } + + @Override + public void draw(Canvas canvas) { + switch (mProgressMode) { + case ProgressView.MODE_DETERMINATE: + drawDeterminate(canvas); + break; + case ProgressView.MODE_INDETERMINATE: + drawIndeterminate(canvas); + break; + } + } + + private void drawDeterminate(Canvas canvas){ + Rect bounds = getBounds(); + float radius = 0f; + float size = 0f; + + if(mRunState == RUN_STATE_STARTING){ + size = (float)mStrokeSize * Math.min(mInAnimationDuration, (SystemClock.uptimeMillis() - mLastRunStateTime)) / mInAnimationDuration; + if(size > 0) + radius = (Math.min(bounds.width(), bounds.height()) - mPadding * 2 - mStrokeSize * 2 + size) / 2f; + } + else if(mRunState == RUN_STATE_STOPPING){ + size = (float)mStrokeSize * Math.max(0, (mOutAnimationDuration - SystemClock.uptimeMillis() + mLastRunStateTime)) / mOutAnimationDuration; + if(size > 0) + radius = (Math.min(bounds.width(), bounds.height()) - mPadding * 2 - mStrokeSize * 2 + size) / 2f; + } + else if(mRunState != RUN_STATE_STOPPED){ + size = mStrokeSize; + radius = (Math.min(bounds.width(), bounds.height()) - mPadding * 2 - mStrokeSize) / 2f; + } + + if(radius > 0){ + float x = (bounds.left + bounds.right) / 2f; + float y = (bounds.top + bounds.bottom) / 2f; + + mPaint.setStrokeWidth(size); + mPaint.setStyle(Paint.Style.STROKE); + + if(mProgressPercent == 1f){ + mPaint.setColor(mStrokeColors[0]); + canvas.drawCircle(x, y, radius, mPaint); + } + else if(mProgressPercent == 0f){ + mPaint.setColor(mStrokeSecondaryColor); + canvas.drawCircle(x, y, radius, mPaint); + } + else{ + float sweepAngle = (mReverse ? -360 : 360) * mProgressPercent; + + mRect.set(x - radius, y - radius, x + radius, y + radius); + mPaint.setColor(mStrokeSecondaryColor); + canvas.drawArc(mRect, mStartAngle + sweepAngle, (mReverse ? -360 : 360) - sweepAngle, false, mPaint); + + mPaint.setColor(mStrokeColors[0]); + canvas.drawArc(mRect, mStartAngle, sweepAngle, false, mPaint); + } + } + } + + private int getIndeterminateStrokeColor(){ + if(mProgressState != PROGRESS_STATE_KEEP_SHRINK || mStrokeColors.length == 1) + return mStrokeColors[mStrokeColorIndex]; + + float value = Math.max(0f, Math.min(1f, (float)(SystemClock.uptimeMillis() - mLastProgressStateTime) / mKeepDuration)); + int prev_index = mStrokeColorIndex == 0 ? mStrokeColors.length - 1 : mStrokeColorIndex - 1; + + return ColorUtil.getMiddleColor(mStrokeColors[prev_index], mStrokeColors[mStrokeColorIndex], value); + } + + private void drawIndeterminate(Canvas canvas){ + if(mRunState == RUN_STATE_STARTING){ + Rect bounds = getBounds(); + float x = (bounds.left + bounds.right) / 2f; + float y = (bounds.top + bounds.bottom) / 2f; + float maxRadius = (Math.min(bounds.width(), bounds.height()) - mPadding * 2) / 2f; + + float stepTime = 1f / (mInStepPercent * (mInColors.length + 2) + 1); + float time = (float)(SystemClock.uptimeMillis() - mLastRunStateTime) / mInAnimationDuration; + float steps = time / stepTime; + + float outerRadius = 0f; + float innerRadius = 0f; + + for(int i = (int)Math.floor(steps); i >= 0; i--){ + innerRadius = outerRadius; + outerRadius = Math.min(1f, (steps - i) * mInStepPercent) * maxRadius; + + if(i >= mInColors.length) + continue; + + if(innerRadius == 0){ + mPaint.setColor(mInColors[i]); + mPaint.setStyle(Paint.Style.FILL); + canvas.drawCircle(x, y, outerRadius, mPaint); + } + else if(outerRadius > innerRadius){ + float radius = (innerRadius + outerRadius) / 2; + mRect.set(x - radius, y - radius, x + radius, y + radius); + + mPaint.setStrokeWidth(outerRadius - innerRadius); + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setColor(mInColors[i]); + + canvas.drawCircle(x, y, radius, mPaint); + } + else + break; + } + + if(mProgressState == PROGRESS_STATE_HIDE){ + if(steps >= 1 / mInStepPercent || time >= 1) { + resetAnimation(); + mProgressState = PROGRESS_STATE_STRETCH; + } + } + else{ + float radius = maxRadius - mStrokeSize / 2f; + + mRect.set(x - radius, y - radius, x + radius, y + radius); + mPaint.setStrokeWidth(mStrokeSize); + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setColor(getIndeterminateStrokeColor()); + + canvas.drawArc(mRect, mStartAngle, mSweepAngle, false, mPaint); + } + } + else if(mRunState == RUN_STATE_STOPPING){ + float size = (float)mStrokeSize * Math.max(0, (mOutAnimationDuration - SystemClock.uptimeMillis() + mLastRunStateTime)) / mOutAnimationDuration; + + if(size > 0){ + Rect bounds = getBounds(); + float radius = (Math.min(bounds.width(), bounds.height()) - mPadding * 2 - mStrokeSize * 2 + size) / 2f; + float x = (bounds.left + bounds.right) / 2f; + float y = (bounds.top + bounds.bottom) / 2f; + + mRect.set(x - radius, y - radius, x + radius, y + radius); + mPaint.setStrokeWidth(size); + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setColor(getIndeterminateStrokeColor()); + + canvas.drawArc(mRect, mStartAngle, mSweepAngle, false, mPaint); + } + } + else if(mRunState != RUN_STATE_STOPPED){ + Rect bounds = getBounds(); + float radius = (Math.min(bounds.width(), bounds.height()) - mPadding * 2 - mStrokeSize) / 2f; + float x = (bounds.left + bounds.right) / 2f; + float y = (bounds.top + bounds.bottom) / 2f; + + mRect.set(x - radius, y - radius, x + radius, y + radius); + mPaint.setStrokeWidth(mStrokeSize); + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setColor(getIndeterminateStrokeColor()); + + canvas.drawArc(mRect, mStartAngle, mSweepAngle, false, mPaint); + } + } + + @Override + public void setAlpha(int alpha) { + mPaint.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter cf) { + mPaint.setColorFilter(cf); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + public int getProgressMode(){ + return mProgressMode; + } + + public void setProgressMode(int mode){ + if(mProgressMode != mode) { + mProgressMode = mode; + invalidateSelf(); + } + } + + public float getProgress(){ + return mProgressPercent; + } + + public float getSecondaryProgress(){ + return mSecondaryProgressPercent; + } + + public void setProgress(float percent){ + percent = Math.min(1f, Math.max(0f, percent)); + if(mProgressPercent != percent){ + mProgressPercent = percent; + if(isRunning()) + invalidateSelf(); + else if(mProgressPercent != 0f) + start(); + } + } + + public void setSecondaryProgress(float percent){ + percent = Math.min(1f, Math.max(0f, percent)); + if(mSecondaryProgressPercent != percent){ + mSecondaryProgressPercent = percent; + if(isRunning()) + invalidateSelf(); + else if(mSecondaryProgressPercent != 0f) + start(); + } + } + + //Animation: based on http://cyrilmottier.com/2012/11/27/actionbar-on-the-move/ + + private void resetAnimation(){ + mLastUpdateTime = SystemClock.uptimeMillis(); + mLastProgressStateTime = mLastUpdateTime; + mStartAngle = mInitialAngle; + mStrokeColorIndex = 0; + mSweepAngle = mReverse ? -mMinSweepAngle : mMinSweepAngle; + } + + @Override + public void start() { + start(mInAnimationDuration > 0); + } + + @Override + public void stop() { + stop(mOutAnimationDuration > 0); + } + + private void start(boolean withAnimation){ + if(isRunning()) + return; + + resetAnimation(); + + if(withAnimation){ + mRunState = RUN_STATE_STARTING; + mLastRunStateTime = SystemClock.uptimeMillis(); + mProgressState = PROGRESS_STATE_HIDE; + } + + scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + invalidateSelf(); + } + + private void stop(boolean withAnimation){ + if(!isRunning()) + return; + + if(withAnimation){ + mLastRunStateTime = SystemClock.uptimeMillis(); + if(mRunState == RUN_STATE_STARTED){ + scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + invalidateSelf(); + } + mRunState = RUN_STATE_STOPPING; + } + else{ + mRunState = RUN_STATE_STOPPED; + unscheduleSelf(mUpdater); + invalidateSelf(); + } + } + + @Override + public boolean isRunning() { + return mRunState != RUN_STATE_STOPPED; + } + + @Override + public void scheduleSelf(Runnable what, long when) { + if(mRunState == RUN_STATE_STOPPED) + mRunState = mInAnimationDuration > 0 ? RUN_STATE_STARTING : RUN_STATE_RUNNING; + super.scheduleSelf(what, when); + } + + private final Runnable mUpdater = new Runnable() { + + @Override + public void run() { + update(); + } + + }; + + private void update(){ + switch (mProgressMode) { + case ProgressView.MODE_DETERMINATE: + updateDeterminate(); + break; + case ProgressView.MODE_INDETERMINATE: + updateIndeterminate(); + break; + } + } + + private void updateDeterminate(){ + long curTime = SystemClock.uptimeMillis(); + float rotateOffset = (curTime - mLastUpdateTime) * 360f / mRotateDuration; + if(mReverse) + rotateOffset = -rotateOffset; + mLastUpdateTime = curTime; + + mStartAngle += rotateOffset; + + if(mRunState == RUN_STATE_STARTING){ + if(curTime - mLastRunStateTime > mInAnimationDuration){ + mRunState = RUN_STATE_RUNNING; + } + } + else if(mRunState == RUN_STATE_STOPPING){ + if(curTime - mLastRunStateTime > mOutAnimationDuration){ + stop(false); + return; + } + } + + if(isRunning()) + scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + + invalidateSelf(); + } + + private void updateIndeterminate(){ + //update animation + long curTime = SystemClock.uptimeMillis(); + float rotateOffset = (curTime - mLastUpdateTime) * 360f / mRotateDuration; + if(mReverse) + rotateOffset = -rotateOffset; + mLastUpdateTime = curTime; + + switch (mProgressState) { + case PROGRESS_STATE_STRETCH: + if(mTransformDuration <= 0){ + mSweepAngle = mReverse ? -mMinSweepAngle : mMinSweepAngle; + mProgressState = PROGRESS_STATE_KEEP_STRETCH; + mStartAngle += rotateOffset; + mLastProgressStateTime = curTime; + } + else{ + float value = (curTime - mLastProgressStateTime) / (float)mTransformDuration; + float maxAngle = mReverse ? -mMaxSweepAngle : mMaxSweepAngle; + float minAngle = mReverse ? -mMinSweepAngle : mMinSweepAngle; + + mStartAngle += rotateOffset; + mSweepAngle = mTransformInterpolator.getInterpolation(value) * (maxAngle - minAngle) + minAngle; + + if(value > 1f){ + mSweepAngle = maxAngle; + mProgressState = PROGRESS_STATE_KEEP_STRETCH; + mLastProgressStateTime = curTime; + } + } + break; + case PROGRESS_STATE_KEEP_STRETCH: + mStartAngle += rotateOffset; + + if(curTime - mLastProgressStateTime > mKeepDuration){ + mProgressState = PROGRESS_STATE_SHRINK; + mLastProgressStateTime = curTime; + } + break; + case PROGRESS_STATE_SHRINK: + if(mTransformDuration <= 0){ + mSweepAngle = mReverse ? -mMinSweepAngle : mMinSweepAngle; + mProgressState = PROGRESS_STATE_KEEP_SHRINK; + mStartAngle += rotateOffset; + mLastProgressStateTime = curTime; + mStrokeColorIndex = (mStrokeColorIndex + 1) % mStrokeColors.length; + } + else{ + float value = (curTime - mLastProgressStateTime) / (float)mTransformDuration; + float maxAngle = mReverse ? -mMaxSweepAngle : mMaxSweepAngle; + float minAngle = mReverse ? -mMinSweepAngle : mMinSweepAngle; + + float newSweepAngle = (1f - mTransformInterpolator.getInterpolation(value)) * (maxAngle - minAngle) + minAngle; + mStartAngle += rotateOffset + mSweepAngle - newSweepAngle; + mSweepAngle = newSweepAngle; + + if(value > 1f){ + mSweepAngle = minAngle; + mProgressState = PROGRESS_STATE_KEEP_SHRINK; + mLastProgressStateTime = curTime; + mStrokeColorIndex = (mStrokeColorIndex + 1) % mStrokeColors.length; + } + } + break; + case PROGRESS_STATE_KEEP_SHRINK: + mStartAngle += rotateOffset; + + if(curTime - mLastProgressStateTime > mKeepDuration){ + mProgressState = PROGRESS_STATE_STRETCH; + mLastProgressStateTime = curTime; + } + break; + } + + if(mRunState == RUN_STATE_STARTING){ + if(curTime - mLastRunStateTime > mInAnimationDuration){ + mRunState = RUN_STATE_RUNNING; + if(mProgressState == PROGRESS_STATE_HIDE) { + resetAnimation(); + mProgressState = PROGRESS_STATE_STRETCH; + } + } + } + else if(mRunState == RUN_STATE_STOPPING){ + if(curTime - mLastRunStateTime > mOutAnimationDuration){ + stop(false); + return; + } + } + + if (isRunning()) + scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + + invalidateSelf(); + } + + public static class Builder{ + private int mPadding; + private float mInitialAngle; + private float mProgressPercent; + private float mSecondaryProgressPercent; + private float mMaxSweepAngle; + private float mMinSweepAngle; + private int mStrokeSize; + private int[] mStrokeColors; + private int mStrokeSecondaryColor; + private boolean mReverse; + private int mRotateDuration; + private int mTransformDuration; + private int mKeepDuration; + private Interpolator mTransformInterpolator; + private int mProgressMode; + private float mInStepPercent; + private int[] mInColors; + private int mInAnimationDuration; + private int mOutAnimationDuration; + + public Builder(){} + + public Builder(Context context, int defStyleRes){ + this(context, null, 0, defStyleRes); + } + + public Builder(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircularProgressDrawable, defStyleAttr, defStyleRes); + int resId; + + padding(a.getDimensionPixelSize(R.styleable.CircularProgressDrawable_cpd_padding, 0)); + initialAngle(a.getInteger(R.styleable.CircularProgressDrawable_cpd_initialAngle, 0)); + progressPercent(a.getFloat(R.styleable.CircularProgressDrawable_pv_progress, 0)); + secondaryProgressPercent(a.getFloat(R.styleable.CircularProgressDrawable_pv_secondaryProgress, 0)); + maxSweepAngle(a.getInteger(R.styleable.CircularProgressDrawable_cpd_maxSweepAngle, 270)); + minSweepAngle(a.getInteger(R.styleable.CircularProgressDrawable_cpd_minSweepAngle, 1)); + strokeSize(a.getDimensionPixelSize(R.styleable.CircularProgressDrawable_cpd_strokeSize, ThemeUtil.dpToPx(context, 4))); + strokeColors(a.getColor(R.styleable.CircularProgressDrawable_cpd_strokeColor, ThemeUtil.colorPrimary(context, 0xFF000000))); + if((resId = a.getResourceId(R.styleable.CircularProgressDrawable_cpd_strokeColors, 0)) != 0){ + TypedArray ta = context.getResources().obtainTypedArray(resId); + int[] colors = new int[ta.length()]; + for(int j = 0; j < ta.length(); j++) + colors[j] = ta.getColor(j, 0); + ta.recycle(); + strokeColors(colors); + } + strokeSecondaryColor(a.getColor(R.styleable.CircularProgressDrawable_cpd_strokeSecondaryColor, 0)); + reverse(a.getBoolean(R.styleable.CircularProgressDrawable_cpd_reverse, false)); + rotateDuration(a.getInteger(R.styleable.CircularProgressDrawable_cpd_rotateDuration, context.getResources().getInteger(android.R.integer.config_longAnimTime))); + transformDuration(a.getInteger(R.styleable.CircularProgressDrawable_cpd_transformDuration, context.getResources().getInteger(android.R.integer.config_mediumAnimTime))); + keepDuration(a.getInteger(R.styleable.CircularProgressDrawable_cpd_keepDuration, context.getResources().getInteger(android.R.integer.config_shortAnimTime))); + if((resId = a.getResourceId(R.styleable.CircularProgressDrawable_cpd_transformInterpolator, 0)) != 0) + transformInterpolator(AnimationUtils.loadInterpolator(context, resId)); + progressMode(a.getInteger(R.styleable.CircularProgressDrawable_pv_progressMode, ProgressView.MODE_INDETERMINATE)); + inAnimDuration(a.getInteger(R.styleable.CircularProgressDrawable_cpd_inAnimDuration, context.getResources().getInteger(android.R.integer.config_mediumAnimTime))); + if((resId = a.getResourceId(R.styleable.CircularProgressDrawable_cpd_inStepColors, 0)) != 0){ + TypedArray ta = context.getResources().obtainTypedArray(resId); + int[] colors = new int[ta.length()]; + for(int j = 0; j < ta.length(); j++) + colors[j] = ta.getColor(j, 0); + ta.recycle(); + inStepColors(colors); + } + inStepPercent(a.getFloat(R.styleable.CircularProgressDrawable_cpd_inStepPercent, 0.5f)); + outAnimDuration(a.getInteger(R.styleable.CircularProgressDrawable_cpd_outAnimDuration, context.getResources().getInteger(android.R.integer.config_mediumAnimTime))); + a.recycle(); + } + + public CircularProgressDrawable build(){ + if(mStrokeColors == null) + mStrokeColors = new int[]{0xFF0099FF}; + + if(mInColors == null && mInAnimationDuration > 0) + mInColors = new int[]{0xFFB5D4FF, 0xFFDEEAFC, 0xFFFAFFFE}; + + if(mTransformInterpolator == null) + mTransformInterpolator = new DecelerateInterpolator(); + + return new CircularProgressDrawable(mPadding, mInitialAngle, mProgressPercent, mSecondaryProgressPercent, mMaxSweepAngle, mMinSweepAngle, mStrokeSize, mStrokeColors, mStrokeSecondaryColor, mReverse, mRotateDuration, mTransformDuration, mKeepDuration, mTransformInterpolator, mProgressMode, mInAnimationDuration, mInStepPercent, mInColors, mOutAnimationDuration); + } + + public Builder padding(int padding){ + mPadding = padding; + return this; + } + + public Builder initialAngle(float angle){ + mInitialAngle = angle; + return this; + } + + public Builder progressPercent(float percent){ + mProgressPercent = percent; + return this; + } + + public Builder secondaryProgressPercent(float percent){ + mSecondaryProgressPercent = percent; + return this; + } + + public Builder maxSweepAngle(float angle){ + mMaxSweepAngle = angle; + return this; + } + + public Builder minSweepAngle(float angle){ + mMinSweepAngle = angle; + return this; + } + + public Builder strokeSize(int strokeSize){ + mStrokeSize = strokeSize; + return this; + } + + public Builder strokeColors(int... strokeColors){ + mStrokeColors = strokeColors; + return this; + } + + public Builder strokeSecondaryColor(int color){ + mStrokeSecondaryColor = color; + return this; + } + + public Builder reverse(boolean reverse){ + mReverse = reverse; + return this; + } + + public Builder reverse(){ + return reverse(true); + } + + public Builder rotateDuration(int duration){ + mRotateDuration = duration; + return this; + } + + public Builder transformDuration(int duration){ + mTransformDuration = duration; + return this; + } + + public Builder keepDuration(int duration){ + mKeepDuration = duration; + return this; + } + + public Builder transformInterpolator(Interpolator interpolator){ + mTransformInterpolator = interpolator; + return this; + } + + public Builder progressMode(int mode){ + mProgressMode = mode; + return this; + } + + public Builder inAnimDuration(int duration){ + mInAnimationDuration = duration; + return this; + } + + public Builder inStepPercent(float percent){ + mInStepPercent = percent; + return this; + } + + public Builder inStepColors(int... colors){ + mInColors = colors; + return this; + } + + public Builder outAnimDuration(int duration){ + mOutAnimationDuration = duration; + return this; + } + + } +} diff --git a/material/src/main/java/com/rey/material/drawable/ContactChipDrawable.java b/material/src/main/java/com/rey/material/drawable/ContactChipDrawable.java new file mode 100644 index 0000000..74f0871 --- /dev/null +++ b/material/src/main/java/com/rey/material/drawable/ContactChipDrawable.java @@ -0,0 +1,160 @@ +package com.rey.material.drawable; + +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Shader; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.text.BoringLayout; +import android.text.Layout; +import android.text.TextPaint; +import android.text.TextUtils; + +public class ContactChipDrawable extends Drawable{ + + private Paint mPaint; + private int mPaddingLeft; + private int mPaddingRight; + private int mBackgroundColor; + + private CharSequence mContactName; + private BoringLayout mBoringLayout; + private BoringLayout.Metrics mMetrics; + private TextPaint mTextPaint; + private RectF mRect; + + private BitmapShader mBitmapShader; + private Bitmap mBitmap; + private Matrix mMatrix; + + public ContactChipDrawable(int paddingLeft, int paddingRight, Typeface typeface, int textColor, int textSize, int backgroundColor) { + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mPaint.setStyle(Paint.Style.FILL); + mPaint.setColor(textColor); + mPaint.setTypeface(typeface); + mPaint.setTextSize(textSize); + + mTextPaint = new TextPaint(mPaint); + mMetrics = new BoringLayout.Metrics(); + Paint.FontMetricsInt temp = mTextPaint.getFontMetricsInt(); + mMetrics.ascent = temp.ascent; + mMetrics.bottom = temp.bottom; + mMetrics.descent = temp.descent; + mMetrics.top = temp.top; + mMetrics.leading = temp.leading; + + mRect = new RectF(); + + mMatrix = new Matrix(); + + mPaddingLeft = paddingLeft; + mPaddingRight = paddingRight; + mBackgroundColor = backgroundColor; + } + + public void setContactName(CharSequence name){ + mContactName = name; + updateLayout(); + invalidateSelf(); + } + + public void setImage(Bitmap bm){ + if(mBitmap != bm){ + mBitmap = bm; + if(mBitmap != null) { + mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); + updateMatrix(); + } + invalidateSelf(); + } + } + + private void updateLayout(){ + if(mContactName == null) + return; + + Rect bounds = getBounds(); + if(bounds.width() == 0 || bounds.height() == 0) + return; + + int outerWidth = Math.max(0, bounds.width() - bounds.height() - mPaddingLeft - mPaddingRight); + mMetrics.width = Math.round(mTextPaint.measureText(mContactName, 0, mContactName.length()) + 0.5f); + + if(mBoringLayout == null) + mBoringLayout = BoringLayout.make(mContactName, mTextPaint, outerWidth, Layout.Alignment.ALIGN_NORMAL, 1f, 1f, mMetrics, true, TextUtils.TruncateAt.END, outerWidth); + else + mBoringLayout = mBoringLayout.replaceOrMake(mContactName, mTextPaint, outerWidth, Layout.Alignment.ALIGN_NORMAL, 1f, 1f, mMetrics, true, TextUtils.TruncateAt.END, outerWidth); + } + + private void updateMatrix(){ + if(mBitmap == null) + return; + + Rect bounds = getBounds(); + if(bounds.width() == 0 || bounds.height() == 0) + return; + + mMatrix.reset(); + float scale = bounds.height() / (float)Math.min(mBitmap.getWidth(), mBitmap.getHeight()); + mMatrix.setScale(scale, scale, 0, 0); + mMatrix.postTranslate((bounds.height() - mBitmap.getWidth() * scale) / 2, (bounds.height() - mBitmap.getHeight() * scale) / 2); + + mBitmapShader.setLocalMatrix(mMatrix); + } + + @Override + protected void onBoundsChange(Rect bounds) { + updateLayout(); + updateMatrix(); + } + + @Override + public void draw(Canvas canvas) { + int saveCount = canvas.save(); + + Rect bounds = getBounds(); + float halfHeight = bounds.height() / 2f; + mPaint.setShader(null); + mPaint.setColor(mBackgroundColor); + mRect.set(1, 0, bounds.height() + 1, bounds.height()); + canvas.drawArc(mRect, 90, 180, true, mPaint); + mRect.set(bounds.width() - bounds.height(), 0, bounds.width(), bounds.height()); + canvas.drawArc(mRect, 270, 180, true, mPaint); + mRect.set(halfHeight, 0, bounds.width() - halfHeight, bounds.height()); + canvas.drawRect(mRect, mPaint); + + if(mBitmap != null){ + mPaint.setShader(mBitmapShader); + canvas.drawCircle(halfHeight, halfHeight, halfHeight, mPaint); + } + + if(mContactName != null && mBoringLayout != null) { + canvas.translate(bounds.height() + mPaddingLeft, (bounds.height() - mBoringLayout.getHeight()) / 2f); + mBoringLayout.draw(canvas); + } + + canvas.restoreToCount(saveCount); + } + + @Override + public void setAlpha(int alpha) { + mPaint.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter cf) { + mPaint.setColorFilter(cf); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } +} diff --git a/material/src/main/java/com/rey/material/drawable/DividerDrawable.java b/material/src/main/java/com/rey/material/drawable/DividerDrawable.java new file mode 100644 index 0000000..1a359fd --- /dev/null +++ b/material/src/main/java/com/rey/material/drawable/DividerDrawable.java @@ -0,0 +1,253 @@ +package com.rey.material.drawable; + +import android.content.res.ColorStateList; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.DashPathEffect; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PathEffect; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; +import android.os.SystemClock; + +import com.rey.material.util.ViewUtil; + +public class DividerDrawable extends Drawable implements Animatable{ + + private boolean mRunning = false; + private long mStartTime; + private float mAnimProgress; + private int mAnimDuration; + + private Paint mPaint; + private ColorStateList mColorStateList; + private int mHeight; + private int mPrevColor; + private int mCurColor; + + private boolean mEnable = true; + private PathEffect mPathEffect; + private Path mPath; + + private boolean mInEditMode = false; + private boolean mAnimEnable = true; + + private int mPaddingLeft; + private int mPaddingRight; + + public DividerDrawable(int height, ColorStateList colorStateList, int animDuration){ + this(height, 0, 0, colorStateList, animDuration); + } + + public DividerDrawable(int height, int paddingLeft, int paddingRight, ColorStateList colorStateList, int animDuration){ + mHeight = height; + mPaddingLeft = paddingLeft; + mPaddingRight = paddingRight; + mAnimDuration = animDuration; + + mPaint = new Paint(); + mPaint.setAntiAlias(true); + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setStrokeWidth(mHeight); + mPaint.setStrokeCap(Paint.Cap.ROUND); + mPaint.setStrokeJoin(Paint.Join.ROUND); + + mPath = new Path(); + + mAnimEnable = false; + setColor(colorStateList); + mAnimEnable = true; + } + + public void setDividerHeight(int height){ + if(mHeight != height){ + mHeight = height; + mPaint.setStrokeWidth(mHeight); + invalidateSelf(); + } + } + + public int getDividerHeight(){ + return mHeight; + } + + public void setPadding(int left, int right){ + if(mPaddingLeft != left || mPaddingRight != right){ + mPaddingLeft = left; + mPaddingRight = right; + invalidateSelf(); + } + } + + public int getPaddingLeft(){ + return mPaddingLeft; + } + + public int getPaddingRight(){ + return mPaddingRight; + } + + public void setInEditMode(boolean b){ + mInEditMode = b; + } + + public void setAnimEnable(boolean b){ + mAnimEnable = b; + } + + public void setColor(ColorStateList colorStateList){ + mColorStateList = colorStateList; + onStateChange(getState()); + } + + public void setAnimationDuration(int duration){ + mAnimDuration = duration; + } + + private PathEffect getPathEffect(){ + if(mPathEffect == null) + mPathEffect = new DashPathEffect(new float[]{0.2f, mHeight * 2}, 0f); + + return mPathEffect; + } + + @Override + public void draw(Canvas canvas) { + if(mHeight == 0) + return; + + Rect bounds = getBounds(); + float y = bounds.bottom - mHeight / 2; + + if(!isRunning()){ + mPath.reset(); + mPath.moveTo(bounds.left + mPaddingLeft, y); + mPath.lineTo(bounds.right - mPaddingRight, y); + mPaint.setPathEffect(mEnable ? null : getPathEffect()); + mPaint.setColor(mCurColor); + canvas.drawPath(mPath, mPaint); + } + else{ + float centerX = (bounds.right + bounds.left - mPaddingRight + mPaddingLeft) / 2f; + float start = centerX * (1f - mAnimProgress) + (bounds.left + mPaddingLeft) * mAnimProgress; + float end = centerX * (1f - mAnimProgress) + (bounds.right + mPaddingRight) * mAnimProgress; + + mPaint.setPathEffect(null); + + if(mAnimProgress < 1f){ + mPaint.setColor(mPrevColor); + mPath.reset(); + mPath.moveTo(bounds.left + mPaddingLeft, y); + mPath.lineTo(start, y); + mPath.moveTo(bounds.right - mPaddingRight, y); + mPath.lineTo(end, y); + canvas.drawPath(mPath, mPaint); + } + + mPaint.setColor(mCurColor); + mPath.reset(); + mPath.moveTo(start, y); + mPath.lineTo(end, y); + canvas.drawPath(mPath, mPaint); + } + } + + @Override + public void setAlpha(int alpha) { + mPaint.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter cf) { + mPaint.setColorFilter(cf); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + @Override + public boolean isStateful() { + return true; + } + + @Override + protected boolean onStateChange(int[] state) { + mEnable = ViewUtil.hasState(state, android.R.attr.state_enabled); + int color = mColorStateList.getColorForState(state, mCurColor); + + if(mCurColor != color){ + if(!mInEditMode && mAnimEnable && mEnable && mAnimDuration > 0){ + mPrevColor = isRunning() ? mPrevColor : mCurColor; + mCurColor = color; + start(); + } + else{ + mPrevColor = color; + mCurColor = color; + } + return true; + } + else if(!isRunning()) + mPrevColor = color; + + return false; + } + + private void resetAnimation(){ + mStartTime = SystemClock.uptimeMillis(); + mAnimProgress = 0f; + } + + @Override + public void start() { + resetAnimation(); + scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + invalidateSelf(); + } + + @Override + public void stop() { + mRunning = false; + unscheduleSelf(mUpdater); + invalidateSelf(); + } + + @Override + public boolean isRunning() { + return mRunning; + } + + @Override + public void scheduleSelf(Runnable what, long when) { + mRunning = true; + super.scheduleSelf(what, when); + } + + private final Runnable mUpdater = new Runnable() { + + @Override + public void run() { + update(); + } + + }; + + private void update(){ + long curTime = SystemClock.uptimeMillis(); + mAnimProgress = Math.min(1f, (float)(curTime - mStartTime) / mAnimDuration); + + if(mAnimProgress == 1f) + mRunning = false; + + if(isRunning()) + scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + + invalidateSelf(); + } + +} diff --git a/material/src/main/java/com/rey/material/drawable/LineMorphingDrawable.java b/material/src/main/java/com/rey/material/drawable/LineMorphingDrawable.java new file mode 100644 index 0000000..cccfb19 --- /dev/null +++ b/material/src/main/java/com/rey/material/drawable/LineMorphingDrawable.java @@ -0,0 +1,664 @@ +package com.rey.material.drawable; + +import android.content.Context; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; +import android.os.SystemClock; +import android.support.v4.text.TextUtilsCompat; +import android.support.v4.view.ViewCompat; +import android.util.AttributeSet; +import android.view.View; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; + +import com.rey.material.R; +import com.rey.material.util.ThemeUtil; +import com.rey.material.util.ViewUtil; + +import org.xmlpull.v1.XmlPullParser; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public class LineMorphingDrawable extends Drawable implements Animatable{ + + private boolean mRunning = false; + + private Paint mPaint; + + private int mPaddingLeft = 12; + private int mPaddingTop = 12; + private int mPaddingRight = 12; + private int mPaddingBottom = 12; + + private RectF mDrawBound; + + private int mPrevState; + private int mCurState; + private long mStartTime; + private float mAnimProgress; + private int mAnimDuration; + private Interpolator mInterpolator; + private int mStrokeSize; + private int mStrokeColor; + private boolean mClockwise; + private Paint.Cap mStrokeCap; + private Paint.Join mStrokeJoin; + private boolean mIsRtl; + + private Path mPath; + + private State[] mStates; + + private LineMorphingDrawable(State[] states, int curState, int paddingLeft, int paddingTop, int paddingRight, int paddingBottom, int animDuration, Interpolator interpolator, int strokeSize, int strokeColor, Paint.Cap strokeCap, Paint.Join strokeJoin, boolean clockwise, boolean isRtl){ + mStates = states; + mPaddingLeft = paddingLeft; + mPaddingTop = paddingTop; + mPaddingRight = paddingRight; + mPaddingBottom = paddingBottom; + + mAnimDuration = animDuration; + mInterpolator = interpolator; + mStrokeSize = strokeSize; + mStrokeColor = strokeColor; + mStrokeCap = strokeCap; + mStrokeJoin = strokeJoin; + mClockwise = clockwise; + mIsRtl = isRtl; + + mPaint = new Paint(); + mPaint.setAntiAlias(true); + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setStrokeCap(mStrokeCap); + mPaint.setStrokeJoin(mStrokeJoin); + mPaint.setColor(mStrokeColor); + mPaint.setStrokeWidth(mStrokeSize); + + mDrawBound = new RectF(); + + mPath = new Path(); + + switchLineState(curState, false); + } + + @Override + public void draw(Canvas canvas) { + int restoreCount = canvas.save(); + float degrees = (mClockwise ? 180 : -180) * ((mPrevState < mCurState ? 0f : 1f) + mAnimProgress); + + if(mIsRtl) + canvas.scale(-1f, 1f, mDrawBound.centerX(), mDrawBound.centerY()); + + canvas.rotate(degrees, mDrawBound.centerX(), mDrawBound.centerY()); + canvas.drawPath(mPath, mPaint); + canvas.restoreToCount(restoreCount); + } + + @Override + public void setAlpha(int alpha) { + mPaint.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter cf) { + mPaint.setColorFilter(cf); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + @Override + protected void onBoundsChange(Rect bounds) { + super.onBoundsChange(bounds); + + mDrawBound.left = bounds.left + mPaddingLeft; + mDrawBound.top = bounds.top + mPaddingTop; + mDrawBound.right = bounds.right - mPaddingRight; + mDrawBound.bottom = bounds.bottom - mPaddingBottom; + + updatePath(); + } + + public void switchLineState(int state, boolean animation){ + if(mCurState != state){ + mPrevState = mCurState; + mCurState = state; + if(animation) + start(); + else{ + mAnimProgress = 1f; + updatePath(); + } + } + else if(!animation){ + mAnimProgress = 1f; + updatePath(); + } + } + + public boolean setLineState(int state, float progress){ + if(mCurState != state){ + mPrevState = mCurState; + mCurState = state; + mAnimProgress = progress; + updatePath(); + return true; + } + else if(mAnimProgress != progress){ + mAnimProgress = progress; + updatePath(); + return true; + } + + return false; + } + + public int getLineState(){ + return mCurState; + } + + public int getLineStateCount(){ + return mStates == null ? 0 : mStates.length; + } + + public float getAnimProgress(){ + return mAnimProgress; + } + + private void updatePath(){ + mPath.reset(); + + if(mStates == null) + return; + + if(mAnimProgress == 0f || (mStates[mPrevState].links != null && mAnimProgress < 0.05f)) + updatePathWithState(mPath, mStates[mPrevState]); + else if(mAnimProgress == 1f || (mStates[mCurState].links != null && mAnimProgress >0.95f)) + updatePathWithState(mPath, mStates[mCurState]); + else + updatePathBetweenStates(mPath, mStates[mPrevState], mStates[mCurState], mInterpolator.getInterpolation(mAnimProgress)); + + invalidateSelf(); + } + + private void updatePathWithState(Path path, State state){ + if(state.links != null){ + for(int i = 0; i < state.links.length; i+= 2){ + int index1 = state.links[i] * 4; + int index2 = state.links[i + 1] * 4; + + float x1 = getX(state.points[index1]); + float y1 = getY(state.points[index1 + 1]); + float x2 = getX(state.points[index1 + 2]); + float y2 = getY(state.points[index1 + 3]); + + float x3 = getX(state.points[index2]); + float y3 = getY(state.points[index2 + 1]); + float x4 = getX(state.points[index2 + 2]); + float y4 = getY(state.points[index2 + 3]); + + if(x1 == x3 && y1 == y3){ + path.moveTo(x2, y2); + path.lineTo(x1, y1); + path.lineTo(x4, y4); + } + else if(x1 == x4 && y1 == y4){ + path.moveTo(x2, y2); + path.lineTo(x1, y1); + path.lineTo(x3, y3); + } + else if(x2 == x3 && y2 == y3){ + path.moveTo(x1, y1); + path.lineTo(x2, y2); + path.lineTo(x4, y4); + } + else{ + path.moveTo(x1, y1); + path.lineTo(x2, y2); + path.lineTo(x3, y3); + } + } + + for(int i = 0, count = state.points.length / 4; i < count; i ++){ + boolean exist = false; + for(int j = 0; j < state.links.length; j++) + if(state.links[j] == i){ + exist = true; + break; + } + + if(exist) + continue; + + int index = i * 4; + + path.moveTo(getX(state.points[index]), getY(state.points[index + 1])); + path.lineTo(getX(state.points[index + 2]), getY(state.points[index + 3])); + } + } + else{ + for(int i = 0, count = state.points.length / 4; i < count; i ++){ + int index = i * 4; + + path.moveTo(getX(state.points[index]), getY(state.points[index + 1])); + path.lineTo(getX(state.points[index + 2]), getY(state.points[index + 3])); + } + } + } + + private void updatePathBetweenStates(Path path, State prev, State cur, float progress){ + int count = Math.max(prev.points.length, cur.points.length) / 4; + + for(int i = 0; i < count; i++){ + int index = i * 4; + + float x1; + float y1; + float x2; + float y2; + if(index >= prev.points.length){ + x1 = 0.5f; + y1 = 0.5f; + x2 = 0.5f; + y2 = 0.5f; + } + else{ + x1 = prev.points[index]; + y1 = prev.points[index + 1]; + x2 = prev.points[index + 2]; + y2 = prev.points[index + 3]; + } + + float x3; + float y3; + float x4; + float y4; + if(index >= cur.points.length){ + x3 = 0.5f; + y3 = 0.5f; + x4 = 0.5f; + y4 = 0.5f; + } + else{ + x3 = cur.points[index]; + y3 = cur.points[index + 1]; + x4 = cur.points[index + 2]; + y4 = cur.points[index + 3]; + } + + mPath.moveTo(getX(x1 + (x3 - x1) * progress), getY(y1 + (y3 - y1) * progress)); + mPath.lineTo(getX(x2 + (x4 - x2) * progress), getY(y2 + (y4 - y2) * progress)); + } + } + + private float getX(float value){ + return mDrawBound.left + mDrawBound.width() * value; + } + + private float getY(float value){ + return mDrawBound.top + mDrawBound.height() * value; + } + + //Animation: based on http://cyrilmottier.com/2012/11/27/actionbar-on-the-move/ + + private void resetAnimation(){ + mStartTime = SystemClock.uptimeMillis(); + mAnimProgress = 0f; + } + + @Override + public void start() { + resetAnimation(); + + scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + invalidateSelf(); + } + + @Override + public void stop() { + if(!isRunning()) + return; + + mRunning = false; + unscheduleSelf(mUpdater); + invalidateSelf(); + } + + @Override + public boolean isRunning() { + return mRunning; + } + + @Override + public void scheduleSelf(Runnable what, long when) { + mRunning = true; + super.scheduleSelf(what, when); + } + + private final Runnable mUpdater = new Runnable() { + + @Override + public void run() { + update(); + } + + }; + + private void update(){ + long curTime = SystemClock.uptimeMillis(); + float value = Math.min(1f, (float)(curTime - mStartTime) / mAnimDuration); + + if(value == 1f){ + setLineState(mCurState, 1f); + mRunning = false; + } + else + setLineState(mCurState, mInterpolator.getInterpolation(value)); + + if(isRunning()) + scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + } + + public static class State{ + float[] points; + int[] links; + + public State(){} + + public State(float[] points, int[] links){ + this.points = points; + this.links = links; + } + } + + public static class Builder{ + private int mCurState; + + private int mPaddingLeft; + private int mPaddingTop; + private int mPaddingRight; + private int mPaddingBottom; + + private int mAnimDuration; + private Interpolator mInterpolator; + private int mStrokeSize; + private int mStrokeColor; + private boolean mClockwise; + private Paint.Cap mStrokeCap; + private Paint.Join mStrokeJoin; + private boolean mIsRtl; + + private State[] mStates; + + private static final String TAG_STATE_LIST = "state-list"; + private static final String TAG_STATE = "state"; + private static final String TAG_POINTS = "points"; + private static final String TAG_LINKS = "links"; + private static final String TAG_ITEM = "item"; + + public Builder(){} + + public Builder(Context context, int defStyleRes){ + this(context, null, 0, defStyleRes); + } + + public Builder(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LineMorphingDrawable, defStyleAttr, defStyleRes); + int resId; + + if((resId = a.getResourceId(R.styleable.LineMorphingDrawable_lmd_state, 0)) != 0) + states(readStates(context, resId)); + curState(a.getInteger(R.styleable.LineMorphingDrawable_lmd_curState, 0)); + padding(a.getDimensionPixelSize(R.styleable.LineMorphingDrawable_lmd_padding, 0)); + paddingLeft(a.getDimensionPixelSize(R.styleable.LineMorphingDrawable_lmd_paddingLeft, mPaddingLeft)); + paddingTop(a.getDimensionPixelSize(R.styleable.LineMorphingDrawable_lmd_paddingTop, mPaddingTop)); + paddingRight(a.getDimensionPixelSize(R.styleable.LineMorphingDrawable_lmd_paddingRight, mPaddingRight)); + paddingBottom(a.getDimensionPixelSize(R.styleable.LineMorphingDrawable_lmd_paddingBottom, mPaddingBottom)); + animDuration(a.getInteger(R.styleable.LineMorphingDrawable_lmd_animDuration, context.getResources().getInteger(android.R.integer.config_mediumAnimTime))); + if((resId = a.getResourceId(R.styleable.LineMorphingDrawable_lmd_interpolator, 0)) != 0) + interpolator(AnimationUtils.loadInterpolator(context, resId)); + strokeSize(a.getDimensionPixelSize(R.styleable.LineMorphingDrawable_lmd_strokeSize, ThemeUtil.dpToPx(context, 3))); + strokeColor(a.getColor(R.styleable.LineMorphingDrawable_lmd_strokeColor, 0xFFFFFFFF)); + int cap = a.getInteger(R.styleable.LineMorphingDrawable_lmd_strokeCap, 0); + if(cap == 0) + strokeCap(Paint.Cap.BUTT); + else if(cap == 1) + strokeCap(Paint.Cap.ROUND); + else + strokeCap(Paint.Cap.SQUARE); + int join = a.getInteger(R.styleable.LineMorphingDrawable_lmd_strokeJoin, 0); + if(join == 0) + strokeJoin(Paint.Join.MITER); + else if(join == 1) + strokeJoin(Paint.Join.ROUND); + else + strokeJoin(Paint.Join.BEVEL); + clockwise(a.getBoolean(R.styleable.LineMorphingDrawable_lmd_clockwise, true)); + + int direction = a.getInteger(R.styleable.LineMorphingDrawable_lmd_layoutDirection, View.LAYOUT_DIRECTION_LTR); + if(direction == View.LAYOUT_DIRECTION_LOCALE) + rtl(TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault()) == ViewCompat.LAYOUT_DIRECTION_RTL); + else + rtl(direction == View.LAYOUT_DIRECTION_RTL); + + a.recycle(); + } + + private State[] readStates(Context context, int id){ + XmlResourceParser parser = null; + List states = new ArrayList<>(); + + try { + parser = context.getResources().getXml(id); + + int eventType = parser.getEventType(); + String tagName; + boolean lookingForEndOfUnknownTag = false; + String unknownTagName = null; + + // This loop will skip to the state-list start tag + do { + if (eventType == XmlPullParser.START_TAG) { + tagName = parser.getName(); + if (tagName.equals(TAG_STATE_LIST)) { + eventType = parser.next(); + break; + } + throw new RuntimeException("Expecting menu, got " + tagName); + } + eventType = parser.next(); + } while (eventType != XmlPullParser.END_DOCUMENT); + + boolean reachedEndOfStateList = false; + State state = null; + List array = new ArrayList<>(); + StringBuilder currentValue = new StringBuilder(); + + while (!reachedEndOfStateList) { + switch (eventType) { + case XmlPullParser.START_TAG: + if (lookingForEndOfUnknownTag) + break; + + tagName = parser.getName(); + switch (tagName) { + case TAG_STATE: + state = new State(); + break; + case TAG_POINTS: + case TAG_LINKS: + array.clear(); + break; + case TAG_ITEM: + currentValue.delete(0, currentValue.length()); + break; + default: + lookingForEndOfUnknownTag = true; + unknownTagName = tagName; + break; + } + break; + + case XmlPullParser.END_TAG: + tagName = parser.getName(); + + if (lookingForEndOfUnknownTag && tagName.equals(unknownTagName)) { + lookingForEndOfUnknownTag = false; + unknownTagName = null; + } + + switch (tagName) { + case TAG_STATE_LIST: + reachedEndOfStateList = true; + break; + case TAG_STATE: + states.add(state); + break; + case TAG_POINTS: + state.points = new float[array.size()]; + for (int i = 0; i < state.points.length; i++) + state.points[i] = Float.parseFloat(array.get(i)); + break; + case TAG_LINKS: + state.links = new int[array.size()]; + for (int i = 0; i < state.links.length; i++) + state.links[i] = Integer.parseInt(array.get(i)); + break; + case TAG_ITEM: + array.add(currentValue.toString()); + break; + } + + break; + + case XmlPullParser.TEXT: + currentValue.append(parser.getText()); + break; + + case XmlPullParser.END_DOCUMENT: + reachedEndOfStateList = true; + break; + } + + eventType = parser.next(); + } + + } + catch (Exception e) {} + finally { + if(parser != null) + parser.close(); + } + + if(states.isEmpty()) + return null; + + return states.toArray(new State[states.size()]); + } + + public LineMorphingDrawable build(){ + if(mStrokeCap == null) + mStrokeCap = Paint.Cap.BUTT; + + if(mStrokeJoin == null) + mStrokeJoin = Paint.Join.MITER; + + if(mInterpolator == null) + mInterpolator = new AccelerateInterpolator(); + + return new LineMorphingDrawable(mStates, mCurState, mPaddingLeft, mPaddingTop, mPaddingRight, mPaddingBottom, mAnimDuration, mInterpolator, mStrokeSize, mStrokeColor, mStrokeCap, mStrokeJoin, mClockwise, mIsRtl); + } + + public Builder states(State... states){ + mStates = states; + return this; + } + + public Builder curState(int state){ + mCurState = state; + return this; + } + + public Builder padding(int padding){ + mPaddingLeft = padding; + mPaddingTop = padding; + mPaddingRight = padding; + mPaddingBottom = padding; + return this; + } + + public Builder paddingLeft(int padding){ + mPaddingLeft = padding; + return this; + } + + public Builder paddingTop(int padding){ + mPaddingTop = padding; + return this; + } + + public Builder paddingRight(int padding){ + mPaddingRight = padding; + return this; + } + + public Builder paddingBottom(int padding){ + mPaddingBottom = padding; + return this; + } + + public Builder animDuration(int duration){ + mAnimDuration = duration; + return this; + } + + public Builder interpolator(Interpolator interpolator){ + mInterpolator = interpolator; + return this; + } + + public Builder strokeSize(int size){ + mStrokeSize = size; + return this; + } + + public Builder strokeColor(int strokeColor){ + mStrokeColor = strokeColor; + return this; + } + + public Builder strokeCap(Paint.Cap cap){ + mStrokeCap = cap; + return this; + } + + public Builder strokeJoin(Paint.Join join){ + mStrokeJoin = join; + return this; + } + + public Builder clockwise(boolean clockwise){ + mClockwise = clockwise; + return this; + } + + public Builder rtl(boolean rtl){ + mIsRtl = rtl; + return this; + } + + } +} diff --git a/material/src/main/java/com/rey/material/drawable/LinearProgressDrawable.java b/material/src/main/java/com/rey/material/drawable/LinearProgressDrawable.java new file mode 100644 index 0000000..45d2cc3 --- /dev/null +++ b/material/src/main/java/com/rey/material/drawable/LinearProgressDrawable.java @@ -0,0 +1,1092 @@ +package com.rey.material.drawable; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.DashPathEffect; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PathEffect; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; +import android.os.SystemClock; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.animation.AnimationUtils; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + +import com.rey.material.R; +import com.rey.material.util.ColorUtil; +import com.rey.material.util.ThemeUtil; +import com.rey.material.util.ViewUtil; +import com.rey.material.widget.ProgressView; + +public class LinearProgressDrawable extends Drawable implements Animatable { + + private long mLastUpdateTime; + private long mLastProgressStateTime; + private long mLastRunStateTime; + + private int mProgressState; + + private static final int PROGRESS_STATE_STRETCH = 0; + private static final int PROGRESS_STATE_KEEP_STRETCH = 1; + private static final int PROGRESS_STATE_SHRINK = 2; + private static final int PROGRESS_STATE_KEEP_SHRINK = 3; + + private int mRunState = RUN_STATE_STOPPED; + + private static final int RUN_STATE_STOPPED = 0; + private static final int RUN_STATE_STARTING = 1; + private static final int RUN_STATE_STARTED = 2; + private static final int RUN_STATE_RUNNING = 3; + private static final int RUN_STATE_STOPPING = 4; + + public static final int ALIGN_TOP = 0; + public static final int ALIGN_CENTER = 1; + public static final int ALIGN_BOTTOM = 2; + + private Paint mPaint; + private float mStartLine; + private float mLineWidth; + private int mStrokeColorIndex; + private float mAnimTime; + + private Path mPath; + private DashPathEffect mPathEffect; + + private float mProgressPercent; + private float mSecondaryProgressPercent; + private int mMaxLineWidth; + private float mMaxLineWidthPercent; + private int mMinLineWidth; + private float mMinLineWidthPercent; + private int mStrokeSize; + private int mVerticalAlign; + private int[] mStrokeColors; + private int mStrokeSecondaryColor; + private boolean mReverse; + private int mTravelDuration; + private int mTransformDuration; + private int mKeepDuration; + private int mInAnimationDuration; + private int mOutAnimationDuration; + private int mProgressMode; + private Interpolator mTransformInterpolator; + + private LinearProgressDrawable(float progressPercent, float secondaryProgressPercent, int maxLineWidth, float maxLineWidthPercent, int minLineWidth, float minLineWidthPercent, int strokeSize, int verticalAlign, int[] strokeColors, int strokeSecondaryColor, boolean reverse, int travelDuration, int transformDuration, int keepDuration, Interpolator transformInterpolator, int progressMode, int inAnimDuration, int outAnimDuration){ + setProgress(progressPercent); + setSecondaryProgress(secondaryProgressPercent); + mMaxLineWidth = maxLineWidth; + mMaxLineWidthPercent = maxLineWidthPercent; + mMinLineWidth = minLineWidth; + mMinLineWidthPercent = minLineWidthPercent; + mStrokeSize = strokeSize; + mVerticalAlign = verticalAlign; + mStrokeColors = strokeColors; + mStrokeSecondaryColor = strokeSecondaryColor; + mReverse = reverse; + mTravelDuration = travelDuration; + mTransformDuration = transformDuration; + mKeepDuration = keepDuration; + mTransformInterpolator = transformInterpolator; + mProgressMode = progressMode; + mInAnimationDuration = inAnimDuration; + mOutAnimationDuration = outAnimDuration; + + mPaint = new Paint(); + mPaint.setAntiAlias(true); + mPaint.setStrokeCap(Paint.Cap.ROUND); + mPaint.setStrokeJoin(Paint.Join.ROUND); + + mPath = new Path(); + } + + public void applyStyle(Context context, int resId){ + TypedArray a = context.obtainStyledAttributes(resId, R.styleable.LinearProgressDrawable); + + int strokeColor = 0; + boolean strokeColorDefined = false; + int[] strokeColors = null; + + for(int i = 0, count = a.getIndexCount(); i < count; i++){ + int attr = a.getIndex(i); + + if(attr == R.styleable.LinearProgressDrawable_pv_progress) + setProgress(a.getFloat(attr, 0)); + else if(attr == R.styleable.LinearProgressDrawable_pv_secondaryProgress) + setSecondaryProgress(a.getFloat(attr, 0)); + else if(attr == R.styleable.LinearProgressDrawable_lpd_maxLineWidth){ + TypedValue value = a.peekValue(attr); + if(value.type == TypedValue.TYPE_FRACTION) { + mMaxLineWidthPercent = a.getFraction(attr, 1, 1, 0.75f); + mMaxLineWidth = 0; + } + else { + mMaxLineWidth = a.getDimensionPixelSize(attr, 0); + mMaxLineWidthPercent = 0f; + } + } + else if(attr == R.styleable.LinearProgressDrawable_lpd_minLineWidth){ + TypedValue value = a.peekValue(attr); + if(value.type == TypedValue.TYPE_FRACTION) { + mMinLineWidthPercent = a.getFraction(attr, 1, 1, 0.25f); + mMinLineWidth = 0; + } + else { + mMinLineWidth = a.getDimensionPixelSize(attr, 0); + mMinLineWidthPercent = 0f; + } + } + else if(attr == R.styleable.LinearProgressDrawable_lpd_strokeSize) + mStrokeSize = a.getDimensionPixelSize(attr, 0); + else if(attr == R.styleable.LinearProgressDrawable_lpd_verticalAlign) + mVerticalAlign = a.getInteger(attr, 0); + else if(attr == R.styleable.LinearProgressDrawable_lpd_strokeColor) { + strokeColor = a.getColor(attr, 0); + strokeColorDefined = true; + } + else if(attr == R.styleable.LinearProgressDrawable_lpd_strokeColors){ + TypedArray ta = context.getResources().obtainTypedArray(a.getResourceId(attr, 0)); + strokeColors = new int[ta.length()]; + for(int j = 0; j < ta.length(); j++) + strokeColors[j] = ta.getColor(j, 0); + ta.recycle(); + } + else if(attr == R.styleable.LinearProgressDrawable_lpd_strokeSecondaryColor) + mStrokeSecondaryColor = a.getColor(attr, 0); + else if(attr == R.styleable.LinearProgressDrawable_lpd_reverse) + mReverse = a.getBoolean(attr, false); + else if(attr == R.styleable.LinearProgressDrawable_lpd_travelDuration) + mTravelDuration = a.getInteger(attr, 0); + else if(attr == R.styleable.LinearProgressDrawable_lpd_transformDuration) + mTransformDuration = a.getInteger(attr, 0); + else if(attr == R.styleable.LinearProgressDrawable_lpd_keepDuration) + mKeepDuration = a.getInteger(attr, 0); + else if(attr == R.styleable.LinearProgressDrawable_lpd_transformInterpolator) + mTransformInterpolator = AnimationUtils.loadInterpolator(context, a.getResourceId(attr, 0)); + else if(attr == R.styleable.LinearProgressDrawable_pv_progressMode) + mProgressMode = a.getInteger(attr, 0); + else if(attr == R.styleable.LinearProgressDrawable_lpd_inAnimDuration) + mInAnimationDuration = a.getInteger(attr, 0); + else if(attr == R.styleable.LinearProgressDrawable_lpd_outAnimDuration) + mOutAnimationDuration = a.getInteger(attr, 0); + } + + a.recycle(); + + if(strokeColors != null) + mStrokeColors = strokeColors; + else if(strokeColorDefined) + mStrokeColors = new int[]{strokeColor}; + + if(mStrokeColorIndex >= mStrokeColors.length) + mStrokeColorIndex = 0; + + invalidateSelf(); + } + + @Override + public void draw(Canvas canvas) { + switch (mProgressMode) { + case ProgressView.MODE_DETERMINATE: + drawDeterminate(canvas); + break; + case ProgressView.MODE_INDETERMINATE: + drawIndeterminate(canvas); + break; + case ProgressView.MODE_BUFFER: + drawBuffer(canvas); + break; + case ProgressView.MODE_QUERY: + drawQuery(canvas); + break; + } + } + + private void drawLinePath(Canvas canvas, float x1, float y1, float x2, float y2, Paint paint){ + mPath.reset(); + mPath.moveTo(x1, y1); + mPath.lineTo(x2, y2); + canvas.drawPath(mPath, paint); + } + + private void drawDeterminate(Canvas canvas){ + Rect bounds = getBounds(); + int width = bounds.width(); + float size = 0f; + + if(mRunState == RUN_STATE_STARTING) + size = (float)mStrokeSize * Math.min(mInAnimationDuration, (SystemClock.uptimeMillis() - mLastRunStateTime)) / mInAnimationDuration; + else if(mRunState == RUN_STATE_STOPPING) + size = (float)mStrokeSize * Math.max(0, (mOutAnimationDuration - SystemClock.uptimeMillis() + mLastRunStateTime)) / mOutAnimationDuration; + else if(mRunState != RUN_STATE_STOPPED) + size = mStrokeSize; + + if(size > 0){ + float y = 0; + float lineWidth = width * mProgressPercent; + + switch (mVerticalAlign) { + case ALIGN_TOP: + y = size / 2; + break; + case ALIGN_CENTER: + y = bounds.height() / 2f; + break; + case ALIGN_BOTTOM: + y = bounds.height() - size / 2; + break; + } + + mPaint.setStrokeWidth(size); + mPaint.setStyle(Paint.Style.STROKE); + + if(mProgressPercent != 1f){ + mPaint.setColor(mStrokeSecondaryColor); + + if(mReverse) + canvas.drawLine(0, y, width - lineWidth, y, mPaint); + else + canvas.drawLine(lineWidth, y, width, y, mPaint); + } + + if(mProgressPercent != 0f){ + mPaint.setColor(mStrokeColors[0]); + if(mReverse) + drawLinePath(canvas, width - lineWidth, y, width, y, mPaint); + else + drawLinePath(canvas, 0, y, lineWidth, y, mPaint); + } + } + } + + private int getIndeterminateStrokeColor(){ + if(mProgressState != PROGRESS_STATE_KEEP_SHRINK || mStrokeColors.length == 1) + return mStrokeColors[mStrokeColorIndex]; + + float value = Math.max(0f, Math.min(1f, (float)(SystemClock.uptimeMillis() - mLastProgressStateTime) / mKeepDuration)); + int prev_index = mStrokeColorIndex == 0 ? mStrokeColors.length - 1 : mStrokeColorIndex - 1; + + return ColorUtil.getMiddleColor(mStrokeColors[prev_index], mStrokeColors[mStrokeColorIndex], value); + } + + private void drawIndeterminate(Canvas canvas){ + Rect bounds = getBounds(); + int width = bounds.width(); + float size = 0f; + + if(mRunState == RUN_STATE_STARTING) + size = (float)mStrokeSize * Math.min(mInAnimationDuration, (SystemClock.uptimeMillis() - mLastRunStateTime)) / mInAnimationDuration; + else if(mRunState == RUN_STATE_STOPPING) + size = (float)mStrokeSize * Math.max(0, (mOutAnimationDuration - SystemClock.uptimeMillis() + mLastRunStateTime)) / mOutAnimationDuration; + else if(mRunState != RUN_STATE_STOPPED) + size = mStrokeSize; + + if(size > 0){ + float y = 0; + + switch (mVerticalAlign) { + case ALIGN_TOP: + y = size / 2; + break; + case ALIGN_CENTER: + y = bounds.height() / 2f; + break; + case ALIGN_BOTTOM: + y = bounds.height() - size / 2; + break; + } + + mPaint.setStrokeWidth(size); + mPaint.setStyle(Paint.Style.STROKE); + + float endLine = offset(mStartLine, mLineWidth, width); + + if(mReverse){ + if(endLine <= mStartLine){ + mPaint.setColor(mStrokeSecondaryColor); + if(endLine > 0) + canvas.drawLine(0, y, endLine, y, mPaint); + if(mStartLine < width) + canvas.drawLine(mStartLine, y, width, y, mPaint); + + mPaint.setColor(getIndeterminateStrokeColor()); + drawLinePath(canvas, endLine, y, mStartLine, y, mPaint); + } + else{ + mPaint.setColor(mStrokeSecondaryColor); + canvas.drawLine(mStartLine, y, endLine, y, mPaint); + + mPaint.setColor(getIndeterminateStrokeColor()); + mPath.reset(); + + if(mStartLine > 0){ + mPath.moveTo(0, y); + mPath.lineTo(mStartLine, y); + } + if(endLine < width){ + mPath.moveTo(endLine, y); + mPath.lineTo(width, y); + } + + canvas.drawPath(mPath, mPaint); + } + } + else{ + if(endLine >= mStartLine){ + mPaint.setColor(mStrokeSecondaryColor); + if(mStartLine > 0) + canvas.drawLine(0, y, mStartLine, y, mPaint); + if(endLine < width) + canvas.drawLine(endLine, y, width, y, mPaint); + + mPaint.setColor(getIndeterminateStrokeColor()); + drawLinePath(canvas, mStartLine, y, endLine, y, mPaint); + } + else{ + mPaint.setColor(mStrokeSecondaryColor); + canvas.drawLine(endLine, y, mStartLine, y, mPaint); + + mPaint.setColor(getIndeterminateStrokeColor()); + mPath.reset(); + + if(endLine > 0){ + mPath.moveTo(0, y); + mPath.lineTo(endLine, y); + } + if(mStartLine < width){ + mPath.moveTo(mStartLine, y); + mPath.lineTo(width, y); + } + + canvas.drawPath(mPath, mPaint); + } + } + } + } + + private PathEffect getPathEffect(){ + if(mPathEffect == null) + mPathEffect = new DashPathEffect(new float[]{0.1f, mStrokeSize * 2}, 0f); + + return mPathEffect; + } + + private void drawBuffer(Canvas canvas){ + Rect bounds = getBounds(); + int width = bounds.width(); + float size = 0f; + + if(mRunState == RUN_STATE_STARTING) + size = (float)mStrokeSize * Math.min(mInAnimationDuration, (SystemClock.uptimeMillis() - mLastRunStateTime)) / mInAnimationDuration; + else if(mRunState == RUN_STATE_STOPPING) + size = (float)mStrokeSize * Math.max(0, (mOutAnimationDuration - SystemClock.uptimeMillis() + mLastRunStateTime)) / mOutAnimationDuration; + else if(mRunState != RUN_STATE_STOPPED) + size = mStrokeSize; + + if(size > 0){ + float y = 0; + float lineWidth = width * mProgressPercent; + float secondaryLineWidth = width * mSecondaryProgressPercent; + + switch (mVerticalAlign) { + case ALIGN_TOP: + y = size / 2; + break; + case ALIGN_CENTER: + y = bounds.height() / 2f; + break; + case ALIGN_BOTTOM: + y = bounds.height() - size / 2; + break; + } + + mPaint.setStyle(Paint.Style.STROKE); + + if(mProgressPercent != 1f){ + mPaint.setStrokeWidth(size); + mPaint.setColor(mStrokeSecondaryColor); + mPaint.setPathEffect(null); + + if(mReverse) + drawLinePath(canvas, width - secondaryLineWidth, y, width - lineWidth, y, mPaint); + else + drawLinePath(canvas, secondaryLineWidth, y, lineWidth, y, mPaint); + + mPaint.setStrokeWidth(mLineWidth); + mPaint.setPathEffect(getPathEffect()); + float offset = mStrokeSize * 2 - mStartLine; + + if(mReverse) + drawLinePath(canvas, -offset, y, width - secondaryLineWidth, y, mPaint); + else + drawLinePath(canvas, width + offset, y, secondaryLineWidth, y, mPaint); + } + + if(mProgressPercent != 0f){ + mPaint.setStrokeWidth(size); + mPaint.setColor(mStrokeColors[0]); + mPaint.setPathEffect(null); + + if(mReverse) + drawLinePath(canvas, width - lineWidth, y, width, y, mPaint); + else + drawLinePath(canvas, 0, y, lineWidth, y, mPaint); + } + } + } + + private int getQueryStrokeColor(){ + return ColorUtil.getColor(mStrokeColors[0], mAnimTime); + } + + private void drawQuery(Canvas canvas){ + Rect bounds = getBounds(); + int width = bounds.width(); + float size = 0f; + + if(mRunState == RUN_STATE_STARTING) + size = (float)mStrokeSize * Math.min(mInAnimationDuration, (SystemClock.uptimeMillis() - mLastRunStateTime)) / mInAnimationDuration; + else if(mRunState == RUN_STATE_STOPPING) + size = (float)mStrokeSize * Math.max(0, (mOutAnimationDuration - SystemClock.uptimeMillis() + mLastRunStateTime)) / mOutAnimationDuration; + else if(mRunState != RUN_STATE_STOPPED) + size = mStrokeSize; + + if(size > 0){ + float y = 0; + + switch (mVerticalAlign) { + case ALIGN_TOP: + y = size / 2; + break; + case ALIGN_CENTER: + y = bounds.height() / 2f; + break; + case ALIGN_BOTTOM: + y = bounds.height() - size / 2; + break; + } + + mPaint.setStrokeWidth(size); + mPaint.setStyle(Paint.Style.STROKE); + + if(mProgressPercent != 1f){ + mPaint.setColor(mStrokeSecondaryColor); + canvas.drawLine(0, y, width, y, mPaint); + + if(mAnimTime < 1f){ + float endLine = Math.max(0, Math.min(width, mStartLine + mLineWidth)); + mPaint.setColor(getQueryStrokeColor()); + drawLinePath(canvas, mStartLine, y, endLine, y, mPaint); + } + } + + if(mProgressPercent != 0f){ + float lineWidth = width * mProgressPercent; + mPaint.setColor(mStrokeColors[0]); + + if(mReverse) + drawLinePath(canvas, width - lineWidth, y, width, y, mPaint); + else + drawLinePath(canvas, 0, y, lineWidth, y, mPaint); + } + + } + } + + @Override + public void setAlpha(int alpha) { + mPaint.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter cf) { + mPaint.setColorFilter(cf); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + public int getProgressMode(){ + return mProgressMode; + } + + public void setProgressMode(int mode){ + if(mProgressMode != mode) { + mProgressMode = mode; + invalidateSelf(); + } + } + + public float getProgress(){ + return mProgressPercent; + } + + public float getSecondaryProgress(){ + return mSecondaryProgressPercent; + } + + public void setProgress(float percent){ + percent = Math.min(1f, Math.max(0f, percent)); + if(mProgressPercent != percent){ + mProgressPercent = percent; + if(isRunning()) + invalidateSelf(); + else if(mProgressPercent != 0f) + start(); + } + } + + public void setSecondaryProgress(float percent){ + percent = Math.min(1f, Math.max(0f, percent)); + if(mSecondaryProgressPercent != percent){ + mSecondaryProgressPercent = percent; + if(isRunning()) + invalidateSelf(); + else if(mSecondaryProgressPercent != 0f) + start(); + } + } + + //Animation: based on http://cyrilmottier.com/2012/11/27/actionbar-on-the-move/ + + private void resetAnimation(){ + mLastUpdateTime = SystemClock.uptimeMillis(); + mLastProgressStateTime = mLastUpdateTime; + if(mProgressMode == ProgressView.MODE_INDETERMINATE){ + mStartLine = mReverse ? getBounds().width() : 0; + mStrokeColorIndex = 0; + mLineWidth = mReverse ? -mMinLineWidth : mMinLineWidth; + mProgressState = PROGRESS_STATE_STRETCH; + } + else if(mProgressMode == ProgressView.MODE_BUFFER){ + mStartLine = 0; + } + else if(mProgressMode == ProgressView.MODE_QUERY){ + mStartLine = !mReverse ? getBounds().width() : 0; + mStrokeColorIndex = 0; + mLineWidth = !mReverse ? -mMaxLineWidth : mMaxLineWidth; + } + } + + @Override + public void start() { + start(mInAnimationDuration > 0); + } + + @Override + public void stop() { + stop(mOutAnimationDuration > 0); + } + + private void start(boolean withAnimation){ + if(isRunning()) + return; + + if(withAnimation){ + mRunState = RUN_STATE_STARTING; + mLastRunStateTime = SystemClock.uptimeMillis(); + } + + resetAnimation(); + + scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + invalidateSelf(); + } + + private void stop(boolean withAnimation){ + if(!isRunning()) + return; + + if(withAnimation){ + mLastRunStateTime = SystemClock.uptimeMillis(); + + if(mRunState == RUN_STATE_STARTED){ + scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + invalidateSelf(); + } + mRunState = RUN_STATE_STOPPING; + } + else{ + mRunState = RUN_STATE_STOPPED; + unscheduleSelf(mUpdater); + invalidateSelf(); + } + } + + @Override + public boolean isRunning() { + return mRunState != RUN_STATE_STOPPED; + } + + @Override + public void scheduleSelf(Runnable what, long when) { + if(mRunState == RUN_STATE_STOPPED) + mRunState = mInAnimationDuration > 0 ? RUN_STATE_STARTING : RUN_STATE_RUNNING; + super.scheduleSelf(what, when); + } + + private final Runnable mUpdater = new Runnable() { + + @Override + public void run() { + update(); + } + + }; + + private void update(){ + switch (mProgressMode) { + case ProgressView.MODE_DETERMINATE: + updateDeterminate(); + break; + case ProgressView.MODE_INDETERMINATE: + updateIndeterminate(); + break; + case ProgressView.MODE_BUFFER: + updateBuffer(); + break; + case ProgressView.MODE_QUERY: + updateQuery(); + break; + } + } + + private void updateDeterminate(){ + long curTime = SystemClock.uptimeMillis(); + + if(mRunState == RUN_STATE_STARTING){ + if(curTime - mLastRunStateTime > mInAnimationDuration){ + mRunState = RUN_STATE_STARTED; + return; + } + } + else if(mRunState == RUN_STATE_STOPPING){ + if(curTime - mLastRunStateTime > mOutAnimationDuration){ + stop(false); + return; + } + } + + if(isRunning()) + scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + + invalidateSelf(); + } + + private float offset(float pos, float offset, float max){ + pos += offset; + if(pos > max) + return pos - max; + if(pos < 0) + return max + pos; + return pos; + } + + private void updateIndeterminate(){ + Rect bounds = getBounds(); + int width = bounds.width(); + + long curTime = SystemClock.uptimeMillis(); + float travelOffset = (float)(curTime - mLastUpdateTime) * width / mTravelDuration; + if(mReverse) + travelOffset = -travelOffset; + mLastUpdateTime = curTime; + + switch (mProgressState) { + case PROGRESS_STATE_STRETCH: + if(mTransformDuration <= 0){ + mLineWidth = mMinLineWidth == 0 ? width * mMinLineWidthPercent : mMinLineWidth; + if(mReverse) + mLineWidth = -mLineWidth; + mStartLine = offset(mStartLine, travelOffset, width); + mProgressState = PROGRESS_STATE_KEEP_STRETCH; + mLastProgressStateTime = curTime; + } + else{ + float value = (curTime - mLastProgressStateTime) / (float)mTransformDuration; + float maxWidth = mMaxLineWidth == 0 ? width * mMaxLineWidthPercent : mMaxLineWidth; + float minWidth = mMinLineWidth == 0 ? width * mMinLineWidthPercent : mMinLineWidth; + + mStartLine = offset(mStartLine, travelOffset, width); + mLineWidth = mTransformInterpolator.getInterpolation(value) * (maxWidth - minWidth) + minWidth; + if(mReverse) + mLineWidth = -mLineWidth; + + if(value > 1f){ + mLineWidth = mReverse ? -maxWidth : maxWidth; + mProgressState = PROGRESS_STATE_KEEP_STRETCH; + mLastProgressStateTime = curTime; + } + } + break; + case PROGRESS_STATE_KEEP_STRETCH: + mStartLine = offset(mStartLine, travelOffset, width); + + if(curTime - mLastProgressStateTime > mKeepDuration){ + mProgressState = PROGRESS_STATE_SHRINK; + mLastProgressStateTime = curTime; + } + break; + case PROGRESS_STATE_SHRINK: + if(mTransformDuration <= 0){ + mLineWidth = mMinLineWidth == 0 ? width * mMinLineWidthPercent : mMinLineWidth; + if(mReverse) + mLineWidth = -mLineWidth; + mStartLine = offset(mStartLine, travelOffset, width); + mProgressState = PROGRESS_STATE_KEEP_SHRINK; + mLastProgressStateTime = curTime; + mStrokeColorIndex = (mStrokeColorIndex + 1) % mStrokeColors.length; + } + else{ + float value = (curTime - mLastProgressStateTime) / (float)mTransformDuration; + float maxWidth = mMaxLineWidth == 0 ? width * mMaxLineWidthPercent : mMaxLineWidth; + float minWidth = mMinLineWidth == 0 ? width * mMinLineWidthPercent : mMinLineWidth; + + float newLineWidth = (1f - mTransformInterpolator.getInterpolation(value)) * (maxWidth - minWidth) + minWidth; + if(mReverse) + newLineWidth = -newLineWidth; + + mStartLine = offset(mStartLine, travelOffset + mLineWidth - newLineWidth, width); + mLineWidth = newLineWidth; + + if(value > 1f){ + mLineWidth = mReverse ? -minWidth : minWidth; + mProgressState = PROGRESS_STATE_KEEP_SHRINK; + mLastProgressStateTime = curTime; + mStrokeColorIndex = (mStrokeColorIndex + 1) % mStrokeColors.length; + } + } + break; + case PROGRESS_STATE_KEEP_SHRINK: + mStartLine = offset(mStartLine, travelOffset, width); + + if(curTime - mLastProgressStateTime > mKeepDuration){ + mProgressState = PROGRESS_STATE_STRETCH; + mLastProgressStateTime = curTime; + } + break; + } + + if(mRunState == RUN_STATE_STARTING){ + if(curTime - mLastRunStateTime > mInAnimationDuration) + mRunState = RUN_STATE_RUNNING; + } + else if(mRunState == RUN_STATE_STOPPING){ + if(curTime - mLastRunStateTime > mOutAnimationDuration){ + stop(false); + return; + } + } + + if (isRunning()) + scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + + invalidateSelf(); + } + + private void updateBuffer(){ + long curTime = SystemClock.uptimeMillis(); + float maxDistance = mStrokeSize * 2; + mStartLine += maxDistance * (float)(curTime - mLastUpdateTime) / mTravelDuration; + while(mStartLine > maxDistance) + mStartLine -= maxDistance; + mLastUpdateTime = curTime; + + switch (mProgressState) { + case PROGRESS_STATE_STRETCH: + if(mTransformDuration <= 0){ + mProgressState = PROGRESS_STATE_KEEP_STRETCH; + mLastProgressStateTime = curTime; + } + else{ + float value = (curTime - mLastProgressStateTime) / (float)mTransformDuration; + mLineWidth = mTransformInterpolator.getInterpolation(value) * mStrokeSize; + + if(value > 1f){ + mLineWidth = mStrokeSize; + mProgressState = PROGRESS_STATE_KEEP_STRETCH; + mLastProgressStateTime = curTime; + } + } + break; + case PROGRESS_STATE_KEEP_STRETCH: + if(curTime - mLastProgressStateTime > mKeepDuration){ + mProgressState = PROGRESS_STATE_SHRINK; + mLastProgressStateTime = curTime; + } + break; + case PROGRESS_STATE_SHRINK: + if(mTransformDuration <= 0){ + mProgressState = PROGRESS_STATE_KEEP_SHRINK; + mLastProgressStateTime = curTime; + } + else{ + float value = (curTime - mLastProgressStateTime) / (float)mTransformDuration; + mLineWidth = (1f - mTransformInterpolator.getInterpolation(value)) * mStrokeSize; + + if(value > 1f){ + mLineWidth = 0; + mProgressState = PROGRESS_STATE_KEEP_SHRINK; + mLastProgressStateTime = curTime; + } + } + break; + case PROGRESS_STATE_KEEP_SHRINK: + if(curTime - mLastProgressStateTime > mKeepDuration){ + mProgressState = PROGRESS_STATE_STRETCH; + mLastProgressStateTime = curTime; + } + break; + } + + if(mRunState == RUN_STATE_STARTING){ + if(curTime - mLastRunStateTime > mInAnimationDuration) + mRunState = RUN_STATE_RUNNING; + } + else if(mRunState == RUN_STATE_STOPPING){ + if(curTime - mLastRunStateTime > mOutAnimationDuration){ + stop(false); + return; + } + } + + if(isRunning()) + scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + + invalidateSelf(); + } + + private void updateQuery(){ + long curTime = SystemClock.uptimeMillis(); + mAnimTime = (float)(curTime - mLastProgressStateTime) / mTravelDuration; + boolean requestUpdate = mRunState == RUN_STATE_STOPPING || mProgressPercent == 0 || mAnimTime < 1f; + + if(mAnimTime > 1f){ + mLastProgressStateTime = Math.round(curTime - (mAnimTime - 1f) * mTravelDuration); + mAnimTime -= 1f; + } + + if(requestUpdate && mRunState != RUN_STATE_STOPPING){ + Rect bounds = getBounds(); + int width = bounds.width(); + + float maxWidth = mMaxLineWidth == 0 ? width * mMaxLineWidthPercent : mMaxLineWidth; + float minWidth = mMinLineWidth == 0 ? width * mMinLineWidthPercent : mMinLineWidth; + mLineWidth = mTransformInterpolator.getInterpolation(mAnimTime) * (minWidth - maxWidth) + maxWidth; + if(mReverse) + mLineWidth = -mLineWidth; + + mStartLine = mReverse ? mTransformInterpolator.getInterpolation(mAnimTime) * (width + minWidth) : ((1f - mTransformInterpolator.getInterpolation(mAnimTime)) * (width + minWidth) - minWidth); + } + + if(mRunState == RUN_STATE_STARTING){ + if(curTime - mLastRunStateTime > mInAnimationDuration) + mRunState = RUN_STATE_RUNNING; + } + else if(mRunState == RUN_STATE_STOPPING){ + if(curTime - mLastRunStateTime > mOutAnimationDuration){ + stop(false); + return; + } + } + + if (isRunning()){ + if(requestUpdate) + scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + else if(mRunState == RUN_STATE_RUNNING) + mRunState = RUN_STATE_STARTED; + } + + invalidateSelf(); + } + + public static class Builder{ + private float mProgressPercent = 0; + private float mSecondaryProgressPercent = 0; + private int mMaxLineWidth; + private float mMaxLineWidthPercent; + private int mMinLineWidth; + private float mMinLineWidthPercent; + private int mStrokeSize = 8; + private int mVerticalAlign = LinearProgressDrawable.ALIGN_BOTTOM; + private int[] mStrokeColors; + private int mStrokeSecondaryColor; + private boolean mReverse = false; + private int mTravelDuration = 1000; + private int mTransformDuration = 800; + private int mKeepDuration = 200; + private Interpolator mTransformInterpolator; + private int mProgressMode = ProgressView.MODE_INDETERMINATE; + private int mInAnimationDuration = 400; + private int mOutAnimationDuration = 400; + + public Builder(){} + + public Builder(Context context, int defStyleRes){ + this(context, null, 0, defStyleRes); + } + + public Builder(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LinearProgressDrawable, defStyleAttr, defStyleRes); + int resId; + + progressPercent(a.getFloat(R.styleable.LinearProgressDrawable_pv_progress, 0)); + secondaryProgressPercent(a.getFloat(R.styleable.LinearProgressDrawable_pv_secondaryProgress, 0)); + + TypedValue value = a.peekValue(R.styleable.LinearProgressDrawable_lpd_maxLineWidth); + if(value == null) + maxLineWidth(0.75f); + else if(value.type == TypedValue.TYPE_FRACTION) + maxLineWidth(a.getFraction(R.styleable.LinearProgressDrawable_lpd_maxLineWidth, 1, 1, 0.75f)); + else + maxLineWidth(a.getDimensionPixelSize(R.styleable.LinearProgressDrawable_lpd_maxLineWidth, 0)); + + value = a.peekValue(R.styleable.LinearProgressDrawable_lpd_minLineWidth); + if(value == null) + minLineWidth(0.25f); + else if(value.type == TypedValue.TYPE_FRACTION) + minLineWidth(a.getFraction(R.styleable.LinearProgressDrawable_lpd_minLineWidth, 1, 1, 0.25f)); + else + minLineWidth(a.getDimensionPixelSize(R.styleable.LinearProgressDrawable_lpd_minLineWidth, 0)); + + strokeSize(a.getDimensionPixelSize(R.styleable.LinearProgressDrawable_lpd_strokeSize, ThemeUtil.dpToPx(context, 4))); + verticalAlign(a.getInteger(R.styleable.LinearProgressDrawable_lpd_verticalAlign, LinearProgressDrawable.ALIGN_BOTTOM)); + strokeColors(a.getColor(R.styleable.LinearProgressDrawable_lpd_strokeColor, ThemeUtil.colorPrimary(context, 0xFF000000))); + if((resId = a.getResourceId(R.styleable.LinearProgressDrawable_lpd_strokeColors, 0)) != 0){ + TypedArray ta = context.getResources().obtainTypedArray(resId); + int[] colors = new int[ta.length()]; + for(int j = 0; j < ta.length(); j++) + colors[j] = ta.getColor(j, 0); + ta.recycle(); + strokeColors(colors); + } + strokeSecondaryColor(a.getColor(R.styleable.LinearProgressDrawable_lpd_strokeSecondaryColor, 0)); + reverse(a.getBoolean(R.styleable.LinearProgressDrawable_lpd_reverse, false)); + travelDuration(a.getInteger(R.styleable.LinearProgressDrawable_lpd_travelDuration, context.getResources().getInteger(android.R.integer.config_longAnimTime))); + transformDuration(a.getInteger(R.styleable.LinearProgressDrawable_lpd_transformDuration, context.getResources().getInteger(android.R.integer.config_mediumAnimTime))); + keepDuration(a.getInteger(R.styleable.LinearProgressDrawable_lpd_keepDuration, context.getResources().getInteger(android.R.integer.config_shortAnimTime))); + if((resId = a.getResourceId(R.styleable.LinearProgressDrawable_lpd_transformInterpolator, 0)) != 0) + transformInterpolator(AnimationUtils.loadInterpolator(context, resId)); + progressMode(a.getInteger(R.styleable.LinearProgressDrawable_pv_progressMode, ProgressView.MODE_INDETERMINATE)); + inAnimDuration(a.getInteger(R.styleable.LinearProgressDrawable_lpd_inAnimDuration, context.getResources().getInteger(android.R.integer.config_mediumAnimTime))); + outAnimDuration(a.getInteger(R.styleable.LinearProgressDrawable_lpd_outAnimDuration, context.getResources().getInteger(android.R.integer.config_mediumAnimTime))); + + a.recycle(); + } + + public LinearProgressDrawable build(){ + if(mStrokeColors == null) + mStrokeColors = new int[]{0xFF0099FF}; + + if(mTransformInterpolator == null) + mTransformInterpolator = new DecelerateInterpolator(); + + return new LinearProgressDrawable(mProgressPercent, mSecondaryProgressPercent, mMaxLineWidth, mMaxLineWidthPercent, mMinLineWidth, mMinLineWidthPercent, mStrokeSize, mVerticalAlign, mStrokeColors, mStrokeSecondaryColor, mReverse, mTravelDuration, mTransformDuration, mKeepDuration, mTransformInterpolator, mProgressMode, mInAnimationDuration, mOutAnimationDuration); + } + + public Builder secondaryProgressPercent(float percent){ + mSecondaryProgressPercent = percent; + return this; + } + + public Builder progressPercent(float percent){ + mProgressPercent = percent; + return this; + } + + public Builder maxLineWidth(int width){ + mMaxLineWidth = width; + return this; + } + + public Builder maxLineWidth(float percent){ + mMaxLineWidthPercent = Math.max(0f, Math.min(1f, percent)); + mMaxLineWidth = 0; + return this; + } + + public Builder minLineWidth(int width){ + mMinLineWidth = width; + return this; + } + + public Builder minLineWidth(float percent){ + mMinLineWidthPercent = Math.max(0f, Math.min(1f, percent)); + mMinLineWidth = 0; + return this; + } + + public Builder strokeSize(int strokeSize){ + mStrokeSize = strokeSize; + return this; + } + + public Builder verticalAlign(int align){ + mVerticalAlign = align; + return this; + } + + public Builder strokeColors(int... strokeColors){ + mStrokeColors = strokeColors; + return this; + } + + public Builder strokeSecondaryColor(int color){ + mStrokeSecondaryColor = color; + return this; + } + + public Builder reverse(boolean reverse){ + mReverse = reverse; + return this; + } + + public Builder reverse(){ + return reverse(true); + } + + public Builder travelDuration(int duration){ + mTravelDuration = duration; + return this; + } + + public Builder transformDuration(int duration){ + mTransformDuration = duration; + return this; + } + + public Builder keepDuration(int duration){ + mKeepDuration = duration; + return this; + } + + public Builder transformInterpolator(Interpolator interpolator){ + mTransformInterpolator = interpolator; + return this; + } + + public Builder progressMode(int mode){ + mProgressMode = mode; + return this; + } + + public Builder inAnimDuration(int duration){ + mInAnimationDuration = duration; + return this; + } + + public Builder outAnimDuration(int duration){ + mOutAnimationDuration = duration; + return this; + } + + } +} diff --git a/material/src/main/java/com/rey/material/drawable/NavigationDrawerDrawable.java b/material/src/main/java/com/rey/material/drawable/NavigationDrawerDrawable.java new file mode 100644 index 0000000..4e5206e --- /dev/null +++ b/material/src/main/java/com/rey/material/drawable/NavigationDrawerDrawable.java @@ -0,0 +1,151 @@ +package com.rey.material.drawable; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.PixelFormat; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; + +import com.rey.material.R; + +public class NavigationDrawerDrawable extends Drawable implements Drawable.Callback{ + + private ToolbarRippleDrawable mRippleDrawable; + private LineMorphingDrawable mLineDrawable; + + public static final int STATE_DRAWER = 0; + public static final int STATE_ARROW = 1; + + public NavigationDrawerDrawable(ToolbarRippleDrawable rippleDrawable, LineMorphingDrawable lineDrawable){ + mRippleDrawable = rippleDrawable; + mLineDrawable = lineDrawable; + + mRippleDrawable.setCallback(this); + mLineDrawable.setCallback(this); + } + + public void switchIconState(int state, boolean animation){ + mLineDrawable.switchLineState(state, animation); + } + + public int getIconState(){ + return mLineDrawable.getLineState(); + } + + public boolean setIconState(int state, float progress){ + return mLineDrawable.setLineState(state, progress); + } + + public float getIconAnimProgress(){ + return mLineDrawable.getAnimProgress(); + } + + @Override + public void draw(Canvas canvas) { + mRippleDrawable.draw(canvas); + mLineDrawable.draw(canvas); + } + + @Override + public void setAlpha(int alpha) { + mRippleDrawable.setAlpha(alpha); + mLineDrawable.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter cf) { + mRippleDrawable.setColorFilter(cf); + mLineDrawable.setColorFilter(cf); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + @Override + public void setBounds(int left, int top, int right, int bottom) { + mRippleDrawable.setBounds(left, top, right, bottom); + mLineDrawable.setBounds(left, top, right, bottom); + } + + @Override + public void setDither(boolean dither) { + mRippleDrawable.setDither(dither); + mLineDrawable.setDither(dither); + } + + @Override + public void invalidateDrawable(Drawable who) { + invalidateSelf(); + } + + @Override + public void scheduleDrawable(Drawable who, Runnable what, long when) { + scheduleSelf(what, when); + } + + @Override + public void unscheduleDrawable(Drawable who, Runnable what) { + unscheduleSelf(what); + } + + @Override + public boolean isStateful() { + return true; + } + + @Override + protected boolean onStateChange(int[] state) { + return mRippleDrawable.onStateChange(state); + } + + public static class Builder{ + private ToolbarRippleDrawable mRippleDrawable; + private LineMorphingDrawable mLineDrawable; + + public Builder(){} + + public Builder(Context context, int defStyleRes){ + this(context, null, 0, defStyleRes); + } + + public Builder(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NavigationDrawerDrawable, defStyleAttr, defStyleRes); + + if(a != null){ + int rippleId = a.getResourceId(R.styleable.NavigationDrawerDrawable_nd_ripple, 0); + int lineId = a.getResourceId(R.styleable.NavigationDrawerDrawable_nd_icon, 0); + + if(rippleId > 0) + ripple(new ToolbarRippleDrawable.Builder(context, rippleId).build()); + + if(lineId > 0){ + LineMorphingDrawable.Builder builder = new LineMorphingDrawable.Builder(context, lineId); + line(builder.build()); + } + + a.recycle(); + } + } + + public NavigationDrawerDrawable build(){ + return new NavigationDrawerDrawable(mRippleDrawable, mLineDrawable); + } + + public Builder ripple(ToolbarRippleDrawable drawable){ + mRippleDrawable = drawable; + + return this; + } + + public Builder line(LineMorphingDrawable drawable){ + mLineDrawable = drawable; + + return this; + } + + } +} diff --git a/material/src/main/java/com/rey/material/drawable/OvalShadowDrawable.java b/material/src/main/java/com/rey/material/drawable/OvalShadowDrawable.java new file mode 100644 index 0000000..87e2aa9 --- /dev/null +++ b/material/src/main/java/com/rey/material/drawable/OvalShadowDrawable.java @@ -0,0 +1,357 @@ +package com.rey.material.drawable; + +import android.content.res.ColorStateList; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PixelFormat; +import android.graphics.RadialGradient; +import android.graphics.RectF; +import android.graphics.Shader; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; +import android.os.SystemClock; + +import com.rey.material.util.ColorUtil; +import com.rey.material.util.ViewUtil; + +public class OvalShadowDrawable extends Drawable implements Animatable { + + private boolean mRunning = false; + private long mStartTime; + private float mAnimProgress; + private int mAnimDuration; + + private boolean mEnable = true; + private boolean mInEditMode = false; + private boolean mAnimEnable = true; + + private Paint mShadowPaint; + private Paint mGlowPaint; + private Paint mPaint; + + private int mRadius; + private float mShadowSize; + private float mShadowOffset; + + private Path mShadowPath; + private Path mGlowPath; + + private RectF mTempRect = new RectF(); + + private ColorStateList mColorStateList; + private int mPrevColor; + private int mCurColor; + + private boolean mNeedBuildShadow = true; + + private static final int COLOR_SHADOW_START = 0x4C000000; + private static final int COLOR_SHADOW_END = 0x00000000; + + public OvalShadowDrawable(int radius, ColorStateList colorStateList, float shadowSize, float shadowOffset, int animDuration){ + mAnimDuration = animDuration; + + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); + mPaint.setStyle(Paint.Style.FILL); + + setColor(colorStateList); + setRadius(radius); + setShadow(shadowSize, shadowOffset); + } + + public boolean setRadius(int radius){ + if(mRadius != radius){ + mRadius = radius; + mNeedBuildShadow = true; + invalidateSelf(); + + return true; + } + + return false; + } + + public boolean setShadow(float size, float offset){ + if(mShadowSize != size || mShadowOffset != offset){ + mShadowSize = size; + mShadowOffset = offset; + mNeedBuildShadow = true; + invalidateSelf(); + + return true; + } + + return false; + } + + public boolean setAnimationDuration(int duration){ + if(mAnimDuration != duration){ + mAnimDuration = duration; + return true; + } + + return false; + } + + public void setColor(ColorStateList colorStateList){ + mColorStateList = colorStateList; + onStateChange(getState()); + } + + public void setColor(int color){ + mColorStateList = ColorStateList.valueOf(color); + onStateChange(getState()); + } + + public ColorStateList getColor(){ + return mColorStateList; + } + + public void setInEditMode(boolean b){ + mInEditMode = b; + } + + public void setAnimEnable(boolean b){ + mAnimEnable = b; + } + + public int getRadius(){ + return mRadius; + } + + public float getShadowSize(){ + return mShadowSize; + } + + public float getShadowOffset(){ + return mShadowOffset; + } + + public float getPaddingLeft(){ + return mShadowSize; + } + + public float getPaddingTop(){ + return mShadowSize; + } + + public float getPaddingRight(){ + return mShadowSize; + } + + public float getPaddingBottom(){ + return mShadowSize + mShadowOffset; + } + + public float getCenterX(){ + return mRadius + mShadowSize; + } + + public float getCenterY(){ + return mRadius + mShadowSize; + } + + public boolean isPointerOver(float x, float y){ + float distance = (float)Math.sqrt(Math.pow(x - getCenterX(), 2) + Math.pow(y - getCenterY(), 2)); + + return distance < mRadius; + } + + @Override + public int getIntrinsicWidth() { + return (int)((mRadius + mShadowSize) * 2 + 0.5f); + } + + @Override + public int getIntrinsicHeight() { + return (int)((mRadius + mShadowSize) * 2 + mShadowOffset + 0.5f); + } + + private void buildShadow(){ + if(mShadowSize <= 0) + return; + + if(mShadowPaint == null){ + mShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); + mShadowPaint.setStyle(Paint.Style.FILL); + mShadowPaint.setDither(true); + } + float startRatio = (float)mRadius / (mRadius + mShadowSize + mShadowOffset); + mShadowPaint.setShader(new RadialGradient(0, 0, mRadius + mShadowSize, + new int[]{COLOR_SHADOW_START, COLOR_SHADOW_START, COLOR_SHADOW_END}, + new float[]{0f, startRatio, 1f} + , Shader.TileMode.CLAMP)); + + if(mShadowPath == null){ + mShadowPath = new Path(); + mShadowPath.setFillType(Path.FillType.EVEN_ODD); + } + else + mShadowPath.reset(); + float radius = mRadius + mShadowSize; + mTempRect.set(-radius, -radius, radius, radius); + mShadowPath.addOval(mTempRect, Path.Direction.CW); + radius = mRadius - 1; + mTempRect.set(-radius, -radius - mShadowOffset, radius, radius - mShadowOffset); + mShadowPath.addOval(mTempRect, Path.Direction.CW); + + if(mGlowPaint == null){ + mGlowPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); + mGlowPaint.setStyle(Paint.Style.FILL); + mGlowPaint.setDither(true); + } + startRatio = (mRadius - mShadowSize / 2f) / (mRadius + mShadowSize / 2f); + mGlowPaint.setShader(new RadialGradient(0, 0, mRadius + mShadowSize / 2f, + new int[]{COLOR_SHADOW_START, COLOR_SHADOW_START, COLOR_SHADOW_END}, + new float[]{0f, startRatio, 1f} + , Shader.TileMode.CLAMP)); + + if(mGlowPath == null){ + mGlowPath = new Path(); + mGlowPath.setFillType(Path.FillType.EVEN_ODD); + } + else + mGlowPath.reset(); + + radius = mRadius + mShadowSize / 2f; + mTempRect.set(-radius, -radius, radius, radius); + mGlowPath.addOval(mTempRect, Path.Direction.CW); + radius = mRadius - 1; + mTempRect.set(-radius, -radius, radius, radius); + mGlowPath.addOval(mTempRect, Path.Direction.CW); + } + + @Override + public void draw(Canvas canvas) { + if(mNeedBuildShadow){ + buildShadow(); + mNeedBuildShadow = false; + } + int saveCount; + + if(mShadowSize > 0){ + saveCount = canvas.save(); + canvas.translate(mShadowSize + mRadius, mShadowSize + mRadius + mShadowOffset); + canvas.drawPath(mShadowPath, mShadowPaint); + canvas.restoreToCount(saveCount); + } + + saveCount = canvas.save(); + canvas.translate(mShadowSize + mRadius, mShadowSize + mRadius); + if(mShadowSize > 0) + canvas.drawPath(mGlowPath, mGlowPaint); + mTempRect.set(-mRadius, -mRadius, mRadius, mRadius); + if(!isRunning()) + mPaint.setColor(mCurColor); + else + mPaint.setColor(ColorUtil.getMiddleColor(mPrevColor, mCurColor, mAnimProgress)); + canvas.drawOval(mTempRect, mPaint); + canvas.restoreToCount(saveCount); + } + + @Override + public void setAlpha(int alpha) { + mShadowPaint.setAlpha(alpha); + mPaint.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter cf) { + mShadowPaint.setColorFilter(cf); + mPaint.setColorFilter(cf); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + @Override + public boolean isStateful() { + return true; + } + + @Override + protected boolean onStateChange(int[] state) { + mEnable = ViewUtil.hasState(state, android.R.attr.state_enabled); + int color = mColorStateList.getColorForState(state, mCurColor); + + if(mCurColor != color){ + if(!mInEditMode && mAnimEnable && mEnable && mAnimDuration > 0){ + mPrevColor = isRunning() ? mPrevColor : mCurColor; + mCurColor = color; + start(); + } + else{ + mPrevColor = color; + mCurColor = color; + invalidateSelf(); + } + return true; + } + else if(!isRunning()) + mPrevColor = color; + + return false; + } + + @Override + public void jumpToCurrentState() { + super.jumpToCurrentState(); + stop(); + } + + private void resetAnimation(){ + mStartTime = SystemClock.uptimeMillis(); + mAnimProgress = 0f; + } + + @Override + public void start() { + resetAnimation(); + scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + invalidateSelf(); + } + + @Override + public void stop() { + mRunning = false; + unscheduleSelf(mUpdater); + invalidateSelf(); + } + + @Override + public boolean isRunning() { + return mRunning; + } + + @Override + public void scheduleSelf(Runnable what, long when) { + mRunning = true; + super.scheduleSelf(what, when); + } + + private final Runnable mUpdater = new Runnable() { + + @Override + public void run() { + update(); + } + + }; + + private void update(){ + long curTime = SystemClock.uptimeMillis(); + mAnimProgress = Math.min(1f, (float)(curTime - mStartTime) / mAnimDuration); + + if(mAnimProgress == 1f) + mRunning = false; + + if(isRunning()) + scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + + invalidateSelf(); + } + +} diff --git a/material/src/main/java/com/rey/material/drawable/PaddingDrawable.java b/material/src/main/java/com/rey/material/drawable/PaddingDrawable.java new file mode 100644 index 0000000..b2a3db3 --- /dev/null +++ b/material/src/main/java/com/rey/material/drawable/PaddingDrawable.java @@ -0,0 +1,258 @@ +package com.rey.material.drawable; + +import android.content.res.ColorStateList; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.PixelFormat; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.Region; +import android.graphics.drawable.Drawable; +import android.support.v4.graphics.drawable.DrawableCompat; + +public class PaddingDrawable extends Drawable implements Drawable.Callback { + + private Drawable mDrawable; + + private int mPaddingLeft; + private int mPaddingTop; + private int mPaddingRight; + private int mPaddingBottom; + + public PaddingDrawable(Drawable drawable) { + setWrappedDrawable(drawable); + } + + public void setPadding(int paddingLeft, int paddingTop, int paddingRight, int paddingBottom){ + mPaddingLeft = paddingLeft; + mPaddingTop = paddingTop; + mPaddingRight = paddingRight; + mPaddingBottom = paddingBottom; + } + + public int getPaddingLeft(){ + return mPaddingLeft; + } + + public int getPaddingTop(){ + return mPaddingTop; + } + + public int getPaddingRight(){ + return mPaddingRight; + } + + public int getPaddingBottom(){ + return mPaddingBottom; + } + + @Override + public void draw(Canvas canvas) { + if(mDrawable != null) + mDrawable.draw(canvas); + } + + @Override + protected void onBoundsChange(Rect bounds) { + if(mDrawable != null) + mDrawable.setBounds(bounds.left + mPaddingLeft, bounds.top + mPaddingTop, bounds.right - mPaddingRight, bounds.bottom - mPaddingBottom); + } + + @Override + public void setChangingConfigurations(int configs) { + if(mDrawable != null) + mDrawable.setChangingConfigurations(configs); + } + + @Override + public int getChangingConfigurations() { + return mDrawable != null ? mDrawable.getChangingConfigurations() : 0; + } + + @Override + public void setDither(boolean dither) { + if(mDrawable != null) + mDrawable.setDither(dither); + } + + @Override + public void setFilterBitmap(boolean filter) { + if(mDrawable != null) + mDrawable.setFilterBitmap(filter); + } + + @Override + public void setAlpha(int alpha) { + if(mDrawable != null) + mDrawable.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter cf) { + if(mDrawable != null) + mDrawable.setColorFilter(cf); + } + + @Override + public boolean isStateful() { + return mDrawable != null && mDrawable.isStateful(); + } + + @Override + public boolean setState(final int[] stateSet) { + return mDrawable != null && mDrawable.setState(stateSet); + } + + @Override + public int[] getState() { + return mDrawable != null ? mDrawable.getState() : null; + } + + public void jumpToCurrentState() { + if(mDrawable != null) + DrawableCompat.jumpToCurrentState(mDrawable); + } + + @Override + public Drawable getCurrent() { + return mDrawable != null ? mDrawable.getCurrent() : null; + } + + @Override + public boolean setVisible(boolean visible, boolean restart) { + return super.setVisible(visible, restart) || (mDrawable != null && mDrawable.setVisible(visible, restart)); + } + + @Override + public int getOpacity() { + return mDrawable != null ? mDrawable.getOpacity() : PixelFormat.UNKNOWN; + } + + @Override + public Region getTransparentRegion() { + return mDrawable != null ? mDrawable.getTransparentRegion() : null; + } + + @Override + public int getIntrinsicWidth() { + return (mDrawable != null ? mDrawable.getIntrinsicWidth() : 0) + mPaddingLeft + mPaddingRight; + } + + @Override + public int getIntrinsicHeight() { + return (mDrawable != null ? mDrawable.getIntrinsicHeight() : 0) + mPaddingTop + mPaddingBottom; + } + + @Override + public int getMinimumWidth() { + return (mDrawable != null ? mDrawable.getMinimumWidth() : 0) + mPaddingLeft + mPaddingRight; + } + + @Override + public int getMinimumHeight() { + return (mDrawable != null ? mDrawable.getMinimumHeight() : 0) + mPaddingTop + mPaddingBottom; + } + + @Override + public boolean getPadding(Rect padding) { + boolean hasPadding = mDrawable != null && mDrawable.getPadding(padding); + if(hasPadding){ + padding.left += mPaddingLeft; + padding.top += mPaddingTop; + padding.right += mPaddingRight; + padding.bottom += mPaddingBottom; + } + else{ + padding.set(mPaddingLeft, mPaddingTop, mPaddingRight, mPaddingBottom); + hasPadding = mPaddingLeft != 0 || mPaddingTop != 0 || mPaddingRight != 0 || mPaddingBottom != 0; + } + + return hasPadding; + } + + /** + * {@inheritDoc} + */ + public void invalidateDrawable(Drawable who) { + invalidateSelf(); + } + + /** + * {@inheritDoc} + */ + public void scheduleDrawable(Drawable who, Runnable what, long when) { + scheduleSelf(what, when); + } + + /** + * {@inheritDoc} + */ + public void unscheduleDrawable(Drawable who, Runnable what) { + unscheduleSelf(what); + } + + @Override + protected boolean onLevelChange(int level) { + return mDrawable != null && mDrawable.setLevel(level); + } + + @Override + public void setAutoMirrored(boolean mirrored) { + if(mDrawable != null) + DrawableCompat.setAutoMirrored(mDrawable, mirrored); + } + + @Override + public boolean isAutoMirrored() { + return mDrawable != null && DrawableCompat.isAutoMirrored(mDrawable); + } + + @Override + public void setTint(int tint) { + if(mDrawable != null) + DrawableCompat.setTint(mDrawable, tint); + } + + @Override + public void setTintList(ColorStateList tint) { + if(mDrawable != null) + DrawableCompat.setTintList(mDrawable, tint); + } + + @Override + public void setTintMode(PorterDuff.Mode tintMode) { + if(mDrawable != null) + DrawableCompat.setTintMode(mDrawable, tintMode); + } + + @Override + public void setHotspot(float x, float y) { + if(mDrawable != null) + DrawableCompat.setHotspot(mDrawable, x, y); + } + + @Override + public void setHotspotBounds(int left, int top, int right, int bottom) { + if(mDrawable != null) + DrawableCompat.setHotspotBounds(mDrawable, left, top, right, bottom); + } + + public Drawable getWrappedDrawable() { + return mDrawable; + } + + public void setWrappedDrawable(Drawable drawable) { + if (mDrawable != null) { + mDrawable.setCallback(null); + } + + mDrawable = drawable; + + if (drawable != null) { + drawable.setCallback(this); + } + + onBoundsChange(getBounds()); + invalidateSelf(); + } +} diff --git a/material/src/main/java/com/rey/material/drawable/RadioButtonDrawable.java b/material/src/main/java/com/rey/material/drawable/RadioButtonDrawable.java new file mode 100644 index 0000000..d4d3637 --- /dev/null +++ b/material/src/main/java/com/rey/material/drawable/RadioButtonDrawable.java @@ -0,0 +1,366 @@ +package com.rey.material.drawable; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; +import android.os.SystemClock; +import android.util.AttributeSet; + +import com.rey.material.R; +import com.rey.material.util.ColorUtil; +import com.rey.material.util.ThemeUtil; +import com.rey.material.util.ViewUtil; + +public class RadioButtonDrawable extends Drawable implements Animatable { + + private boolean mRunning = false; + + private Paint mPaint; + + private long mStartTime; + private float mAnimProgress; + private int mAnimDuration; + private int mStrokeSize; + private int mWidth; + private int mHeight; + private int mRadius; + private int mInnerRadius; + private int mPrevColor; + private int mCurColor; + private ColorStateList mStrokeColor; + private boolean mChecked = false; + + private boolean mInEditMode = false; + private boolean mAnimEnable = true; + + private RadioButtonDrawable(int width, int height, int strokeSize, ColorStateList strokeColor, int radius, int innerRadius, int animDuration){ + mAnimDuration = animDuration; + mStrokeSize = strokeSize; + mWidth = width; + mHeight = height; + mRadius = radius; + mInnerRadius = innerRadius; + mStrokeColor = strokeColor; + + mPaint = new Paint(); + mPaint.setAntiAlias(true); + } + + public void setInEditMode(boolean b){ + mInEditMode = b; + } + + public void setAnimEnable(boolean b){ + mAnimEnable = b; + } + + public boolean isAnimEnable(){ + return mAnimEnable; + } + + @Override + public int getIntrinsicWidth() { + return mWidth; + } + + @Override + public int getIntrinsicHeight() { + return mHeight; + } + + @Override + public int getMinimumWidth() { + return mWidth; + } + + @Override + public int getMinimumHeight() { + return mHeight; + } + + @Override + public boolean isStateful() { + return true; + } + + @Override + public void draw(Canvas canvas) { + if(mChecked) + drawChecked(canvas); + else + drawUnchecked(canvas); + } + + private void drawChecked(Canvas canvas){ + float cx = getBounds().exactCenterX(); + float cy = getBounds().exactCenterY(); + + if(isRunning()){ + float halfStrokeSize = mStrokeSize / 2f; + float inTime = (mRadius - halfStrokeSize) / (mRadius - halfStrokeSize + mRadius - mStrokeSize - mInnerRadius); + + if(mAnimProgress < inTime){ + float inProgress = mAnimProgress / inTime; + float outerRadius = mRadius + halfStrokeSize * (1f - inProgress); + float innerRadius = (mRadius - halfStrokeSize) * (1f - inProgress); + + mPaint.setColor(ColorUtil.getMiddleColor(mPrevColor, mCurColor, inProgress)); + mPaint.setStrokeWidth(outerRadius - innerRadius); + mPaint.setStyle(Paint.Style.STROKE); + canvas.drawCircle(cx, cy, (outerRadius + innerRadius) / 2, mPaint); + } + else{ + float outProgress = (mAnimProgress - inTime) / (1f - inTime); + float innerRadius = (mRadius - mStrokeSize) * (1 - outProgress) + mInnerRadius * outProgress; + + mPaint.setColor(mCurColor); + mPaint.setStyle(Paint.Style.FILL); + canvas.drawCircle(cx, cy, innerRadius, mPaint); + + float outerRadius = mRadius + halfStrokeSize * outProgress; + mPaint.setStrokeWidth(mStrokeSize); + mPaint.setStyle(Paint.Style.STROKE); + canvas.drawCircle(cx, cy, outerRadius - halfStrokeSize, mPaint); + } + } + else{ + mPaint.setColor(mCurColor); + mPaint.setStrokeWidth(mStrokeSize); + mPaint.setStyle(Paint.Style.STROKE); + canvas.drawCircle(cx, cy, mRadius, mPaint); + + mPaint.setStyle(Paint.Style.FILL); + canvas.drawCircle(cx, cy, mInnerRadius, mPaint); + } + } + + private void drawUnchecked(Canvas canvas){ + float cx = getBounds().exactCenterX(); + float cy = getBounds().exactCenterY(); + + if(isRunning()){ + float halfStrokeSize = mStrokeSize / 2f; + float inTime = (mRadius - mStrokeSize - mInnerRadius) / (mRadius - halfStrokeSize + mRadius - mStrokeSize - mInnerRadius); + + if(mAnimProgress < inTime){ + float inProgress = mAnimProgress / inTime; + float innerRadius = (mRadius - mStrokeSize) * inProgress + mInnerRadius * (1f - inProgress); + + mPaint.setColor(ColorUtil.getMiddleColor(mPrevColor, mCurColor, inProgress)); + mPaint.setStyle(Paint.Style.FILL); + canvas.drawCircle(cx, cy, innerRadius, mPaint); + + float outerRadius = mRadius + halfStrokeSize * (1f - inProgress); + mPaint.setStrokeWidth(mStrokeSize); + mPaint.setStyle(Paint.Style.STROKE); + canvas.drawCircle(cx, cy, outerRadius - halfStrokeSize, mPaint); + } + else{ + float outProgress = (mAnimProgress - inTime) / (1f - inTime); + float outerRadius = mRadius + halfStrokeSize * outProgress; + float innerRadius = (mRadius - halfStrokeSize) * outProgress; + + mPaint.setColor(mCurColor); + mPaint.setStrokeWidth(outerRadius - innerRadius); + mPaint.setStyle(Paint.Style.STROKE); + canvas.drawCircle(cx, cy, (outerRadius + innerRadius) / 2, mPaint); + } + } + else{ + mPaint.setColor(mCurColor); + mPaint.setStrokeWidth(mStrokeSize); + mPaint.setStyle(Paint.Style.STROKE); + canvas.drawCircle(cx, cy, mRadius, mPaint); + } + } + + @Override + protected boolean onStateChange(int[] state) { + boolean checked = ViewUtil.hasState(state, android.R.attr.state_checked); + int color = mStrokeColor.getColorForState(state, mCurColor); + boolean needRedraw = false; + + if(mChecked != checked){ + mChecked = checked; + needRedraw = true; + if(!mInEditMode && mAnimEnable) + start(); + } + + if(mCurColor != color){ + mPrevColor = isRunning() ? mCurColor : color; + mCurColor = color; + needRedraw = true; + } + else if(!isRunning()) + mPrevColor = color; + + return needRedraw; + } + + @Override + public void setAlpha(int alpha) { + mPaint.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter cf) { + mPaint.setColorFilter(cf); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + //Animation: based on http://cyrilmottier.com/2012/11/27/actionbar-on-the-move/ + + private void resetAnimation(){ + mStartTime = SystemClock.uptimeMillis(); + mAnimProgress = 0f; + } + + @Override + public void start() { + resetAnimation(); + + scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + invalidateSelf(); + } + + @Override + public void stop() { + mRunning = false; + unscheduleSelf(mUpdater); + invalidateSelf(); + } + + @Override + public boolean isRunning() { + return mRunning; + } + + @Override + public void scheduleSelf(Runnable what, long when) { + mRunning = true; + super.scheduleSelf(what, when); + } + + private final Runnable mUpdater = new Runnable() { + + @Override + public void run() { + update(); + } + + }; + + private void update(){ + long curTime = SystemClock.uptimeMillis(); + mAnimProgress = Math.min(1f, (float)(curTime - mStartTime) / mAnimDuration); + + if(mAnimProgress == 1f) + mRunning = false; + + if(isRunning()) + scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + + invalidateSelf(); + } + + public static class Builder{ + + private int mAnimDuration = 400; + private int mStrokeSize = 4; + private int mWidth = 64; + private int mHeight = 64; + private int mRadius = 18; + private int mInnerRadius = 10; + private ColorStateList mStrokeColor; + + public Builder(){} + + public Builder(Context context, int defStyleRes){ + this(context, null, 0, defStyleRes); + } + + public Builder(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RadioButtonDrawable, defStyleAttr, defStyleRes); + + width(a.getDimensionPixelSize(R.styleable.RadioButtonDrawable_rbd_width, ThemeUtil.dpToPx(context, 32))); + height(a.getDimensionPixelSize(R.styleable.RadioButtonDrawable_rbd_height, ThemeUtil.dpToPx(context, 32))); + strokeSize(a.getDimensionPixelSize(R.styleable.RadioButtonDrawable_rbd_strokeSize, ThemeUtil.dpToPx(context, 2))); + radius(a.getDimensionPixelSize(R.styleable.RadioButtonDrawable_rbd_radius, ThemeUtil.dpToPx(context, 10))); + innerRadius(a.getDimensionPixelSize(R.styleable.RadioButtonDrawable_rbd_innerRadius, ThemeUtil.dpToPx(context, 5))); + strokeColor(a.getColorStateList(R.styleable.RadioButtonDrawable_rbd_strokeColor)); + animDuration(a.getInt(R.styleable.RadioButtonDrawable_rbd_animDuration, context.getResources().getInteger(android.R.integer.config_mediumAnimTime))); + + a.recycle(); + + if(mStrokeColor == null){ + int[][] states = new int[][]{ + new int[]{-android.R.attr.state_checked}, + new int[]{android.R.attr.state_checked}, + }; + int[] colors = new int[]{ + ThemeUtil.colorControlNormal(context, 0xFF000000), + ThemeUtil.colorControlActivated(context, 0xFF000000), + }; + strokeColor(new ColorStateList(states, colors)); + } + } + + public RadioButtonDrawable build(){ + if(mStrokeColor == null) + mStrokeColor = ColorStateList.valueOf(0xFF000000); + + return new RadioButtonDrawable(mWidth, mHeight, mStrokeSize, mStrokeColor, mRadius, mInnerRadius, mAnimDuration); + } + + public Builder width(int width){ + mWidth = width; + return this; + } + + public Builder height(int height){ + mHeight = height; + return this; + } + + public Builder strokeSize(int size){ + mStrokeSize = size; + return this; + } + + public Builder strokeColor(int color){ + mStrokeColor = ColorStateList.valueOf(color); + return this; + } + + public Builder strokeColor(ColorStateList color){ + mStrokeColor = color; + return this; + } + + public Builder radius(int radius){ + mRadius = radius; + return this; + } + + public Builder innerRadius(int radius){ + mInnerRadius = radius; + return this; + } + + public Builder animDuration(int duration){ + mAnimDuration = duration; + return this; + } + } +} diff --git a/material/src/main/java/com/rey/material/drawable/RevealDrawable.java b/material/src/main/java/com/rey/material/drawable/RevealDrawable.java new file mode 100644 index 0000000..a24ebbe --- /dev/null +++ b/material/src/main/java/com/rey/material/drawable/RevealDrawable.java @@ -0,0 +1,294 @@ +package com.rey.material.drawable; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.RadialGradient; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Shader; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; +import android.os.SystemClock; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + +import com.rey.material.util.ColorUtil; +import com.rey.material.util.ViewUtil; + +public class RevealDrawable extends Drawable implements Animatable { + + private boolean mRunning = false; + private long mStartTime; + private float mAnimProgress; + + private Paint mShaderPaint; + private Paint mFillPaint; + private int mCurColor; + private RadialGradient mShader; + private Matrix mMatrix; + private RectF mRect; + private float mMaxRadius; + + private ColorChangeTask[] mTasks; + private int mCurTask; + + private boolean mCurColorTransparent; + private boolean mNextColorTransparent; + + private static final float[] GRADIENT_STOPS = new float[]{0f, 0.99f, 1f}; + private static final float GRADIENT_RADIUS = 16; + + public RevealDrawable(int color){ + mShaderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mShaderPaint.setStyle(Paint.Style.FILL); + + mFillPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mFillPaint.setStyle(Paint.Style.FILL); + + mCurColor = color; + + mRect = new RectF(); + + mMatrix = new Matrix(); + } + + public int getCurColor(){ + return mCurColor; + } + + public void setCurColor(int color){ + if(mCurColor != color){ + mCurColor = color; + mCurColorTransparent = Color.alpha(mCurColor) == 0; + invalidateSelf(); + } + } + + private float getMaxRadius(float x, float y, Rect bounds){ + float x1 = x < bounds.centerX() ? bounds.right : bounds.left; + float y1 = y < bounds.centerY() ? bounds.bottom : bounds.top; + + return (float)Math.sqrt(Math.pow(x1 - x, 2) + Math.pow(y1 - y, 2)); + } + + private RadialGradient getShader(ColorChangeTask task){ + if(mShader == null){ + if(task.isOut){ + int color_middle = ColorUtil.getColor(mCurColor, 0f); + mShader = new RadialGradient(task.x, task.y, GRADIENT_RADIUS, new int[]{0, color_middle, mCurColor}, GRADIENT_STOPS, Shader.TileMode.CLAMP); + } + else{ + int color_middle = ColorUtil.getColor(task.color, 0f); + mShader = new RadialGradient(task.x, task.y, GRADIENT_RADIUS, new int[]{0, color_middle, task.color}, GRADIENT_STOPS, Shader.TileMode.CLAMP); + } + } + + return mShader; + } + + private void fillCanvas(Canvas canvas, int color, boolean transparent){ + if(transparent) + return; + + mFillPaint.setColor(color); + canvas.drawRect(getBounds(), mFillPaint); + } + + private void fillCanvasWithHole(Canvas canvas, ColorChangeTask task, float radius, boolean transparent){ + if(transparent) + return; + + float scale = radius / GRADIENT_RADIUS; + + mMatrix.reset(); + mMatrix.postScale(scale, scale, task.x, task.y); + RadialGradient shader = getShader(task); + shader.setLocalMatrix(mMatrix); + mShaderPaint.setShader(shader); + canvas.drawRect(getBounds(), mShaderPaint); + } + + private void fillCircle(Canvas canvas, float x, float y, float radius, int color, boolean transparent){ + if(transparent) + return; + + mFillPaint.setColor(color); + mRect.set(x - radius, y - radius, x + radius, y + radius); + canvas.drawOval(mRect, mFillPaint); + } + + @Override + public void draw(Canvas canvas) { + if(!isRunning()) + fillCanvas(canvas, mCurColor, mCurColorTransparent); + else{ + ColorChangeTask task = mTasks[mCurTask]; + + if(mAnimProgress == 0f) + fillCanvas(canvas, mCurColor, mCurColorTransparent); + else if(mAnimProgress == 1f) + fillCanvas(canvas, task.color, mNextColorTransparent); + else if(task.isOut){ + float radius = mMaxRadius * task.interpolator.getInterpolation(mAnimProgress); + + if(Color.alpha(task.color) == 255) + fillCanvas(canvas, mCurColor, mCurColorTransparent); + else + fillCanvasWithHole(canvas, task, radius, mCurColorTransparent); + + fillCircle(canvas, task.x, task.y, radius, task.color, mNextColorTransparent); + } + else{ + float radius = mMaxRadius * task.interpolator.getInterpolation(mAnimProgress); + + if(Color.alpha(mCurColor) == 255) + fillCanvas(canvas, task.color, mNextColorTransparent); + else + fillCanvasWithHole(canvas, task, radius, mNextColorTransparent); + + fillCircle(canvas, task.x, task.y, radius, mCurColor, mCurColorTransparent); + } + } + } + + public void changeColor(int color, int duration, Interpolator interpolator, float x, float y, boolean out){ + changeColor(new ColorChangeTask(color, duration, interpolator, x, y, out)); + } + + public void changeColor(ColorChangeTask... tasks){ + synchronized (RevealDrawable.class){ + if(!isRunning()){ + for(int i = 0; i < tasks.length; i++) + if(tasks[i].color != mCurColor){ + mCurTask = i; + mTasks = tasks; + start(); + break; + } + } + else{ + int curLength = mTasks.length - mCurTask; + ColorChangeTask[] newTasks = new ColorChangeTask[curLength + tasks.length]; + System.arraycopy(mTasks, mCurTask, newTasks, 0, curLength); + System.arraycopy(tasks, 0, newTasks, curLength, tasks.length); + mTasks = newTasks; + mCurTask = 0; + } + } + } + + @Override + public void setAlpha(int alpha) { + mShaderPaint.setAlpha(alpha); + mFillPaint.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter cf) { + mShaderPaint.setColorFilter(cf); + mFillPaint.setColorFilter(cf); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + private void resetAnimation(){ + mStartTime = SystemClock.uptimeMillis(); + mAnimProgress = 0f; + mCurColorTransparent = Color.alpha(mCurColor) == 0; + mNextColorTransparent = Color.alpha(mTasks[mCurTask].color) == 0; + mMaxRadius = getMaxRadius(mTasks[mCurTask].x, mTasks[mCurTask].y, getBounds()); + mShader = null; + } + + @Override + public void start() { + if(isRunning()) + return; + + resetAnimation(); + + scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + invalidateSelf(); + } + + @Override + public void stop() { + if(!isRunning()) + return; + + mTasks = null; + mRunning = false; + unscheduleSelf(mUpdater); + invalidateSelf(); + } + + @Override + public boolean isRunning() { + return mRunning; + } + + @Override + public void scheduleSelf(Runnable what, long when) { + mRunning = true; + super.scheduleSelf(what, when); + } + + private final Runnable mUpdater = new Runnable() { + + @Override + public void run() { + update(); + } + + }; + + private void update(){ + long curTime = SystemClock.uptimeMillis(); + synchronized (RevealDrawable.class) { + mAnimProgress = Math.min(1f, (float) (curTime - mStartTime) / mTasks[mCurTask].duration); + + if (mAnimProgress == 1f) { + setCurColor(mTasks[mCurTask].color); + for (mCurTask = mCurTask + 1; mCurTask < mTasks.length; mCurTask++) + if (mTasks[mCurTask].color != mCurColor) { + resetAnimation(); + break; + } + + if (mCurTask == mTasks.length) + stop(); + } + } + + invalidateSelf(); + + if(isRunning()) + scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + } + + public static class ColorChangeTask{ + public final int color; + public final int duration; + public final Interpolator interpolator; + public final float x; + public final float y; + public final boolean isOut; + + public ColorChangeTask(int color, int duration, Interpolator interpolator, float x, float y, boolean out){ + this.color = color; + this.duration = duration; + this.interpolator = interpolator == null ? new DecelerateInterpolator() : interpolator; + this.x = x; + this.y = y; + this.isOut = out; + } + } + +} \ No newline at end of file diff --git a/material/src/main/java/com/rey/material/drawable/RippleDrawable.java b/material/src/main/java/com/rey/material/drawable/RippleDrawable.java new file mode 100644 index 0000000..0d1a736 --- /dev/null +++ b/material/src/main/java/com/rey/material/drawable/RippleDrawable.java @@ -0,0 +1,680 @@ +package com.rey.material.drawable; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Path.Direction; +import android.graphics.PixelFormat; +import android.graphics.PointF; +import android.graphics.RadialGradient; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Shader; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; +import android.os.SystemClock; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnTouchListener; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.AnimationUtils; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + +import com.rey.material.R; +import com.rey.material.util.ColorUtil; +import com.rey.material.util.ThemeUtil; +import com.rey.material.util.ViewUtil; + +public class RippleDrawable extends Drawable implements Animatable, OnTouchListener { + + private boolean mRunning = false; + + private Paint mShaderPaint; + private Paint mFillPaint; + private Mask mMask; + private RadialGradient mInShader; + private RadialGradient mOutShader; + private Matrix mMatrix; + private int mAlpha = 255; + + private Drawable mBackgroundDrawable; + private RectF mBackgroundBounds; + private Path mBackground; + private int mBackgroundAnimDuration; + private int mBackgroundColor; + private float mBackgroundAlphaPercent; + + private PointF mRipplePoint; + private float mRippleRadius; + private int mRippleType; + private int mMaxRippleRadius; + private int mRippleAnimDuration; + private int mRippleColor; + private float mRippleAlphaPercent; + private int mDelayClickType; + + private Interpolator mInInterpolator; + private Interpolator mOutInterpolator; + + private long mStartTime; + + private int mState = STATE_OUT; + + public static final int DELAY_CLICK_NONE = 0; + public static final int DELAY_CLICK_UNTIL_RELEASE = 1; + public static final int DELAY_CLICK_AFTER_RELEASE = 2; + + private static final int STATE_OUT = 0; + private static final int STATE_PRESS = 1; + private static final int STATE_HOVER = 2; + private static final int STATE_RELEASE_ON_HOLD = 3; + private static final int STATE_RELEASE = 4; + + private static final int TYPE_TOUCH_MATCH_VIEW = -1; + private static final int TYPE_TOUCH = 0; + private static final int TYPE_WAVE = 1; + + private static final float[] GRADIENT_STOPS = new float[]{0f, 0.99f, 1f}; + private static final float GRADIENT_RADIUS = 16; + + private RippleDrawable(Drawable backgroundDrawable, int backgroundAnimDuration, int backgroundColor, int rippleType, int delayClickType, int maxRippleRadius, int rippleAnimDuration, int rippleColor, Interpolator inInterpolator, Interpolator outInterpolator, int type, int topLeftCornerRadius, int topRightCornerRadius, int bottomRightCornerRadius, int bottomLeftCornerRadius, int left, int top, int right, int bottom){ + setBackgroundDrawable(backgroundDrawable); + mBackgroundAnimDuration = backgroundAnimDuration; + mBackgroundColor = backgroundColor; + + mRippleType = rippleType; + setDelayClickType(delayClickType); + mMaxRippleRadius = maxRippleRadius; + mRippleAnimDuration = rippleAnimDuration; + mRippleColor = rippleColor; + + if(mRippleType == TYPE_TOUCH && mMaxRippleRadius <= 0) + mRippleType = TYPE_TOUCH_MATCH_VIEW; + + mInInterpolator = inInterpolator; + mOutInterpolator = outInterpolator; + + setMask(type, topLeftCornerRadius, topRightCornerRadius, bottomRightCornerRadius, bottomLeftCornerRadius, left, top, right, bottom); + + mFillPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mFillPaint.setStyle(Paint.Style.FILL); + + mShaderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mShaderPaint.setStyle(Paint.Style.FILL); + + mBackground = new Path(); + mBackgroundBounds = new RectF(); + + mRipplePoint = new PointF(); + + mMatrix = new Matrix(); + + mInShader = new RadialGradient(0, 0, GRADIENT_RADIUS, new int[]{mRippleColor, mRippleColor, 0}, GRADIENT_STOPS, Shader.TileMode.CLAMP); + if(mRippleType == TYPE_WAVE) + mOutShader = new RadialGradient(0, 0, GRADIENT_RADIUS, new int[]{0, ColorUtil.getColor(mRippleColor, 0f), mRippleColor}, GRADIENT_STOPS, Shader.TileMode.CLAMP); + } + + public void setBackgroundDrawable(Drawable backgroundDrawable){ + mBackgroundDrawable = backgroundDrawable; + if(mBackgroundDrawable != null) + mBackgroundDrawable.setBounds(getBounds()); + } + + public Drawable getBackgroundDrawable(){ + return mBackgroundDrawable; + } + + public int getDelayClickType(){ + return mDelayClickType; + } + + public void setDelayClickType(int type){ + mDelayClickType = type; + } + + public void setMask(int type, int topLeftCornerRadius, int topRightCornerRadius, int bottomRightCornerRadius, int bottomLeftCornerRadius, int left, int top, int right, int bottom){ + mMask = new Mask(type, topLeftCornerRadius, topRightCornerRadius, bottomRightCornerRadius, bottomLeftCornerRadius, left, top, right, bottom); + } + + @Override + public void setAlpha(int alpha) { + mAlpha = alpha; + } + + @Override + public void setColorFilter(ColorFilter filter) { + mFillPaint.setColorFilter(filter); + mShaderPaint.setColorFilter(filter); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + public long getClickDelayTime(){ + switch (mDelayClickType){ + case DELAY_CLICK_NONE: + return -1; + case DELAY_CLICK_UNTIL_RELEASE: + if(mState == STATE_RELEASE_ON_HOLD) + return Math.max(mBackgroundAnimDuration, mRippleAnimDuration) - (SystemClock.uptimeMillis() - mStartTime); + break; + case DELAY_CLICK_AFTER_RELEASE: + if(mState == STATE_RELEASE_ON_HOLD) + return 2 * Math.max(mBackgroundAnimDuration, mRippleAnimDuration) - (SystemClock.uptimeMillis() - mStartTime); + else if(mState == STATE_RELEASE) + return Math.max(mBackgroundAnimDuration, mRippleAnimDuration) - (SystemClock.uptimeMillis() - mStartTime); + break; + } + + return -1; + } + + private void setRippleState(int state){ + if(mState != state){ + //fix bug incorrect state switch + if(mState == STATE_OUT && state != STATE_PRESS) + return; + +// Log.v(RippleDrawable.class.getSimpleName(), "state: " + mState + " " + state); + + mState = state; + + if(mState == STATE_OUT || mState == STATE_HOVER) + stop(); + else + start(); + } + } + + private boolean setRippleEffect(float x, float y, float radius){ + if(mRipplePoint.x != x || mRipplePoint.y != y || mRippleRadius != radius){ + mRipplePoint.set(x, y); + mRippleRadius = radius; + radius = mRippleRadius / GRADIENT_RADIUS; + mMatrix.reset(); + mMatrix.postTranslate(x, y); + mMatrix.postScale(radius, radius, x, y); + mInShader.setLocalMatrix(mMatrix); + if(mOutShader != null) + mOutShader.setLocalMatrix(mMatrix); + + return true; + } + + return false; + } + + @Override + protected void onBoundsChange(Rect bounds) { + if(mBackgroundDrawable != null) + mBackgroundDrawable.setBounds(bounds); + + mBackgroundBounds.set(bounds.left + mMask.left, bounds.top + mMask.top, bounds.right - mMask.right, bounds.bottom - mMask.bottom); + mBackground.reset(); + + switch (mMask.type) { + case Mask.TYPE_OVAL: + mBackground.addOval(mBackgroundBounds, Direction.CW); + break; + case Mask.TYPE_RECTANGLE: + mBackground.addRoundRect(mBackgroundBounds, mMask.cornerRadius, Direction.CW); + break; + } + } + + @Override + public boolean isStateful() { + return mBackgroundDrawable != null && mBackgroundDrawable.isStateful(); + } + + @Override + protected boolean onStateChange(int[] state) { + return mBackgroundDrawable != null && mBackgroundDrawable.setState(state); + + } + + @Override + public void draw(Canvas canvas) { + if(mBackgroundDrawable != null) + mBackgroundDrawable.draw(canvas); + + switch (mRippleType) { + case TYPE_TOUCH: + case TYPE_TOUCH_MATCH_VIEW: + drawTouch(canvas); + break; + case TYPE_WAVE: + drawWave(canvas); + break; + } + } + + private void drawTouch(Canvas canvas){ + if(mState != STATE_OUT){ + if(mBackgroundAlphaPercent > 0){ + mFillPaint.setColor(mBackgroundColor); + mFillPaint.setAlpha(Math.round(mAlpha * mBackgroundAlphaPercent)); + canvas.drawPath(mBackground, mFillPaint); + } + + if(mRippleRadius > 0 && mRippleAlphaPercent > 0){ + mShaderPaint.setAlpha(Math.round(mAlpha * mRippleAlphaPercent)); + mShaderPaint.setShader(mInShader); + canvas.drawPath(mBackground, mShaderPaint); + } + } + } + + private void drawWave(Canvas canvas){ + if(mState != STATE_OUT){ + if(mState == STATE_RELEASE){ + if(mRippleRadius == 0){ + mFillPaint.setColor(mRippleColor); + canvas.drawPath(mBackground, mFillPaint); + } + else{ + mShaderPaint.setShader(mOutShader); + canvas.drawPath(mBackground, mShaderPaint); + } + } + else if(mRippleRadius > 0){ + mShaderPaint.setShader(mInShader); + canvas.drawPath(mBackground, mShaderPaint); + } + } + } + + private int getMaxRippleRadius(float x, float y){ + float x1 = x < mBackgroundBounds.centerX() ? mBackgroundBounds.right : mBackgroundBounds.left; + float y1 = y < mBackgroundBounds.centerY() ? mBackgroundBounds.bottom : mBackgroundBounds.top; + + return (int)Math.round(Math.sqrt(Math.pow(x1 - x, 2) + Math.pow(y1 - y, 2))); + } + + @Override + public boolean onTouch(View v, MotionEvent event) { +// Log.v(RippleDrawable.class.getSimpleName(), "touch: " + event.getAction() + " " + mState); + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_MOVE: + if(mState == STATE_OUT || mState == STATE_RELEASE){ + if(mRippleType == TYPE_WAVE || mRippleType == TYPE_TOUCH_MATCH_VIEW) + mMaxRippleRadius = getMaxRippleRadius(event.getX(), event.getY()); + + setRippleEffect(event.getX(), event.getY(), 0); + setRippleState(STATE_PRESS); + } + else if(mRippleType == TYPE_TOUCH){ + if(setRippleEffect(event.getX(), event.getY(), mRippleRadius)) + invalidateSelf(); + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + if(mState != STATE_OUT){ + if(mState == STATE_HOVER){ + if(mRippleType == TYPE_WAVE || mRippleType == TYPE_TOUCH_MATCH_VIEW) + setRippleEffect(mRipplePoint.x, mRipplePoint.y, 0); + + setRippleState(STATE_RELEASE); + } + else + setRippleState(STATE_RELEASE_ON_HOLD); + } + break; + } + return true; + } + + //Animation: based on http://cyrilmottier.com/2012/11/27/actionbar-on-the-move/ + + public void cancel(){ + setRippleState(STATE_OUT); + } + + private void resetAnimation(){ + mStartTime = SystemClock.uptimeMillis(); + } + + @Override + public void start() { + if(isRunning()) + return; + + resetAnimation(); + scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + invalidateSelf(); + } + + @Override + public void stop() { + mRunning = false; + unscheduleSelf(mUpdater); + invalidateSelf(); + } + + @Override + public boolean isRunning() { + return mState != STATE_OUT && mState != STATE_HOVER && mRunning; + } + + @Override + public void scheduleSelf(Runnable what, long when) { + mRunning = true; + super.scheduleSelf(what, when); + } + + private final Runnable mUpdater = new Runnable() { + + @Override + public void run() { + switch (mRippleType) { + case TYPE_TOUCH: + case TYPE_TOUCH_MATCH_VIEW: + updateTouch(); + break; + case TYPE_WAVE: + updateWave(); + break; + } + } + + }; + + private void updateTouch(){ + if(mState != STATE_RELEASE){ + float backgroundProgress = Math.min(1f, (float)(SystemClock.uptimeMillis() - mStartTime) / mBackgroundAnimDuration); + mBackgroundAlphaPercent = mInInterpolator.getInterpolation(backgroundProgress) * Color.alpha(mBackgroundColor) / 255f; + + float touchProgress = Math.min(1f, (float)(SystemClock.uptimeMillis() - mStartTime) / mRippleAnimDuration); + mRippleAlphaPercent = mInInterpolator.getInterpolation(touchProgress); + + setRippleEffect(mRipplePoint.x, mRipplePoint.y, mMaxRippleRadius * mInInterpolator.getInterpolation(touchProgress)); + + if(backgroundProgress == 1f && touchProgress == 1f){ + mStartTime = SystemClock.uptimeMillis(); + setRippleState(mState == STATE_PRESS ? STATE_HOVER : STATE_RELEASE); + } + } + else{ + float backgroundProgress = Math.min(1f, (float)(SystemClock.uptimeMillis() - mStartTime) / mBackgroundAnimDuration); + mBackgroundAlphaPercent = (1f - mOutInterpolator.getInterpolation(backgroundProgress)) * Color.alpha(mBackgroundColor) / 255f; + + float touchProgress = Math.min(1f, (float)(SystemClock.uptimeMillis() - mStartTime) / mRippleAnimDuration); + mRippleAlphaPercent = 1f - mOutInterpolator.getInterpolation(touchProgress); + + setRippleEffect(mRipplePoint.x, mRipplePoint.y, mMaxRippleRadius * (1f + 0.5f * mOutInterpolator.getInterpolation(touchProgress))); + + if(backgroundProgress == 1f && touchProgress == 1f) + setRippleState(STATE_OUT); + } + + if(isRunning()) + scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + + invalidateSelf(); + } + + private void updateWave(){ + float progress = Math.min(1f, (float)(SystemClock.uptimeMillis() - mStartTime) / mRippleAnimDuration); + + if(mState != STATE_RELEASE){ + setRippleEffect(mRipplePoint.x, mRipplePoint.y, mMaxRippleRadius * mInInterpolator.getInterpolation(progress)); + + if(progress == 1f){ + mStartTime = SystemClock.uptimeMillis(); + if(mState == STATE_PRESS) + setRippleState(STATE_HOVER); + else{ + setRippleEffect(mRipplePoint.x, mRipplePoint.y, 0); + setRippleState(STATE_RELEASE); + } + } + } + else{ + setRippleEffect(mRipplePoint.x, mRipplePoint.y, mMaxRippleRadius * mOutInterpolator.getInterpolation(progress)); + + if(progress == 1f) + setRippleState(STATE_OUT); + } + + if(isRunning()) + scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + + invalidateSelf(); + } + + public static class Mask{ + + public static final int TYPE_RECTANGLE = 0; + public static final int TYPE_OVAL = 1; + + final int type; + + final float[] cornerRadius = new float[8]; + + final int left; + final int top; + final int right; + final int bottom; + + public Mask(int type, int topLeftCornerRadius, int topRightCornerRadius, int bottomRightCornerRadius, int bottomLeftCornerRadius, int left, int top, int right, int bottom){ + this.type = type; + + cornerRadius[0] = topLeftCornerRadius; + cornerRadius[1] = topLeftCornerRadius; + + cornerRadius[2] = topRightCornerRadius; + cornerRadius[3] = topRightCornerRadius; + + cornerRadius[4] = bottomRightCornerRadius; + cornerRadius[5] = bottomRightCornerRadius; + + cornerRadius[6] = bottomLeftCornerRadius; + cornerRadius[7] = bottomLeftCornerRadius; + + this.left = left; + this.top = top; + this.right = right; + this.bottom = bottom; + } + + } + + public static class Builder{ + private Drawable mBackgroundDrawable; + private int mBackgroundAnimDuration = 200; + private int mBackgroundColor; + + private int mRippleType; + private int mMaxRippleRadius; + private int mRippleAnimDuration = 400; + private int mRippleColor; + private int mDelayClickType; + + private Interpolator mInInterpolator; + private Interpolator mOutInterpolator; + + private int mMaskType; + private int mMaskTopLeftCornerRadius; + private int mMaskTopRightCornerRadius; + private int mMaskBottomLeftCornerRadius; + private int mMaskBottomRightCornerRadius; + private int mMaskLeft; + private int mMaskTop; + private int mMaskRight; + private int mMaskBottom; + + public Builder(){} + + public Builder(Context context, int defStyleRes){ + this(context, null, 0, defStyleRes); + } + + public Builder(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RippleDrawable, defStyleAttr, defStyleRes); + int type, resId; + + backgroundColor(a.getColor(R.styleable.RippleDrawable_rd_backgroundColor, 0)); + backgroundAnimDuration(a.getInteger(R.styleable.RippleDrawable_rd_backgroundAnimDuration, context.getResources().getInteger(android.R.integer.config_mediumAnimTime))); + rippleType(a.getInteger(R.styleable.RippleDrawable_rd_rippleType, RippleDrawable.TYPE_TOUCH)); + delayClickType(a.getInteger(R.styleable.RippleDrawable_rd_delayClick, RippleDrawable.DELAY_CLICK_NONE)); + type = ThemeUtil.getType(a, R.styleable.RippleDrawable_rd_maxRippleRadius); + if(type >= TypedValue.TYPE_FIRST_INT && type <= TypedValue.TYPE_LAST_INT) + maxRippleRadius(a.getInteger(R.styleable.RippleDrawable_rd_maxRippleRadius, -1)); + else + maxRippleRadius(a.getDimensionPixelSize(R.styleable.RippleDrawable_rd_maxRippleRadius, ThemeUtil.dpToPx(context, 48))); + rippleColor(a.getColor(R.styleable.RippleDrawable_rd_rippleColor, ThemeUtil.colorControlHighlight(context, 0))); + rippleAnimDuration(a.getInteger(R.styleable.RippleDrawable_rd_rippleAnimDuration, context.getResources().getInteger(android.R.integer.config_mediumAnimTime))); + if((resId = a.getResourceId(R.styleable.RippleDrawable_rd_inInterpolator, 0)) != 0) + inInterpolator(AnimationUtils.loadInterpolator(context, resId)); + if((resId = a.getResourceId(R.styleable.RippleDrawable_rd_outInterpolator, 0)) != 0) + outInterpolator(AnimationUtils.loadInterpolator(context, resId)); + maskType(a.getInteger(R.styleable.RippleDrawable_rd_maskType, Mask.TYPE_RECTANGLE)); + cornerRadius(a.getDimensionPixelSize(R.styleable.RippleDrawable_rd_cornerRadius, 0)); + topLeftCornerRadius(a.getDimensionPixelSize(R.styleable.RippleDrawable_rd_topLeftCornerRadius, mMaskTopLeftCornerRadius)); + topRightCornerRadius(a.getDimensionPixelSize(R.styleable.RippleDrawable_rd_topRightCornerRadius, mMaskTopRightCornerRadius)); + bottomRightCornerRadius(a.getDimensionPixelSize(R.styleable.RippleDrawable_rd_bottomRightCornerRadius, mMaskBottomRightCornerRadius)); + bottomLeftCornerRadius(a.getDimensionPixelSize(R.styleable.RippleDrawable_rd_bottomLeftCornerRadius, mMaskBottomLeftCornerRadius)); + padding(a.getDimensionPixelSize(R.styleable.RippleDrawable_rd_padding, 0)); + left(a.getDimensionPixelSize(R.styleable.RippleDrawable_rd_leftPadding, mMaskLeft)); + right(a.getDimensionPixelSize(R.styleable.RippleDrawable_rd_rightPadding, mMaskRight)); + top(a.getDimensionPixelSize(R.styleable.RippleDrawable_rd_topPadding, mMaskTop)); + bottom(a.getDimensionPixelSize(R.styleable.RippleDrawable_rd_bottomPadding, mMaskBottom)); + + a.recycle(); + } + + public RippleDrawable build(){ + if(mInInterpolator == null) + mInInterpolator = new AccelerateInterpolator(); + + if(mOutInterpolator == null) + mOutInterpolator = new DecelerateInterpolator(); + + return new RippleDrawable(mBackgroundDrawable, mBackgroundAnimDuration, mBackgroundColor, mRippleType, mDelayClickType, mMaxRippleRadius, mRippleAnimDuration, mRippleColor, mInInterpolator, mOutInterpolator, mMaskType, mMaskTopLeftCornerRadius, mMaskTopRightCornerRadius, mMaskBottomRightCornerRadius, mMaskBottomLeftCornerRadius, mMaskLeft, mMaskTop, mMaskRight, mMaskBottom); + } + + public Builder backgroundDrawable(Drawable drawable){ + mBackgroundDrawable = drawable; + return this; + } + + public Builder backgroundAnimDuration(int duration){ + mBackgroundAnimDuration = duration; + return this; + } + + public Builder backgroundColor(int color){ + mBackgroundColor = color; + return this; + } + + public Builder rippleType(int type){ + mRippleType = type; + return this; + } + + public Builder delayClickType(int type){ + mDelayClickType = type; + return this; + } + + public Builder maxRippleRadius(int radius){ + mMaxRippleRadius = radius; + return this; + } + + public Builder rippleAnimDuration(int duration){ + mRippleAnimDuration = duration; + return this; + } + + public Builder rippleColor(int color){ + mRippleColor = color; + return this; + } + + public Builder inInterpolator(Interpolator interpolator){ + mInInterpolator = interpolator; + return this; + } + + public Builder outInterpolator(Interpolator interpolator){ + mOutInterpolator = interpolator; + return this; + } + + public Builder maskType(int type){ + mMaskType = type; + return this; + } + + public Builder cornerRadius(int radius){ + mMaskTopLeftCornerRadius = radius; + mMaskTopRightCornerRadius = radius; + mMaskBottomLeftCornerRadius = radius; + mMaskBottomRightCornerRadius = radius; + return this; + } + + public Builder topLeftCornerRadius(int radius){ + mMaskTopLeftCornerRadius = radius; + return this; + } + + public Builder topRightCornerRadius(int radius){ + mMaskTopRightCornerRadius = radius; + return this; + } + + public Builder bottomLeftCornerRadius(int radius){ + mMaskBottomLeftCornerRadius = radius; + return this; + } + + public Builder bottomRightCornerRadius(int radius){ + mMaskBottomRightCornerRadius = radius; + return this; + } + + public Builder padding(int padding){ + mMaskLeft = padding; + mMaskTop = padding; + mMaskRight = padding; + mMaskBottom = padding; + return this; + } + + public Builder left(int padding){ + mMaskLeft = padding; + return this; + } + + public Builder top(int padding){ + mMaskTop = padding; + return this; + } + + public Builder right(int padding){ + mMaskRight = padding; + return this; + } + + public Builder bottom(int padding){ + mMaskBottom = padding; + return this; + } + } +} diff --git a/material/src/main/java/com/rey/material/drawable/ThemeDrawable.java b/material/src/main/java/com/rey/material/drawable/ThemeDrawable.java new file mode 100644 index 0000000..4f35e0d --- /dev/null +++ b/material/src/main/java/com/rey/material/drawable/ThemeDrawable.java @@ -0,0 +1,41 @@ +package com.rey.material.drawable; + +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LevelListDrawable; + +import com.rey.material.util.ThemeManager; + +/** + * Created by Rey on 5/27/2015. + */ +public class ThemeDrawable extends LevelListDrawable implements ThemeManager.OnThemeChangedListener { + private int mStyleId; + + public ThemeDrawable(int styleId) { + mStyleId = styleId; + + if(mStyleId != 0) { + ThemeManager.getInstance().registerOnThemeChangedListener(this); + initDrawables(); + } + } + + private void initDrawables(){ + ThemeManager themeManager = ThemeManager.getInstance(); + int count = themeManager.getThemeCount(); + + for(int i = 0; i < count; i++){ + Drawable drawable = themeManager.getContext().getResources().getDrawable(themeManager.getStyle(mStyleId, i)); + addLevel(i, i, drawable); + } + + setLevel(themeManager.getCurrentTheme()); + } + + @Override + public void onThemeChanged(ThemeManager.OnThemeChangedEvent event) { + if(getLevel() != event.theme) + setLevel(event.theme); + } + +} diff --git a/material/src/main/java/com/rey/material/drawable/ToolbarRippleDrawable.java b/material/src/main/java/com/rey/material/drawable/ToolbarRippleDrawable.java new file mode 100644 index 0000000..bd549f8 --- /dev/null +++ b/material/src/main/java/com/rey/material/drawable/ToolbarRippleDrawable.java @@ -0,0 +1,519 @@ +package com.rey.material.drawable; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Path.Direction; +import android.graphics.PixelFormat; +import android.graphics.PointF; +import android.graphics.RadialGradient; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Shader; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; +import android.os.SystemClock; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.AnimationUtils; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + +import com.rey.material.R; +import com.rey.material.util.ColorUtil; +import com.rey.material.util.ThemeUtil; +import com.rey.material.util.ViewUtil; + +public class ToolbarRippleDrawable extends Drawable implements Animatable { + + private boolean mRunning = false; + + private Paint mShaderPaint; + private Paint mFillPaint; + private RadialGradient mInShader; + private RadialGradient mOutShader; + private Matrix mMatrix; + private int mAlpha = 255; + + private RectF mBackgroundBounds; + private Path mBackground; + private int mBackgroundAnimDuration; + private int mBackgroundColor; + private float mBackgroundAlphaPercent; + + private PointF mRipplePoint; + private float mRippleRadius; + private int mRippleType; + private int mMaxRippleRadius; + private int mRippleAnimDuration; + private int mRippleColor; + private float mRippleAlphaPercent; + private int mDelayClickType; + + private Interpolator mInInterpolator; + private Interpolator mOutInterpolator; + + private long mStartTime; + + private boolean mPressed = false; + + private int mState = STATE_OUT; + + private static final int STATE_OUT = 0; + private static final int STATE_PRESS = 1; + private static final int STATE_HOVER = 2; + private static final int STATE_RELEASE_ON_HOLD = 3; + private static final int STATE_RELEASE = 4; + + private static final int TYPE_TOUCH_MATCH_VIEW = -1; + private static final int TYPE_TOUCH = 0; + private static final int TYPE_WAVE = 1; + + private static final float[] GRADIENT_STOPS = new float[]{0f, 0.99f, 1f}; + private static final float GRADIENT_RADIUS = 16; + + private ToolbarRippleDrawable(int backgroundAnimDuration, int backgroundColor, int rippleType, int delayClickType, int maxTouchRadius, int touchAnimDuration, int touchColor, Interpolator inInterpolator, Interpolator outInterpolator){ + mBackgroundAnimDuration = backgroundAnimDuration; + mBackgroundColor = backgroundColor; + + mRippleType = rippleType; + mMaxRippleRadius = maxTouchRadius; + mRippleAnimDuration = touchAnimDuration; + mRippleColor = touchColor; + mDelayClickType = delayClickType; + + if(mRippleType == TYPE_TOUCH && mMaxRippleRadius <= 0) + mRippleType = TYPE_TOUCH_MATCH_VIEW; + + mInInterpolator = inInterpolator; + mOutInterpolator = outInterpolator; + + mFillPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mFillPaint.setStyle(Paint.Style.FILL); + + mShaderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mShaderPaint.setStyle(Paint.Style.FILL); + + mBackground = new Path(); + mBackgroundBounds = new RectF(); + + mRipplePoint = new PointF(); + + mMatrix = new Matrix(); + + mInShader = new RadialGradient(0, 0, GRADIENT_RADIUS, new int[]{mRippleColor, mRippleColor, 0}, GRADIENT_STOPS, Shader.TileMode.CLAMP); + if(mRippleType == TYPE_WAVE) + mOutShader = new RadialGradient(0, 0, GRADIENT_RADIUS, new int[]{0, ColorUtil.getColor(mRippleColor, 0f), mRippleColor}, GRADIENT_STOPS, Shader.TileMode.CLAMP); + } + + public int getDelayClickType(){ + return mDelayClickType; + } + + public void setDelayClickType(int type){ + mDelayClickType = type; + } + + @Override + public void setAlpha(int alpha) { + mAlpha = alpha; + } + + @Override + public void setColorFilter(ColorFilter filter) { + mFillPaint.setColorFilter(filter); + mShaderPaint.setColorFilter(filter); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + public long getClickDelayTime(){ + switch (mDelayClickType){ + case RippleDrawable.DELAY_CLICK_NONE: + return -1; + case RippleDrawable.DELAY_CLICK_UNTIL_RELEASE: + if(mState == STATE_RELEASE_ON_HOLD) + return Math.max(mBackgroundAnimDuration, mRippleAnimDuration) - (SystemClock.uptimeMillis() - mStartTime); + break; + case RippleDrawable.DELAY_CLICK_AFTER_RELEASE: + if(mState == STATE_RELEASE_ON_HOLD) + return 2 * Math.max(mBackgroundAnimDuration, mRippleAnimDuration) - (SystemClock.uptimeMillis() - mStartTime); + else if(mState == STATE_RELEASE) + return Math.max(mBackgroundAnimDuration, mRippleAnimDuration) - (SystemClock.uptimeMillis() - mStartTime); + break; + } + + return -1; + } + + private void setRippleState(int state){ + if(mState != state){ + mState = state; + + if(mState != STATE_OUT){ + if(mState != STATE_HOVER) + start(); + else + stop(); + } + else + stop(); + } + } + + private boolean setRippleEffect(float x, float y, float radius){ + if(mRipplePoint.x != x || mRipplePoint.y != y || mRippleRadius != radius){ + mRipplePoint.set(x, y); + mRippleRadius = radius; + radius = mRippleRadius / GRADIENT_RADIUS; + mMatrix.reset(); + mMatrix.postTranslate(x, y); + mMatrix.postScale(radius, radius, x, y); + mInShader.setLocalMatrix(mMatrix); + if(mOutShader != null) + mOutShader.setLocalMatrix(mMatrix); + + return true; + } + + return false; + } + + @Override + protected void onBoundsChange(Rect bounds) { + mBackgroundBounds.set(bounds.left, bounds.top, bounds.right, bounds.bottom); + mBackground.reset(); + mBackground.addRect(mBackgroundBounds, Direction.CW); + } + + @Override + public boolean isStateful() { + return true; + } + + @Override + protected boolean onStateChange(int[] state) { + boolean pressed = ViewUtil.hasState(state, android.R.attr.state_pressed); + + if(mPressed != pressed){ + mPressed = pressed; + + if(mPressed){ + Rect bounds = getBounds(); + + if(mState == STATE_OUT || mState == STATE_RELEASE){ + if(mRippleType == TYPE_WAVE || mRippleType == TYPE_TOUCH_MATCH_VIEW) + mMaxRippleRadius = getMaxRippleRadius(bounds.exactCenterX(), bounds.exactCenterY()); + + setRippleEffect(bounds.exactCenterX(), bounds.exactCenterY(), 0); + setRippleState(STATE_PRESS); + } + else if(mRippleType == TYPE_TOUCH) + setRippleEffect(bounds.exactCenterX(), bounds.exactCenterY(), mRippleRadius); + } + else{ + if(mState != STATE_OUT){ + if(mState == STATE_HOVER){ + if(mRippleType == TYPE_WAVE|| mRippleType == TYPE_TOUCH_MATCH_VIEW) + setRippleEffect(mRipplePoint.x, mRipplePoint.y, 0); + + setRippleState(STATE_RELEASE); + } + else + setRippleState(STATE_RELEASE_ON_HOLD); + } + } + + return true; + } + + return false; + } + + @Override + public void draw(Canvas canvas) { + switch (mRippleType) { + case TYPE_TOUCH: + case TYPE_TOUCH_MATCH_VIEW: + drawTouch(canvas); + break; + case TYPE_WAVE: + drawWave(canvas); + break; + } + } + + private void drawTouch(Canvas canvas){ + if(mState != STATE_OUT){ + if(mBackgroundAlphaPercent > 0){ + mFillPaint.setColor(mBackgroundColor); + mFillPaint.setAlpha(Math.round(mAlpha * mBackgroundAlphaPercent)); + canvas.drawPath(mBackground, mFillPaint); + } + + if(mRippleRadius > 0 && mRippleAlphaPercent > 0){ + mShaderPaint.setAlpha(Math.round(mAlpha * mRippleAlphaPercent)); + mShaderPaint.setShader(mInShader); + canvas.drawPath(mBackground, mShaderPaint); + } + } + } + + private void drawWave(Canvas canvas){ + if(mState != STATE_OUT){ + if(mState == STATE_RELEASE){ + if(mRippleRadius == 0){ + mFillPaint.setColor(mRippleColor); + canvas.drawPath(mBackground, mFillPaint); + } + else{ + mShaderPaint.setShader(mOutShader); + canvas.drawPath(mBackground, mShaderPaint); + } + } + else if(mRippleRadius > 0){ + mShaderPaint.setShader(mInShader); + canvas.drawPath(mBackground, mShaderPaint); + } + } + } + + private int getMaxRippleRadius(float x, float y){ + float x1 = x < mBackgroundBounds.centerX() ? mBackgroundBounds.right : mBackgroundBounds.left; + float y1 = y < mBackgroundBounds.centerY() ? mBackgroundBounds.bottom : mBackgroundBounds.top; + + return (int)Math.round(Math.sqrt(Math.pow(x1 - x, 2) + Math.pow(y1 - y, 2))); + } + + //Animation: based on http://cyrilmottier.com/2012/11/27/actionbar-on-the-move/ + + public void cancel(){ + setRippleState(STATE_OUT); + } + + private void resetAnimation(){ + mStartTime = SystemClock.uptimeMillis(); + } + + @Override + public void start() { + if(isRunning()) + return; + + resetAnimation(); + + scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + invalidateSelf(); + } + + @Override + public void stop() { + if(!isRunning()) + return; + + mRunning = false; + unscheduleSelf(mUpdater); + invalidateSelf(); + } + + @Override + public boolean isRunning() { + return mRunning; + } + + @Override + public void scheduleSelf(Runnable what, long when) { + mRunning = true; + super.scheduleSelf(what, when); + } + + private final Runnable mUpdater = new Runnable() { + + @Override + public void run() { + switch (mRippleType) { + case TYPE_TOUCH: + case TYPE_TOUCH_MATCH_VIEW: + updateTouch(); + break; + case TYPE_WAVE: + updateWave(); + break; + } + + } + + }; + + private void updateTouch(){ + if(mState != STATE_RELEASE){ + float backgroundProgress = Math.min(1f, (float)(SystemClock.uptimeMillis() - mStartTime) / mBackgroundAnimDuration); + mBackgroundAlphaPercent = mInInterpolator.getInterpolation(backgroundProgress) * Color.alpha(mBackgroundColor) / 255f; + + float touchProgress = Math.min(1f, (float)(SystemClock.uptimeMillis() - mStartTime) / mRippleAnimDuration); + mRippleAlphaPercent = mInInterpolator.getInterpolation(touchProgress); + + setRippleEffect(mRipplePoint.x, mRipplePoint.y, mMaxRippleRadius * mInInterpolator.getInterpolation(touchProgress)); + + if(backgroundProgress == 1f && touchProgress == 1f){ + mStartTime = SystemClock.uptimeMillis(); + setRippleState(mState == STATE_PRESS ? STATE_HOVER : STATE_RELEASE); + } + + } + else{ + float backgroundProgress = Math.min(1f, (float)(SystemClock.uptimeMillis() - mStartTime) / mBackgroundAnimDuration); + mBackgroundAlphaPercent = (1f - mOutInterpolator.getInterpolation(backgroundProgress)) * Color.alpha(mBackgroundColor) / 255f; + + float touchProgress = Math.min(1f, (float)(SystemClock.uptimeMillis() - mStartTime) / mRippleAnimDuration); + mRippleAlphaPercent = 1f - mOutInterpolator.getInterpolation(touchProgress); + + setRippleEffect(mRipplePoint.x, mRipplePoint.y, mMaxRippleRadius * (1f + 0.5f * mOutInterpolator.getInterpolation(touchProgress))); + + if(backgroundProgress == 1f && touchProgress == 1f) + setRippleState(STATE_OUT); + } + + if(isRunning()) + scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + + invalidateSelf(); + } + + private void updateWave(){ + float progress = Math.min(1f, (float)(SystemClock.uptimeMillis() - mStartTime) / mRippleAnimDuration); + + if(mState != STATE_RELEASE){ + setRippleEffect(mRipplePoint.x, mRipplePoint.y, mMaxRippleRadius * mInInterpolator.getInterpolation(progress)); + + if(progress == 1f){ + mStartTime = SystemClock.uptimeMillis(); + if(mState == STATE_PRESS) + setRippleState(STATE_HOVER); + else{ + setRippleEffect(mRipplePoint.x, mRipplePoint.y, 0); + setRippleState(STATE_RELEASE); + } + } + } + else{ + setRippleEffect(mRipplePoint.x, mRipplePoint.y, mMaxRippleRadius * mOutInterpolator.getInterpolation(progress)); + + if(progress == 1f) + setRippleState(STATE_OUT); + } + + if(isRunning()) + scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + + invalidateSelf(); + } + + public static class Builder{ + private int mBackgroundAnimDuration = 200; + private int mBackgroundColor; + + private int mRippleType; + private int mMaxRippleRadius; + private int mRippleAnimDuration = 400; + private int mRippleColor; + private int mDelayClickType; + + private Interpolator mInInterpolator; + private Interpolator mOutInterpolator; + + public Builder(){} + + public Builder(Context context, int defStyleRes){ + this(context, null, 0, defStyleRes); + } + + public Builder(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RippleDrawable, defStyleAttr, defStyleRes); + int resId; + + backgroundColor(a.getColor(R.styleable.RippleDrawable_rd_backgroundColor, 0)); + backgroundAnimDuration(a.getInteger(R.styleable.RippleDrawable_rd_backgroundAnimDuration, context.getResources().getInteger(android.R.integer.config_mediumAnimTime))); + rippleType(a.getInteger(R.styleable.RippleDrawable_rd_rippleType, ToolbarRippleDrawable.TYPE_TOUCH)); + delayClickType(a.getInteger(R.styleable.RippleDrawable_rd_delayClick, RippleDrawable.DELAY_CLICK_NONE)); + int type = ThemeUtil.getType(a, R.styleable.RippleDrawable_rd_maxRippleRadius); + if(type >= TypedValue.TYPE_FIRST_INT && type <= TypedValue.TYPE_LAST_INT) + maxRippleRadius(a.getInteger(R.styleable.RippleDrawable_rd_maxRippleRadius, -1)); + else + maxRippleRadius(a.getDimensionPixelSize(R.styleable.RippleDrawable_rd_maxRippleRadius, ThemeUtil.dpToPx(context, 48))); + rippleColor(a.getColor(R.styleable.RippleDrawable_rd_rippleColor, ThemeUtil.colorControlHighlight(context, 0))); + rippleAnimDuration(a.getInteger(R.styleable.RippleDrawable_rd_rippleAnimDuration, context.getResources().getInteger(android.R.integer.config_mediumAnimTime))); + if((resId = a.getResourceId(R.styleable.RippleDrawable_rd_inInterpolator, 0)) != 0) + inInterpolator(AnimationUtils.loadInterpolator(context, resId)); + if((resId = a.getResourceId(R.styleable.RippleDrawable_rd_outInterpolator, 0)) != 0) + outInterpolator(AnimationUtils.loadInterpolator(context, resId)); + + a.recycle(); + } + + public ToolbarRippleDrawable build(){ + if(mInInterpolator == null) + mInInterpolator = new AccelerateInterpolator(); + + if(mOutInterpolator == null) + mOutInterpolator = new DecelerateInterpolator(); + + return new ToolbarRippleDrawable(mBackgroundAnimDuration, mBackgroundColor, mRippleType, mDelayClickType, mMaxRippleRadius, mRippleAnimDuration, mRippleColor, mInInterpolator, mOutInterpolator); + } + + public Builder backgroundAnimDuration(int duration){ + mBackgroundAnimDuration = duration; + return this; + } + + public Builder backgroundColor(int color){ + mBackgroundColor = color; + return this; + } + + public Builder rippleType(int type){ + mRippleType = type; + return this; + } + + public Builder delayClickType(int type){ + mDelayClickType = type; + return this; + } + + public Builder maxRippleRadius(int radius){ + mMaxRippleRadius = radius; + return this; + } + + public Builder rippleAnimDuration(int duration){ + mRippleAnimDuration = duration; + return this; + } + + public Builder rippleColor(int color){ + mRippleColor = color; + return this; + } + + public Builder inInterpolator(Interpolator interpolator){ + mInInterpolator = interpolator; + return this; + } + + public Builder outInterpolator(Interpolator interpolator){ + mOutInterpolator = interpolator; + return this; + } + + } +} diff --git a/material/src/main/java/com/rey/material/text/style/ContactChipSpan.java b/material/src/main/java/com/rey/material/text/style/ContactChipSpan.java new file mode 100644 index 0000000..19d7b18 --- /dev/null +++ b/material/src/main/java/com/rey/material/text/style/ContactChipSpan.java @@ -0,0 +1,124 @@ +package com.rey.material.text.style; + +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.RectF; +import android.graphics.Shader; +import android.graphics.Typeface; +import android.text.BoringLayout; +import android.text.Layout; +import android.text.TextPaint; +import android.text.TextUtils; +import android.text.style.ReplacementSpan; + +public class ContactChipSpan extends ReplacementSpan { + + private Paint mPaint; + private int mPaddingLeft; + private int mPaddingRight; + private int mBackgroundColor; + private int mHeight; + private int mWidth; + + private CharSequence mContactName; + private BoringLayout mBoringLayout; + private TextPaint mTextPaint; + private RectF mRect; + + private BitmapShader mBitmapShader; + private Bitmap mBitmap; + private Matrix mMatrix; + + public ContactChipSpan(CharSequence name, int height, int maxWidth, int paddingLeft, int paddingRight, Typeface typeface, int textColor, int textSize, int backgroundColor) { + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mPaint.setStyle(Paint.Style.FILL); + mPaint.setColor(textColor); + mPaint.setTypeface(typeface); + mPaint.setTextSize(textSize); + + mTextPaint = new TextPaint(mPaint); + + + mRect = new RectF(); + + mMatrix = new Matrix(); + + mContactName = name; + mPaddingLeft = paddingLeft; + mPaddingRight = paddingRight; + mBackgroundColor = backgroundColor; + mHeight = height; + mWidth = Math.round(Math.min(maxWidth, mPaint.measureText(name, 0, name.length()) + paddingLeft + paddingRight + height)); + + int outerWidth = Math.max(0, mWidth - mPaddingLeft - mPaddingRight - mHeight); + Paint.FontMetricsInt temp = mTextPaint.getFontMetricsInt(); + BoringLayout.Metrics mMetrics = new BoringLayout.Metrics(); + mMetrics.width = Math.round(mTextPaint.measureText(mContactName, 0, mContactName.length()) + 0.5f); + mMetrics.ascent = temp.ascent; + mMetrics.bottom = temp.bottom; + mMetrics.descent = temp.descent; + mMetrics.top = temp.top; + mMetrics.leading = temp.leading; + mBoringLayout = BoringLayout.make(mContactName, mTextPaint, outerWidth, Layout.Alignment.ALIGN_NORMAL, 1f, 1f, mMetrics, true, TextUtils.TruncateAt.END, outerWidth); + } + + public void setImage(Bitmap bm){ + if(mBitmap != bm){ + mBitmap = bm; + if(mBitmap != null) { + mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); + mMatrix.reset(); + float scale = mHeight / (float)Math.min(mBitmap.getWidth(), mBitmap.getHeight()); + mMatrix.setScale(scale, scale, 0, 0); + mMatrix.postTranslate((mHeight - mBitmap.getWidth() * scale) / 2, (mHeight - mBitmap.getHeight() * scale) / 2); + + mBitmapShader.setLocalMatrix(mMatrix); + } + } + } + + @Override + public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) { + if (fm != null) { + int cy = (fm.ascent + fm.descent) / 2; + fm.ascent = Math.min(fm.ascent, cy - mHeight / 2); + fm.descent = Math.max(fm.descent, cy + mHeight / 2); + fm.top = Math.min(fm.top, fm.ascent); + fm.bottom = Math.max(fm.bottom, fm.descent); + } + + return mWidth; + } + + @Override + public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) { + canvas.save(); + + canvas.translate(x, top); + + float halfHeight = mHeight / 2f; + mPaint.setShader(null); + mPaint.setColor(mBackgroundColor); + mRect.set(1, 0, mHeight + 1, mHeight); + canvas.drawArc(mRect, 90, 180, true, mPaint); + mRect.set(mWidth - mHeight, 0, mWidth, mHeight); + canvas.drawArc(mRect, 270, 180, true, mPaint); + mRect.set(halfHeight, 0, mWidth - halfHeight, mHeight); + canvas.drawRect(mRect, mPaint); + + if(mBitmap != null){ + mPaint.setShader(mBitmapShader); + canvas.drawCircle(halfHeight, halfHeight, halfHeight, mPaint); + } + + if(mContactName != null && mBoringLayout != null) { + canvas.translate(mHeight + mPaddingLeft, (mHeight - mBoringLayout.getHeight()) / 2f); + mBoringLayout.draw(canvas); + } + + canvas.restore(); + } +} diff --git a/material/src/main/java/com/rey/material/util/ColorUtil.java b/material/src/main/java/com/rey/material/util/ColorUtil.java new file mode 100644 index 0000000..b343989 --- /dev/null +++ b/material/src/main/java/com/rey/material/util/ColorUtil.java @@ -0,0 +1,33 @@ +package com.rey.material.util; + +import android.graphics.Color; + +public class ColorUtil { + + private static int getMiddleValue(int prev, int next, float factor){ + return Math.round(prev + (next - prev) * factor); + } + + public static int getMiddleColor(int prevColor, int curColor, float factor){ + if(prevColor == curColor) + return curColor; + + if(factor == 0f) + return prevColor; + else if(factor == 1f) + return curColor; + + int a = getMiddleValue(Color.alpha(prevColor), Color.alpha(curColor), factor); + int r = getMiddleValue(Color.red(prevColor), Color.red(curColor), factor); + int g = getMiddleValue(Color.green(prevColor), Color.green(curColor), factor); + int b = getMiddleValue(Color.blue(prevColor), Color.blue(curColor), factor); + + return Color.argb(a, r, g, b); + } + + public static int getColor(int baseColor, float alphaPercent){ + int alpha = Math.round(Color.alpha(baseColor) * alphaPercent); + + return (baseColor & 0x00FFFFFF) | (alpha << 24); + } +} diff --git a/material/src/main/java/com/rey/material/util/RippleManager.java b/material/src/main/java/com/rey/material/util/RippleManager.java new file mode 100644 index 0000000..1eb8b0f --- /dev/null +++ b/material/src/main/java/com/rey/material/util/RippleManager.java @@ -0,0 +1,123 @@ +package com.rey.material.util; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; + +import com.rey.material.R; +import com.rey.material.drawable.RippleDrawable; +import com.rey.material.drawable.ToolbarRippleDrawable; + +public final class RippleManager implements View.OnClickListener{ + + private View.OnClickListener mClickListener; + private boolean mClickScheduled = false; + + public RippleManager(){} + + /** + * Should be called in the construction method of view to create a RippleDrawable. + */ + public void onCreate(View v, Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + if(v.isInEditMode()) + return; + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RippleView, defStyleAttr, defStyleRes); + int rippleStyle = a.getResourceId(R.styleable.RippleView_rd_style, 0); + RippleDrawable drawable = null; + + if(rippleStyle != 0) + drawable = new RippleDrawable.Builder(context, rippleStyle).backgroundDrawable(getBackground(v)).build(); + else{ + boolean rippleEnable = a.getBoolean(R.styleable.RippleView_rd_enable, false); + if(rippleEnable) + drawable = new RippleDrawable.Builder(context, attrs, defStyleAttr, defStyleRes).backgroundDrawable(getBackground(v)).build(); + } + + a.recycle(); + + if(drawable != null) + ViewUtil.setBackground(v, drawable); + } + + private Drawable getBackground(View v){ + Drawable background = v.getBackground(); + if(background == null) + return null; + + if(background instanceof RippleDrawable) + return ((RippleDrawable)background).getBackgroundDrawable(); + + return background; + } + + public void setOnClickListener(View.OnClickListener l) { + mClickListener = l; + } + + public boolean onTouchEvent(View v, MotionEvent event){ + Drawable background = v.getBackground(); + return background != null && background instanceof RippleDrawable && ((RippleDrawable) background).onTouch(v, event); + } + + @Override + public void onClick(View v) { + Drawable background = v.getBackground(); + long delay = 0; + + if(background != null) { + if (background instanceof RippleDrawable) + delay = ((RippleDrawable) background).getClickDelayTime(); + else if (background instanceof ToolbarRippleDrawable) + delay = ((ToolbarRippleDrawable) background).getClickDelayTime(); + } + + if(delay > 0 && v.getHandler() != null && !mClickScheduled) { + mClickScheduled = true; + v.getHandler().postDelayed(new ClickRunnable(v), delay); + } + else + dispatchClickEvent(v); + } + + private void dispatchClickEvent(View v){ + if(mClickListener != null) + mClickListener.onClick(v); + } + + /** + * Cancel the ripple effect of this view and all of it's children. + */ + public static void cancelRipple(View v){ + Drawable background = v.getBackground(); + if(background instanceof RippleDrawable) + ((RippleDrawable)background).cancel(); + else if(background instanceof ToolbarRippleDrawable) + ((ToolbarRippleDrawable)background).cancel(); + + if(v instanceof ViewGroup){ + ViewGroup vg = (ViewGroup)v; + for(int i = 0, count = vg.getChildCount(); i < count; i++) + RippleManager.cancelRipple(vg.getChildAt(i)); + } + } + + class ClickRunnable implements Runnable{ + View mView; + + public ClickRunnable(View v){ + mView = v; + } + + @Override + public void run() { + mClickScheduled = false; + dispatchClickEvent(mView); + } + } + +} diff --git a/material/src/main/java/com/rey/material/util/ThemeManager.java b/material/src/main/java/com/rey/material/util/ThemeManager.java new file mode 100644 index 0000000..ec6e5ba --- /dev/null +++ b/material/src/main/java/com/rey/material/util/ThemeManager.java @@ -0,0 +1,265 @@ +package com.rey.material.util; + +import android.app.Application; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.TypedArray; +import android.os.Looper; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; +import android.util.AttributeSet; +import android.util.SparseArray; + +import com.rey.material.R; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; + +public class ThemeManager { + + private volatile static ThemeManager mInstance; + + private Context mContext; + private SparseArray mStyles = new SparseArray<>(); + private int mCurrentTheme; + private int mThemeCount; + private EventDispatcher mDispatcher; + + private static final String PREF = "theme.pref"; + private static final String KEY_THEME = "theme"; + + public static final int THEME_UNDEFINED = Integer.MIN_VALUE; + + /** + * Get the styleId from attributes. + */ + public static int getStyleId(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ThemableView, defStyleAttr, defStyleRes); + int styleId = a.getResourceId(R.styleable.ThemableView_v_styleId, 0); + a.recycle(); + + return styleId; + } + + /** + * Init ThemeManager. Should be call in {@link Application#onCreate()}. + * @param context The context object. Should be {#link Application} object. + * @param totalTheme The total theme. + * @param defaultTheme The default theme if current theme isn't set. + * @param dispatcher The {@link EventDispatcher} will be used to dispatch {@link OnThemeChangedEvent}. If null, then use {@link SimpleDispatcher}. + */ + public static void init(Context context, int totalTheme, int defaultTheme, @Nullable EventDispatcher dispatcher){ + getInstance().setup(context, totalTheme, defaultTheme, dispatcher); + } + + /** + * Get the singleton instance of ThemeManager. + * @return The singleton instance of ThemeManager. + */ + public static ThemeManager getInstance(){ + if(mInstance == null){ + synchronized (ThemeManager.class){ + if(mInstance == null) + mInstance = new ThemeManager(); + } + } + + return mInstance; + } + + protected void setup(Context context, int totalTheme, int defaultTheme, @Nullable EventDispatcher dispatcher){ + mContext = context; + mDispatcher = dispatcher != null ? dispatcher : new SimpleDispatcher(); + mThemeCount = totalTheme; + SharedPreferences pref = getSharedPreferences(mContext); + if(pref != null) + mCurrentTheme = pref.getInt(KEY_THEME, defaultTheme); + else + mCurrentTheme = defaultTheme; + if(mCurrentTheme >= mThemeCount) + setCurrentTheme(defaultTheme); + } + + private int[] loadStyleList(Context context, int resId){ + if(context == null) + return null; + + TypedArray array = context.getResources().obtainTypedArray(resId); + int[] result = new int[array.length()]; + for(int i = 0; i < result.length; i++) + result[i] = array.getResourceId(i, 0); + array.recycle(); + + return result; + } + + private int[] getStyleList(int styleId){ + if(mStyles == null) + return null; + + int[] list = mStyles.get(styleId); + if(list == null){ + list = loadStyleList(mContext, styleId); + mStyles.put(styleId, list); + } + + return list; + } + + private SharedPreferences getSharedPreferences(Context context){ + return context == null ? null : context.getSharedPreferences(PREF, Context.MODE_PRIVATE); + } + + private void dispatchThemeChanged(int theme){ + if(mDispatcher != null) + mDispatcher.dispatchThemeChanged(theme); + } + + public Context getContext(){ + return mContext; + } + + /** + * Get the current theme. + * @return The current theme. + */ + @UiThread + public int getCurrentTheme(){ + return mCurrentTheme; + } + + /** + * Set the current theme. Should be called in main thread (UI thread). + * @param theme The current theme. + * @return True if set theme successfully, False if method's called on main thread or theme already set. + */ + public boolean setCurrentTheme(int theme){ + if (Looper.getMainLooper().getThread() != Thread.currentThread()) + return false; + + if(mCurrentTheme != theme){ + mCurrentTheme = theme; + SharedPreferences pref = getSharedPreferences(mContext); + if(pref != null) + pref.edit().putInt(KEY_THEME, mCurrentTheme).apply(); + dispatchThemeChanged(mCurrentTheme); + return true; + } + + return false; + } + + /** + * Get the total theme. + * @return The total theme. + */ + public int getThemeCount(){ + return mThemeCount; + } + + /** + * Get current style of a styleId. + * @param styleId The styleId. + * @return The current style. + */ + public int getCurrentStyle(int styleId){ + return getStyle(styleId, mCurrentTheme); + } + + /** + * Get a specific style of a styleId. + * @param styleId The styleId. + * @param theme The theme. + * @return The specific style. + */ + public int getStyle(int styleId, int theme){ + int[] styles = getStyleList(styleId); + return styles == null ? 0 : styles[theme]; + } + + /** + * Register a listener will be called when current theme changed. + * @param listener A {@link OnThemeChangedListener} will be registered. + */ + public void registerOnThemeChangedListener(@NonNull OnThemeChangedListener listener){ + if(mDispatcher != null) + mDispatcher.registerListener(listener); + } + + /** + * Unregister a listener from be called when current theme changed. + * @param listener A {@link OnThemeChangedListener} will be unregistered. + */ + public void unregisterOnThemeChangedListener(@NonNull OnThemeChangedListener listener){ + if(mDispatcher != null) + mDispatcher.unregisterListener(listener); + } + + public interface EventDispatcher{ + + void registerListener(OnThemeChangedListener listener); + + void unregisterListener(OnThemeChangedListener listener); + + void dispatchThemeChanged(int theme); + } + + public static class SimpleDispatcher implements EventDispatcher{ + + ArrayList> mListeners = new ArrayList<>(); + + @Override + public void registerListener(OnThemeChangedListener listener) { + boolean exist = false; + for(int i = mListeners.size() - 1; i >= 0; i--){ + WeakReference ref = mListeners.get(i); + if(ref.get() == null) + mListeners.remove(i); + else if(ref.get() == listener) + exist = true; + } + + if(!exist) + mListeners.add(new WeakReference<>(listener)); + } + + @Override + public void unregisterListener(OnThemeChangedListener listener) { + for(int i = mListeners.size() - 1; i >= 0; i--){ + WeakReference ref = mListeners.get(i); + if(ref.get() == null || ref.get() == listener) + mListeners.remove(i); + } + } + + @Override + public void dispatchThemeChanged(int theme) { + OnThemeChangedEvent event = new OnThemeChangedEvent(theme); + + for(int i = mListeners.size() - 1; i >= 0; i--){ + WeakReference ref = mListeners.get(i); + if(ref.get() == null) + mListeners.remove(i); + else + ref.get().onThemeChanged(event); + } + } + } + + public interface OnThemeChangedListener{ + + void onThemeChanged(@Nullable OnThemeChangedEvent event); + + } + + public static class OnThemeChangedEvent{ + public final int theme; + + public OnThemeChangedEvent(int theme){ + this.theme = theme; + } + } + + +} diff --git a/material/src/main/java/com/rey/material/util/ThemeUtil.java b/material/src/main/java/com/rey/material/util/ThemeUtil.java new file mode 100644 index 0000000..eeefdfc --- /dev/null +++ b/material/src/main/java/com/rey/material/util/ThemeUtil.java @@ -0,0 +1,130 @@ +package com.rey.material.util; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Resources.Theme; +import android.content.res.TypedArray; +import android.os.Build; +import android.util.TypedValue; + +import com.rey.material.R; + +public class ThemeUtil { + + private static TypedValue value; + + public static int dpToPx(Context context, int dp) { + return (int) (TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics()) + 0.5f); + } + + public static int spToPx(Context context, int sp) { + return (int) (TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, context.getResources().getDisplayMetrics()) + 0.5f); + } + + private static int getColor(Context context, int id, int defaultValue) { + if (value == null) + value = new TypedValue(); + + try { + Theme theme = context.getTheme(); + if (theme != null && theme.resolveAttribute(id, value, true)) { + if (value.type >= TypedValue.TYPE_FIRST_INT && value.type <= TypedValue.TYPE_LAST_INT) + return value.data; + else if (value.type == TypedValue.TYPE_STRING) + return context.getResources().getColor(value.resourceId); + } + } catch (Exception ex) { + return defaultValue; + } + + return defaultValue; + } + + public static int windowBackground(Context context, int defaultValue) { + return getColor(context, android.R.attr.windowBackground, defaultValue); + } + + public static int textColorPrimary(Context context, int defaultValue) { + return getColor(context, android.R.attr.textColorPrimary, defaultValue); + } + + public static int textColorSecondary(Context context, int defaultValue) { + return getColor(context, android.R.attr.textColorSecondary, defaultValue); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public static int colorPrimary(Context context, int defaultValue) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + return getColor(context, android.R.attr.colorPrimary, defaultValue); + + return getColor(context, R.attr.colorPrimary, defaultValue); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public static int colorPrimaryDark(Context context, int defaultValue) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + return getColor(context, android.R.attr.colorPrimaryDark, defaultValue); + + return getColor(context, R.attr.colorPrimaryDark, defaultValue); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public static int colorAccent(Context context, int defaultValue) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + return getColor(context, android.R.attr.colorAccent, defaultValue); + + return getColor(context, R.attr.colorAccent, defaultValue); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public static int colorControlNormal(Context context, int defaultValue) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + return getColor(context, android.R.attr.colorControlNormal, defaultValue); + + return getColor(context, R.attr.colorControlNormal, defaultValue); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public static int colorControlActivated(Context context, int defaultValue) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + return getColor(context, android.R.attr.colorControlActivated, defaultValue); + + return getColor(context, R.attr.colorControlActivated, defaultValue); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public static int colorControlHighlight(Context context, int defaultValue) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + return getColor(context, android.R.attr.colorControlHighlight, defaultValue); + + return getColor(context, R.attr.colorControlHighlight, defaultValue); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public static int colorButtonNormal(Context context, int defaultValue) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + return getColor(context, android.R.attr.colorButtonNormal, defaultValue); + + return getColor(context, R.attr.colorButtonNormal, defaultValue); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public static int colorSwitchThumbNormal(Context context, int defaultValue) { + return getColor(context, R.attr.colorSwitchThumbNormal, defaultValue); + } + + public static int getType(TypedArray array, int index) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + return array.getType(index); + else { + TypedValue value = array.peekValue(index); + return value == null ? TypedValue.TYPE_NULL : value.type; + } + } + + public static CharSequence getString(TypedArray array, int index, CharSequence defaultValue) { + String result = array.getString(index); + return result == null ? defaultValue : result; + } + +} diff --git a/material/src/main/java/com/rey/material/util/ToolbarManager.java b/material/src/main/java/com/rey/material/util/ToolbarManager.java new file mode 100644 index 0000000..d1d3c85 --- /dev/null +++ b/material/src/main/java/com/rey/material/util/ToolbarManager.java @@ -0,0 +1,545 @@ +package com.rey.material.util; + +import android.os.Build; +import android.support.annotation.Nullable; +import android.support.v4.app.FragmentManager; +import android.support.v4.view.GravityCompat; +import android.support.v4.widget.DrawerLayout; +import android.support.v7.app.AppCompatDelegate; +import android.support.v7.widget.ActionMenuView; +import android.support.v7.widget.Toolbar; +import android.view.Gravity; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewTreeObserver; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; + +import com.rey.material.drawable.NavigationDrawerDrawable; +import com.rey.material.drawable.ToolbarRippleDrawable; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; + +/** + * A Manager class to help handling Toolbar used as ActionBar in ActionBarActivity. + * It help grouping ActionItem in Toolbar and only show the items of current group. + * It also help manager state of navigation icon. + * Created by Rey on 1/6/2015. + */ +public class ToolbarManager { + + private AppCompatDelegate mAppCompatDelegate; + private Toolbar mToolbar; + private int mRippleStyle; + private Animator mAnimator; + private ActionMenuView mMenuView; + private ToolbarRippleDrawable.Builder mBuilder; + + private int mCurrentGroup = 0; + private boolean mGroupChanged = false; + private boolean mMenuDataChanged = true; + + /** + * Interface definition for a callback to be invoked when the current group is changed. + */ + public interface OnToolbarGroupChangedListener { + + /** + * Called when the current group changed. + * @param oldGroupId The id of old group. + * @param groupId The id of new group. + */ + public void onToolbarGroupChanged(int oldGroupId, int groupId); + + } + + private ArrayList> mListeners = new ArrayList<>(); + + private ViewTreeObserver.OnGlobalLayoutListener mOnGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + ToolbarManager.this.onGlobalLayout(); + } + }; + + private ArrayList mAnimations = new ArrayList<>(); + + private Animation.AnimationListener mOutAnimationEndListener = new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + } + + @Override + public void onAnimationEnd(Animation animation) { + if(mAppCompatDelegate != null) + mAppCompatDelegate.invalidateOptionsMenu(); + else + onPrepareMenu(); + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + }; + + private NavigationManager mNavigationManager; + + public ToolbarManager(AppCompatDelegate delegate, Toolbar toolbar, int defaultGroupId, int rippleStyle, int animIn, int animOut){ + this(delegate, toolbar, defaultGroupId, rippleStyle, new SimpleAnimator(animIn, animOut)); + } + + public ToolbarManager(AppCompatDelegate delegate, Toolbar toolbar, int defaultGroupId, int rippleStyle, Animator animator){ + mAppCompatDelegate = delegate; + mToolbar = toolbar; + mCurrentGroup = defaultGroupId; + mRippleStyle = rippleStyle; + mAnimator = animator; + mAppCompatDelegate.setSupportActionBar(toolbar); + } + + /** + * Register a listener for current group changed event. Note that it doesn't hold any strong reference to listener, so don't use anonymous listener. + */ + public void registerOnToolbarGroupChangedListener(OnToolbarGroupChangedListener listener){ + for(int i = mListeners.size() - 1; i >= 0; i--){ + WeakReference ref = mListeners.get(i); + if(ref.get() == null) + mListeners.remove(i); + else if(ref.get() == listener) + return; + } + + mListeners.add(new WeakReference(listener)); + } + + /** + * Unregister a listener. + * @param listener + */ + public void unregisterOnToolbarGroupChangedListener(OnToolbarGroupChangedListener listener){ + for(int i = mListeners.size() - 1; i >= 0; i--){ + WeakReference ref = mListeners.get(i); + if(ref.get() == null || ref.get() == listener) + mListeners.remove(i); + } + } + + private void dispatchOnToolbarGroupChanged(int oldGroupId, int groupId){ + for(int i = mListeners.size() - 1; i >= 0; i--){ + WeakReference ref = mListeners.get(i); + if(ref.get() == null) + mListeners.remove(i); + else + ref.get().onToolbarGroupChanged(oldGroupId, groupId); + } + } + + /** + * @return The current group of the Toolbar. + */ + public int getCurrentGroup(){ + return mCurrentGroup; + } + + /** + * Set current group of the Toolbar. + * @param groupId The id of group. + */ + public void setCurrentGroup(int groupId){ + if(mCurrentGroup != groupId){ + int oldGroupId = mCurrentGroup; + mCurrentGroup = groupId; + mGroupChanged = true; + dispatchOnToolbarGroupChanged(oldGroupId, mCurrentGroup); + animateOut(); + } + } + + /** + * This funcction should be called in onCreateOptionsMenu of Activity or Fragment to inflate a new menu. + * @param menuId + */ + public void createMenu(int menuId){ + mToolbar.inflateMenu(menuId); + mMenuDataChanged = true; + if(mAppCompatDelegate == null) + onPrepareMenu(); + } + + /** + * This function should be called in onPrepareOptionsMenu(Menu) of Activity that use + * Toolbar as ActionBar, or after inflating menu. + */ + public void onPrepareMenu(){ + if(mGroupChanged || mMenuDataChanged){ + mToolbar.getViewTreeObserver().addOnGlobalLayoutListener(mOnGlobalLayoutListener); + + Menu menu = mToolbar.getMenu(); + for(int i = 0, count = menu.size(); i < count; i++){ + MenuItem item = menu.getItem(i); + item.setVisible(item.getGroupId() == mCurrentGroup || item.getGroupId() == 0); + } + + mMenuDataChanged = false; + } + } + + /** + * Set a NavigationManager to manage navigation icon state. + */ + public void setNavigationManager(NavigationManager navigationManager){ + mNavigationManager = navigationManager; + notifyNavigationStateInvalidated(); + } + + /** + * Notify the current state of navigation icon is invalid. It should update the state immediately without showing animation. + */ + public void notifyNavigationStateInvalidated(){ + if(mNavigationManager != null) + mNavigationManager.notifyStateInvalidated(); + } + + /** + * Notify the current state of navigation icon is invalid. It should update the state immediately without showing animation. + */ + public void notifyNavigationStateChanged(){ + if(mNavigationManager != null) + mNavigationManager.notifyStateChanged(); + } + + /** + * Notify the progress of animation between 2 states changed. Use this function to sync the progress with another animation. + * @param isBackState the current state (the end state of animation) is back state or not. + * @param progress the current progress of animation. + */ + public void notifyNavigationStateProgressChanged(boolean isBackState, float progress){ + if(mNavigationManager != null) + mNavigationManager.notifyStateProgressChanged(isBackState, progress); + } + + /** + * @return The navigation is in back state or not. + */ + public boolean isNavigationBackState(){ + return mNavigationManager != null && mNavigationManager.isBackState(); + } + + private ToolbarRippleDrawable getBackground(){ + if(mBuilder == null) + mBuilder = new ToolbarRippleDrawable.Builder(mToolbar.getContext(), mRippleStyle); + + return mBuilder.build(); + } + + private ActionMenuView getMenuView(){ + if(mMenuView == null){ + for (int i = 0; i < mToolbar.getChildCount(); i++) { + View child = mToolbar.getChildAt(i); + if (child instanceof ActionMenuView) { + mMenuView = (ActionMenuView) child; + break; + } + } + } + + return mMenuView; + } + + private void onGlobalLayout() { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) + mToolbar.getViewTreeObserver().removeOnGlobalLayoutListener(mOnGlobalLayoutListener); + else + mToolbar.getViewTreeObserver().removeGlobalOnLayoutListener(mOnGlobalLayoutListener); + + ActionMenuView menuView = getMenuView(); + for(int i = 0, count = menuView == null ? 0 : menuView.getChildCount(); i < count; i++){ + View child = menuView.getChildAt(i); + if(mRippleStyle != 0){ + if(child.getBackground() == null || !(child.getBackground() instanceof ToolbarRippleDrawable)) + ViewUtil.setBackground(child, getBackground()); + } + } + + if(mGroupChanged){ + animateIn(); + mGroupChanged = false; + } + } + + private void animateOut(){ + ActionMenuView menuView = getMenuView(); + int count = menuView == null ? 0 : menuView.getChildCount(); + Animation slowestAnimation = null; + mAnimations.clear(); + mAnimations.ensureCapacity(count); + + for(int i = 0; i < count; i++){ + View child = menuView.getChildAt(i); + Animation anim = mAnimator.getOutAnimation(child, i); + mAnimations.add(anim); + if(anim != null) + if(slowestAnimation == null || slowestAnimation.getStartOffset() + slowestAnimation.getDuration() < anim.getStartOffset() + anim.getDuration()) + slowestAnimation = anim; + } + + if(slowestAnimation == null) + mOutAnimationEndListener.onAnimationEnd(null); + else { + slowestAnimation.setAnimationListener(mOutAnimationEndListener); + + for(int i = 0; i < count; i++){ + Animation anim = mAnimations.get(i); + if(anim != null) + menuView.getChildAt(i).startAnimation(anim); + } + } + + mAnimations.clear(); + } + + private void animateIn(){ + ActionMenuView menuView = getMenuView(); + + for(int i = 0, count = menuView == null ? 0 : menuView.getChildCount(); i < count; i++){ + View child = menuView.getChildAt(i); + Animation anim = mAnimator.getInAnimation(child, i); + if(anim != null) + child.startAnimation(anim); + } + } + + /** + * Interface definition for creating animation of menu item view when switch group. + */ + public interface Animator{ + + /** + * Get the animation of the menu item view will be removed. + * @param v The menu item view. + * @param position The position of item. + * @return + */ + public Animation getOutAnimation(View v, int position); + + /** + * Get the animation of the menu item view will be added. + * @param v The menu item view. + * @param position The position of item. + * @return + */ + public Animation getInAnimation(View v, int position); + } + + private static class SimpleAnimator implements Animator{ + private int mAnimationIn; + private int mAnimationOut; + + public SimpleAnimator(int animIn, int animOut){ + mAnimationIn = animIn; + mAnimationOut = animOut; + } + + @Override + public Animation getOutAnimation(View v, int position) { + return mAnimationOut == 0 ? null : AnimationUtils.loadAnimation(v.getContext(), mAnimationOut); + } + + @Override + public Animation getInAnimation(View v, int position) { + return mAnimationIn == 0 ? null : AnimationUtils.loadAnimation(v.getContext(), mAnimationIn); + } + } + + /** + * Abstract class to manage the state of navigation icon. + */ + public static abstract class NavigationManager{ + + protected NavigationDrawerDrawable mNavigationIcon; + protected Toolbar mToolbar; + + /** + * @param styleId the style res of navigation icon. + */ + public NavigationManager(NavigationDrawerDrawable navigationIcon, Toolbar toolbar){ + mToolbar = toolbar; + mNavigationIcon = navigationIcon; + mToolbar.setNavigationIcon(mNavigationIcon); + mToolbar.setNavigationOnClickListener(new View.OnClickListener(){ + @Override + public void onClick(View v) { + onNavigationClick(); + } + }); + } + + /** + * Check if current state of navigation icon is back state or not. + * @return + */ + public abstract boolean isBackState(); + + /** + * Hangle event click navigation icon. Subclass should override this function. + */ + public abstract void onNavigationClick(); + + /** + * Notify the current state of navigation icon is invalid. It should update the state immediately without showing animation. + */ + public void notifyStateInvalidated(){ + mNavigationIcon.switchIconState(isBackState() ? NavigationDrawerDrawable.STATE_ARROW : NavigationDrawerDrawable.STATE_DRAWER, false); + } + + /** + * Notify the current state of navigation icon is changed. It should update the state with animation. + */ + public void notifyStateChanged(){ + mNavigationIcon.switchIconState(isBackState() ? NavigationDrawerDrawable.STATE_ARROW : NavigationDrawerDrawable.STATE_DRAWER, true); + } + + /** + * Notify the progress of animation between 2 states changed. Use this function to sync the progress with another animation. + * @param isBackState the current state (the end state of animation) is back state or not. + * @param progress the current progress of animation. + */ + public void notifyStateProgressChanged(boolean isBackState, float progress){ + mNavigationIcon.setIconState(isBackState ? NavigationDrawerDrawable.STATE_ARROW : NavigationDrawerDrawable.STATE_DRAWER, progress); + } + + } + + /** + * A Base Navigation Manager that handle navigation state between fragment changing and navigation drawer. + * If you want to handle state in another case, you should override isBackState(), shouldSyncDrawerSlidingProgress(), and call notify notifyStateChanged() if need. + */ + public static class BaseNavigationManager extends NavigationManager{ + protected DrawerLayout mDrawerLayout; + protected FragmentManager mFragmentManager; + + /** + * + * @param styleId the resourceId of navigation icon style. + * @param drawerLayout can be null if you don't need to handle navigation state when open/close navigation drawer. + */ + public BaseNavigationManager(int styleId, FragmentManager fragmentManager, Toolbar toolbar, DrawerLayout drawerLayout){ + super(new NavigationDrawerDrawable.Builder(toolbar.getContext(), styleId).build(), toolbar); + mDrawerLayout = drawerLayout; + mFragmentManager = fragmentManager; + + if(mDrawerLayout != null) + mDrawerLayout.setDrawerListener(new DrawerLayout.DrawerListener() { + + @Override + public void onDrawerSlide(View drawerView, float slideOffset) { + BaseNavigationManager.this.onDrawerSlide(drawerView, slideOffset); + } + + @Override + public void onDrawerOpened(View drawerView) { + BaseNavigationManager.this.onDrawerOpened(drawerView); + } + + @Override + public void onDrawerClosed(View drawerView) { + BaseNavigationManager.this.onDrawerClosed(drawerView); + } + + @Override + public void onDrawerStateChanged(int newState) { + BaseNavigationManager.this.onDrawerStateChanged(newState); + } + + }); + + mFragmentManager.addOnBackStackChangedListener(new FragmentManager.OnBackStackChangedListener() { + @Override + public void onBackStackChanged() { + onFragmentChanged(); + } + }); + } + + @Override + public boolean isBackState() { + return mFragmentManager.getBackStackEntryCount() > 1 || (mDrawerLayout != null && mDrawerLayout.isDrawerOpen(Gravity.START)); + } + + @Override + public void onNavigationClick() {} + + /** + * Check if should sync progress of drawer sliding animation with navigation state changing animation. + */ + protected boolean shouldSyncDrawerSlidingProgress(){ + if(mFragmentManager.getBackStackEntryCount() > 1) + return false; + + return true; + } + + protected void onFragmentChanged(){ + notifyStateChanged(); + } + + /** + * Handling onDrawerSlide event of DrawerLayout. It'll sync progress of drawer sliding animation with navigation state changing animation if needed. + * If you also want to handle this event, make sure to call super method. + */ + protected void onDrawerSlide(View drawerView, float slideOffset){ + if(!shouldSyncDrawerSlidingProgress()){ + notifyStateInvalidated(); + } + else { + if (mDrawerLayout.isDrawerOpen(GravityCompat.START)) + notifyStateProgressChanged(false, 1f - slideOffset); + else + notifyStateProgressChanged(true, slideOffset); + } + } + + protected void onDrawerOpened(View drawerView){} + + protected void onDrawerClosed(View drawerView) {} + + protected void onDrawerStateChanged(int newState) {} + + } + + /** + * A Manager class extend from {@link BaseNavigationManager} class and add theme supporting. + */ + public static class ThemableNavigationManager extends BaseNavigationManager implements ThemeManager.OnThemeChangedListener{ + + private int mStyleId; + private int mCurrentStyle; + + /** + * + * @param styleId the styleId of navigation icon. + * @param drawerLayout can be null if you don't need to handle navigation state when open/close navigation drawer. + */ + public ThemableNavigationManager(int styleId, FragmentManager fragmentManager, Toolbar toolbar, DrawerLayout drawerLayout){ + super(ThemeManager.getInstance().getCurrentStyle(styleId), fragmentManager, toolbar, drawerLayout); + mStyleId = styleId; + mCurrentStyle = ThemeManager.getInstance().getCurrentStyle(styleId); + ThemeManager.getInstance().registerOnThemeChangedListener(this); + } + + @Override + public void onThemeChanged(@Nullable ThemeManager.OnThemeChangedEvent event) { + int style = ThemeManager.getInstance().getCurrentStyle(mStyleId); + if(mCurrentStyle != style){ + mCurrentStyle = style; + NavigationDrawerDrawable drawable = new NavigationDrawerDrawable.Builder(mToolbar.getContext(), mCurrentStyle).build(); + drawable.switchIconState(mNavigationIcon.getIconState(), false); + mNavigationIcon = drawable; + mToolbar.setNavigationIcon(mNavigationIcon); + } + } + + } +} diff --git a/material/src/main/java/com/rey/material/util/TypefaceUtil.java b/material/src/main/java/com/rey/material/util/TypefaceUtil.java new file mode 100644 index 0000000..3a06b2f --- /dev/null +++ b/material/src/main/java/com/rey/material/util/TypefaceUtil.java @@ -0,0 +1,37 @@ +package com.rey.material.util; + +import android.content.Context; +import android.graphics.Typeface; + +import java.util.HashMap; + +public class TypefaceUtil { + + private static final HashMap sCachedFonts = new HashMap<>(); + private static final String PREFIX_ASSET = "asset:"; + + private TypefaceUtil() { + } + + /** + * @param familyName if start with 'asset:' prefix, then load font from asset folder. + */ + public static Typeface load(Context context, String familyName, int style) { + if (familyName != null && familyName.startsWith(PREFIX_ASSET)) + synchronized (sCachedFonts) { + try { + if (!sCachedFonts.containsKey(familyName)) { + final Typeface typeface = Typeface.createFromAsset(context.getAssets(), familyName.substring(PREFIX_ASSET.length())); + sCachedFonts.put(familyName, typeface); + return typeface; + } + } catch (Exception e) { + return Typeface.DEFAULT; + } + + return sCachedFonts.get(familyName); + } + + return Typeface.create(familyName, style); + } +} diff --git a/material/src/main/java/com/rey/material/util/ViewUtil.java b/material/src/main/java/com/rey/material/util/ViewUtil.java new file mode 100644 index 0000000..0fcf405 --- /dev/null +++ b/material/src/main/java/com/rey/material/util/ViewUtil.java @@ -0,0 +1,774 @@ +package com.rey.material.util; + +import android.annotation.SuppressLint; +import android.content.res.TypedArray; +import android.graphics.PorterDuff; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AutoCompleteTextView; +import android.widget.ImageView; +import android.widget.TextView; + +import com.rey.material.R; + +import java.util.concurrent.atomic.AtomicInteger; + +public class ViewUtil { + + public static final long FRAME_DURATION = 1000 / 60; + + private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1); + + @SuppressLint("NewApi") + public static int generateViewId() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) { + for (;;) { + final int result = sNextGeneratedId.get(); + // aapt-generated IDs have the high byte nonzero; clamp to the range under that. + int newValue = result + 1; + if (newValue > 0x00FFFFFF) + newValue = 1; // Roll over to 1, not 0. + if (sNextGeneratedId.compareAndSet(result, newValue)) + return result; + } + } + else + return View.generateViewId(); + } + + public static boolean hasState(int[] states, int state){ + if(states == null) + return false; + + for (int state1 : states) + if (state1 == state) + return true; + + return false; + } + + public static void setBackground(View v, Drawable drawable){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) + v.setBackground(drawable); + else + v.setBackgroundDrawable(drawable); + } + + /** + * Apply any View style attributes to a view. + * @param v The view is applied. + * @param resId The style resourceId. + */ + public static void applyStyle(View v, int resId){ + applyStyle(v, null, 0, resId); + } + + /** + * Apply any View style attributes to a view. + * @param v The view is applied. + * @param attrs + * @param defStyleAttr + * @param defStyleRes + */ + public static void applyStyle(View v, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + TypedArray a = v.getContext().obtainStyledAttributes(attrs, R.styleable.View, defStyleAttr, defStyleRes); + + int leftPadding = -1; + int topPadding = -1; + int rightPadding = -1; + int bottomPadding = -1; + int startPadding = Integer.MIN_VALUE; + int endPadding = Integer.MIN_VALUE; + int padding = -1; + + boolean startPaddingDefined = false; + boolean endPaddingDefined = false; + boolean leftPaddingDefined = false; + boolean rightPaddingDefined = false; + + for(int i = 0, count = a.getIndexCount(); i < count; i++){ + int attr = a.getIndex(i); + if(attr == R.styleable.View_android_background) { + Drawable bg = a.getDrawable(attr); + ViewUtil.setBackground(v, bg); + } + else if(attr == R.styleable.View_android_backgroundTint){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + v.setBackgroundTintList(a.getColorStateList(attr)); + } + else if(attr == R.styleable.View_android_backgroundTintMode){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){ + int value = a.getInt(attr, 3); + switch (value){ + case 3: + v.setBackgroundTintMode(PorterDuff.Mode.SRC_OVER); + break; + case 5: + v.setBackgroundTintMode(PorterDuff.Mode.SRC_IN); + break; + case 9: + v.setBackgroundTintMode(PorterDuff.Mode.SRC_ATOP); + break; + case 14: + v.setBackgroundTintMode(PorterDuff.Mode.MULTIPLY); + break; + case 15: + v.setBackgroundTintMode(PorterDuff.Mode.SCREEN); + break; + case 16: + v.setBackgroundTintMode(PorterDuff.Mode.ADD); + break; + } + } + } + else if(attr == R.styleable.View_android_elevation){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + v.setElevation(a.getDimensionPixelOffset(attr, 0)); + } + else if(attr == R.styleable.View_android_padding) { + padding = a.getDimensionPixelSize(attr, -1); + leftPaddingDefined = true; + rightPaddingDefined = true; + } + else if(attr == R.styleable.View_android_paddingLeft) { + leftPadding = a.getDimensionPixelSize(attr, -1); + leftPaddingDefined = true; + } + else if(attr == R.styleable.View_android_paddingTop) + topPadding = a.getDimensionPixelSize(attr, -1); + else if(attr == R.styleable.View_android_paddingRight) { + rightPadding = a.getDimensionPixelSize(attr, -1); + rightPaddingDefined = true; + } + else if(attr == R.styleable.View_android_paddingBottom) + bottomPadding = a.getDimensionPixelSize(attr, -1); + else if(attr == R.styleable.View_android_paddingStart) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + startPadding = a.getDimensionPixelSize(attr, Integer.MIN_VALUE); + startPaddingDefined = (startPadding != Integer.MIN_VALUE); + } + } + else if(attr == R.styleable.View_android_paddingEnd) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + endPadding = a.getDimensionPixelSize(attr, Integer.MIN_VALUE); + endPaddingDefined = (endPadding != Integer.MIN_VALUE); + } + } + else if(attr == R.styleable.View_android_fadeScrollbars) + v.setScrollbarFadingEnabled(a.getBoolean(attr, true)); + else if(attr == R.styleable.View_android_fadingEdgeLength) + v.setFadingEdgeLength(a.getDimensionPixelOffset(attr, 0)); + else if(attr == R.styleable.View_android_minHeight) + v.setMinimumHeight(a.getDimensionPixelSize(attr, 0)); + else if(attr == R.styleable.View_android_minWidth) + v.setMinimumWidth(a.getDimensionPixelSize(attr, 0)); + else if(attr == R.styleable.View_android_requiresFadingEdge) + v.setVerticalFadingEdgeEnabled(a.getBoolean(attr, true)); + else if(attr == R.styleable.View_android_scrollbarDefaultDelayBeforeFade) { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) + v.setScrollBarDefaultDelayBeforeFade(a.getInteger(attr, 0)); + } + else if(attr == R.styleable.View_android_scrollbarFadeDuration) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) + v.setScrollBarFadeDuration(a.getInteger(attr, 0)); + } + else if(attr == R.styleable.View_android_scrollbarSize) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) + v.setScrollBarSize(a.getDimensionPixelSize(attr, 0)); + } + else if(attr == R.styleable.View_android_scrollbarStyle) { + int value = a.getInteger(attr, 0); + switch (value){ + case 0x0: + v.setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY); + break; + case 0x01000000: + v.setScrollBarStyle(View.SCROLLBARS_INSIDE_INSET); + break; + case 0x02000000: + v.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY); + break; + case 0x03000000: + v.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_INSET); + break; + } + } + else if(attr == R.styleable.View_android_soundEffectsEnabled) + v.setSoundEffectsEnabled(a.getBoolean(attr, true)); + else if(attr == R.styleable.View_android_textAlignment){ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + int value = a.getInteger(attr, 0); + switch (value){ + case 0: + v.setTextAlignment(View.TEXT_ALIGNMENT_INHERIT); + break; + case 1: + v.setTextAlignment(View.TEXT_ALIGNMENT_GRAVITY); + break; + case 2: + v.setTextAlignment(View.TEXT_ALIGNMENT_TEXT_START); + break; + case 3: + v.setTextAlignment(View.TEXT_ALIGNMENT_TEXT_END); + break; + case 4: + v.setTextAlignment(View.TEXT_ALIGNMENT_CENTER); + break; + case 5: + v.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); + break; + case 6: + v.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_END); + break; + } + } + } + else if(attr == R.styleable.View_android_textDirection){ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + int value = a.getInteger(attr, 0); + switch (value){ + case 0: + v.setTextDirection(View.TEXT_DIRECTION_INHERIT); + break; + case 1: + v.setTextDirection(View.TEXT_DIRECTION_FIRST_STRONG); + break; + case 2: + v.setTextDirection(View.TEXT_DIRECTION_ANY_RTL); + break; + case 3: + v.setTextDirection(View.TEXT_DIRECTION_LTR); + break; + case 4: + v.setTextDirection(View.TEXT_DIRECTION_RTL); + break; + case 5: + v.setTextDirection(View.TEXT_DIRECTION_LOCALE); + break; + } + } + } + else if(attr == R.styleable.View_android_visibility){ + int value = a.getInteger(attr, 0); + switch (value){ + case 0: + v.setVisibility(View.VISIBLE); + break; + case 1: + v.setVisibility(View.INVISIBLE); + break; + case 2: + v.setVisibility(View.GONE); + break; + } + } + else if(attr == R.styleable.View_android_layoutDirection){ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + int value = a.getInteger(attr, 0); + switch (value){ + case 0: + v.setLayoutDirection(View.LAYOUT_DIRECTION_LTR); + break; + case 1: + v.setLayoutDirection(View.LAYOUT_DIRECTION_RTL); + break; + case 2: + v.setLayoutDirection(View.LAYOUT_DIRECTION_INHERIT); + break; + case 3: + v.setLayoutDirection(View.LAYOUT_DIRECTION_LOCALE); + break; + } + } + } + else if(attr == R.styleable.View_android_src){ + if(v instanceof ImageView){ + int resId = a.getResourceId(attr, 0); + ((ImageView)v).setImageResource(resId); + } + } + } + + if (padding >= 0) + v.setPadding(padding, padding, padding, padding); + else if(Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1){ + if(startPaddingDefined) + leftPadding = startPadding; + if(endPaddingDefined) + rightPadding = endPadding; + + v.setPadding(leftPadding >= 0 ? leftPadding : v.getPaddingLeft(), + topPadding >= 0 ? topPadding : v.getPaddingTop(), + rightPadding >= 0 ? rightPadding : v.getPaddingRight(), + bottomPadding >= 0 ? bottomPadding : v.getPaddingBottom()); + } + else{ + if(leftPaddingDefined || rightPaddingDefined) + v.setPadding(leftPaddingDefined ? leftPadding : v.getPaddingLeft(), + topPadding >= 0 ? topPadding : v.getPaddingTop(), + rightPaddingDefined ? rightPadding : v.getPaddingRight(), + bottomPadding >= 0 ? bottomPadding : v.getPaddingBottom()); + + if(startPaddingDefined || endPaddingDefined) + v.setPaddingRelative(startPaddingDefined ? startPadding : v.getPaddingStart(), + topPadding >= 0 ? topPadding : v.getPaddingTop(), + endPaddingDefined ? endPadding : v.getPaddingEnd(), + bottomPadding >= 0 ? bottomPadding : v.getPaddingBottom()); + } + + a.recycle(); + + if(v instanceof TextView) + applyStyle((TextView)v, attrs, defStyleAttr, defStyleRes); + } + + public static void applyFont(TextView v, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + TypedArray a = v.getContext().obtainStyledAttributes(attrs, new int[]{R.attr.tv_fontFamily}, defStyleAttr, defStyleRes); + String fontFamily = a.getString(0); + + if(fontFamily != null){ + Typeface typeface = TypefaceUtil.load(v.getContext(), fontFamily, 0); + v.setTypeface(typeface); + } + + a.recycle(); + } + + public static void applyTextAppearance(TextView v, int resId){ + if(resId == 0) + return; + + String fontFamily = null; + int typefaceIndex = -1; + int styleIndex = -1; + int shadowColor = 0; + float dx = 0, dy = 0, r = 0; + + TypedArray appearance = v.getContext().obtainStyledAttributes(resId, R.styleable.TextAppearance); + if (appearance != null) { + int n = appearance.getIndexCount(); + for (int i = 0; i < n; i++) { + int attr = appearance.getIndex(i); + + if (attr == R.styleable.TextAppearance_android_textColorHighlight) { + v.setHighlightColor(appearance.getColor(attr, 0)); + + } else if (attr == R.styleable.TextAppearance_android_textColor) { + v.setTextColor(appearance.getColorStateList(attr)); + + } else if (attr == R.styleable.TextAppearance_android_textColorHint) { + v.setHintTextColor(appearance.getColorStateList(attr)); + + } else if (attr == R.styleable.TextAppearance_android_textColorLink) { + v.setLinkTextColor(appearance.getColorStateList(attr)); + + } else if (attr == R.styleable.TextAppearance_android_textSize) { + v.setTextSize(TypedValue.COMPLEX_UNIT_PX, appearance.getDimensionPixelSize(attr, 0)); + + } else if (attr == R.styleable.TextAppearance_android_typeface) { + typefaceIndex = appearance.getInt(attr, -1); + + } else if (attr == R.styleable.TextAppearance_android_fontFamily) { + fontFamily = appearance.getString(attr); + + } else if (attr == R.styleable.TextAppearance_tv_fontFamily) { + fontFamily = appearance.getString(attr); + + } else if (attr == R.styleable.TextAppearance_android_textStyle) { + styleIndex = appearance.getInt(attr, -1); + + } else if (attr == R.styleable.TextAppearance_android_textAllCaps) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) + v.setAllCaps(appearance.getBoolean(attr, false)); + + } else if (attr == R.styleable.TextAppearance_android_shadowColor) { + shadowColor = appearance.getInt(attr, 0); + + } else if (attr == R.styleable.TextAppearance_android_shadowDx) { + dx = appearance.getFloat(attr, 0); + + } else if (attr == R.styleable.TextAppearance_android_shadowDy) { + dy = appearance.getFloat(attr, 0); + + } else if (attr == R.styleable.TextAppearance_android_shadowRadius) { + r = appearance.getFloat(attr, 0); + + } else if (attr == R.styleable.TextAppearance_android_elegantTextHeight) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + v.setElegantTextHeight(appearance.getBoolean(attr, false)); + + } else if (attr == R.styleable.TextAppearance_android_letterSpacing) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + v.setLetterSpacing(appearance.getFloat(attr, 0)); + + } else if (attr == R.styleable.TextAppearance_android_fontFeatureSettings) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + v.setFontFeatureSettings(appearance.getString(attr)); + + } + } + + appearance.recycle(); + } + + if (shadowColor != 0) + v.setShadowLayer(r, dx, dy, shadowColor); + + Typeface tf = null; + if (fontFamily != null) { + tf = TypefaceUtil.load(v.getContext(), fontFamily, styleIndex); + if (tf != null) + v.setTypeface(tf); + } + if(tf != null) { + switch (typefaceIndex) { + case 1: + tf = Typeface.SANS_SERIF; + break; + case 2: + tf = Typeface.SERIF; + break; + case 3: + tf = Typeface.MONOSPACE; + break; + } + v.setTypeface(tf, styleIndex); + } + } + + /** + * Apply any TextView style attributes to a view. + * @param v + * @param attrs + * @param defStyleAttr + * @param defStyleRes + */ + private static void applyStyle(TextView v, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + String fontFamily = null; + int typefaceIndex = -1; + int styleIndex = -1; + int shadowColor = 0; + float dx = 0, dy = 0, r = 0; + + Drawable drawableLeft = null, drawableTop = null, drawableRight = null, + drawableBottom = null, drawableStart = null, drawableEnd = null; + boolean drawableDefined = false; + boolean drawableRelativeDefined = false; + + /* + * Look the appearance up without checking first if it exists because + * almost every TextView has one and it greatly simplifies the logic + * to be able to parse the appearance first and then let specific tags + * for this View override it. + */ + TypedArray a = v.getContext().obtainStyledAttributes(attrs, R.styleable.TextViewAppearance, defStyleAttr, defStyleRes); + TypedArray appearance = null; + int ap = a.getResourceId(R.styleable.TextViewAppearance_android_textAppearance, 0); + a.recycle(); + + if (ap != 0) + appearance = v.getContext().obtainStyledAttributes(ap, R.styleable.TextAppearance); + + if (appearance != null) { + int n = appearance.getIndexCount(); + for (int i = 0; i < n; i++) { + int attr = appearance.getIndex(i); + + if (attr == R.styleable.TextAppearance_android_textColorHighlight) { + v.setHighlightColor(appearance.getColor(attr, 0)); + + } else if (attr == R.styleable.TextAppearance_android_textColor) { + v.setTextColor(appearance.getColorStateList(attr)); + + } else if (attr == R.styleable.TextAppearance_android_textColorHint) { + v.setHintTextColor(appearance.getColorStateList(attr)); + + } else if (attr == R.styleable.TextAppearance_android_textColorLink) { + v.setLinkTextColor(appearance.getColorStateList(attr)); + + } else if (attr == R.styleable.TextAppearance_android_textSize) { + v.setTextSize(TypedValue.COMPLEX_UNIT_PX, appearance.getDimensionPixelSize(attr, 0)); + + } else if (attr == R.styleable.TextAppearance_android_typeface) { + typefaceIndex = appearance.getInt(attr, -1); + + } else if (attr == R.styleable.TextAppearance_android_fontFamily) { + fontFamily = appearance.getString(attr); + + } else if (attr == R.styleable.TextAppearance_tv_fontFamily) { + fontFamily = appearance.getString(attr); + + } else if (attr == R.styleable.TextAppearance_android_textStyle) { + styleIndex = appearance.getInt(attr, -1); + + } else if (attr == R.styleable.TextAppearance_android_textAllCaps) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) + v.setAllCaps(appearance.getBoolean(attr, false)); + + } else if (attr == R.styleable.TextAppearance_android_shadowColor) { + shadowColor = appearance.getInt(attr, 0); + + } else if (attr == R.styleable.TextAppearance_android_shadowDx) { + dx = appearance.getFloat(attr, 0); + + } else if (attr == R.styleable.TextAppearance_android_shadowDy) { + dy = appearance.getFloat(attr, 0); + + } else if (attr == R.styleable.TextAppearance_android_shadowRadius) { + r = appearance.getFloat(attr, 0); + + } else if (attr == R.styleable.TextAppearance_android_elegantTextHeight) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + v.setElegantTextHeight(appearance.getBoolean(attr, false)); + + } else if (attr == R.styleable.TextAppearance_android_letterSpacing) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + v.setLetterSpacing(appearance.getFloat(attr, 0)); + + } else if (attr == R.styleable.TextAppearance_android_fontFeatureSettings) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + v.setFontFeatureSettings(appearance.getString(attr)); + + } + } + + appearance.recycle(); + } + + a = v.getContext().obtainStyledAttributes(attrs, R.styleable.TextView, defStyleAttr, defStyleRes); + + int n = a.getIndexCount(); + for (int i = 0; i < n; i++) { + int attr = a.getIndex(i); + + if (attr == R.styleable.TextView_android_drawableLeft) { + drawableLeft = a.getDrawable(attr); + drawableDefined = true; + + } else if (attr == R.styleable.TextView_android_drawableTop) { + drawableTop = a.getDrawable(attr); + drawableDefined = true; + + } else if (attr == R.styleable.TextView_android_drawableRight) { + drawableRight = a.getDrawable(attr); + drawableDefined = true; + + } else if (attr == R.styleable.TextView_android_drawableBottom) { + drawableBottom = a.getDrawable(attr); + drawableDefined = true; + + } else if (attr == R.styleable.TextView_android_drawableStart) { + drawableStart = a.getDrawable(attr); + drawableRelativeDefined = true; + + } else if (attr == R.styleable.TextView_android_drawableEnd) { + drawableEnd = a.getDrawable(attr); + drawableRelativeDefined = true; + + } else if (attr == R.styleable.TextView_android_drawablePadding) { + v.setCompoundDrawablePadding(a.getDimensionPixelSize(attr, 0)); + + } else if (attr == R.styleable.TextView_android_maxLines) { + v.setMaxLines(a.getInt(attr, -1)); + + } else if (attr == R.styleable.TextView_android_maxHeight) { + v.setMaxHeight(a.getDimensionPixelSize(attr, -1)); + + } else if (attr == R.styleable.TextView_android_lines) { + v.setLines(a.getInt(attr, -1)); + + } else if (attr == R.styleable.TextView_android_height) { + v.setHeight(a.getDimensionPixelSize(attr, -1)); + + } else if (attr == R.styleable.TextView_android_minLines) { + v.setMinLines(a.getInt(attr, -1)); + + } else if (attr == R.styleable.TextView_android_minHeight) { + v.setMinHeight(a.getDimensionPixelSize(attr, -1)); + + } else if (attr == R.styleable.TextView_android_maxEms) { + v.setMaxEms(a.getInt(attr, -1)); + + } else if (attr == R.styleable.TextView_android_maxWidth) { + v.setMaxWidth(a.getDimensionPixelSize(attr, -1)); + + } else if (attr == R.styleable.TextView_android_ems) { + v.setEms(a.getInt(attr, -1)); + + } else if (attr == R.styleable.TextView_android_width) { + v.setWidth(a.getDimensionPixelSize(attr, -1)); + + } else if (attr == R.styleable.TextView_android_minEms) { + v.setMinEms(a.getInt(attr, -1)); + + } else if (attr == R.styleable.TextView_android_minWidth) { + v.setMinWidth(a.getDimensionPixelSize(attr, -1)); + + } else if (attr == R.styleable.TextView_android_gravity) { + v.setGravity(a.getInt(attr, -1)); + + } else if (attr == R.styleable.TextView_android_scrollHorizontally) { + v.setHorizontallyScrolling(a.getBoolean(attr, false)); + + } else if (attr == R.styleable.TextView_android_includeFontPadding) { + v.setIncludeFontPadding(a.getBoolean(attr, true)); + + } else if (attr == R.styleable.TextView_android_cursorVisible) { + v.setCursorVisible(a.getBoolean(attr, true)); + + } else if (attr == R.styleable.TextView_android_textScaleX) { + v.setTextScaleX(a.getFloat(attr, 1.0f)); + + } else if (attr == R.styleable.TextView_android_shadowColor) { + shadowColor = a.getInt(attr, 0); + + } else if (attr == R.styleable.TextView_android_shadowDx) { + dx = a.getFloat(attr, 0); + + } else if (attr == R.styleable.TextView_android_shadowDy) { + dy = a.getFloat(attr, 0); + + } else if (attr == R.styleable.TextView_android_shadowRadius) { + r = a.getFloat(attr, 0); + + } else if (attr == R.styleable.TextView_android_textColorHighlight) { + v.setHighlightColor(a.getColor(attr, 0)); + + } else if (attr == R.styleable.TextView_android_textColor) { + v.setTextColor(a.getColorStateList(attr)); + + } else if (attr == R.styleable.TextView_android_textColorHint) { + v.setHintTextColor(a.getColorStateList(attr)); + + } else if (attr == R.styleable.TextView_android_textColorLink) { + v.setLinkTextColor(a.getColorStateList(attr)); + + } else if (attr == R.styleable.TextView_android_textSize) { + v.setTextSize(TypedValue.COMPLEX_UNIT_PX, a.getDimensionPixelSize(attr, 0)); + + } else if (attr == R.styleable.TextView_android_typeface) { + typefaceIndex = a.getInt(attr, -1); + + } else if (attr == R.styleable.TextView_android_textStyle) { + styleIndex = a.getInt(attr, -1); + + } else if (attr == R.styleable.TextView_android_fontFamily) { + fontFamily = a.getString(attr); + + } else if (attr == R.styleable.TextView_tv_fontFamily) { + fontFamily = a.getString(attr); + + } else if (attr == R.styleable.TextView_android_textAllCaps) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) + v.setAllCaps(a.getBoolean(attr, false)); + + } else if (attr == R.styleable.TextView_android_elegantTextHeight) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + v.setElegantTextHeight(a.getBoolean(attr, false)); + + } else if (attr == R.styleable.TextView_android_letterSpacing) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + v.setLetterSpacing(a.getFloat(attr, 0)); + + } else if (attr == R.styleable.TextView_android_fontFeatureSettings) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + v.setFontFeatureSettings(a.getString(attr)); + + } + } + a.recycle(); + + if (shadowColor != 0) + v.setShadowLayer(r, dx, dy, shadowColor); + + if(drawableDefined) { + Drawable[] drawables = v.getCompoundDrawables(); + if (drawableStart != null) + drawables[0] = drawableStart; + else if (drawableLeft != null) + drawables[0] = drawableLeft; + if (drawableTop != null) + drawables[1] = drawableTop; + if (drawableEnd != null) + drawables[2] = drawableEnd; + else if (drawableRight != null) + drawables[2] = drawableRight; + if (drawableBottom != null) + drawables[3] = drawableBottom; + v.setCompoundDrawablesWithIntrinsicBounds(drawables[0], drawables[1], drawables[2], drawables[3]); + } + + if(drawableRelativeDefined && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1){ + Drawable[] drawables = v.getCompoundDrawablesRelative(); + if (drawableStart != null) + drawables[0] = drawableStart; + if (drawableEnd != null) + drawables[2] = drawableEnd; + v.setCompoundDrawablesRelativeWithIntrinsicBounds(drawables[0], drawables[1], drawables[2], drawables[3]); + } + + Typeface tf = null; + if (fontFamily != null) { + tf = TypefaceUtil.load(v.getContext(), fontFamily, styleIndex); + if (tf != null) + v.setTypeface(tf); + } + if(tf != null) { + switch (typefaceIndex) { + case 1: + tf = Typeface.SANS_SERIF; + break; + case 2: + tf = Typeface.SERIF; + break; + case 3: + tf = Typeface.MONOSPACE; + break; + } + v.setTypeface(tf, styleIndex); + } + + if(v instanceof AutoCompleteTextView) + applyStyle((AutoCompleteTextView)v, attrs, defStyleAttr, defStyleRes); + } + + /** + * Apply any AutoCompleteTextView style attributes to a view. + * @param v + * @param attrs + * @param defStyleAttr + * @param defStyleRes + */ + private static void applyStyle(AutoCompleteTextView v, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + TypedArray a = v.getContext().obtainStyledAttributes(attrs, R.styleable.AutoCompleteTextView, defStyleAttr, defStyleRes); + + int n = a.getIndexCount(); + for (int i = 0; i < n; i++) { + int attr = a.getIndex(i); + + if(attr == R.styleable.AutoCompleteTextView_android_completionHint) + v.setCompletionHint(a.getString(attr)); + else if(attr == R.styleable.AutoCompleteTextView_android_completionThreshold) + v.setThreshold(a.getInteger(attr, 0)); + else if(attr == R.styleable.AutoCompleteTextView_android_dropDownAnchor) + v.setDropDownAnchor(a.getResourceId(attr, 0)); + else if(attr == R.styleable.AutoCompleteTextView_android_dropDownHeight) + v.setDropDownHeight(a.getLayoutDimension(attr, ViewGroup.LayoutParams.WRAP_CONTENT)); + else if(attr == R.styleable.AutoCompleteTextView_android_dropDownWidth) + v.setDropDownWidth(a.getLayoutDimension(attr, ViewGroup.LayoutParams.WRAP_CONTENT)); + else if(attr == R.styleable.AutoCompleteTextView_android_dropDownHorizontalOffset) + v.setDropDownHorizontalOffset(a.getDimensionPixelSize(attr, 0)); + else if(attr == R.styleable.AutoCompleteTextView_android_dropDownVerticalOffset) + v.setDropDownVerticalOffset(a.getDimensionPixelSize(attr, 0)); + else if(attr == R.styleable.AutoCompleteTextView_android_popupBackground) + v.setDropDownBackgroundDrawable(a.getDrawable(attr)); + } + a.recycle(); + } + +} diff --git a/material/src/main/java/com/rey/material/widget/Button.java b/material/src/main/java/com/rey/material/widget/Button.java new file mode 100644 index 0000000..709fe20 --- /dev/null +++ b/material/src/main/java/com/rey/material/widget/Button.java @@ -0,0 +1,128 @@ +package com.rey.material.widget; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import com.rey.material.util.ThemeManager; +import com.rey.material.drawable.RippleDrawable; +import com.rey.material.util.RippleManager; +import com.rey.material.util.ViewUtil; + +public class Button extends android.widget.Button implements ThemeManager.OnThemeChangedListener{ + + private RippleManager mRippleManager; + + protected int mStyleId; + protected int mCurrentStyle = ThemeManager.THEME_UNDEFINED; + + public Button(Context context) { + super(context); + init(context, null, 0, 0); + } + + public Button(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs, 0, 0); + } + + public Button(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs, defStyleAttr, 0); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public Button(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context, attrs, defStyleAttr, defStyleRes); + } + + protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + ViewUtil.applyFont(this, attrs, defStyleAttr, defStyleRes); + applyStyle(context, attrs, defStyleAttr, defStyleRes); + if(!isInEditMode()) + mStyleId = ThemeManager.getStyleId(context, attrs, defStyleAttr, defStyleRes); + } + + public void applyStyle(int resId){ + ViewUtil.applyStyle(this, resId); + applyStyle(getContext(), null, 0, resId); + } + + protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + getRippleManager().onCreate(this, context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public void setTextAppearance(int resId) { + ViewUtil.applyTextAppearance(this, resId); + } + + @Override + public void onThemeChanged(ThemeManager.OnThemeChangedEvent event) { + int style = ThemeManager.getInstance().getCurrentStyle(mStyleId); + if(mCurrentStyle != style){ + mCurrentStyle = style; + applyStyle(mCurrentStyle); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if(mStyleId != 0) { + ThemeManager.getInstance().registerOnThemeChangedListener(this); + onThemeChanged(null); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + RippleManager.cancelRipple(this); + if(mStyleId != 0) + ThemeManager.getInstance().unregisterOnThemeChangedListener(this); + } + + @Override + public void setBackground(Drawable drawable) { + Drawable background = getBackground(); + if(background instanceof RippleDrawable && !(drawable instanceof RippleDrawable)) + ((RippleDrawable) background).setBackgroundDrawable(drawable); + else + super.setBackground(drawable); + } + + + protected RippleManager getRippleManager(){ + if(mRippleManager == null){ + synchronized (RippleManager.class){ + if(mRippleManager == null) + mRippleManager = new RippleManager(); + } + } + return mRippleManager; + } + + @Override + public void setOnClickListener(OnClickListener l) { + RippleManager rippleManager = getRippleManager(); + if (l == rippleManager) + super.setOnClickListener(l); + else { + rippleManager.setOnClickListener(l); + setOnClickListener(rippleManager); + } + } + + @Override + public boolean onTouchEvent(@NonNull MotionEvent event) { + boolean result = super.onTouchEvent(event); + return getRippleManager().onTouchEvent(this, event) || result; + } + +} diff --git a/material/src/main/java/com/rey/material/widget/CheckBox.java b/material/src/main/java/com/rey/material/widget/CheckBox.java new file mode 100644 index 0000000..52cddfc --- /dev/null +++ b/material/src/main/java/com/rey/material/widget/CheckBox.java @@ -0,0 +1,50 @@ +package com.rey.material.widget; + +import android.content.Context; +import android.util.AttributeSet; + +import com.rey.material.drawable.CheckBoxDrawable; + +public class CheckBox extends CompoundButton { + public CheckBox(Context context) { + super(context); + } + + public CheckBox(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public CheckBox(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public CheckBox(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + super.applyStyle(context, attrs, defStyleAttr, defStyleRes); + CheckBoxDrawable drawable = new CheckBoxDrawable.Builder(context, attrs, defStyleAttr, defStyleRes).build(); + drawable.setInEditMode(isInEditMode()); + drawable.setAnimEnable(false); + setButtonDrawable(drawable); + drawable.setAnimEnable(true); + } + + /** + * Change the checked state of this button immediately without showing animation. + * @param checked The checked state. + */ + public void setCheckedImmediately(boolean checked){ + if(getButtonDrawable() instanceof CheckBoxDrawable){ + CheckBoxDrawable drawable = (CheckBoxDrawable)getButtonDrawable(); + drawable.setAnimEnable(false); + setChecked(checked); + drawable.setAnimEnable(true); + } + else + setChecked(checked); + } + +} diff --git a/material/src/main/java/com/rey/material/widget/CheckedImageView.java b/material/src/main/java/com/rey/material/widget/CheckedImageView.java new file mode 100644 index 0000000..beba309 --- /dev/null +++ b/material/src/main/java/com/rey/material/widget/CheckedImageView.java @@ -0,0 +1,140 @@ +package com.rey.material.widget; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.widget.Checkable; + +import com.rey.material.util.ThemeManager; +import com.rey.material.drawable.RippleDrawable; +import com.rey.material.util.RippleManager; +import com.rey.material.util.ViewUtil; + +public class CheckedImageView extends android.widget.ImageView implements Checkable, ThemeManager.OnThemeChangedListener { + + private RippleManager mRippleManager; + protected int mStyleId; + protected int mCurrentStyle = ThemeManager.THEME_UNDEFINED; + + private boolean mChecked = false; + + public CheckedImageView(Context context) { + super(context); + init(context, null, 0, 0); + } + + public CheckedImageView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs, 0, 0); + } + + public CheckedImageView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs, defStyleAttr, 0); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public CheckedImageView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context, attrs, defStyleAttr, defStyleRes); + } + + protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + applyStyle(context, attrs, defStyleAttr, defStyleRes); + if(!isInEditMode()) + mStyleId = ThemeManager.getStyleId(context, attrs, defStyleAttr, defStyleRes); + } + + public void applyStyle(int resId){ + ViewUtil.applyStyle(this, resId); + applyStyle(getContext(), null, 0, resId); + } + + protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + getRippleManager().onCreate(this, context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public void onThemeChanged(ThemeManager.OnThemeChangedEvent event) { + int style = ThemeManager.getInstance().getCurrentStyle(mStyleId); + if(mCurrentStyle != style){ + mCurrentStyle = style; + applyStyle(mCurrentStyle); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if(mStyleId != 0) { + ThemeManager.getInstance().registerOnThemeChangedListener(this); + onThemeChanged(null); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + RippleManager.cancelRipple(this); + if(mStyleId != 0) + ThemeManager.getInstance().unregisterOnThemeChangedListener(this); + } + + @Override + public void setBackground(Drawable drawable) { + Drawable background = getBackground(); + if(background instanceof RippleDrawable && !(drawable instanceof RippleDrawable)) + ((RippleDrawable) background).setBackgroundDrawable(drawable); + else + super.setBackground(drawable); + } + + protected RippleManager getRippleManager(){ + if(mRippleManager == null){ + synchronized (RippleManager.class){ + if(mRippleManager == null) + mRippleManager = new RippleManager(); + } + } + + return mRippleManager; + } + + @Override + public void setOnClickListener(OnClickListener l) { + RippleManager rippleManager = getRippleManager(); + if (l == rippleManager) + super.setOnClickListener(l); + else { + rippleManager.setOnClickListener(l); + setOnClickListener(rippleManager); + } + } + + @Override + public boolean onTouchEvent(@NonNull MotionEvent event) { + boolean result = super.onTouchEvent(event); + return getRippleManager().onTouchEvent(this, event) || result; + } + + @Override + public void setChecked(boolean b) { + if(mChecked != b){ + mChecked = b; + } + } + + @Override + public boolean isChecked() { + return mChecked; + } + + @Override + public void toggle() { + setChecked(!isChecked()); + } +} diff --git a/material/src/main/java/com/rey/material/widget/CheckedTextView.java b/material/src/main/java/com/rey/material/widget/CheckedTextView.java new file mode 100644 index 0000000..6df063f --- /dev/null +++ b/material/src/main/java/com/rey/material/widget/CheckedTextView.java @@ -0,0 +1,126 @@ +package com.rey.material.widget; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import com.rey.material.util.ThemeManager; +import com.rey.material.drawable.RippleDrawable; +import com.rey.material.util.RippleManager; +import com.rey.material.util.ViewUtil; + +public class CheckedTextView extends android.widget.CheckedTextView implements ThemeManager.OnThemeChangedListener { + + private RippleManager mRippleManager; + protected int mStyleId; + protected int mCurrentStyle = ThemeManager.THEME_UNDEFINED; + + public CheckedTextView(Context context) { + super(context); + init(context, null, 0, 0); + } + + public CheckedTextView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs, 0, 0); + } + + public CheckedTextView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs, defStyleAttr, 0); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public CheckedTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context, attrs, defStyleAttr, defStyleRes); + } + + protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + ViewUtil.applyFont(this, attrs, defStyleAttr, defStyleRes); + applyStyle(context, attrs, defStyleAttr, defStyleRes); + if(!isInEditMode()) + mStyleId = ThemeManager.getStyleId(context, attrs, defStyleAttr, defStyleRes); + } + + public void applyStyle(int resId){ + ViewUtil.applyStyle(this, resId); + applyStyle(getContext(), null, 0, resId); + } + + protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + getRippleManager().onCreate(this, context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public void setTextAppearance(int resId) { + ViewUtil.applyTextAppearance(this, resId); + } + + @Override + public void onThemeChanged(ThemeManager.OnThemeChangedEvent event) { + int style = ThemeManager.getInstance().getCurrentStyle(mStyleId); + if(mCurrentStyle != style){ + mCurrentStyle = style; + applyStyle(mCurrentStyle); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if(mStyleId != 0) { + ThemeManager.getInstance().registerOnThemeChangedListener(this); + onThemeChanged(null); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + RippleManager.cancelRipple(this); + if(mStyleId != 0) + ThemeManager.getInstance().unregisterOnThemeChangedListener(this); + } + + @Override + public void setBackgroundDrawable(Drawable drawable) { + Drawable background = getBackground(); + if(background instanceof RippleDrawable && !(drawable instanceof RippleDrawable)) + ((RippleDrawable) background).setBackgroundDrawable(drawable); + else + super.setBackgroundDrawable(drawable); + } + + protected RippleManager getRippleManager(){ + if(mRippleManager == null){ + synchronized (RippleManager.class){ + if(mRippleManager == null) + mRippleManager = new RippleManager(); + } + } + + return mRippleManager; + } + + @Override + public void setOnClickListener(OnClickListener l) { + RippleManager rippleManager = getRippleManager(); + if (l == rippleManager) + super.setOnClickListener(l); + else { + rippleManager.setOnClickListener(l); + setOnClickListener(rippleManager); + } + } + + @Override + public boolean onTouchEvent(@NonNull MotionEvent event) { + boolean result = super.onTouchEvent(event); + return getRippleManager().onTouchEvent(this, event) || result; + } +} diff --git a/material/src/main/java/com/rey/material/widget/CircleCheckedTextView.java b/material/src/main/java/com/rey/material/widget/CircleCheckedTextView.java new file mode 100644 index 0000000..2ebb3ee --- /dev/null +++ b/material/src/main/java/com/rey/material/widget/CircleCheckedTextView.java @@ -0,0 +1,98 @@ +package com.rey.material.widget; + +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.animation.Interpolator; + +import com.rey.material.drawable.CircleDrawable; +import com.rey.material.util.ViewUtil; + +public class CircleCheckedTextView extends android.widget.CheckedTextView { + + private CircleDrawable mBackground; + + public interface OnCheckedChangeListener{ + void onCheckedChanged(CircleCheckedTextView view, boolean checked); + } + + private OnCheckedChangeListener mCheckedChangeListener; + + public CircleCheckedTextView(Context context) { + super(context); + init(); + } + + public CircleCheckedTextView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public CircleCheckedTextView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public CircleCheckedTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + + protected void init(){ + setGravity(Gravity.CENTER); + setPadding(0, 0, 0, 0); + mBackground = new CircleDrawable(); + mBackground.setInEditMode(isInEditMode()); + mBackground.setAnimEnable(false); + ViewUtil.setBackground(this, mBackground); + mBackground.setAnimEnable(true); + } + + public void setOnCheckedChangeListener(OnCheckedChangeListener listener){ + mCheckedChangeListener = listener; + } + + @Override + public void setTextAppearance(int resId) { + ViewUtil.applyTextAppearance(this, resId); + } + + @Override + public void setBackgroundColor(int color) { + mBackground.setColor(color); + } + + /** + * Set the duration of background's animation. + * @param duration The duration + */ + public void setAnimDuration(int duration) { + mBackground.setAnimDuration(duration); + } + + public void setInterpolator(Interpolator in, Interpolator out) { + mBackground.setInterpolator(in, out); + } + + @Override + public void setChecked(boolean checked) { + boolean oldCheck = isChecked(); + + if(oldCheck != checked) { + super.setChecked(checked); + + if(mCheckedChangeListener != null) + mCheckedChangeListener.onCheckedChanged(this, checked); + } + } + + public void setCheckedImmediately(boolean checked){ + mBackground.setAnimEnable(false); + setChecked(checked); + mBackground.setAnimEnable(true); + } + +} diff --git a/material/src/main/java/com/rey/material/widget/CompoundButton.java b/material/src/main/java/com/rey/material/widget/CompoundButton.java new file mode 100644 index 0000000..cb6dae7 --- /dev/null +++ b/material/src/main/java/com/rey/material/widget/CompoundButton.java @@ -0,0 +1,296 @@ +package com.rey.material.widget; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import com.rey.material.util.ThemeManager; +import com.rey.material.drawable.PaddingDrawable; +import com.rey.material.drawable.RippleDrawable; +import com.rey.material.util.RippleManager; +import com.rey.material.util.ViewUtil; + +public class CompoundButton extends android.widget.CompoundButton implements ThemeManager.OnThemeChangedListener { + + private RippleManager mRippleManager; + private volatile PaddingDrawable mPaddingDrawable; + private boolean mIsRtl = false; + + protected int mStyleId; + protected int mCurrentStyle = ThemeManager.THEME_UNDEFINED; + + public CompoundButton(Context context) { + super(context); + init(context, null, 0, 0); + } + + public CompoundButton(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs, 0, 0); + } + + public CompoundButton(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs, defStyleAttr, 0); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public CompoundButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context, attrs, defStyleAttr, defStyleRes); + } + + protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) + applyPadding(context, attrs, defStyleAttr, defStyleRes); + setClickable(true); + ViewUtil.applyFont(this, attrs, defStyleAttr, defStyleRes); + applyStyle(context, attrs, defStyleAttr, defStyleRes); + if(!isInEditMode()) + mStyleId = ThemeManager.getStyleId(context, attrs, defStyleAttr, defStyleRes); + } + + public void applyStyle(int resId){ + ViewUtil.applyStyle(this, resId); + applyStyle(getContext(), null, 0, resId); + } + + protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + getRippleManager().onCreate(this, context, attrs, defStyleAttr, defStyleRes); + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + private void applyPadding(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + TypedArray a = context.obtainStyledAttributes(attrs, new int[]{android.R.attr.padding, android.R.attr.paddingLeft, android.R.attr.paddingTop, android.R.attr.paddingRight, android.R.attr.paddingBottom, android.R.attr.paddingStart, android.R.attr.paddingEnd}, defStyleAttr, defStyleRes); + + int padding = -1; + int leftPadding = -1; + int topPadding = -1; + int rightPadding = -1; + int bottomPadding = -1; + int startPadding = Integer.MIN_VALUE; + int endPadding = Integer.MIN_VALUE; + + boolean startPaddingDefined = false; + boolean endPaddingDefined = false; + boolean leftPaddingDefined = false; + boolean rightPaddingDefined = false; + + for(int i = 0, count = a.getIndexCount(); i < count; i++) { + int attr = a.getIndex(i); + if(attr == 0) { + padding = a.getDimensionPixelSize(attr, -1); + leftPaddingDefined = true; + rightPaddingDefined = true; + } + else if(attr == 1) { + leftPadding = a.getDimensionPixelSize(attr, -1); + leftPaddingDefined = true; + } + else if(attr == 2) + topPadding = a.getDimensionPixelSize(attr, -1); + else if(attr == 3) { + rightPadding = a.getDimensionPixelSize(attr, -1); + rightPaddingDefined = true; + } + else if(attr == 4) + bottomPadding = a.getDimensionPixelSize(attr, -1); + else if(attr == 5) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + startPadding = a.getDimensionPixelSize(attr, Integer.MIN_VALUE); + startPaddingDefined = (startPadding != Integer.MIN_VALUE); + } + } + else if(attr == 6) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + endPadding = a.getDimensionPixelSize(attr, Integer.MIN_VALUE); + endPaddingDefined = (endPadding != Integer.MIN_VALUE); + } + } + } + + a.recycle(); + + if (padding >= 0) + setPadding(padding, padding, padding, padding); + else{ + if(leftPaddingDefined || rightPaddingDefined) + setPadding(leftPaddingDefined ? leftPadding : getPaddingLeft(), + topPadding >= 0 ? topPadding : getPaddingTop(), + rightPaddingDefined ? rightPadding : getPaddingRight(), + bottomPadding >= 0 ? bottomPadding : getPaddingBottom()); + + if(startPaddingDefined || endPaddingDefined) + setPaddingRelative(startPaddingDefined ? startPadding : getPaddingStart(), + topPadding >= 0 ? topPadding : getPaddingTop(), + endPaddingDefined ? endPadding : getPaddingEnd(), + bottomPadding >= 0 ? bottomPadding : getPaddingBottom()); + } + } + + private PaddingDrawable getPaddingDrawable(){ + if(mPaddingDrawable == null){ + synchronized (this){ + if(mPaddingDrawable == null) + mPaddingDrawable = new PaddingDrawable(null); + } + } + + return mPaddingDrawable; + } + + @Override + public void setTextAppearance(int resId) { + ViewUtil.applyTextAppearance(this, resId); + } + + @Override + public void onThemeChanged(ThemeManager.OnThemeChangedEvent event) { + int style = ThemeManager.getInstance().getCurrentStyle(mStyleId); + if(mCurrentStyle != style){ + mCurrentStyle = style; + applyStyle(mCurrentStyle); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if(mStyleId != 0) { + ThemeManager.getInstance().registerOnThemeChangedListener(this); + onThemeChanged(null); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + RippleManager.cancelRipple(this); + if(mStyleId != 0) + ThemeManager.getInstance().unregisterOnThemeChangedListener(this); + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + @Override + public void onRtlPropertiesChanged(int layoutDirection) { + boolean rtl = layoutDirection == LAYOUT_DIRECTION_RTL; + if(mIsRtl != rtl) { + mIsRtl = rtl; + + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) + setPaddingRelative(getPaddingStart(), getPaddingTop(), getPaddingEnd(), getPaddingBottom()); + else + setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), getPaddingBottom()); + + setCompoundDrawablePadding(getCompoundDrawablePadding()); + invalidate(); + } + } + + @Override + public void setBackground(Drawable drawable) { + Drawable background = getBackground(); + if(background instanceof RippleDrawable && !(drawable instanceof RippleDrawable)) + ((RippleDrawable) background).setBackgroundDrawable(drawable); + else + super.setBackground(drawable); + } + + protected RippleManager getRippleManager(){ + if(mRippleManager == null){ + synchronized (RippleManager.class){ + if(mRippleManager == null) + mRippleManager = new RippleManager(); + } + } + + return mRippleManager; + } + + @Override + public void setOnClickListener(OnClickListener l) { + RippleManager rippleManager = getRippleManager(); + if (l == rippleManager) + super.setOnClickListener(l); + else { + rippleManager.setOnClickListener(l); + setOnClickListener(rippleManager); + } + } + + @Override + public boolean onTouchEvent(@NonNull MotionEvent event) { + boolean result = super.onTouchEvent(event); + return getRippleManager().onTouchEvent(this, event) || result; + } + + @Override + public void setButtonDrawable(Drawable d) { + super.setButtonDrawable(null); + getPaddingDrawable().setWrappedDrawable(d); + super.setButtonDrawable(getPaddingDrawable()); + } + + @Override + public Drawable getButtonDrawable(){ + return getPaddingDrawable().getWrappedDrawable(); + } + + @Override + public void setPadding(int left, int top, int right, int bottom) { + PaddingDrawable drawable = getPaddingDrawable(); + if (mIsRtl) + drawable.setPadding(drawable.getPaddingLeft(), top, right, bottom); + else + drawable.setPadding(left, top, drawable.getPaddingRight(), bottom); + + super.setPadding(left, top, right, bottom); + } + + @Override + public void setPaddingRelative(int start, int top, int end, int bottom) { + PaddingDrawable drawable = getPaddingDrawable(); + if (mIsRtl) + drawable.setPadding(drawable.getPaddingLeft(), top, start, bottom); + else + drawable.setPadding(start, top, drawable.getPaddingRight(), bottom); + + super.setPaddingRelative(start, top, end, bottom); + } + + @Override + public void setCompoundDrawablePadding(int pad) { + PaddingDrawable drawable = getPaddingDrawable(); + if (mIsRtl) + drawable.setPadding(pad, drawable.getPaddingTop(), drawable.getPaddingRight(), drawable.getPaddingBottom()); + else + drawable.setPadding(drawable.getPaddingLeft(), drawable.getPaddingTop(), pad, drawable.getPaddingBottom()); + + super.setCompoundDrawablePadding(pad); + } + + @Override + public int getCompoundPaddingLeft() { + if(mIsRtl) + return getPaddingLeft(); + else { + PaddingDrawable drawable = getPaddingDrawable(); + return drawable.getIntrinsicWidth(); + } + } + + @Override + public int getCompoundPaddingRight() { + if(!mIsRtl) + return getPaddingRight(); + else{ + PaddingDrawable drawable = getPaddingDrawable(); + return drawable.getIntrinsicWidth(); + } + } +} diff --git a/material/src/main/java/com/rey/material/widget/DatePicker.java b/material/src/main/java/com/rey/material/widget/DatePicker.java new file mode 100644 index 0000000..1c9d19a --- /dev/null +++ b/material/src/main/java/com/rey/material/widget/DatePicker.java @@ -0,0 +1,878 @@ +package com.rey.material.widget; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.os.Build; +import android.os.SystemClock; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.animation.AnimationUtils; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; +import android.widget.AbsListView; +import android.widget.BaseAdapter; + +import com.rey.material.R; +import com.rey.material.drawable.BlankDrawable; +import com.rey.material.util.ThemeUtil; +import com.rey.material.util.TypefaceUtil; +import com.rey.material.util.ViewUtil; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Locale; + +import io.github.xhinliang.lunarcalendar.LunarCalendar; + +/** + * Created by Rey on 12/31/2014. + * Date Picker + */ +public class DatePicker extends ListView implements AbsListView.OnScrollListener { + + protected static final int SCROLL_DURATION = 250; + protected static final int SCROLL_CHANGE_DELAY = 40; + protected static final int LIST_TOP_OFFSET = -1; + + private Typeface mTypeface; + private int mTextSize; + private int mTextColor; + private int mTextLabelColor; + private int mTextHighlightColor; + private int mSelectionColor; + private int mAnimDuration; + private Interpolator mInInterpolator; + private Interpolator mOutInterpolator; + + private Paint mPaint; + private float mDayBaseWidth; + private float mDayBaseHeight; + private float mDayHeight; + private float mDayWidth; + private int mDayPadding; + private float mSelectionRadius; + private int mMonthRealWidth; + private int mMonthRealHeight; + + private Calendar mCalendar; + private int mFirstDayOfWeek; + private String[] mLabels; + private MonthAdapter mAdapter; + + private OnDateChangedListener mOnDateChangedListener; + + /** + * Interface definition for a callback to be invoked when the selected date is changed. + */ + public interface OnDateChangedListener { + + /** + * Called when the selected date is changed. + * + * @param oldDay The day value of old date. + * @param oldMonth The month value of old date. + * @param oldYear The year value of old date. + * @param newDay The day value of new date. + * @param newMonth The month value of new date. + * @param newYear The year value of new date. + */ + void onDateChanged(int oldDay, int oldMonth, int oldYear, int newDay, int newMonth, int newYear); + } + + + protected int mCurrentScrollState = 0; + protected long mPreviousScrollPosition; + protected int mPreviousScrollState = 0; + protected float mFriction = 1.0F; + protected ScrollStateRunnable mScrollStateChangedRunnable = new ScrollStateRunnable(); + + private int mPaddingLeft; + private int mPaddingTop; + private int mPaddingRight; + private int mPaddingBottom; + + private static final String YEAR_FORMAT = "%4d"; + + public DatePicker(Context context) { + super(context); + + init(context, null, 0, 0); + } + + public DatePicker(Context context, AttributeSet attrs) { + super(context, attrs); + + init(context, attrs, 0, 0); + } + + public DatePicker(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + init(context, attrs, defStyleAttr, 0); + } + + public DatePicker(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + init(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + mTypeface = Typeface.DEFAULT; + mTextSize = -1; + mTextColor = 0xFF000000; + mTextLabelColor = 0xFF767676; + mTextHighlightColor = 0xFFFFFFFF; + mAnimDuration = -1; + mLabels = new String[7]; + mFriction = 1.0F; + + setWillNotDraw(false); + setSelector(BlankDrawable.getInstance()); + setCacheColorHint(0); + setDivider(null); + setItemsCanFocus(true); + setFastScrollEnabled(false); + setVerticalScrollBarEnabled(false); + setOnScrollListener(this); + setFadingEdgeLength(0); + setFriction(ViewConfiguration.getScrollFriction() * mFriction); + + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mPaint.setStyle(Paint.Style.FILL); + mPaint.setTextAlign(Paint.Align.CENTER); + + mDayPadding = ThemeUtil.dpToPx(context, 4); + + mSelectionColor = ThemeUtil.colorPrimary(context, 0xFF000000); + + mCalendar = Calendar.getInstance(); + mFirstDayOfWeek = mCalendar.getFirstDayOfWeek(); + + int index = mCalendar.get(Calendar.DAY_OF_WEEK) - 1; + DateFormat format = new SimpleDateFormat(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 ? "EEEEE" : "E", Locale.getDefault()); + for (int i = 0; i < 7; i++) { + mLabels[index] = format.format(mCalendar.getTime()); + index = (index + 1) % 7; + mCalendar.add(Calendar.DAY_OF_MONTH, 1); + } + + mAdapter = new MonthAdapter(); + setAdapter(mAdapter); + + super.init(context, attrs, defStyleAttr, defStyleRes); + } + + @SuppressLint("PrivateResource") + @Override + protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super.applyStyle(context, attrs, defStyleAttr, defStyleRes); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DatePicker, defStyleAttr, defStyleRes); + + String familyName = null; + int style = -1; + + int padding = -1; + int paddingLeft = -1; + int paddingRight = -1; + int paddingTop = -1; + int paddingBottom = -1; + boolean paddingDefined = false; + + for (int i = 0, count = a.getIndexCount(); i < count; i++) { + int attr = a.getIndex(i); + + if (attr == R.styleable.DatePicker_dp_dayTextSize) + mTextSize = a.getDimensionPixelSize(attr, 0); + else if (attr == R.styleable.DatePicker_dp_textColor) + mTextColor = a.getColor(attr, 0); + else if (attr == R.styleable.DatePicker_dp_textHighlightColor) + mTextHighlightColor = a.getColor(attr, 0); + else if (attr == R.styleable.DatePicker_dp_textLabelColor) + mTextLabelColor = a.getColor(attr, 0); + else if (attr == R.styleable.DatePicker_dp_selectionColor) + mSelectionColor = a.getColor(attr, 0); + else if (attr == R.styleable.DatePicker_dp_animDuration) + mAnimDuration = a.getInteger(attr, 0); + else if (attr == R.styleable.DatePicker_dp_inInterpolator) + mInInterpolator = AnimationUtils.loadInterpolator(context, a.getResourceId(attr, 0)); + else if (attr == R.styleable.DatePicker_dp_outInterpolator) + mOutInterpolator = AnimationUtils.loadInterpolator(context, a.getResourceId(attr, 0)); + else if (attr == R.styleable.DatePicker_dp_fontFamily) + familyName = a.getString(attr); + else if (attr == R.styleable.DatePicker_dp_textStyle) + style = a.getInteger(attr, 0); + else if (attr == R.styleable.DatePicker_android_padding) { + padding = a.getDimensionPixelSize(attr, 0); + paddingDefined = true; + } else if (attr == R.styleable.DatePicker_android_paddingLeft) { + paddingLeft = a.getDimensionPixelSize(attr, 0); + paddingDefined = true; + } else if (attr == R.styleable.DatePicker_android_paddingTop) { + paddingTop = a.getDimensionPixelSize(attr, 0); + paddingDefined = true; + } else if (attr == R.styleable.DatePicker_android_paddingRight) { + paddingRight = a.getDimensionPixelSize(attr, 0); + paddingDefined = true; + } else if (attr == R.styleable.DatePicker_android_paddingBottom) { + paddingBottom = a.getDimensionPixelSize(attr, 0); + paddingDefined = true; + } + } + + if (mTextSize < 0) + mTextSize = context.getResources().getDimensionPixelOffset(R.dimen.abc_text_size_caption_material); + + if (mAnimDuration < 0) + mAnimDuration = context.getResources().getInteger(android.R.integer.config_mediumAnimTime); + + if (mInInterpolator == null) + mInInterpolator = new DecelerateInterpolator(); + + if (mOutInterpolator == null) + mOutInterpolator = new DecelerateInterpolator(); + + if (familyName != null || style >= 0) + mTypeface = TypefaceUtil.load(context, familyName, style); + + a.recycle(); + + if (paddingDefined) { + if (padding >= 0) + setContentPadding(padding, padding, padding, padding); + + if (paddingLeft >= 0) + mPaddingLeft = paddingLeft; + + if (paddingTop >= 0) + mPaddingTop = paddingTop; + + if (paddingRight >= 0) + mPaddingRight = paddingRight; + + if (paddingBottom >= 0) + mPaddingBottom = paddingBottom; + } + + requestLayout(); + mAdapter.notifyDataSetInvalidated(); + } + + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { + MonthView child = (MonthView) view.getChildAt(0); + if (child == null) + return; + // Figure out where we are + mPreviousScrollPosition = getFirstVisiblePosition() * child.getHeight() - child.getBottom(); + mPreviousScrollState = mCurrentScrollState; + } + + @Override + public void onScrollStateChanged(AbsListView absListView, int scroll) { + mScrollStateChangedRunnable.doScrollStateChange(scroll); + } + + private void measureBaseSize() { + mPaint.setTextSize(mTextSize); + mPaint.setTypeface(mTypeface); + mDayBaseWidth = mPaint.measureText("88", 0, 2) + mDayPadding * 2; + + Rect bounds = new Rect(); + mPaint.getTextBounds("88", 0, 2, bounds); + mDayBaseHeight = bounds.height(); + } + + private void measureMonthView(int widthMeasureSpec, int heightMeasureSpec) { + int widthMode = View.MeasureSpec.getMode(widthMeasureSpec); + int widthSize = View.MeasureSpec.getSize(widthMeasureSpec); + int heightMode = View.MeasureSpec.getMode(heightMeasureSpec); + int heightSize = View.MeasureSpec.getSize(heightMeasureSpec); + + measureBaseSize(); + + int size = Math.round(Math.max(mDayBaseWidth, mDayBaseHeight)); + + int width = size * 7 + mPaddingLeft + mPaddingRight; + int height = Math.round(size * 7 + mDayBaseHeight + mDayPadding * 2 + mPaddingTop + mPaddingBottom); + + switch (widthMode) { + case View.MeasureSpec.AT_MOST: + width = Math.min(width, widthSize); + break; + case View.MeasureSpec.EXACTLY: + width = widthSize; + break; + } + + switch (heightMode) { + case View.MeasureSpec.AT_MOST: + height = Math.min(height, heightSize); + break; + case View.MeasureSpec.EXACTLY: + height = heightSize; + break; + } + + mMonthRealWidth = width; + mMonthRealHeight = height; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + measureMonthView(widthMeasureSpec, heightMeasureSpec); + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + mDayWidth = (w - mPaddingLeft - mPaddingRight) / 7f; + mDayHeight = (h - mDayBaseHeight - mDayPadding * 2 - mPaddingTop - mPaddingBottom) / 7f; + mSelectionRadius = Math.min(mDayWidth, mDayHeight) / 2f; + } + + @Override + public void setPadding(int left, int top, int right, int bottom) { + super.setPadding(0, 0, 0, 0); + } + + public void setContentPadding(int left, int top, int right, int bottom) { + mPaddingLeft = left; + mPaddingTop = top; + mPaddingRight = right; + mPaddingBottom = bottom; + } + + /** + * Set the range of selectable dates. + * + * @param minDay The day value of minimum date. + * @param minMonth The month value of minimum date. + * @param minYear The year value of minimum date. + * @param maxDay The day value of maximum date. + * @param maxMonth The month value of maximum date. + * @param maxYear The year value of maximum date. + */ + public void setDateRange(int minDay, int minMonth, int minYear, int maxDay, int maxMonth, int maxYear) { + mAdapter.setDateRange(minDay, minMonth, minYear, maxDay, maxMonth, maxYear); + } + + /** + * Jump to the view of a specific month. + */ + public void goTo(int month, int year) { + int position = mAdapter.positionOfMonth(month, year); + postSetSelectionFromTop(position, 0); + } + + public void postSetSelectionFromTop(final int position, final int offset) { + post(new Runnable() { + @Override + public void run() { + setSelectionFromTop(position, offset); + requestLayout(); + } + }); + } + + /** + * Set the selected date of this DatePicker. + * + * @param day The day value of selected date. + * @param month The month value of selected date. + * @param year The year value of selected date. + */ + public void setDate(int day, int month, int year) { + if (mAdapter.getYear() == year && mAdapter.getMonth() == month && mAdapter.getDay() == day) + return; + + mAdapter.setDate(day, month, year, false); + goTo(month, year); + } + + /** + * Set the listener will be called when the selected date is changed. + * + * @param listener The {@link OnDateChangedListener} will be called. + */ + public void setOnDateChangedListener(OnDateChangedListener listener) { + mOnDateChangedListener = listener; + } + + /** + * @return The day value of selected date. + */ + public int getDay() { + return mAdapter.getDay(); + } + + /** + * @return The month value of selected date. + */ + public int getMonth() { + return mAdapter.getMonth(); + } + + /** + * @return The year value of selected date. + */ + public int getYear() { + return mAdapter.getYear(); + } + + /** + * Get the formatted string of selected date. + */ + public String getFormattedDate(DateFormat formatter) { + mCalendar.set(Calendar.YEAR, mAdapter.getYear()); + mCalendar.set(Calendar.MONTH, mAdapter.getMonth()); + mCalendar.set(Calendar.DAY_OF_MONTH, mAdapter.getDay()); + return formatter.format(mCalendar.getTime()); + } + + public int getSelectionColor() { + return mSelectionColor; + } + + public int getTextSize() { + return mTextSize; + } + + public Typeface getTypeface() { + return mTypeface; + } + + public int getTextColor() { + return mTextColor; + } + + + public int getTextHighlightColor() { + return mTextHighlightColor; + } + + + public Calendar getCalendar() { + return mCalendar; + } + + private class ScrollStateRunnable implements Runnable { + private int mNewState; + + /** + * Sets up the runnable with a short delay in case the scroll state + * immediately changes again. + * + * @param scrollState The new state it changed to + */ + public void doScrollStateChange(int scrollState) { + mNewState = scrollState; + postDelayed(this, SCROLL_CHANGE_DELAY); + } + + @Override + public void run() { + mCurrentScrollState = mNewState; + // Fix the position after a scroll or a fling ends + if (mNewState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE && mPreviousScrollState != AbsListView.OnScrollListener.SCROLL_STATE_IDLE && mPreviousScrollState != AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { + mPreviousScrollState = mNewState; + int i = 0; + View child = getChildAt(i); + while (child != null && child.getBottom() <= 0) + child = getChildAt(++i); + if (child == null) + return; + int firstPosition = getFirstVisiblePosition(); + int lastPosition = getLastVisiblePosition(); + boolean scroll = firstPosition != 0 && lastPosition != getCount() - 1; + final int top = child.getTop(); + final int bottom = child.getBottom(); + final int midpoint = getHeight() / 2; + if (scroll && top < LIST_TOP_OFFSET) { + if (bottom > midpoint) + smoothScrollBy(top, SCROLL_DURATION); + else + smoothScrollBy(bottom, SCROLL_DURATION); + } + } else + mPreviousScrollState = mNewState; + } + } + + private class MonthView extends View { + + private long mStartTime; + private float mAnimProgress; + private boolean mRunning; + + private int mTouchedDay = -1; + private int mMonth; + private int mYear; + private int mMaxDay; + private int mFirstDayCol; + private int mMinAvailDay = -1; + private int mMaxAvailDay = -1; + private int mSelectedDay = -1; + private int mPreviousSelectedDay = -1; + private String mMonthText; + + public MonthView(Context context) { + super(context); + setWillNotDraw(false); + } + + public void setMonth(int month, int year) { + if (mMonth != month || mYear != year) { + mMonth = month; + mYear = year; + calculateMonthView(); + invalidate(); + } + } + + public void setSelectedDay(int day, boolean animation) { + if (mSelectedDay != day) { + mPreviousSelectedDay = mSelectedDay; + mSelectedDay = day; + if (animation) + startAnimation(); + else + invalidate(); + } + } + + + public void setAvailableDay(int min, int max) { + if (mMinAvailDay != min || mMaxAvailDay != max) { + mMinAvailDay = min; + mMaxAvailDay = max; + invalidate(); + } + } + + private void calculateMonthView() { + mCalendar.set(Calendar.DAY_OF_MONTH, 1); + mCalendar.set(Calendar.MONTH, mMonth); + mCalendar.set(Calendar.YEAR, mYear); + + mMaxDay = mCalendar.getActualMaximum(Calendar.DAY_OF_MONTH); + int dayOfWeek = mCalendar.get(Calendar.DAY_OF_WEEK); + mFirstDayCol = dayOfWeek < mFirstDayOfWeek ? dayOfWeek + 7 - mFirstDayOfWeek : dayOfWeek - mFirstDayOfWeek; + mMonthText = mCalendar.getDisplayName(Calendar.MONTH, Calendar.LONG, Locale.getDefault()) + " " + String.format(YEAR_FORMAT, mYear); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + setMeasuredDimension(mMonthRealWidth, mMonthRealHeight); + } + + @Override + protected void onDraw(Canvas canvas) { + //draw month text + mPaint.setTextSize(mTextSize); + mPaint.setTypeface(mTypeface); + float x = 3.5f * mDayWidth + getPaddingLeft(); + float y = mDayPadding * 2 + mDayBaseHeight + getPaddingTop(); + mPaint.setFakeBoldText(true); + mPaint.setColor(mTextColor); + canvas.drawText(mMonthText, x, y, mPaint); + + //draw selection + float paddingLeft = getPaddingLeft(); + float paddingTop = mDayPadding * 2 + mDayBaseHeight + getPaddingTop(); + if (mSelectedDay > 0) { + int col = (mFirstDayCol + mSelectedDay - 1) % 7; + int row = (mFirstDayCol + mSelectedDay - 1) / 7 + 1; + x = (col + 0.5f) * mDayWidth + paddingLeft; + y = (row + 0.5f) * mDayHeight + paddingTop; + float radius = mRunning ? mInInterpolator.getInterpolation(mAnimProgress) * mSelectionRadius : mSelectionRadius; + mPaint.setColor(mSelectionColor); + canvas.drawCircle(x, y, radius, mPaint); + } + + if (mRunning && mPreviousSelectedDay > 0) { + int col = (mFirstDayCol + mPreviousSelectedDay - 1) % 7; + int row = (mFirstDayCol + mPreviousSelectedDay - 1) / 7 + 1; + x = (col + 0.5f) * mDayWidth + paddingLeft; + y = (row + 0.5f) * mDayHeight + paddingTop; + float radius = (1f - mOutInterpolator.getInterpolation(mAnimProgress)) * mSelectionRadius; + mPaint.setColor(mSelectionColor); + canvas.drawCircle(x, y, radius, mPaint); + } + + //draw label + mPaint.setFakeBoldText(false); + mPaint.setColor(mTextLabelColor); + paddingTop += (mDayHeight + mDayBaseHeight) / 2f; + for (int i = 0; i < 7; i++) { + x = (i + 0.5f) * mDayWidth + paddingLeft; + y = paddingTop; + int index = (i + mFirstDayOfWeek - 1) % 7; + canvas.drawText(mLabels[index], x, y, mPaint); + } + + //draw date text +// int col = mFirstDayCol; +// int row = 1; +// int maxDay = mMaxAvailDay > 0 ? Math.min(mMaxAvailDay, mMaxDay) : mMaxDay; +// for (int day = 1; day <= mMaxDay; day++) { +// if (day == mSelectedDay) +// mPaint.setColor(mTextHighlightColor); +// else if (day < mMinAvailDay || day > maxDay) +// mPaint.setColor(mTextDisableColor); +// else if (day == mToday) +// mPaint.setColor(mSelectionColor); +// else +// mPaint.setColor(mTextColor); +// x = (col + 0.5f) * mDayWidth + paddingLeft; +// y = row * mDayHeight + paddingTop; +// canvas.drawText("fefe", x, y, mPaint); +// col++; +// if (col == 7) { +// col = 0; +// row++; +// } +// } + + float tempX = 0; + float tempY = paddingTop; + LunarCalendar[][] month = LunarCalendar.getInstanceMonth(mYear, mMonth + 1); + for (LunarCalendar[] week : month) { + for (int j = 0; j < week.length; ++j) { + if (j == 0) { + tempY += mDayHeight; + tempX = -0.5f * mDayWidth + paddingLeft; + } + tempX += mDayWidth; + if (week[j] == null) { + continue; + } + if (week[j].isToday()) + mPaint.setColor(mTextLabelColor); + else if (week[j].getDay() == mSelectedDay) + mPaint.setColor(mTextHighlightColor); + else + mPaint.setColor(mTextColor); + mPaint.setTextSize(mTextSize); + canvas.drawText(week[j].getDay() + "", tempX, tempY - 10, mPaint); + mPaint.setTextSize(mTextSize - 5); + if (week[j].getLunar().day == 1) + canvas.drawText(week[j].getLunarMonth() + "月", tempX, tempY + 7, mPaint); + else + canvas.drawText(week[j].getLunarDay() + "", tempX, tempY + 7, mPaint); + } + } + + + } + + private int getTouchedDay(float x, float y) { + float paddingTop = mDayPadding * 2 + mDayBaseHeight + getPaddingTop() + mDayHeight; + if (x < getPaddingLeft() || x > getWidth() - getPaddingRight() || y < paddingTop || y > getHeight() - getPaddingBottom()) + return -1; + + int col = (int) Math.floor((x - getPaddingLeft()) / mDayWidth); + int row = (int) Math.floor((y - paddingTop) / mDayHeight); + int maxDay = mMaxAvailDay > 0 ? Math.min(mMaxAvailDay, mMaxDay) : mMaxDay; + + int day = row * 7 + col - mFirstDayCol + 1; + if (day < 0 || day < mMinAvailDay || day > maxDay) + return -1; + + return day; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + mTouchedDay = getTouchedDay(event.getX(), event.getY()); + return true; + case MotionEvent.ACTION_UP: + if (getTouchedDay(event.getX(), event.getY()) == mTouchedDay && mTouchedDay > 0) { + mAdapter.setDate(mTouchedDay, mMonth, mYear, true); + mTouchedDay = -1; + } + return true; + case MotionEvent.ACTION_CANCEL: + mTouchedDay = -1; + return true; + } + return true; + } + + private void resetAnimation() { + mStartTime = SystemClock.uptimeMillis(); + mAnimProgress = 0f; + } + + private void startAnimation() { + if (getHandler() != null) { + resetAnimation(); + mRunning = true; + getHandler().postAtTime(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + } + invalidate(); + } + + private void stopAnimation() { + mRunning = false; + mAnimProgress = 1f; + if (getHandler() != null) + getHandler().removeCallbacks(mUpdater); + invalidate(); + } + + private final Runnable mUpdater = new Runnable() { + @Override + public void run() { + update(); + } + }; + + private void update() { + long curTime = SystemClock.uptimeMillis(); + mAnimProgress = Math.min(1f, (float) (curTime - mStartTime) / mAnimDuration); + if (mAnimProgress == 1f) + stopAnimation(); + if (mRunning) { + if (getHandler() != null) + getHandler().postAtTime(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + else + stopAnimation(); + } + invalidate(); + } + } + + private class MonthAdapter extends BaseAdapter { + private int mDay = -1; + private int mMonth = -1; + private int mYear = -1; + private int mMinDay = -1; + private int mMinMonth = -1; + private int mMinYear = -1; + private int mMaxDay = -1; + private int mMaxMonth = -1; + private int mMaxYear = -1; + private int mMinMonthValue; + private int mMaxMonthValue; + + public void setDateRange(int minDay, int minMonth, int minYear, int maxDay, int maxMonth, int maxYear) { + int minMonthValue = minDay < 0 || minMonth < 0 || minYear < 0 ? 0 : minYear * 12 + minMonth; + int maxMonthValue = maxDay < 0 || maxMonth < 0 || maxYear < 0 ? Integer.MAX_VALUE - 1 : maxYear * 12 + maxMonth; + + if (minDay != mMinDay || mMinMonthValue != minMonthValue || maxDay != mMaxDay || mMaxMonthValue != maxMonthValue) { + mMinDay = minDay; + mMinMonth = minMonth; + mMinYear = minYear; + + mMaxDay = maxDay; + mMaxMonth = maxMonth; + mMaxYear = maxYear; + + mMinMonthValue = minMonthValue; + mMaxMonthValue = maxMonthValue; + notifyDataSetChanged(); + } + } + + public void setDate(int day, int month, int year, boolean animation) { + if (mMonth != month || mYear != year) { + MonthView v = (MonthView) getChildAt(positionOfMonth(mMonth, mYear) - getFirstVisiblePosition()); + if (v != null) + v.setSelectedDay(-1, false); + + int oldDay = mDay; + int oldMonth = mMonth; + int oldYear = mYear; + + mDay = day; + mMonth = month; + mYear = year; + + v = (MonthView) getChildAt(positionOfMonth(mMonth, mYear) - getFirstVisiblePosition()); + if (v != null) + v.setSelectedDay(mDay, animation); + + if (mOnDateChangedListener != null) + mOnDateChangedListener.onDateChanged(oldDay, oldMonth, oldYear, mDay, mMonth, mYear); + } else if (day != mDay) { + int oldDay = mDay; + + mDay = day; + + MonthView v = (MonthView) getChildAt(positionOfMonth(mMonth, mYear) - getFirstVisiblePosition()); + if (v != null) + v.setSelectedDay(mDay, animation); + + if (mOnDateChangedListener != null) + mOnDateChangedListener.onDateChanged(oldDay, mMonth, mYear, mDay, mMonth, mYear); + } + } + + public int positionOfMonth(int month, int year) { + return year * 12 + month - mMinMonthValue; + } + + public int getDay() { + return mDay; + } + + public int getMonth() { + return mMonth; + } + + public int getYear() { + return mYear; + } + + @Override + public int getCount() { + return mMaxMonthValue - mMinMonthValue + 1; + } + + @Override + public Object getItem(int position) { + return position + mMinMonthValue; + } + + @Override + public long getItemId(int position) { + return 0; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + MonthView v = (MonthView) convertView; + if (v == null) { + v = new MonthView(parent.getContext()); + v.setPadding(mPaddingLeft, mPaddingTop, mPaddingRight, mPaddingBottom); + } + int monthValue = (Integer) getItem(position); + int year = monthValue / 12; + int month = monthValue % 12; + int minDay = month == mMinMonth && year == mMinYear ? mMinDay : -1; + int maxDay = month == mMaxMonth && year == mMaxYear ? mMaxDay : -1; + int day = month == mMonth && year == mYear ? mDay : -1; + + v.setMonth(month, year); + v.setAvailableDay(minDay, maxDay); + v.setSelectedDay(day, false); + + return v; + } + } +} diff --git a/material/src/main/java/com/rey/material/widget/EditText.java b/material/src/main/java/com/rey/material/widget/EditText.java new file mode 100644 index 0000000..214c6ae --- /dev/null +++ b/material/src/main/java/com/rey/material/widget/EditText.java @@ -0,0 +1,4171 @@ +package com.rey.material.widget; + +import android.annotation.TargetApi; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.view.GravityCompat; +import android.text.Editable; +import android.text.InputFilter; +import android.text.Layout; +import android.text.Spannable; +import android.text.TextPaint; +import android.text.TextUtils; +import android.text.TextUtils.TruncateAt; +import android.text.TextWatcher; +import android.text.method.KeyListener; +import android.text.method.MovementMethod; +import android.text.method.PasswordTransformationMethod; +import android.text.method.TransformationMethod; +import android.text.style.URLSpan; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.ActionMode; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.CorrectionInfo; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.ExtractedText; +import android.view.inputmethod.ExtractedTextRequest; +import android.view.inputmethod.InputConnection; +import android.widget.AdapterView; +import android.widget.AutoCompleteTextView; +import android.widget.CursorAdapter; +import android.widget.Filter; +import android.widget.Filterable; +import android.widget.ListAdapter; +import android.widget.MultiAutoCompleteTextView; +import android.widget.Scroller; + +import com.rey.material.R; +import com.rey.material.util.ThemeManager; +import com.rey.material.drawable.DividerDrawable; +import com.rey.material.util.ThemeUtil; +import com.rey.material.util.ViewUtil; + +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Locale; + +public class EditText extends FrameLayout implements ThemeManager.OnThemeChangedListener{ + + private boolean mLabelEnable; + private boolean mLabelVisible; + protected int mSupportMode; + protected int mAutoCompleteMode; + + /** + * Indicate this EditText should not show a support text. + */ + public static final int SUPPORT_MODE_NONE = 0; + /** + * Indicate this EditText should show a helper text, or error text if it's set. + */ + public static final int SUPPORT_MODE_HELPER = 1; + /** + * Indicate this EditText should show a helper text, along with error text if it's set. + */ + public static final int SUPPORT_MODE_HELPER_WITH_ERROR = 2; + /** + * Indicate this EditText should show a char counter text. + */ + public static final int SUPPORT_MODE_CHAR_COUNTER = 3; + + public static final int AUTOCOMPLETE_MODE_NONE = 0; + public static final int AUTOCOMPLETE_MODE_SINGLE = 1; + public static final int AUTOCOMPLETE_MODE_MULTI = 2; + + private ColorStateList mDividerColors; + private ColorStateList mDividerErrorColors; + private boolean mDividerCompoundPadding; + private int mDividerPadding; + + private ColorStateList mSupportColors; + private ColorStateList mSupportErrorColors; + private int mSupportMaxChars; + private CharSequence mSupportHelper; + private CharSequence mSupportError; + + private int mLabelInAnimId; + private int mLabelOutAnimId; + + protected LabelView mLabelView; + protected android.widget.EditText mInputView; + protected LabelView mSupportView; + private DividerDrawable mDivider; + + private TextView.OnSelectionChangedListener mOnSelectionChangedListener; + + private boolean mIsRtl; + + public EditText(Context context) { + super(context); + } + + public EditText(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public EditText(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public EditText(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @SuppressWarnings("deprecation") + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + @Override + protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + mLabelEnable = false; + mLabelVisible = false; + mSupportMode = SUPPORT_MODE_NONE; + mAutoCompleteMode = AUTOCOMPLETE_MODE_NONE; + mDividerCompoundPadding = true; + mDividerPadding = -1; + mIsRtl = false; + + super.init(context, attrs, defStyleAttr, defStyleRes); + + if(isInEditMode()) + applyStyle(R.style.Material_Widget_EditText); + } + + private LabelView getLabelView(){ + if(mLabelView == null){ + mLabelView = new LabelView(getContext()); + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) + mLabelView.setTextDirection(mIsRtl ? TEXT_DIRECTION_RTL : TEXT_DIRECTION_LTR); + mLabelView.setGravity(Gravity.START); + mLabelView.setSingleLine(true); + } + + return mLabelView; + } + + private LabelView getSupportView(){ + if(mSupportView == null) + mSupportView = new LabelView(getContext()); + + return mSupportView; + } + + private boolean needCreateInputView(int autoCompleteMode){ + if(mInputView == null) + return true; + + switch (autoCompleteMode){ + case AUTOCOMPLETE_MODE_NONE: + return !(mInputView instanceof InternalEditText); + case AUTOCOMPLETE_MODE_SINGLE: + return !(mInputView instanceof InternalAutoCompleteTextView); + case AUTOCOMPLETE_MODE_MULTI: + return !(mInputView instanceof InternalMultiAutoCompleteTextView); + default: + return false; + } + } + + @Override + protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + super.applyStyle(context, attrs, defStyleAttr, defStyleRes); + + CharSequence text = mInputView == null ? null : mInputView.getText(); + removeAllViews(); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.EditText, defStyleAttr, defStyleRes); + + int labelPadding = -1; + int labelTextSize = -1; + ColorStateList labelTextColor = null; + int supportPadding = -1; + int supportTextSize = -1; + ColorStateList supportColors = null; + ColorStateList supportErrorColors = null; + String supportHelper = null; + String supportError = null; + ColorStateList dividerColors = null; + ColorStateList dividerErrorColors = null; + int dividerHeight = -1; + int dividerPadding = -1; + int dividerAnimDuration = -1; + + mAutoCompleteMode = a.getInteger(R.styleable.EditText_et_autoCompleteMode, mAutoCompleteMode); + if(needCreateInputView(mAutoCompleteMode)){ + switch (mAutoCompleteMode){ + case AUTOCOMPLETE_MODE_SINGLE: + mInputView = new InternalAutoCompleteTextView(context, attrs, defStyleAttr); + break; + case AUTOCOMPLETE_MODE_MULTI: + mInputView = new InternalMultiAutoCompleteTextView(context, attrs, defStyleAttr); + break; + default: + mInputView = new InternalEditText(context, attrs, defStyleAttr); + break; + } + ViewUtil.applyFont(mInputView, attrs, defStyleAttr, defStyleRes); + if(text != null) + mInputView.setText(text); + + mInputView.addTextChangedListener(new InputTextWatcher()); + + if(mDivider != null){ + mDivider.setAnimEnable(false); + ViewUtil.setBackground(mInputView, mDivider); + mDivider.setAnimEnable(true); + } + } + else + ViewUtil.applyStyle(mInputView, attrs, defStyleAttr, defStyleRes); + mInputView.setVisibility(View.VISIBLE); + mInputView.setFocusableInTouchMode(true); + + for(int i = 0, count = a.getIndexCount(); i < count; i++){ + int attr = a.getIndex(i); + + if(attr == R.styleable.EditText_et_labelEnable) + mLabelEnable = a.getBoolean(attr, false); + else if(attr == R.styleable.EditText_et_labelPadding) + labelPadding = a.getDimensionPixelSize(attr, 0); + else if(attr == R.styleable.EditText_et_labelTextSize) + labelTextSize = a.getDimensionPixelSize(attr, 0); + else if(attr == R.styleable.EditText_et_labelTextColor) + labelTextColor = a.getColorStateList(attr); + else if(attr == R.styleable.EditText_et_labelTextAppearance) + getLabelView().setTextAppearance(context, a.getResourceId(attr, 0)); + else if(attr == R.styleable.EditText_et_labelEllipsize){ + int labelEllipsize = a.getInteger(attr, 0); + switch (labelEllipsize) { + case 1: + getLabelView().setEllipsize(TruncateAt.START); + break; + case 2: + getLabelView().setEllipsize(TruncateAt.MIDDLE); + break; + case 3: + getLabelView().setEllipsize(TruncateAt.END); + break; + case 4: + getLabelView().setEllipsize(TruncateAt.MARQUEE); + break; + default: + getLabelView().setEllipsize(TruncateAt.END); + break; + } + } + else if(attr == R.styleable.EditText_et_labelInAnim) + mLabelInAnimId = a.getResourceId(attr, 0); + else if(attr == R.styleable.EditText_et_labelOutAnim) + mLabelOutAnimId = a.getResourceId(attr, 0); + else if(attr == R.styleable.EditText_et_supportMode) + mSupportMode = a.getInteger(attr, 0); + else if(attr == R.styleable.EditText_et_supportPadding) + supportPadding = a.getDimensionPixelSize(attr, 0); + else if(attr == R.styleable.EditText_et_supportTextSize) + supportTextSize = a.getDimensionPixelSize(attr, 0); + else if(attr == R.styleable.EditText_et_supportTextColor) + supportColors = a.getColorStateList(attr); + else if(attr == R.styleable.EditText_et_supportTextErrorColor) + supportErrorColors = a.getColorStateList(attr); + else if(attr == R.styleable.EditText_et_supportTextAppearance) + getSupportView().setTextAppearance(context, a.getResourceId(attr, 0)); + else if(attr == R.styleable.EditText_et_supportEllipsize){ + int supportEllipsize = a.getInteger(attr, 0); + switch (supportEllipsize) { + case 1: + getSupportView().setEllipsize(TruncateAt.START); + break; + case 2: + getSupportView().setEllipsize(TruncateAt.MIDDLE); + break; + case 3: + getSupportView().setEllipsize(TruncateAt.END); + break; + case 4: + getSupportView().setEllipsize(TruncateAt.MARQUEE); + break; + default: + getSupportView().setEllipsize(TruncateAt.END); + break; + } + } + else if(attr == R.styleable.EditText_et_supportMaxLines) + getSupportView().setMaxLines(a.getInteger(attr, 0)); + else if(attr == R.styleable.EditText_et_supportLines) + getSupportView().setLines(a.getInteger(attr, 0)); + else if(attr == R.styleable.EditText_et_supportSingleLine) + getSupportView().setSingleLine(a.getBoolean(attr, false)); + else if(attr == R.styleable.EditText_et_supportMaxChars) + mSupportMaxChars = a.getInteger(attr, 0); + else if(attr == R.styleable.EditText_et_helper) + supportHelper = a.getString(attr); + else if(attr == R.styleable.EditText_et_error) + supportError = a.getString(attr); + else if(attr == R.styleable.EditText_et_inputId) + mInputView.setId(a.getResourceId(attr, 0)); + else if(attr == R.styleable.EditText_et_dividerColor) + dividerColors = a.getColorStateList(attr); + else if(attr == R.styleable.EditText_et_dividerErrorColor) + dividerErrorColors = a.getColorStateList(attr); + else if(attr == R.styleable.EditText_et_dividerHeight) + dividerHeight = a.getDimensionPixelSize(attr, 0); + else if(attr == R.styleable.EditText_et_dividerPadding) + dividerPadding = a.getDimensionPixelSize(attr, 0); + else if(attr == R.styleable.EditText_et_dividerAnimDuration) + dividerAnimDuration = a.getInteger(attr, 0); + else if(attr == R.styleable.EditText_et_dividerCompoundPadding) + mDividerCompoundPadding = a.getBoolean(attr, true); + } + + a.recycle(); + + if(mInputView.getId() == 0) + mInputView.setId(ViewUtil.generateViewId()); + + if(mDivider == null){ + mDividerColors = dividerColors; + mDividerErrorColors = dividerErrorColors; + + if(mDividerColors == null){ + int[][] states = new int[][]{ + new int[]{-android.R.attr.state_focused}, + new int[]{android.R.attr.state_focused, android.R.attr.state_enabled}, + }; + int[] colors = new int[]{ + ThemeUtil.colorControlNormal(context, 0xFF000000), + ThemeUtil.colorControlActivated(context, 0xFF000000), + }; + + mDividerColors = new ColorStateList(states, colors); + } + + if(mDividerErrorColors == null) + mDividerErrorColors = ColorStateList.valueOf(ThemeUtil.colorAccent(context, 0xFFFF0000)); + + if(dividerHeight < 0) + dividerHeight = 0; + + if(dividerPadding < 0) + dividerPadding = 0; + + if(dividerAnimDuration < 0) + dividerAnimDuration = context.getResources().getInteger(android.R.integer.config_shortAnimTime); + + mDividerPadding = dividerPadding; + mInputView.setPadding(0, 0, 0, mDividerPadding + dividerHeight); + + mDivider = new DividerDrawable(dividerHeight, mDividerCompoundPadding ? mInputView.getTotalPaddingLeft() : 0, mDividerCompoundPadding ? mInputView.getTotalPaddingRight() : 0, mDividerColors, dividerAnimDuration); + mDivider.setInEditMode(isInEditMode()); + mDivider.setAnimEnable(false); + ViewUtil.setBackground(mInputView, mDivider); + mDivider.setAnimEnable(true); + } + else{ + if(dividerHeight >= 0 || dividerPadding >= 0) { + if (dividerHeight < 0) + dividerHeight = mDivider.getDividerHeight(); + + if (dividerPadding >= 0) + mDividerPadding = dividerPadding; + + mInputView.setPadding(0, 0, 0, mDividerPadding + dividerHeight); + mDivider.setDividerHeight(dividerHeight); + mDivider.setPadding(mDividerCompoundPadding ? mInputView.getTotalPaddingLeft() : 0, mDividerCompoundPadding ? mInputView.getTotalPaddingRight() : 0); + } + + if(dividerColors != null) + mDividerColors = dividerColors; + + if(dividerErrorColors != null) + mDividerErrorColors = dividerErrorColors; + + mDivider.setColor(getError() == null ? mDividerColors : mDividerErrorColors); + + if(dividerAnimDuration >= 0) + mDivider.setAnimationDuration(dividerAnimDuration); + } + + if(labelPadding >= 0) + getLabelView().setPadding(mDivider.getPaddingLeft(), 0, mDivider.getPaddingRight(), labelPadding); + + if(labelTextSize >= 0) + getLabelView().setTextSize(TypedValue.COMPLEX_UNIT_PX, labelTextSize); + + if(labelTextColor != null) + getLabelView().setTextColor(labelTextColor); + + if(mLabelEnable){ + mLabelVisible = true; + getLabelView().setText(mInputView.getHint()); + addView(getLabelView(), 0, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + setLabelVisible(!TextUtils.isEmpty(mInputView.getText().toString()), false); + } + + if(supportTextSize >= 0) + getSupportView().setTextSize(TypedValue.COMPLEX_UNIT_PX, supportTextSize); + + if(supportColors != null) + mSupportColors = supportColors; + else if(mSupportColors == null) + mSupportColors = context.getResources().getColorStateList(R.color.abc_secondary_text_material_light); + + if(supportErrorColors != null) + mSupportErrorColors = supportErrorColors; + else if(mSupportErrorColors == null) + mSupportErrorColors = ColorStateList.valueOf(ThemeUtil.colorAccent(context, 0xFFFF0000)); + + if(supportPadding >= 0) + getSupportView().setPadding(mDivider.getPaddingLeft(), supportPadding, mDivider.getPaddingRight(), 0); + + if(supportHelper == null && supportError == null) + getSupportView().setTextColor(getError() == null ? mSupportColors : mSupportErrorColors); + else if(supportHelper != null) + setHelper(supportHelper); + else + setError(supportError); + + if(mSupportMode != SUPPORT_MODE_NONE){ + switch (mSupportMode) { + case SUPPORT_MODE_CHAR_COUNTER: + getSupportView().setGravity(Gravity.END); + updateCharCounter(mInputView.getText().length()); + break; + case SUPPORT_MODE_HELPER: + case SUPPORT_MODE_HELPER_WITH_ERROR: + getSupportView().setGravity(GravityCompat.START); + break; + } + addView(getSupportView(), new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + } + + addView(mInputView, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + + requestLayout(); + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + @Override + public void onRtlPropertiesChanged(int layoutDirection) { + boolean rtl = layoutDirection == LAYOUT_DIRECTION_RTL; + if(mIsRtl != rtl) { + mIsRtl = rtl; + + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1){ + if(mLabelView != null) + mLabelView.setTextDirection(mIsRtl ? TEXT_DIRECTION_RTL : TEXT_DIRECTION_LTR); + + if(mSupportView != null) + mSupportView.setTextDirection(mIsRtl ? TEXT_DIRECTION_RTL : TEXT_DIRECTION_LTR); + } + + requestLayout(); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + int tempWidthSpec = widthMode == MeasureSpec.UNSPECIFIED ? widthMeasureSpec : MeasureSpec.makeMeasureSpec(widthSize - getPaddingLeft() - getPaddingRight(), widthMode); + int tempHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + + int labelWidth = 0; + int labelHeight = 0; + int inputWidth = 0; + int inputHeight = 0; + int supportWidth = 0; + int supportHeight = 0; + boolean measureLabelView = mLabelView != null && mLabelView.getLayoutParams() != null; + boolean measureSupportView = mSupportView != null && mSupportView.getLayoutParams() != null; + + if(measureLabelView){ + mLabelView.measure(tempWidthSpec, tempHeightSpec); + labelWidth = mLabelView.getMeasuredWidth(); + labelHeight = mLabelView.getMeasuredHeight(); + } + + mInputView.measure(tempWidthSpec, tempHeightSpec); + inputWidth = mInputView.getMeasuredWidth(); + inputHeight = mInputView.getMeasuredHeight(); + + if(measureSupportView){ + mSupportView.measure(tempWidthSpec, tempHeightSpec); + supportWidth = mSupportView.getMeasuredWidth(); + supportHeight = mSupportView.getMeasuredHeight(); + } + + int width = 0; + int height = 0; + + switch (widthMode) { + case MeasureSpec.UNSPECIFIED: + width = Math.max(labelWidth, Math.max(inputWidth, supportWidth)) + getPaddingLeft() + getPaddingRight(); + break; + case MeasureSpec.AT_MOST: + width = Math.min(widthSize, Math.max(labelWidth, Math.max(inputWidth, supportWidth)) + getPaddingLeft() + getPaddingRight()); + break; + case MeasureSpec.EXACTLY: + width = widthSize; + break; + } + + inputWidth = width - getPaddingLeft() - getPaddingRight(); + tempWidthSpec = MeasureSpec.makeMeasureSpec(inputWidth, MeasureSpec.EXACTLY); + if(measureLabelView && mLabelView.getWidth() != inputWidth) { + mLabelView.measure(tempWidthSpec, tempHeightSpec); + labelHeight = mLabelView.getMeasuredHeight(); + } + + if(measureSupportView && mSupportView.getWidth() != inputWidth) { + mSupportView.measure(tempWidthSpec, tempHeightSpec); + supportHeight = mSupportView.getMeasuredHeight(); + } + + switch (heightMode) { + case MeasureSpec.UNSPECIFIED: + height = labelHeight + inputHeight + supportHeight + getPaddingTop() + getPaddingBottom(); + break; + case MeasureSpec.AT_MOST: + height = Math.min(heightSize, labelHeight + inputHeight + supportHeight + getPaddingTop() + getPaddingBottom()); + break; + case MeasureSpec.EXACTLY: + height = heightSize; + break; + } + + setMeasuredDimension(width, height); + + inputHeight = height - labelHeight - supportHeight - getPaddingTop() - getPaddingBottom(); + if(mInputView.getMeasuredWidth() != inputWidth || mInputView.getMeasuredHeight() != inputHeight) + mInputView.measure(tempWidthSpec, MeasureSpec.makeMeasureSpec(inputHeight, MeasureSpec.EXACTLY)); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + int childLeft = getPaddingLeft(); + int childRight = r - l - getPaddingRight(); + int childTop = getPaddingTop(); + int childBottom = b - t - getPaddingBottom(); + + if(mLabelView != null){ + mLabelView.layout(childLeft, childTop, childRight, childTop + mLabelView.getMeasuredHeight()); + childTop += mLabelView.getMeasuredHeight(); + } + + if(mSupportView != null){ + mSupportView.layout(childLeft, childBottom - mSupportView.getMeasuredHeight(), childRight, childBottom); + childBottom -= mSupportView.getMeasuredHeight(); + } + + mInputView.layout(childLeft, childTop, childRight, childBottom); + } + + /** + * Set the helper text of this EditText. Only work with support mode {@link #SUPPORT_MODE_HELPER} and {@link #SUPPORT_MODE_HELPER_WITH_ERROR}. + * @param helper The helper text. + */ + public void setHelper(CharSequence helper){ + mSupportHelper = helper; + setError(mSupportError); + } + + /** + * @return The helper text of this EditText. + */ + public CharSequence getHelper(){ + return mSupportHelper; + } + + /** + * Set the error text of this EditText. Only work with support mode {@link #SUPPORT_MODE_HELPER} and {@link #SUPPORT_MODE_HELPER_WITH_ERROR}. + * @param error The error text. Set null will clear the error. + */ + public void setError(CharSequence error){ + mSupportError = error; + + if(mSupportMode != SUPPORT_MODE_HELPER && mSupportMode != SUPPORT_MODE_HELPER_WITH_ERROR) + return; + + if(mSupportError != null){ + getSupportView().setTextColor(mSupportErrorColors); + mDivider.setColor(mDividerErrorColors); + getSupportView().setText(mSupportMode == SUPPORT_MODE_HELPER ? mSupportError : TextUtils.concat(mSupportHelper, ", ", mSupportError)); + } + else{ + getSupportView().setTextColor(mSupportColors); + mDivider.setColor(mDividerColors); + getSupportView().setText(mSupportHelper); + } + } + + /** + * @return The error text of this EditText. + */ + public CharSequence getError(){ + return mSupportError; + } + + /** + * Clear the error text. Only work with support mode {@link #SUPPORT_MODE_HELPER} and {@link #SUPPORT_MODE_HELPER_WITH_ERROR}. + */ + public void clearError(){ + setError(null); + } + + private void updateCharCounter(int count){ + if(count == 0){ + getSupportView().setTextColor(mSupportColors); + mDivider.setColor(mDividerColors); + getSupportView().setText(null); + } + else{ + if(mSupportMaxChars > 0){ + getSupportView().setTextColor(count > mSupportMaxChars ? mSupportErrorColors : mSupportColors); + mDivider.setColor(count > mSupportMaxChars ? mDividerErrorColors : mDividerColors); + getSupportView().setText(count + " / " + mSupportMaxChars); + } + else + getSupportView().setText(String.valueOf(count)); + } + } + + private void setLabelVisible(boolean visible, boolean animation){ + if(!mLabelEnable || mLabelVisible == visible) + return; + + mLabelVisible = visible; + + if(animation){ + if(mLabelVisible){ + if(mLabelInAnimId != 0){ + Animation anim = AnimationUtils.loadAnimation(getContext(), mLabelInAnimId); + anim.setAnimationListener(new Animation.AnimationListener() { + + @Override + public void onAnimationStart(Animation animation) { + mLabelView.setVisibility(View.VISIBLE); + } + + @Override + public void onAnimationRepeat(Animation animation) {} + + @Override + public void onAnimationEnd(Animation animation) {} + }); + mLabelView.clearAnimation(); + mLabelView.startAnimation(anim); + } + else + mLabelView.setVisibility(View.VISIBLE); + } + else{ + if(mLabelOutAnimId != 0){ + Animation anim = AnimationUtils.loadAnimation(getContext(), mLabelOutAnimId); + anim.setAnimationListener(new Animation.AnimationListener() { + + @Override + public void onAnimationStart(Animation animation) { + mLabelView.setVisibility(View.VISIBLE); + } + + @Override + public void onAnimationRepeat(Animation animation) {} + + @Override + public void onAnimationEnd(Animation animation) { + mLabelView.setVisibility(View.INVISIBLE); + } + + }); + mLabelView.clearAnimation(); + mLabelView.startAnimation(anim); + } + else + mLabelView.setVisibility(View.INVISIBLE); + } + } + else + mLabelView.setVisibility(mLabelVisible ? View.VISIBLE : View.INVISIBLE); + } + + /* protected method of AutoCompleteTextView */ + + /** + *

Converts the selected item from the drop down list into a sequence + * of character that can be used in the edit box.

+ * + * @param selectedItem the item selected by the user for completion + * + * @return a sequence of characters representing the selected suggestion + */ + protected CharSequence convertSelectionToString(Object selectedItem) { + switch (mAutoCompleteMode){ + case AUTOCOMPLETE_MODE_SINGLE: + return ((InternalAutoCompleteTextView)mInputView).superConvertSelectionToString(selectedItem); + case AUTOCOMPLETE_MODE_MULTI: + return ((InternalMultiAutoCompleteTextView)mInputView).superConvertSelectionToString(selectedItem); + default: + return null; + } + } + + /** + *

Starts filtering the content of the drop down list. The filtering + * pattern is the content of the edit box. Subclasses should override this + * method to filter with a different pattern, for instance a substring of + * text.

+ * + * @param text the filtering pattern + * @param keyCode the last character inserted in the edit box; beware that + * this will be null when text is being added through a soft input method. + */ + protected void performFiltering(CharSequence text, int keyCode) { + switch (mAutoCompleteMode){ + case AUTOCOMPLETE_MODE_SINGLE: + ((InternalAutoCompleteTextView)mInputView).superPerformFiltering(text, keyCode); + break; + case AUTOCOMPLETE_MODE_MULTI: + ((InternalMultiAutoCompleteTextView)mInputView).superPerformFiltering(text, keyCode); + break; + } + } + + /** + *

Performs the text completion by replacing the current text by the + * selected item. Subclasses should override this method to avoid replacing + * the whole content of the edit box.

+ * + * @param text the selected suggestion in the drop down list + */ + protected void replaceText(CharSequence text) { + switch (mAutoCompleteMode){ + case AUTOCOMPLETE_MODE_SINGLE: + ((InternalAutoCompleteTextView)mInputView).superReplaceText(text); + break; + case AUTOCOMPLETE_MODE_MULTI: + ((InternalMultiAutoCompleteTextView)mInputView).superReplaceText(text); + break; + } + } + + /** + * Returns the Filter obtained from {@link Filterable#getFilter}, + * or null if {@link #setAdapter} was not called with + * a Filterable. + */ + protected Filter getFilter() { + switch (mAutoCompleteMode){ + case AUTOCOMPLETE_MODE_SINGLE: + return ((InternalAutoCompleteTextView)mInputView).superGetFilter(); + case AUTOCOMPLETE_MODE_MULTI: + return ((InternalMultiAutoCompleteTextView)mInputView).superGetFilter(); + default: + return null; + } + } + + /** + *

Starts filtering the content of the drop down list. The filtering + * pattern is the specified range of text from the edit box. Subclasses may + * override this method to filter with a different pattern, for + * instance a smaller substring of text.

+ */ + protected void performFiltering(CharSequence text, int start, int end, int keyCode) { + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_MULTI) + ((InternalMultiAutoCompleteTextView)mInputView).superPerformFiltering(text, start, end, keyCode); + } + + /* public method of AutoCompleteTextView */ + + /** + *

Sets the optional hint text that is displayed at the bottom of the + * the matching list. This can be used as a cue to the user on how to + * best use the list, or to provide extra information.

+ *

Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}

+ * + * @param hint the text to be displayed to the user + * + * @see #getCompletionHint() + * + * @attr ref android.R.styleable#AutoCompleteTextView_completionHint + */ + public void setCompletionHint(CharSequence hint) { + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return; + ((AutoCompleteTextView)mInputView).setCompletionHint(hint); + } + + /** + * Gets the optional hint text displayed at the bottom of the the matching list. + *

Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}

+ * + * @return The hint text, if any + * + * @see #setCompletionHint(CharSequence) + * + * @attr ref android.R.styleable#AutoCompleteTextView_completionHint + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public CharSequence getCompletionHint() { + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE || Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) + return null; + return ((AutoCompleteTextView)mInputView).getCompletionHint(); + } + + /** + *

Returns the current width for the auto-complete drop down list. This can + * be a fixed width, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill the screen, or + * {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the width of its anchor view.

+ *

Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}

+ * + * @return the width for the drop down list + * + * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth + */ + public int getDropDownWidth() { + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return 0; + return ((AutoCompleteTextView)mInputView).getDropDownWidth(); + } + + /** + *

Sets the current width for the auto-complete drop down list. This can + * be a fixed width, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill the screen, or + * {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the width of its anchor view.

+ *

Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}

+ * + * @param width the width to use + * + * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth + */ + public void setDropDownWidth(int width) { + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return; + ((AutoCompleteTextView)mInputView).setDropDownWidth(width); + } + + /** + *

Returns the current height for the auto-complete drop down list. This can + * be a fixed height, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill + * the screen, or {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the height + * of the drop down's content.

+ *

Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}

+ * + * @return the height for the drop down list + * + * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight + */ + public int getDropDownHeight() { + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return 0; + return ((AutoCompleteTextView)mInputView).getDropDownHeight(); + } + + /** + *

Sets the current height for the auto-complete drop down list. This can + * be a fixed height, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill + * the screen, or {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the height + * of the drop down's content.

+ *

Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}

+ * + * @param height the height to use + * + * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight + */ + public void setDropDownHeight(int height) { + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return; + ((AutoCompleteTextView)mInputView).setDropDownHeight(height); + } + + /** + *

Returns the id for the view that the auto-complete drop down list is anchored to.

+ *

Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}

+ * + * @return the view's id, or {@link View#NO_ID} if none specified + * + * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor + */ + public int getDropDownAnchor() { + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return 0; + return ((AutoCompleteTextView)mInputView).getDropDownAnchor(); + } + + /** + *

Sets the view to which the auto-complete drop down list should anchor. The view + * corresponding to this id will not be loaded until the next time it is needed to avoid + * loading a view which is not yet instantiated.

+ *

Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}

+ * + * @param id the id to anchor the drop down list view to + * + * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor + */ + public void setDropDownAnchor(int id) { + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return; + ((AutoCompleteTextView)mInputView).setDropDownAnchor(id); + } + + /** + *

Gets the background of the auto-complete drop-down list.

+ *

Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}

+ * + * @return the background drawable + * + * @attr ref android.R.styleable#PopupWindow_popupBackground + */ + public Drawable getDropDownBackground() { + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return null; + return ((AutoCompleteTextView)mInputView).getDropDownBackground(); + } + + /** + *

Sets the background of the auto-complete drop-down list.

+ *

Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}

+ * + * @param d the drawable to set as the background + * + * @attr ref android.R.styleable#PopupWindow_popupBackground + */ + public void setDropDownBackgroundDrawable(Drawable d) { + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return; + ((AutoCompleteTextView)mInputView).setDropDownBackgroundDrawable(d); + } + + /** + *

Sets the background of the auto-complete drop-down list.

+ *

Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}

+ * + * @param id the id of the drawable to set as the background + * + * @attr ref android.R.styleable#PopupWindow_popupBackground + */ + public void setDropDownBackgroundResource(int id) { + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return; + ((AutoCompleteTextView)mInputView).setDropDownBackgroundResource(id); + } + + /** + *

Sets the vertical offset used for the auto-complete drop-down list.

+ *

Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}

+ * + * @param offset the vertical offset + * + * @attr ref android.R.styleable#ListPopupWindow_dropDownVerticalOffset + */ + public void setDropDownVerticalOffset(int offset) { + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return; + ((AutoCompleteTextView)mInputView).setDropDownVerticalOffset(offset); + } + + /** + *

Gets the vertical offset used for the auto-complete drop-down list.

+ *

Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}

+ * + * @return the vertical offset + * + * @attr ref android.R.styleable#ListPopupWindow_dropDownVerticalOffset + */ + public int getDropDownVerticalOffset() { + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return 0; + return ((AutoCompleteTextView)mInputView).getDropDownVerticalOffset(); + } + + /** + *

Sets the horizontal offset used for the auto-complete drop-down list.

+ *

Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}

+ * + * @param offset the horizontal offset + * + * @attr ref android.R.styleable#ListPopupWindow_dropDownHorizontalOffset + */ + public void setDropDownHorizontalOffset(int offset) { + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return; + ((AutoCompleteTextView)mInputView).setDropDownHorizontalOffset(offset); + } + + /** + *

Gets the horizontal offset used for the auto-complete drop-down list.

+ *

Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}

+ * + * @return the horizontal offset + * + * @attr ref android.R.styleable#ListPopupWindow_dropDownHorizontalOffset + */ + public int getDropDownHorizontalOffset() { + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return 0; + return ((AutoCompleteTextView)mInputView).getDropDownHorizontalOffset(); + } + + /** + *

Returns the number of characters the user must type before the drop + * down list is shown.

+ *

Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}

+ * + * @return the minimum number of characters to type to show the drop down + * + * @see #setThreshold(int) + * + * @attr ref android.R.styleable#AutoCompleteTextView_completionThreshold + */ + public int getThreshold() { + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return 0; + return ((AutoCompleteTextView)mInputView).getThreshold(); + } + + /** + *

Specifies the minimum number of characters the user has to type in the + * edit box before the drop down list is shown.

+ * + *

When threshold is less than or equals 0, a threshold of + * 1 is applied.

+ * + *

Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}

+ * + * @param threshold the number of characters to type before the drop down + * is shown + * + * @see #getThreshold() + * + * @attr ref android.R.styleable#AutoCompleteTextView_completionThreshold + */ + public void setThreshold(int threshold) { + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return; + ((AutoCompleteTextView)mInputView).setThreshold(threshold); + } + + /** + *

Sets the listener that will be notified when the user clicks an item + * in the drop down list.

+ *

Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}

+ * + * @param l the item click listener + */ + public void setOnItemClickListener(AdapterView.OnItemClickListener l) { + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return; + ((AutoCompleteTextView)mInputView).setOnItemClickListener(l); + } + + /** + *

Sets the listener that will be notified when the user selects an item + * in the drop down list.

+ *

Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}

+ * + * @param l the item selected listener + */ + public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener l) { + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return; + ((AutoCompleteTextView)mInputView).setOnItemSelectedListener(l); + } + + /** + *

Returns the listener that is notified whenever the user clicks an item + * in the drop down list.

+ *

Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}

+ * + * @return the item click listener + */ + public AdapterView.OnItemClickListener getOnItemClickListener() { + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return null; + return ((AutoCompleteTextView)mInputView).getOnItemClickListener(); + } + + /** + *

Returns the listener that is notified whenever the user selects an + * item in the drop down list.

+ *

Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}

+ * + * @return the item selected listener + */ + public AdapterView.OnItemSelectedListener getOnItemSelectedListener() { + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return null; + return ((AutoCompleteTextView)mInputView).getOnItemSelectedListener(); + } + + /** + *

Returns a filterable list adapter used for auto completion.

+ *

Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}

+ * + * @return a data adapter used for auto completion + */ + public ListAdapter getAdapter() { + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return null; + return ((AutoCompleteTextView)mInputView).getAdapter(); + } + + /** + *

Changes the list of data used for auto completion. The provided list + * must be a filterable list adapter.

+ * + *

The caller is still responsible for managing any resources used by the adapter. + * Notably, when the AutoCompleteTextView is closed or released, the adapter is not notified. + * A common case is the use of {@link CursorAdapter}, which + * contains a {@link android.database.Cursor} that must be closed. This can be done + * automatically (see + * {@link android.app.Activity#startManagingCursor(android.database.Cursor) + * startManagingCursor()}), + * or by manually closing the cursor when the AutoCompleteTextView is dismissed.

+ * + *

Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}

+ * + * @param adapter the adapter holding the auto completion data + * + * @see #getAdapter() + * @see Filterable + * @see ListAdapter + */ + public void setAdapter(T adapter) { + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return; + ((AutoCompleteTextView)mInputView).setAdapter(adapter); + } + + /** + * Returns true if the amount of text in the field meets + * or exceeds the {@link #getThreshold} requirement. You can override + * this to impose a different standard for when filtering will be + * triggered. + *

Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}

+ */ + public boolean enoughToFilter() { + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return false; + return ((AutoCompleteTextView)mInputView).enoughToFilter(); + } + + /** + *

Indicates whether the popup menu is showing.

+ *

Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}

+ * + * @return true if the popup menu is showing, false otherwise + */ + public boolean isPopupShowing() { + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return false; + return ((AutoCompleteTextView)mInputView).isPopupShowing(); + } + + /** + *

Clear the list selection. This may only be temporary, as user input will often bring + * it back. + *

Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}

+ */ + public void clearListSelection() { + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return; + ((AutoCompleteTextView)mInputView).clearListSelection(); + } + + /** + * Set the position of the dropdown view selection. + *

Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}

+ * + * @param position The position to move the selector to. + */ + public void setListSelection(int position) { + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return; + ((AutoCompleteTextView)mInputView).setListSelection(position); + } + + /** + * Get the position of the dropdown view selection, if there is one. Returns + * {@link ListView#INVALID_POSITION ListView.INVALID_POSITION} if there is no dropdown or if + * there is no selection. + *

Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}

+ * + * @return the position of the current selection, if there is one, or + * {@link ListView#INVALID_POSITION ListView.INVALID_POSITION} if not. + * + * @see ListView#getSelectedItemPosition() + */ + public int getListSelection() { + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return 0; + return ((AutoCompleteTextView)mInputView).getListSelection(); + } + + /** + *

Performs the text completion by converting the selected item from + * the drop down list into a string, replacing the text box's content with + * this string and finally dismissing the drop down menu.

+ *

Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}

+ */ + public void performCompletion() { + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return; + ((AutoCompleteTextView)mInputView).performCompletion(); + } + + /** + * Identifies whether the view is currently performing a text completion, so subclasses + * can decide whether to respond to text changed events. + *

Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}

+ */ + public boolean isPerformingCompletion() { + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return false; + return ((AutoCompleteTextView)mInputView).isPerformingCompletion(); + } + + /**

Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}

*/ + public void onFilterComplete(int count) { + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_SINGLE) + ((InternalAutoCompleteTextView)mInputView).superOnFilterComplete(count); + else if(mAutoCompleteMode == AUTOCOMPLETE_MODE_MULTI) + ((InternalMultiAutoCompleteTextView)mInputView).superOnFilterComplete(count); + } + + /** + *

Closes the drop down if present on screen.

+ *

Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}

+ */ + public void dismissDropDown() { + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return; + ((AutoCompleteTextView)mInputView).dismissDropDown(); + } + + /** + *

Displays the drop down on screen.

+ *

Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}

+ */ + public void showDropDown() { + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return; + ((AutoCompleteTextView)mInputView).showDropDown(); + } + + /** + * Sets the validator used to perform text validation. + *

Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}

+ * + * @param validator The validator used to validate the text entered in this widget. + * + * @see #getValidator() + * @see #performValidation() + */ + public void setValidator(AutoCompleteTextView.Validator validator) { + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return; + ((AutoCompleteTextView)mInputView).setValidator(validator); + } + + /** + * Returns the Validator set with {@link #setValidator}, + * or null if it was not set. + *

Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}

+ * + * @see #setValidator(AutoCompleteTextView.Validator) + * @see #performValidation() + */ + public AutoCompleteTextView.Validator getValidator() { + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return null; + return ((AutoCompleteTextView)mInputView).getValidator(); + } + + /** + * If a validator was set on this view and the current string is not valid, + * ask the validator to fix it. + *

Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}

+ * + * @see #getValidator() + * @see #setValidator(AutoCompleteTextView.Validator) + */ + public void performValidation() { + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return; + ((AutoCompleteTextView)mInputView).performValidation(); + } + + /** + * Sets the Tokenizer that will be used to determine the relevant + * range of the text where the user is typing. + *

Only work when autoCompleMode is AUTOCOMPLETE_MODE_MULTI

+ */ + public void setTokenizer(MultiAutoCompleteTextView.Tokenizer t) { + if(mAutoCompleteMode != AUTOCOMPLETE_MODE_MULTI) + return; + ((MultiAutoCompleteTextView)mInputView).setTokenizer(t); + } + + /* public method of EditText */ + + @Override + public void setEnabled(boolean enabled){ + mInputView.setEnabled(enabled); + } + + /** + * Convenience for {@link android.text.Selection#extendSelection}. + */ + public void extendSelection (int index){ + mInputView.extendSelection(index); + } + + public Editable getText (){ + return mInputView.getText(); + } + + /** + * Convenience for {@link android.text.Selection#selectAll}. + */ + public void selectAll (){ + mInputView.selectAll(); + } + + /** + * Causes words in the text that are longer than the view is wide + * to be ellipsized instead of broken in the middle. You may also + * want to {@link #setSingleLine} or {@link #setHorizontallyScrolling} + * to constrain the text to a single line. Use null + * to turn off ellipsizing. + * + * If {@link #setMaxLines} has been used to set two or more lines, + * only {@link TruncateAt#END} and + * {@link TruncateAt#MARQUEE} are supported + * (other ellipsizing types will not do anything). + * + * @attr ref android.R.styleable#TextView_ellipsize + */ + public void setEllipsize (TruncateAt ellipsis){ + mInputView.setEllipsize(ellipsis); + } + + /** + * Convenience for {@link android.text.Selection#setSelection(Spannable, int)}. + */ + public void setSelection (int index){ + mInputView.setSelection(index); + } + + /** + * Convenience for {@link android.text.Selection#setSelection(Spannable, int, int)}. + */ + public void setSelection (int start, int stop){ + mInputView.setSelection(start, stop); + } + + public void setText (CharSequence text, TextView.BufferType type){ + mInputView.setText(text, type); + } + + /** + * Adds a TextWatcher to the list of those whose methods are called + * whenever this TextView's text changes. + *

+ * In 1.0, the {@link TextWatcher#afterTextChanged} method was erroneously + * not called after {@link #setText} calls. Now, doing {@link #setText} + * if there are any text changed listeners forces the buffer type to + * Editable if it would not otherwise be and does call this method. + */ + public void addTextChangedListener(TextWatcher textWatcher){ + mInputView.addTextChangedListener(textWatcher); + } + + /** + * Convenience method: Append the specified text to the TextView's + * display buffer, upgrading it to BufferType.EDITABLE if it was + * not already editable. + */ + public final void append (CharSequence text){ + mInputView.append(text); + } + + /** + * Convenience method: Append the specified text slice to the TextView's + * display buffer, upgrading it to BufferType.EDITABLE if it was + * not already editable. + */ + public void append (CharSequence text, int start, int end){ + mInputView.append(text, start, end); + } + + public void beginBatchEdit (){ + mInputView.beginBatchEdit(); + } + + /** + * Move the point, specified by the offset, into the view if it is needed. + * This has to be called after layout. Returns true if anything changed. + */ + public boolean bringPointIntoView (int offset){ + return mInputView.bringPointIntoView(offset); + } + + public void cancelLongPress (){ + mInputView.cancelLongPress(); + } + + /** + * Use {@link android.view.inputmethod.BaseInputConnection#removeComposingSpans + * BaseInputConnection.removeComposingSpans()} to remove any IME composing + * state from this text view. + */ + public void clearComposingText (){ + mInputView.clearComposingText(); + } + + @Override + public void computeScroll (){ + mInputView.computeScroll(); + } + + @Override + public void debug (int depth){ + mInputView.debug(depth); + } + + /** + * Returns true, only while processing a touch gesture, if the initial + * touch down event caused focus to move to the text view and as a result + * its selection changed. Only valid while processing the touch gesture + * of interest, in an editable text view. + */ + public boolean didTouchFocusSelect (){ + return mInputView.didTouchFocusSelect(); + } + + public void endBatchEdit (){ + mInputView.endBatchEdit(); + } + + /** + * If this TextView contains editable content, extract a portion of it + * based on the information in request in to outText. + * @return Returns true if the text was successfully extracted, else false. + */ + public boolean extractText (ExtractedTextRequest request, ExtractedText outText){ + return mInputView.extractText(request, outText); + } + + @Override + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + public void findViewsWithText (ArrayList outViews, CharSequence searched, int flags){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) + mInputView.findViewsWithText(outViews, searched, flags); + } + + /** + * Gets the autolink mask of the text. See {@link + * android.text.util.Linkify#ALL Linkify.ALL} and peers for + * possible values. + * + * @attr ref android.R.styleable#TextView_autoLink + */ + public final int getAutoLinkMask (){ + return mInputView.getAutoLinkMask(); + } + + @Override + public int getBaseline (){ + return mInputView.getBaseline(); + } + + /** + * Returns the padding between the compound drawables and the text. + * + * @attr ref android.R.styleable#TextView_drawablePadding + */ + public int getCompoundDrawablePadding (){ + return mInputView.getCompoundDrawablePadding(); + } + + /** + * Returns drawables for the left, top, right, and bottom borders. + * + * @attr ref android.R.styleable#TextView_drawableLeft + * @attr ref android.R.styleable#TextView_drawableTop + * @attr ref android.R.styleable#TextView_drawableRight + * @attr ref android.R.styleable#TextView_drawableBottom + */ + public Drawable[] getCompoundDrawables (){ + return mInputView.getCompoundDrawables(); + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public Drawable[] getCompoundDrawablesRelative (){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) + return mInputView.getCompoundDrawablesRelative(); + + return mInputView.getCompoundDrawables(); + } + + /** + * Returns the bottom padding of the view, plus space for the bottom + * Drawable if any. + */ + public int getCompoundPaddingBottom (){ + return mInputView.getCompoundPaddingBottom(); + } + + /** + * Returns the end padding of the view, plus space for the end + * Drawable if any. + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public int getCompoundPaddingEnd (){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) + return mInputView.getCompoundPaddingEnd(); + + return mInputView.getCompoundPaddingRight(); + } + + /** + * Returns the left padding of the view, plus space for the left + * Drawable if any. + */ + public int getCompoundPaddingLeft (){ + return mInputView.getCompoundPaddingLeft(); + } + + /** + * Returns the right padding of the view, plus space for the right + * Drawable if any. + */ + public int getCompoundPaddingRight (){ + return mInputView.getCompoundPaddingRight(); + } + + /** + * Returns the start padding of the view, plus space for the start + * Drawable if any. + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public int getCompoundPaddingStart (){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) + return mInputView.getCompoundPaddingStart(); + + return mInputView.getCompoundPaddingLeft(); + } + + /** + * Returns the top padding of the view, plus space for the top + * Drawable if any. + */ + public int getCompoundPaddingTop (){ + return mInputView.getCompoundPaddingTop(); + } + + /** + *

Return the current color selected to paint the hint text.

+ * + * @return Returns the current hint text color. + */ + public final int getCurrentHintTextColor (){ + return mInputView.getCurrentHintTextColor(); + } + + /** + *

Return the current color selected for normal text.

+ * + * @return Returns the current text color. + */ + public final int getCurrentTextColor (){ + return mInputView.getCurrentTextColor(); + } + + /** + * Retrieves the value set in {@link #setCustomSelectionActionModeCallback}. Default is null. + * + * @return The current custom selection callback. + */ + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public ActionMode.Callback getCustomSelectionActionModeCallback (){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) + return mInputView.getCustomSelectionActionModeCallback(); + + return null; + } + + /** + * Return the text the TextView is displaying as an Editable object. If + * the text is not editable, null is returned. + * + * @see #getText + */ + public Editable getEditableText (){ + return mInputView.getEditableText(); + } + + /** + * Returns where, if anywhere, words that are longer than the view + * is wide should be ellipsized. + */ + public TruncateAt getEllipsize (){ + return mInputView.getEllipsize(); + } + + /** + * Returns the extended bottom padding of the view, including both the + * bottom Drawable if any and any extra space to keep more than maxLines + * of text from showing. It is only valid to call this after measuring. + */ + public int getExtendedPaddingBottom (){ + return mInputView.getExtendedPaddingBottom(); + } + + /** + * Returns the extended top padding of the view, including both the + * top Drawable if any and any extra space to keep more than maxLines + * of text from showing. It is only valid to call this after measuring. + */ + public int getExtendedPaddingTop (){ + return mInputView.getExtendedPaddingTop(); + } + + /** + * Returns the current list of input filters. + * + * @attr ref android.R.styleable#TextView_maxLength + */ + public InputFilter[] getFilters (){ + return mInputView.getFilters(); + } + + @Override + public void getFocusedRect (@NonNull Rect r){ + mInputView.getFocusedRect(r); + } + + /** + * @return the currently set font feature settings. Default is null. + * + * @see #setFontFeatureSettings(String) + * @see android.graphics.Paint#setFontFeatureSettings + */ + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public String getFontFeatureSettings (){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + return mInputView.getFontFeatureSettings(); + return null; + } + + /** + * Return whether this text view is including its entire text contents + * in frozen icicles. + * + * @return Returns true if text is included, false if it isn't. + * + * @see #setFreezesText + */ + public boolean getFreezesText (){ + return mInputView.getFreezesText(); + } + + /** + * Returns the horizontal and vertical alignment of this TextView. + * + * @see Gravity + * @attr ref android.R.styleable#TextView_gravity + */ + public int getGravity (){ + return mInputView.getGravity(); + } + + /** + * @return the color used to display the selection highlight + * + * @see #setHighlightColor(int) + * + * @attr ref android.R.styleable#TextView_textColorHighlight + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public int getHighlightColor (){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) + return mInputView.getHighlightColor(); + + return 0; + } + + /** + * Returns the hint that is displayed when the text of the TextView + * is empty. + * + * @attr ref android.R.styleable#TextView_hint + */ + public CharSequence getHint (){ + return mInputView.getHint(); + } + + /** + * @return the color of the hint text, for the different states of this TextView. + * + * @see #setHintTextColor(ColorStateList) + * @see #setHintTextColor(int) + * @see #setTextColor(ColorStateList) + * @see #setLinkTextColor(ColorStateList) + * + * @attr ref android.R.styleable#TextView_textColorHint + */ + public final ColorStateList getHintTextColors (){ + return mInputView.getHintTextColors(); + } + + /** + * Get the IME action ID previous set with {@link #setImeActionLabel}. + * + * @see #setImeActionLabel + * @see EditorInfo + */ + public int getImeActionId (){ + return mInputView.getImeActionId(); + } + + /** + * Get the IME action label previous set with {@link #setImeActionLabel}. + * + * @see #setImeActionLabel + * @see EditorInfo + */ + public CharSequence getImeActionLabel (){ + return mInputView.getImeActionLabel(); + } + + /** + * Get the type of the IME editor. + * + * @see #setImeOptions(int) + * @see EditorInfo + */ + public int getImeOptions (){ + return mInputView.getImeOptions(); + } + + /** + * Gets whether the TextView includes extra top and bottom padding to make + * room for accents that go above the normal ascent and descent. + * + * @see #setIncludeFontPadding(boolean) + * + * @attr ref android.R.styleable#TextView_includeFontPadding + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public boolean getIncludeFontPadding (){ + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && mInputView.getIncludeFontPadding(); + } + + /** + * Retrieve the input extras currently associated with the text view, which + * can be viewed as well as modified. + * + * @param create If true, the extras will be created if they don't already + * exist. Otherwise, null will be returned if none have been created. + * @see #setInputExtras(int) + * @see EditorInfo#extras + * @attr ref android.R.styleable#TextView_editorExtras + */ + public Bundle getInputExtras (boolean create){ + return mInputView.getInputExtras(create); + } + + /** + * Get the type of the editable content. + * + * @see #setInputType(int) + * @see android.text.InputType + */ + public int getInputType (){ + return mInputView.getInputType(); + } + + /** + * @return the current key listener for this TextView. + * This will frequently be null for non-EditText TextViews. + * + * @attr ref android.R.styleable#TextView_numeric + * @attr ref android.R.styleable#TextView_digits + * @attr ref android.R.styleable#TextView_phoneNumber + * @attr ref android.R.styleable#TextView_inputMethod + * @attr ref android.R.styleable#TextView_capitalize + * @attr ref android.R.styleable#TextView_autoText + */ + public final KeyListener getKeyListener (){ + return mInputView.getKeyListener(); + } + + /** + * @return the Layout that is currently being used to display the text. + * This can be null if the text or width has recently changes. + */ + public final Layout getLayout (){ + return mInputView.getLayout(); + } + + /** + * @return the extent by which text is currently being letter-spaced. + * This will normally be 0. + * + * @see #setLetterSpacing(float) + * @see android.graphics.Paint#setLetterSpacing + */ + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public float getLetterSpacing (){ + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? mInputView.getLetterSpacing() : 0; + } + + /** + * Return the baseline for the specified line (0...getLineCount() - 1) + * If bounds is not null, return the top, left, right, bottom extents + * of the specified line in it. If the internal Layout has not been built, + * return 0 and set bounds to (0, 0, 0, 0) + * @param line which line to examine (0..getLineCount() - 1) + * @param bounds Optional. If not null, it returns the extent of the line + * @return the Y-coordinate of the baseline + */ + public int getLineBounds (int line, Rect bounds){ + return mInputView.getLineBounds(line, bounds); + } + + /** + * Return the number of lines of text, or 0 if the internal Layout has not + * been built. + */ + public int getLineCount (){ + return mInputView.getLineCount(); + } + + /** + * @return the height of one standard line in pixels. Note that markup + * within the text can cause individual lines to be taller or shorter + * than this height, and the layout may contain additional first- + * or last-line padding. + */ + public int getLineHeight (){ + return mInputView.getLineHeight(); + } + + /** + * Gets the line spacing extra space + * + * @return the extra space that is added to the height of each lines of this TextView. + * + * @see #setLineSpacing(float, float) + * @see #getLineSpacingMultiplier() + * + * @attr ref android.R.styleable#TextView_lineSpacingExtra + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public float getLineSpacingExtra (){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) + return mInputView.getLineSpacingExtra(); + return 0f; + } + + /** + * Gets the line spacing multiplier + * + * @return the value by which each line's height is multiplied to get its actual height. + * + * @see #setLineSpacing(float, float) + * @see #getLineSpacingExtra() + * + * @attr ref android.R.styleable#TextView_lineSpacingMultiplier + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public float getLineSpacingMultiplier (){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) + return mInputView.getLineSpacingMultiplier(); + return 0f; + } + + /** + * @return the list of colors used to paint the links in the text, for the different states of + * this TextView + * + * @see #setLinkTextColor(ColorStateList) + * @see #setLinkTextColor(int) + * + * @attr ref android.R.styleable#TextView_textColorLink + */ + public final ColorStateList getLinkTextColors (){ + return mInputView.getLinkTextColors(); + } + + /** + * Returns whether the movement method will automatically be set to + * {@link android.text.method.LinkMovementMethod} if {@link #setAutoLinkMask} has been + * set to nonzero and links are detected in {@link #setText}. + * The default is true. + * + * @attr ref android.R.styleable#TextView_linksClickable + */ + public final boolean getLinksClickable (){ + return mInputView.getLinksClickable(); + } + + /** + * Gets the number of times the marquee animation is repeated. Only meaningful if the + * TextView has marquee enabled. + * + * @return the number of times the marquee animation is repeated. -1 if the animation + * repeats indefinitely + * + * @see #setMarqueeRepeatLimit(int) + * + * @attr ref android.R.styleable#TextView_marqueeRepeatLimit + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public int getMarqueeRepeatLimit (){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) + return mInputView.getMarqueeRepeatLimit(); + + return -1; + } + + /** + * @return the maximum width of the TextView, expressed in ems or -1 if the maximum width + * was set in pixels instead (using {@link #setMaxWidth(int)} or {@link #setWidth(int)}). + * + * @see #setMaxEms(int) + * @see #setEms(int) + * + * @attr ref android.R.styleable#TextView_maxEms + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public int getMaxEms (){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) + return mInputView.getMaxEms(); + + return -1; + } + + /** + * @return the maximum height of this TextView expressed in pixels, or -1 if the maximum + * height was set in number of lines instead using {@link #setMaxLines(int) or #setLines(int)}. + * + * @see #setMaxHeight(int) + * + * @attr ref android.R.styleable#TextView_maxHeight + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public int getMaxHeight (){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) + return mInputView.getMaxHeight(); + + return -1; + } + + /** + * @return the maximum number of lines displayed in this TextView, or -1 if the maximum + * height was set in pixels instead using {@link #setMaxHeight(int) or #setDividerHeight(int)}. + * + * @see #setMaxLines(int) + * + * @attr ref android.R.styleable#TextView_maxLines + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public int getMaxLines (){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) + return mInputView.getMaxLines(); + + return -1; + } + + /** + * @return the maximum width of the TextView, in pixels or -1 if the maximum width + * was set in ems instead (using {@link #setMaxEms(int)} or {@link #setEms(int)}). + * + * @see #setMaxWidth(int) + * @see #setWidth(int) + * + * @attr ref android.R.styleable#TextView_maxWidth + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public int getMaxWidth (){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) + return mInputView.getMaxWidth(); + + return -1; + } + + /** + * @return the minimum width of the TextView, expressed in ems or -1 if the minimum width + * was set in pixels instead (using {@link #setMinWidth(int)} or {@link #setWidth(int)}). + * + * @see #setMinEms(int) + * @see #setEms(int) + * + * @attr ref android.R.styleable#TextView_minEms + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public int getMinEms (){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) + return mInputView.getMinEms(); + + return -1; + } + + /** + * @return the minimum height of this TextView expressed in pixels, or -1 if the minimum + * height was set in number of lines instead using {@link #setMinLines(int) or #setLines(int)}. + * + * @see #setMinHeight(int) + * + * @attr ref android.R.styleable#TextView_minHeight + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public int getMinHeight (){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) + return mInputView.getMinHeight(); + + return -1; + } + + /** + * @return the minimum number of lines displayed in this TextView, or -1 if the minimum + * height was set in pixels instead using {@link #setMinHeight(int) or #setDividerHeight(int)}. + * + * @see #setMinLines(int) + * + * @attr ref android.R.styleable#TextView_minLines + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public int getMinLines (){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) + return mInputView.getMinLines(); + + return -1; + } + + /** + * @return the minimum width of the TextView, in pixels or -1 if the minimum width + * was set in ems instead (using {@link #setMinEms(int)} or {@link #setEms(int)}). + * + * @see #setMinWidth(int) + * @see #setWidth(int) + * + * @attr ref android.R.styleable#TextView_minWidth + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public int getMinWidth (){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) + return mInputView.getMinWidth(); + + return -1; + } + + /** + * @return the movement method being used for this TextView. + * This will frequently be null for non-EditText TextViews. + */ + public final MovementMethod getMovementMethod (){ + return mInputView.getMovementMethod(); + } + + /** + * Get the character offset closest to the specified absolute position. A typical use case is to + * pass the result of {@link android.view.MotionEvent#getX()} and {@link android.view.MotionEvent#getY()} to this method. + * + * @param x The horizontal absolute position of a point on screen + * @param y The vertical absolute position of a point on screen + * @return the character offset for the character whose position is closest to the specified + * position. Returns -1 if there is no layout. + */ + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + public int getOffsetForPosition (float x, float y){ + if (getLayout() == null) return -1; + final int line = getLineAtCoordinate(y); + final int offset = getOffsetAtCoordinate(line, x); + return offset; + } + + protected float convertToLocalHorizontalCoordinate(float x) { + x -= getTotalPaddingLeft(); + // Clamp the position to inside of the view. + x = Math.max(0.0f, x); + x = Math.min(getWidth() - getTotalPaddingRight() - 1, x); + x += getScrollX(); + return x; + } + + protected int getLineAtCoordinate(float y) { + y -= getTotalPaddingTop(); + // Clamp the position to inside of the view. + y = Math.max(0.0f, y); + y = Math.min(getHeight() - getTotalPaddingBottom() - 1, y); + y += getScrollY(); + return getLayout().getLineForVertical((int) y); + } + + protected int getOffsetAtCoordinate(int line, float x) { + x = convertToLocalHorizontalCoordinate(x); + return getLayout().getOffsetForHorizontal(line, x); + } + + /** + * @return the base paint used for the text. Please use this only to + * consult the Paint's properties and not to change them. + */ + public TextPaint getPaint (){ + return mInputView.getPaint(); + } + + /** + * @return the flags on the Paint being used to display the text. + * @see android.graphics.Paint#getFlags + */ + public int getPaintFlags (){ + return mInputView.getPaintFlags(); + } + + /** + * Get the private type of the content. + * + * @see #setPrivateImeOptions(String) + * @see EditorInfo#privateImeOptions + */ + public String getPrivateImeOptions (){ + return mInputView.getPrivateImeOptions(); + } + + /** + * Convenience for {@link android.text.Selection#getSelectionEnd}. + */ + public int getSelectionEnd (){ + return mInputView.getSelectionEnd(); + } + + /** + * Convenience for {@link android.text.Selection#getSelectionStart}. + */ + public int getSelectionStart (){ + return mInputView.getSelectionStart(); + } + + /** + * @return the color of the shadow layer + * + * @see #setShadowLayer(float, float, float, int) + * + * @attr ref android.R.styleable#TextView_shadowColor + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public int getShadowColor (){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) + return mInputView.getShadowColor(); + + return 0; + } + + /** + * @return the horizontal offset of the shadow layer + * + * @see #setShadowLayer(float, float, float, int) + * + * @attr ref android.R.styleable#TextView_shadowDx + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public float getShadowDx (){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) + return mInputView.getShadowDx(); + + return 0; + } + + /** + * @return the vertical offset of the shadow layer + * + * @see #setShadowLayer(float, float, float, int) + * + * @attr ref android.R.styleable#TextView_shadowDy + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public float getShadowDy (){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) + return mInputView.getShadowDy(); + + return 0; + } + + /** + * Gets the radius of the shadow layer. + * + * @return the radius of the shadow layer. If 0, the shadow layer is not visible + * + * @see #setShadowLayer(float, float, float, int) + * + * @attr ref android.R.styleable#TextView_shadowRadius + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public float getShadowRadius (){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) + return mInputView.getShadowRadius(); + + return 0; + } + + /** + * Returns whether the soft input method will be made visible when this + * TextView gets focused. The default is true. + */ + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public final boolean getShowSoftInputOnFocus (){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + return mInputView.getShowSoftInputOnFocus(); + return true; + } + + /** + * Gets the text colors for the different states (normal, selected, focused) of the TextView. + * + * @see #setTextColor(ColorStateList) + * @see #setTextColor(int) + * + * @attr ref android.R.styleable#TextView_textColor + */ + public final ColorStateList getTextColors (){ + return mInputView.getTextColors(); + } + + /** + * Get the default {@link Locale} of the text in this TextView. + * @return the default {@link Locale} of the text in this TextView. + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public Locale getTextLocale (){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) + return mInputView.getTextLocale(); + + return Locale.getDefault(); + } + + /** + * @return the extent by which text is currently being stretched + * horizontally. This will usually be 1. + */ + public float getTextScaleX (){ + return mInputView.getTextScaleX(); + } + + /** + * @return the size (in pixels) of the default text size in this TextView. + */ + public float getTextSize (){ + return mInputView.getTextSize(); + } + + /** + * Returns the total bottom padding of the view, including the bottom + * Drawable if any, the extra space to keep more than maxLines + * from showing, and the vertical offset for gravity, if any. + */ + public int getTotalPaddingBottom (){ + return getPaddingBottom() + mInputView.getTotalPaddingBottom() + (mSupportMode != SUPPORT_MODE_NONE ? mSupportView.getHeight() : 0); + } + + /** + * Returns the total end padding of the view, including the end + * Drawable if any. + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public int getTotalPaddingEnd (){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) + return getPaddingEnd() + mInputView.getTotalPaddingEnd(); + + return getTotalPaddingRight(); + } + + /** + * Returns the total left padding of the view, including the left + * Drawable if any. + */ + public int getTotalPaddingLeft (){ + return getPaddingLeft() + mInputView.getTotalPaddingLeft(); + } + + /** + * Returns the total right padding of the view, including the right + * Drawable if any. + */ + public int getTotalPaddingRight (){ + return getPaddingRight() + mInputView.getTotalPaddingRight(); + } + + /** + * Returns the total start padding of the view, including the start + * Drawable if any. + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public int getTotalPaddingStart (){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) + return getPaddingStart() + mInputView.getTotalPaddingStart(); + + return getTotalPaddingLeft(); + } + + /** + * Returns the total top padding of the view, including the top + * Drawable if any, the extra space to keep more than maxLines + * from showing, and the vertical offset for gravity, if any. + */ + public int getTotalPaddingTop (){ + return getPaddingTop() + mInputView.getTotalPaddingTop() + (mLabelEnable ? mLabelView.getHeight() : 0); + } + + /** + * @return the current transformation method for this TextView. + * This will frequently be null except for single-line and password + * fields. + * + * @attr ref android.R.styleable#TextView_password + * @attr ref android.R.styleable#TextView_singleLine + */ + public final TransformationMethod getTransformationMethod (){ + return mInputView.getTransformationMethod(); + } + + /** + * @return the current typeface and style in which the text is being + * displayed. + * + * @see #setTypeface(Typeface) + * + * @attr ref android.R.styleable#TextView_fontFamily + * @attr ref android.R.styleable#TextView_typeface + * @attr ref android.R.styleable#TextView_textStyle + */ + public Typeface getTypeface (){ + return mInputView.getTypeface(); + } + + /** + * Returns the list of URLSpans attached to the text + * (by {@link android.text.util.Linkify} or otherwise) if any. You can call + * {@link URLSpan#getURL} on them to find where they link to + * or use {@link android.text.Spanned#getSpanStart} and {@link android.text.Spanned#getSpanEnd} + * to find the region of the text they are attached to. + */ + public URLSpan[] getUrls (){ + return mInputView.getUrls(); + } + + @Override + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public boolean hasOverlappingRendering (){ + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && mInputView.hasOverlappingRendering(); + } + + /** + * Return true iff there is a selection inside this text view. + */ + public boolean hasSelection (){ + return mInputView.hasSelection(); + } + + /** + * @return whether or not the cursor is visible (assuming this TextView is editable) + * + * @see #setCursorVisible(boolean) + * + * @attr ref android.R.styleable#TextView_cursorVisible + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public boolean isCursorVisible (){ + return Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN || mInputView.isCursorVisible(); + } + + /** + * Returns whether this text view is a current input method target. The + * default implementation just checks with {@link android.view.inputmethod.InputMethodManager}. + */ + public boolean isInputMethodTarget (){ + return mInputView.isInputMethodTarget(); + } + + /** + * Return whether or not suggestions are enabled on this TextView. The suggestions are generated + * by the IME or by the spell checker as the user types. This is done by adding + * {@link android.text.style.SuggestionSpan}s to the text. + * + * When suggestions are enabled (default), this list of suggestions will be displayed when the + * user asks for them on these parts of the text. This value depends on the inputType of this + * TextView. + * + * The class of the input type must be {@link android.text.InputType#TYPE_CLASS_TEXT}. + * + * In addition, the type variation must be one of + * {@link android.text.InputType#TYPE_TEXT_VARIATION_NORMAL}, + * {@link android.text.InputType#TYPE_TEXT_VARIATION_EMAIL_SUBJECT}, + * {@link android.text.InputType#TYPE_TEXT_VARIATION_LONG_MESSAGE}, + * {@link android.text.InputType#TYPE_TEXT_VARIATION_SHORT_MESSAGE} or + * {@link android.text.InputType#TYPE_TEXT_VARIATION_WEB_EDIT_TEXT}. + * + * And finally, the {@link android.text.InputType#TYPE_TEXT_FLAG_NO_SUGGESTIONS} flag must not be set. + * + * @return true if the suggestions popup window is enabled, based on the inputType. + */ + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + public boolean isSuggestionsEnabled (){ + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH && mInputView.isSuggestionsEnabled(); + } + + /** + * + * Returns the state of the {@code textIsSelectable} flag (See + * {@link #setTextIsSelectable setTextIsSelectable()}). Although you have to set this flag + * to allow users to select and copy text in a non-editable TextView, the content of an + * {@link EditText} can always be selected, independently of the value of this flag. + *

+ * + * @return True if the text displayed in this TextView can be selected by the user. + * + * @attr ref android.R.styleable#TextView_textIsSelectable + */ + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public boolean isTextSelectable (){ + return Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB || mInputView.isTextSelectable(); + } + + /** + * Returns the length, in characters, of the text managed by this TextView + */ + public int length (){ + return mInputView.length(); + } + + /** + * Move the cursor, if needed, so that it is at an offset that is visible + * to the user. This will not move the cursor if it represents more than + * one character (a selection range). This will only work if the + * TextView contains spannable text; otherwise it will do nothing. + * + * @return True if the cursor was actually moved, false otherwise. + */ + public boolean moveCursorToVisibleOffset (){ + return mInputView.moveCursorToVisibleOffset(); + } + + /** + * Called by the framework in response to a text completion from + * the current input method, provided by it calling + * {@link InputConnection#commitCompletion + * InputConnection.commitCompletion()}. The default implementation does + * nothing; text views that are supporting auto-completion should override + * this to do their desired behavior. + * + * @param text The auto complete text the user has selected. + */ + public void onCommitCompletion (CompletionInfo text){ + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + ((InternalEditText)mInputView).superOnCommitCompletion(text); + else if(mAutoCompleteMode == AUTOCOMPLETE_MODE_SINGLE) + ((InternalAutoCompleteTextView)mInputView).superOnCommitCompletion(text); + else + ((InternalMultiAutoCompleteTextView)mInputView).superOnCommitCompletion(text); + } + + /** + * Called by the framework in response to a text auto-correction (such as fixing a typo using a + * a dictionnary) from the current input method, provided by it calling + * {@link InputConnection#commitCorrection} InputConnection.commitCorrection()}. The default + * implementation flashes the background of the corrected word to provide feedback to the user. + * + * @param info The auto correct info about the text that was corrected. + */ + public void onCommitCorrection (CorrectionInfo info){ + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + ((InternalEditText)mInputView).superOnCommitCorrection(info); + else if(mAutoCompleteMode == AUTOCOMPLETE_MODE_SINGLE) + ((InternalAutoCompleteTextView)mInputView).superOnCommitCorrection(info); + else + ((InternalMultiAutoCompleteTextView)mInputView).superOnCommitCorrection(info); + } + + @Override + public InputConnection onCreateInputConnection (EditorInfo outAttrs){ + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return ((InternalEditText)mInputView).superOnCreateInputConnection(outAttrs); + else if(mAutoCompleteMode == AUTOCOMPLETE_MODE_SINGLE) + return ((InternalAutoCompleteTextView)mInputView).superOnCreateInputConnection(outAttrs); + else + return ((InternalMultiAutoCompleteTextView)mInputView).superOnCreateInputConnection(outAttrs); + } + + /** + * Called when an attached input method calls + * {@link InputConnection#performEditorAction(int) + * InputConnection.performEditorAction()} + * for this text view. The default implementation will call your action + * listener supplied to {@link #setOnEditorActionListener}, or perform + * a standard operation for {@link EditorInfo#IME_ACTION_NEXT + * EditorInfo.IME_ACTION_NEXT}, {@link EditorInfo#IME_ACTION_PREVIOUS + * EditorInfo.IME_ACTION_PREVIOUS}, or {@link EditorInfo#IME_ACTION_DONE + * EditorInfo.IME_ACTION_DONE}. + * + *

For backwards compatibility, if no IME options have been set and the + * text view would not normally advance focus on enter, then + * the NEXT and DONE actions received here will be turned into an enter + * key down/up pair to go through the normal key handling. + * + * @param actionCode The code of the action being performed. + * + * @see #setOnEditorActionListener + */ + public void onEditorAction (int actionCode){ + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + ((InternalEditText)mInputView).superOnEditorAction(actionCode); + else if(mAutoCompleteMode == AUTOCOMPLETE_MODE_SINGLE) + ((InternalAutoCompleteTextView)mInputView).superOnEditorAction(actionCode); + else + ((InternalMultiAutoCompleteTextView)mInputView).superOnEditorAction(actionCode); + } + + @Override + public boolean onKeyDown (int keyCode, KeyEvent event){ + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return ((InternalEditText)mInputView).superOnKeyDown(keyCode, event); + else if(mAutoCompleteMode == AUTOCOMPLETE_MODE_SINGLE) + return ((InternalAutoCompleteTextView)mInputView).superOnKeyDown(keyCode, event); + else + return ((InternalMultiAutoCompleteTextView)mInputView).superOnKeyDown(keyCode, event); + } + + @Override + public boolean onKeyMultiple (int keyCode, int repeatCount, KeyEvent event){ + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return ((InternalEditText)mInputView).superOnKeyMultiple(keyCode, repeatCount, event); + else if(mAutoCompleteMode == AUTOCOMPLETE_MODE_SINGLE) + return ((InternalAutoCompleteTextView)mInputView).superOnKeyMultiple(keyCode, repeatCount, event); + else + return ((InternalMultiAutoCompleteTextView)mInputView).superOnKeyMultiple(keyCode, repeatCount, event); + } + + @Override + public boolean onKeyPreIme (int keyCode, KeyEvent event){ + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return ((InternalEditText)mInputView).superOnKeyPreIme(keyCode, event); + else if(mAutoCompleteMode == AUTOCOMPLETE_MODE_SINGLE) + return ((InternalAutoCompleteTextView)mInputView).superOnKeyPreIme(keyCode, event); + else + return ((InternalMultiAutoCompleteTextView)mInputView).superOnKeyPreIme(keyCode, event); + } + + @Override + public boolean onKeyShortcut (int keyCode, KeyEvent event){ + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return ((InternalEditText)mInputView).superOnKeyShortcut(keyCode, event); + else if(mAutoCompleteMode == AUTOCOMPLETE_MODE_SINGLE) + return ((InternalAutoCompleteTextView)mInputView).superOnKeyShortcut(keyCode, event); + else + return ((InternalMultiAutoCompleteTextView)mInputView).superOnKeyShortcut(keyCode, event); + } + + @Override + public boolean onKeyUp (int keyCode, KeyEvent event){ + if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE) + return ((InternalEditText)mInputView).superOnKeyUp(keyCode, event); + else if(mAutoCompleteMode == AUTOCOMPLETE_MODE_SINGLE) + return ((InternalAutoCompleteTextView)mInputView).superOnKeyUp(keyCode, event); + else + return ((InternalMultiAutoCompleteTextView)mInputView).superOnKeyUp(keyCode, event); + } + + public void setOnSelectionChangedListener(TextView.OnSelectionChangedListener listener){ + mOnSelectionChangedListener = listener; + } + + /** + * This method is called when the selection has changed, in case any + * subclasses would like to know. + * + * @param selStart The new selection start location. + * @param selEnd The new selection end location. + */ + protected void onSelectionChanged(int selStart, int selEnd) { + if(mInputView == null) + return; + + if(mInputView instanceof InternalEditText) + ((InternalEditText)mInputView).superOnSelectionChanged(selStart, selEnd); + else if(mInputView instanceof InternalAutoCompleteTextView) + ((InternalAutoCompleteTextView)mInputView).superOnSelectionChanged(selStart, selEnd); + else + ((InternalMultiAutoCompleteTextView)mInputView).superOnSelectionChanged(selStart, selEnd); + + if(mOnSelectionChangedListener != null) + mOnSelectionChangedListener.onSelectionChanged(this, selStart, selEnd); + } + + /** + * Removes the specified TextWatcher from the list of those whose + * methods are called + * whenever this TextView's text changes. + */ + public void removeTextChangedListener (TextWatcher watcher){ + mInputView.removeTextChangedListener(watcher); + } + + /** + * Sets the properties of this field to transform input to ALL CAPS + * display. This may use a "small caps" formatting if available. + * This setting will be ignored if this field is editable or selectable. + * + * This call replaces the current transformation method. Disabling this + * will not necessarily restore the previous behavior from before this + * was enabled. + * + * @see #setTransformationMethod(TransformationMethod) + * @attr ref android.R.styleable#TextView_textAllCaps + */ + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + public void setAllCaps (boolean allCaps){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) + mInputView.setAllCaps(allCaps); + } + + /** + * Sets the autolink mask of the text. See {@link + * android.text.util.Linkify#ALL Linkify.ALL} and peers for + * possible values. + * + * @attr ref android.R.styleable#TextView_autoLink + */ + public final void setAutoLinkMask (int mask){ + mInputView.setAutoLinkMask(mask); + } + + /** + * Sets the size of the padding between the compound drawables and + * the text. + * + * @attr ref android.R.styleable#TextView_drawablePadding + */ + public void setCompoundDrawablePadding (int pad){ + mInputView.setCompoundDrawablePadding(pad); + if(mDividerCompoundPadding) { + mDivider.setPadding(mInputView.getTotalPaddingLeft(), mInputView.getTotalPaddingRight()); + if(mLabelEnable) + mLabelView.setPadding(mDivider.getPaddingLeft(), mLabelView.getPaddingTop(), mDivider.getPaddingRight(), mLabelView.getPaddingBottom()); + if(mSupportMode != SUPPORT_MODE_NONE) + mSupportView.setPadding(mDivider.getPaddingLeft(), mSupportView.getPaddingTop(), mDivider.getPaddingRight(), mSupportView.getPaddingBottom()); + } + } + + /** + * Sets the Drawables (if any) to appear to the left of, above, to the + * right of, and below the text. Use {@code null} if you do not want a + * Drawable there. The Drawables must already have had + * {@link Drawable#setBounds} called. + *

+ * Calling this method will overwrite any Drawables previously set using + * {@link #setCompoundDrawablesRelative} or related methods. + * + * @attr ref android.R.styleable#TextView_drawableLeft + * @attr ref android.R.styleable#TextView_drawableTop + * @attr ref android.R.styleable#TextView_drawableRight + * @attr ref android.R.styleable#TextView_drawableBottom + */ + public void setCompoundDrawables (Drawable left, Drawable top, Drawable right, Drawable bottom){ + mInputView.setCompoundDrawables(left, top, right, bottom); + if(mDividerCompoundPadding) { + mDivider.setPadding(mInputView.getTotalPaddingLeft(), mInputView.getTotalPaddingRight()); + if(mLabelEnable) + mLabelView.setPadding(mDivider.getPaddingLeft(), mLabelView.getPaddingTop(), mDivider.getPaddingRight(), mLabelView.getPaddingBottom()); + if(mSupportMode != SUPPORT_MODE_NONE) + mSupportView.setPadding(mDivider.getPaddingLeft(), mSupportView.getPaddingTop(), mDivider.getPaddingRight(), mSupportView.getPaddingBottom()); + } + } + + /** + * Sets the Drawables (if any) to appear to the start of, above, to the end + * of, and below the text. Use {@code null} if you do not want a Drawable + * there. The Drawables must already have had {@link Drawable#setBounds} + * called. + *

+ * Calling this method will overwrite any Drawables previously set using + * {@link #setCompoundDrawables} or related methods. + * + * @attr ref android.R.styleable#TextView_drawableStart + * @attr ref android.R.styleable#TextView_drawableTop + * @attr ref android.R.styleable#TextView_drawableEnd + * @attr ref android.R.styleable#TextView_drawableBottom + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public void setCompoundDrawablesRelative (Drawable start, Drawable top, Drawable end, Drawable bottom){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) + mInputView.setCompoundDrawablesRelative(start, top, end, bottom); + else + mInputView.setCompoundDrawables(start, top, end, bottom); + } + + /** + * Sets the Drawables (if any) to appear to the start of, above, to the end + * of, and below the text. Use {@code null} if you do not want a Drawable + * there. The Drawables' bounds will be set to their intrinsic bounds. + *

+ * Calling this method will overwrite any Drawables previously set using + * {@link #setCompoundDrawables} or related methods. + * + * @attr ref android.R.styleable#TextView_drawableStart + * @attr ref android.R.styleable#TextView_drawableTop + * @attr ref android.R.styleable#TextView_drawableEnd + * @attr ref android.R.styleable#TextView_drawableBottom + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public void setCompoundDrawablesRelativeWithIntrinsicBounds (Drawable start, Drawable top, Drawable end, Drawable bottom){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) + mInputView.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom); + else + mInputView.setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom); + } + + /** + * Sets the Drawables (if any) to appear to the start of, above, to the end + * of, and below the text. Use 0 if you do not want a Drawable there. The + * Drawables' bounds will be set to their intrinsic bounds. + *

+ * Calling this method will overwrite any Drawables previously set using + * {@link #setCompoundDrawables} or related methods. + * + * @param start Resource identifier of the start Drawable. + * @param top Resource identifier of the top Drawable. + * @param end Resource identifier of the end Drawable. + * @param bottom Resource identifier of the bottom Drawable. + * + * @attr ref android.R.styleable#TextView_drawableStart + * @attr ref android.R.styleable#TextView_drawableTop + * @attr ref android.R.styleable#TextView_drawableEnd + * @attr ref android.R.styleable#TextView_drawableBottom + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public void setCompoundDrawablesRelativeWithIntrinsicBounds (int start, int top, int end, int bottom){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) + mInputView.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom); + else + mInputView.setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom); + } + + /** + * Sets the Drawables (if any) to appear to the left of, above, to the + * right of, and below the text. Use {@code null} if you do not want a + * Drawable there. The Drawables' bounds will be set to their intrinsic + * bounds. + *

+ * Calling this method will overwrite any Drawables previously set using + * {@link #setCompoundDrawablesRelative} or related methods. + * + * @attr ref android.R.styleable#TextView_drawableLeft + * @attr ref android.R.styleable#TextView_drawableTop + * @attr ref android.R.styleable#TextView_drawableRight + * @attr ref android.R.styleable#TextView_drawableBottom + */ + public void setCompoundDrawablesWithIntrinsicBounds (Drawable left, Drawable top, Drawable right, Drawable bottom){ + mInputView.setCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom); + } + + /** + * Sets the Drawables (if any) to appear to the left of, above, to the + * right of, and below the text. Use 0 if you do not want a Drawable there. + * The Drawables' bounds will be set to their intrinsic bounds. + *

+ * Calling this method will overwrite any Drawables previously set using + * {@link #setCompoundDrawablesRelative} or related methods. + * + * @param left Resource identifier of the left Drawable. + * @param top Resource identifier of the top Drawable. + * @param right Resource identifier of the right Drawable. + * @param bottom Resource identifier of the bottom Drawable. + * + * @attr ref android.R.styleable#TextView_drawableLeft + * @attr ref android.R.styleable#TextView_drawableTop + * @attr ref android.R.styleable#TextView_drawableRight + * @attr ref android.R.styleable#TextView_drawableBottom + */ + public void setCompoundDrawablesWithIntrinsicBounds (int left, int top, int right, int bottom){ + mInputView.setCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom); + } + + /** + * Set whether the cursor is visible. The default is true. Note that this property only + * makes sense for editable TextView. + * + * @see #isCursorVisible() + * + * @attr ref android.R.styleable#TextView_cursorVisible + */ + public void setCursorVisible (boolean visible){ + mInputView.setCursorVisible(visible); + } + + /** + * If provided, this ActionMode.Callback will be used to create the ActionMode when text + * selection is initiated in this View. + * + * The standard implementation populates the menu with a subset of Select All, Cut, Copy and + * Paste actions, depending on what this View supports. + * + * A custom implementation can add new entries in the default menu in its + * {@link ActionMode.Callback#onPrepareActionMode(ActionMode, android.view.Menu)} method. The + * default actions can also be removed from the menu using {@link android.view.Menu#removeItem(int)} and + * passing {@link android.R.id#selectAll}, {@link android.R.id#cut}, {@link android.R.id#copy} + * or {@link android.R.id#paste} ids as parameters. + * + * Returning false from + * {@link ActionMode.Callback#onCreateActionMode(ActionMode, android.view.Menu)} will prevent + * the action mode from being started. + * + * Action click events should be handled by the custom implementation of + * {@link ActionMode.Callback#onActionItemClicked(ActionMode, android.view.MenuItem)}. + * + * Note that text selection mode is not started when a TextView receives focus and the + * {@link android.R.attr#selectAllOnFocus} flag has been set. The content is highlighted in + * that case, to allow for quick replacement. + */ + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public void setCustomSelectionActionModeCallback (ActionMode.Callback actionModeCallback){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) + mInputView.setCustomSelectionActionModeCallback(actionModeCallback); + } + + /** + * Sets the Factory used to create new Editables. + */ + public final void setEditableFactory (Editable.Factory factory){ + mInputView.setEditableFactory(factory); + } + + /** + * Set the TextView's elegant height metrics flag. This setting selects font + * variants that have not been compacted to fit Latin-based vertical + * metrics, and also increases top and bottom bounds to provide more space. + * + * @param elegant set the paint's elegant metrics flag. + * + * @attr ref android.R.styleable#TextView_elegantTextHeight + */ + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public void setElegantTextHeight (boolean elegant){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + mInputView.setElegantTextHeight(elegant); + } + + /** + * Makes the TextView exactly this many ems wide + * + * @see #setMaxEms(int) + * @see #setMinEms(int) + * @see #getMinEms() + * @see #getMaxEms() + * + * @attr ref android.R.styleable#TextView_ems + */ + public void setEms (int ems){ + mInputView.setEms(ems); + } + + /** + * Apply to this text view the given extracted text, as previously + * returned by {@link #extractText(ExtractedTextRequest, ExtractedText)}. + */ + public void setExtractedText (ExtractedText text){ + mInputView.setExtractedText(text); + } + + /** + * Sets the list of input filters that will be used if the buffer is + * Editable. Has no effect otherwise. + * + * @attr ref android.R.styleable#TextView_maxLength + */ + public void setFilters (InputFilter[] filters){ + mInputView.setFilters(filters); + } + + /** + * Sets font feature settings. The format is the same as the CSS + * font-feature-settings attribute: + * http://dev.w3.org/csswg/css-fonts/#propdef-font-feature-settings + * + * @param fontFeatureSettings font feature settings represented as CSS compatible string + * @see #getFontFeatureSettings() + * @see android.graphics.Paint#getFontFeatureSettings + * + * @attr ref android.R.styleable#TextView_fontFeatureSettings + */ + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public void setFontFeatureSettings (String fontFeatureSettings){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + mInputView.setFontFeatureSettings(fontFeatureSettings); + } + + /** + * Control whether this text view saves its entire text contents when + * freezing to an icicle, in addition to dynamic state such as cursor + * position. By default this is false, not saving the text. Set to true + * if the text in the text view is not being saved somewhere else in + * persistent storage (such as in a content provider) so that if the + * view is later thawed the user will not lose their data. + * + * @param freezesText Controls whether a frozen icicle should include the + * entire text data: true to include it, false to not. + * + * @attr ref android.R.styleable#TextView_freezesText + */ + public void setFreezesText (boolean freezesText){ + mInputView.setFreezesText(freezesText); + } + + /** + * Sets the horizontal alignment of the text and the + * vertical gravity that will be used when there is extra space + * in the TextView beyond what is required for the text itself. + * + * @see Gravity + * @attr ref android.R.styleable#TextView_gravity + */ + public void setGravity (int gravity){ + mInputView.setGravity(gravity); + } + + /** + * Sets the color used to display the selection highlight. + * + * @attr ref android.R.styleable#TextView_textColorHighlight + */ + public void setHighlightColor (int color){ + mInputView.setHighlightColor(color); + } + + /** + * Sets the text to be displayed when the text of the TextView is empty. + * Null means to use the normal empty text. The hint does not currently + * participate in determining the size of the view. + * + * @attr ref android.R.styleable#TextView_hint + */ + public final void setHint (CharSequence hint){ + mInputView.setHint(hint); + if(mLabelView != null) + mLabelView.setText(hint); + } + + /** + * Sets the text to be displayed when the text of the TextView is empty, + * from a resource. + * + * @attr ref android.R.styleable#TextView_hint + */ + public final void setHint (int resid){ + mInputView.setHint(resid); + if(mLabelView != null) + mLabelView.setText(resid); + } + + /** + * Sets the color of the hint text. + * + * @see #getHintTextColors() + * @see #setHintTextColor(int) + * @see #setTextColor(ColorStateList) + * @see #setLinkTextColor(ColorStateList) + * + * @attr ref android.R.styleable#TextView_textColorHint + */ + public final void setHintTextColor (ColorStateList colors){ + mInputView.setHintTextColor(colors); + } + + /** + * Sets the color of the hint text for all the states (disabled, focussed, selected...) of this + * TextView. + * + * @see #setHintTextColor(ColorStateList) + * @see #getHintTextColors() + * @see #setTextColor(int) + * + * @attr ref android.R.styleable#TextView_textColorHint + */ + public final void setHintTextColor (int color){ + mInputView.setHintTextColor(color); + } + + /** + * Sets whether the text should be allowed to be wider than the + * View is. If false, it will be wrapped to the width of the View. + * + * @attr ref android.R.styleable#TextView_scrollHorizontally + */ + public void setHorizontallyScrolling (boolean whether){ + mInputView.setHorizontallyScrolling(whether); + } + + /** + * Change the custom IME action associated with the text view, which + * will be reported to an IME with {@link EditorInfo#actionLabel} + * and {@link EditorInfo#actionId} when it has focus. + * @see #getImeActionLabel + * @see #getImeActionId + * @see EditorInfo + * @attr ref android.R.styleable#TextView_imeActionLabel + * @attr ref android.R.styleable#TextView_imeActionId + */ + public void setImeActionLabel (CharSequence label, int actionId){ + mInputView.setImeActionLabel(label, actionId); + } + + /** + * Change the editor type integer associated with the text view, which + * will be reported to an IME with {@link EditorInfo#imeOptions} when it + * has focus. + * @see #getImeOptions + * @see EditorInfo + * @attr ref android.R.styleable#TextView_imeOptions + */ + public void setImeOptions (int imeOptions){ + mInputView.setImeOptions(imeOptions); + } + + /** + * Set whether the TextView includes extra top and bottom padding to make + * room for accents that go above the normal ascent and descent. + * The default is true. + * + * @see #getIncludeFontPadding() + * + * @attr ref android.R.styleable#TextView_includeFontPadding + */ + public void setIncludeFontPadding (boolean includepad){ + mInputView.setIncludeFontPadding(includepad); + } + + /** + * Set the extra input data of the text, which is the + * {@link EditorInfo#extras TextBoxAttribute.extras} + * Bundle that will be filled in when creating an input connection. The + * given integer is the resource ID of an XML resource holding an + * {@link android.R.styleable#InputExtras <input-extras>} XML tree. + * + * @see #getInputExtras(boolean) + * @see EditorInfo#extras + * @attr ref android.R.styleable#TextView_editorExtras + */ + public void setInputExtras (int xmlResId) throws XmlPullParserException, IOException{ + mInputView.setInputExtras(xmlResId); + } + + /** + * Set the type of the content with a constant as defined for {@link EditorInfo#inputType}. This + * will take care of changing the key listener, by calling {@link #setKeyListener(KeyListener)}, + * to match the given content type. If the given content type is {@link EditorInfo#TYPE_NULL} + * then a soft keyboard will not be displayed for this text view. + * + * Note that the maximum number of displayed lines (see {@link #setMaxLines(int)}) will be + * modified if you change the {@link EditorInfo#TYPE_TEXT_FLAG_MULTI_LINE} flag of the input + * type. + * + * @see #getInputType() + * @see #setRawInputType(int) + * @see android.text.InputType + * @attr ref android.R.styleable#TextView_inputType + */ + public void setInputType (int type){ + mInputView.setInputType(type); + } + + /** + * Sets the key listener to be used with this TextView. This can be null + * to disallow user input. Note that this method has significant and + * subtle interactions with soft keyboards and other input method: + * see {@link KeyListener#getInputType() KeyListener.getContentType()} + * for important details. Calling this method will replace the current + * content type of the text view with the content type returned by the + * key listener. + *

+ * Be warned that if you want a TextView with a key listener or movement + * method not to be focusable, or if you want a TextView without a + * key listener or movement method to be focusable, you must call + * {@link #setFocusable} again after calling this to get the focusability + * back the way you want it. + * + * @attr ref android.R.styleable#TextView_numeric + * @attr ref android.R.styleable#TextView_digits + * @attr ref android.R.styleable#TextView_phoneNumber + * @attr ref android.R.styleable#TextView_inputMethod + * @attr ref android.R.styleable#TextView_capitalize + * @attr ref android.R.styleable#TextView_autoText + */ + public void setKeyListener (KeyListener input){ + mInputView.setKeyListener(input); + } + + /** + * Sets text letter-spacing. The value is in 'EM' units. Typical values + * for slight expansion will be around 0.05. Negative values tighten text. + * + * @see #getLetterSpacing() + * @see android.graphics.Paint#getLetterSpacing + * + * @attr ref android.R.styleable#TextView_letterSpacing + */ + public void setLetterSpacing (float letterSpacing){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + mInputView.setLetterSpacing(letterSpacing); + } + + /** + * Sets line spacing for this TextView. Each line will have its height + * multiplied by mult and have add added to it. + * + * @attr ref android.R.styleable#TextView_lineSpacingExtra + * @attr ref android.R.styleable#TextView_lineSpacingMultiplier + */ + public void setLineSpacing (float add, float mult){ + mInputView.setLineSpacing(add, mult); + } + + /** + * Makes the TextView exactly this many lines tall. + * + * Note that setting this value overrides any other (minimum / maximum) number of lines or + * height setting. A single line TextView will set this value to 1. + * + * @attr ref android.R.styleable#TextView_lines + */ + public void setLines (int lines){ + mInputView.setLines(lines); + } + + /** + * Sets the color of links in the text. + * + * @see #setLinkTextColor(int) + * @see #getLinkTextColors() + * @see #setTextColor(ColorStateList) + * @see #setHintTextColor(ColorStateList) + * + * @attr ref android.R.styleable#TextView_textColorLink + */ + public final void setLinkTextColor (ColorStateList colors){ + mInputView.setLinkTextColor(colors); + } + + /** + * Sets the color of links in the text. + * + * @see #setLinkTextColor(int) + * @see #getLinkTextColors() + * @see #setTextColor(ColorStateList) + * @see #setHintTextColor(ColorStateList) + * + * @attr ref android.R.styleable#TextView_textColorLink + */ + public final void setLinkTextColor (int color){ + mInputView.setLinkTextColor(color); + } + + /** + * Sets whether the movement method will automatically be set to + * {@link android.text.method.LinkMovementMethod} if {@link #setAutoLinkMask} has been + * set to nonzero and links are detected in {@link #setText}. + * The default is true. + * + * @attr ref android.R.styleable#TextView_linksClickable + */ + public final void setLinksClickable (boolean whether){ + mInputView.setLinksClickable(whether); + } + + /** + * Sets how many times to repeat the marquee animation. Only applied if the + * TextView has marquee enabled. Set to -1 to repeat indefinitely. + * + * @see #getMarqueeRepeatLimit() + * + * @attr ref android.R.styleable#TextView_marqueeRepeatLimit + */ + public void setMarqueeRepeatLimit (int marqueeLimit){ + mInputView.setMarqueeRepeatLimit(marqueeLimit); + } + + /** + * Makes the TextView at most this many ems wide + * + * @attr ref android.R.styleable#TextView_maxEms + */ + public void setMaxEms (int maxems){ + mInputView.setMaxEms(maxems); + } + + /** + * Makes the TextView at most this many pixels tall. This option is mutually exclusive with the + * {@link #setMaxLines(int)} method. + * + * Setting this value overrides any other (maximum) number of lines setting. + * + * @attr ref android.R.styleable#TextView_maxHeight + */ + public void setMaxHeight (int maxHeight){ + mInputView.setMaxHeight(maxHeight); + } + + /** + * Makes the TextView at most this many lines tall. + * + * Setting this value overrides any other (maximum) height setting. + * + * @attr ref android.R.styleable#TextView_maxLines + */ + public void setMaxLines (int maxlines){ + mInputView.setMaxLines(maxlines); + } + + /** + * Makes the TextView at most this many pixels wide + * + * @attr ref android.R.styleable#TextView_maxWidth + */ + public void setMaxWidth (int maxpixels){ + mInputView.setMaxWidth(maxpixels); + } + + /** + * Makes the TextView at least this many ems wide + * + * @attr ref android.R.styleable#TextView_minEms + */ + public void setMinEms (int minems){ + mInputView.setMinEms(minems); + } + + /** + * Makes the TextView at least this many pixels tall. + * + * Setting this value overrides any other (minimum) number of lines setting. + * + * @attr ref android.R.styleable#TextView_minHeight + */ + public void setMinHeight (int minHeight){ + mInputView.setMinHeight(minHeight); + } + + /** + * Makes the TextView at least this many lines tall. + * + * Setting this value overrides any other (minimum) height setting. A single line TextView will + * set this value to 1. + * + * @see #getMinLines() + * + * @attr ref android.R.styleable#TextView_minLines + */ + public void setMinLines (int minlines){ + mInputView.setMinLines(minlines); + } + + /** + * Makes the TextView at least this many pixels wide + * + * @attr ref android.R.styleable#TextView_minWidth + */ + public void setMinWidth (int minpixels){ + mInputView.setMinWidth(minpixels); + } + + /** + * Sets the movement method (arrow key handler) to be used for + * this TextView. This can be null to disallow using the arrow keys + * to move the cursor or scroll the view. + *

+ * Be warned that if you want a TextView with a key listener or movement + * method not to be focusable, or if you want a TextView without a + * key listener or movement method to be focusable, you must call + * {@link #setFocusable} again after calling this to get the focusability + * back the way you want it. + */ + public final void setMovementMethod (MovementMethod movement){ + mInputView.setMovementMethod(movement); + } + + /** + * Set a special listener to be called when an action is performed + * on the text view. This will be called when the enter key is pressed, + * or when an action supplied to the IME is selected by the user. Setting + * this means that the normal hard key event will not insert a newline + * into the text view, even if it is multi-line; holding down the ALT + * modifier will, however, allow the user to insert a newline character. + */ + public void setOnEditorActionListener (TextView.OnEditorActionListener l){ + mInputView.setOnEditorActionListener(l); + } + + /** + * Register a callback to be invoked when a hardware key is pressed in this view. + * Key presses in software input methods will generally not trigger the methods of + * this listener. + * @param l the key listener to attach to this view + */ + @Override + public void setOnKeyListener(OnKeyListener l) { + mInputView.setOnKeyListener(l); + } + + /** + * Register a callback to be invoked when focus of this view changed. + * + * @param l The callback that will run. + */ + @Override + public void setOnFocusChangeListener(OnFocusChangeListener l) { + mInputView.setOnFocusChangeListener(l); + } + + /** + * Directly change the content type integer of the text view, without + * modifying any other state. + * @see #setInputType(int) + * @see android.text.InputType + * @attr ref android.R.styleable#TextView_inputType + */ + public void setRawInputType (int type){ + mInputView.setRawInputType(type); + } + + public void setScroller (Scroller s){ + mInputView.setScroller(s); + } + + /** + * Set the TextView so that when it takes focus, all the text is + * selected. + * + * @attr ref android.R.styleable#TextView_selectAllOnFocus + */ + public void setSelectAllOnFocus (boolean selectAllOnFocus){ + mInputView.setSelectAllOnFocus(selectAllOnFocus); + } + + @Override + public void setSelected (boolean selected){ + mInputView.setSelected(selected); + } + + /** + * Gives the text a shadow of the specified blur radius and color, the specified + * distance from its drawn position. + *

+ * The text shadow produced does not interact with the properties on view + * that are responsible for real time shadows, + * {@link View#getElevation() elevation} and + * {@link View#getTranslationZ() translationZ}. + * + * @see android.graphics.Paint#setShadowLayer(float, float, float, int) + * + * @attr ref android.R.styleable#TextView_shadowColor + * @attr ref android.R.styleable#TextView_shadowDx + * @attr ref android.R.styleable#TextView_shadowDy + * @attr ref android.R.styleable#TextView_shadowRadius + */ + public void setShadowLayer (float radius, float dx, float dy, int color){ + mInputView.setShadowLayer(radius, dx, dy, color); + } + + /** + * Sets whether the soft input method will be made visible when this + * TextView gets focused. The default is true. + */ + public final void setShowSoftInputOnFocus (boolean show){ + mInputView.setShowSoftInputOnFocus(show); + } + + /** + * Sets the properties of this field (lines, horizontally scrolling, + * transformation method) to be for a single-line input. + * + * @attr ref android.R.styleable#TextView_singleLine + */ + public void setSingleLine (){ + mInputView.setSingleLine(); + } + + /** + * Sets the Factory used to create new Spannables. + */ + public final void setSpannableFactory (Spannable.Factory factory){ + mInputView.setSpannableFactory(factory); + } + + public final void setText (int resid){ + mInputView.setText(resid); + } + + public final void setText (char[] text, int start, int len){ + mInputView.setText(text, start, len); + } + + public final void setText (int resid, TextView.BufferType type){ + mInputView.setText(resid, type); + } + + public final void setText (CharSequence text){ + mInputView.setText(text); + } + + /** + * Sets the text color, size, style, hint color, and highlight color + * from the specified TextAppearance resource. + */ + public void setTextAppearance (Context context, int resid){ + mInputView.setTextAppearance(context, resid); + } + + /** + * Sets the text color. + * + * @see #setTextColor(int) + * @see #getTextColors() + * @see #setHintTextColor(ColorStateList) + * @see #setLinkTextColor(ColorStateList) + * + * @attr ref android.R.styleable#TextView_textColor + */ + public void setTextColor (ColorStateList colors){ + mInputView.setTextColor(colors); + } + + /** + * Sets the text color for all the states (normal, selected, + * focused) to be this color. + * + * @see #setTextColor(ColorStateList) + * @see #getTextColors() + * + * @attr ref android.R.styleable#TextView_textColor + */ + public void setTextColor (int color){ + mInputView.setTextColor(color); + } + + /** + * Sets whether the content of this view is selectable by the user. The default is + * {@code false}, meaning that the content is not selectable. + *

+ * When you use a TextView to display a useful piece of information to the user (such as a + * contact's address), make it selectable, so that the user can select and copy its + * content. You can also use set the XML attribute + * {@link android.R.styleable#TextView_textIsSelectable} to "true". + *

+ * When you call this method to set the value of {@code textIsSelectable}, it sets + * the flags {@code focusable}, {@code focusableInTouchMode}, {@code clickable}, + * and {@code longClickable} to the same value. These flags correspond to the attributes + * {@link android.R.styleable#View_focusable android:focusable}, + * {@link android.R.styleable#View_focusableInTouchMode android:focusableInTouchMode}, + * {@link android.R.styleable#View_clickable android:clickable}, and + * {@link android.R.styleable#View_longClickable android:longClickable}. To restore any of these + * flags to a state you had set previously, call one or more of the following methods: + * {@link #setFocusable(boolean) setFocusable()}, + * {@link #setFocusableInTouchMode(boolean) setFocusableInTouchMode()}, + * {@link #setClickable(boolean) setClickable()} or + * {@link #setLongClickable(boolean) setLongClickable()}. + * + * @param selectable Whether the content of this TextView should be selectable. + */ + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public void setTextIsSelectable (boolean selectable){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) + mInputView.setTextIsSelectable(selectable); + } + + /** + * Like {@link #setText(CharSequence)}, + * except that the cursor position (if any) is retained in the new text. + * + * @param text The new text to place in the text view. + * + * @see #setText(CharSequence) + */ + public final void setTextKeepState (CharSequence text){ + mInputView.setTextKeepState(text); + } + + /** + * Like {@link #setText(CharSequence, TextView.BufferType)}, + * except that the cursor position (if any) is retained in the new text. + * + * @see #setText(CharSequence, TextView.BufferType) + */ + public final void setTextKeepState (CharSequence text, TextView.BufferType type){ + mInputView.setTextKeepState(text, type); + } + + /** + * Set the default {@link Locale} of the text in this TextView to the given value. This value + * is used to choose appropriate typefaces for ambiguous characters. Typically used for CJK + * locales to disambiguate Hanzi/Kanji/Hanja characters. + * + * @param locale the {@link Locale} for drawing text, must not be null. + * + * @see android.graphics.Paint#setTextLocale + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public void setTextLocale (Locale locale){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) + mInputView.setTextLocale(locale); + } + + /** + * Sets the extent by which text should be stretched horizontally. + * + * @attr ref android.R.styleable#TextView_textScaleX + */ + public void setTextScaleX (float size){ + mInputView.setTextScaleX(size); + } + + /** + * Set the default text size to the given value, interpreted as "scaled + * pixel" units. This size is adjusted based on the current density and + * user font size preference. + * + * @param size The scaled pixel size. + * + * @attr ref android.R.styleable#TextView_textSize + */ + public void setTextSize (float size){ + mInputView.setTextSize(size); + } + + /** + * Set the default text size to a given unit and value. See {@link + * TypedValue} for the possible dimension units. + * + * @param unit The desired dimension unit. + * @param size The desired size in the given units. + * + * @attr ref android.R.styleable#TextView_textSize + */ + public void setTextSize (int unit, float size){ + mInputView.setTextSize(unit, size); + } + + /** + * Sets the transformation that is applied to the text that this + * TextView is displaying. + * + * @attr ref android.R.styleable#TextView_password + * @attr ref android.R.styleable#TextView_singleLine + */ + public final void setTransformationMethod (TransformationMethod method){ + mInputView.setTransformationMethod(method); + } + + /** + * Sets the typeface and style in which the text should be displayed, + * and turns on the fake bold and italic bits in the Paint if the + * Typeface that you provided does not have all the bits in the + * style that you specified. + * + * @attr ref android.R.styleable#TextView_typeface + * @attr ref android.R.styleable#TextView_textStyle + */ + public void setTypeface (Typeface tf, int style){ + mInputView.setTypeface(tf, style); + } + + /** + * Sets the typeface and style in which the text should be displayed. + * Note that not all Typeface families actually have bold and italic + * variants, so you may need to use + * {@link #setTypeface(Typeface, int)} to get the appearance + * that you actually want. + * + * @see #getTypeface() + * + * @attr ref android.R.styleable#TextView_fontFamily + * @attr ref android.R.styleable#TextView_typeface + * @attr ref android.R.styleable#TextView_textStyle + */ + public void setTypeface (Typeface tf){ + mInputView.setTypeface(tf); + } + + /** + * It would be better to rely on the input type for everything. A password inputType should have + * a password transformation. We should hence use isPasswordInputType instead of this method. + * + * We should: + * - Call setInputType in setKeyListener instead of changing the input type directly (which + * would install the correct transformation). + * - Refuse the installation of a non-password transformation in setTransformation if the input + * type is password. + * + * However, this is like this for legacy reasons and we cannot break existing apps. This method + * is useful since it matches what the user can see (obfuscated text or not). + * + * @return true if the current transformation method is of the password type. + */ + private boolean hasPasswordTransformationMethod() { + return getTransformationMethod() != null && getTransformationMethod() instanceof PasswordTransformationMethod; + } + + public boolean canCut() { + return !hasPasswordTransformationMethod() && getText().length() > 0 && hasSelection() && getKeyListener() != null; + } + + public boolean canCopy() { + return !hasPasswordTransformationMethod() && getText().length() > 0 && hasSelection(); + } + + public boolean canPaste() { + return (getKeyListener() != null && + getSelectionStart() >= 0 && + getSelectionEnd() >= 0 && + ((ClipboardManager)getContext().getSystemService(Context.CLIPBOARD_SERVICE)).hasPrimaryClip()); + } + + /* Inner class */ + + private class InputTextWatcher implements TextWatcher { + @Override + public void afterTextChanged(Editable s) { + int count = s.length(); + setLabelVisible(count != 0, true); + if(mSupportMode == SUPPORT_MODE_CHAR_COUNTER) + updateCharCounter(count); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + } + + private class LabelView extends TextView{ + + public LabelView(Context context) { + super(context); + } + + @Override + public void setTextAppearance(int resId) { + ViewUtil.applyTextAppearance(this, resId); + } + + @Override + public void setTextAppearance(Context context, int resId) { + ViewUtil.applyTextAppearance(this, resId); + } + + @Override + protected int[] onCreateDrawableState(int extraSpace) { + return mInputView.getDrawableState(); + } + + } + + private class InternalEditText extends android.widget.EditText{ + + public InternalEditText(Context context) { + super(context); + } + + public InternalEditText(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public InternalEditText(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + public void setTextAppearance(int resId) { + ViewUtil.applyTextAppearance(this, resId); + } + + @Override + public void setTextAppearance(Context context, int resId) { + ViewUtil.applyTextAppearance(this, resId); + } + + @Override + public void refreshDrawableState() { + super.refreshDrawableState(); + + if(mLabelView != null) + mLabelView.refreshDrawableState(); + + if(mSupportView != null) + mSupportView.refreshDrawableState(); + } + + @Override + public void onCommitCompletion(CompletionInfo text) { + EditText.this.onCommitCompletion(text); + } + + @Override + public void onCommitCorrection(CorrectionInfo info) { + EditText.this.onCommitCorrection(info); + } + + @Override + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + return EditText.this.onCreateInputConnection(outAttrs); + } + + @Override + public void onEditorAction(int actionCode) { + EditText.this.onEditorAction(actionCode); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return EditText.this.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { + return EditText.this.onKeyMultiple(keyCode, repeatCount, event); + } + + @Override + public boolean onKeyPreIme(int keyCode, KeyEvent event) { + return EditText.this.onKeyPreIme(keyCode, event); + } + + @Override + public boolean onKeyShortcut(int keyCode, KeyEvent event) { + return EditText.this.onKeyShortcut(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + return EditText.this.onKeyUp(keyCode, event); + } + + @Override + protected void onSelectionChanged(int selStart, int selEnd) { + EditText.this.onSelectionChanged(selStart, selEnd); + } + + void superOnCommitCompletion(CompletionInfo text) { + super.onCommitCompletion(text); + } + + void superOnCommitCorrection(CorrectionInfo info) { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) + super.onCommitCorrection(info); + } + + InputConnection superOnCreateInputConnection(EditorInfo outAttrs) { + return super.onCreateInputConnection(outAttrs); + } + + void superOnEditorAction(int actionCode) { + super.onEditorAction(actionCode); + } + + boolean superOnKeyDown(int keyCode, KeyEvent event) { + return super.onKeyDown(keyCode, event); + } + + boolean superOnKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { + return super.onKeyMultiple(keyCode, repeatCount, event); + } + + boolean superOnKeyPreIme(int keyCode, KeyEvent event) { + return super.onKeyPreIme(keyCode, event); + } + + boolean superOnKeyShortcut(int keyCode, KeyEvent event) { + return super.onKeyShortcut(keyCode, event); + } + + boolean superOnKeyUp(int keyCode, KeyEvent event) { + return super.onKeyUp(keyCode, event); + } + + void superOnSelectionChanged(int selStart, int selEnd) { + super.onSelectionChanged(selStart, selEnd); + } + } + + private class InternalAutoCompleteTextView extends AutoCompleteTextView{ + + public InternalAutoCompleteTextView(Context context) { + super(context); + } + + public InternalAutoCompleteTextView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public InternalAutoCompleteTextView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + public void setTextAppearance(int resId) { + ViewUtil.applyTextAppearance(this, resId); + } + + @Override + public void setTextAppearance(Context context, int resId) { + ViewUtil.applyTextAppearance(this, resId); + } + + @Override + public void refreshDrawableState() { + super.refreshDrawableState(); + + if(mLabelView != null) + mLabelView.refreshDrawableState(); + + if(mSupportView != null) + mSupportView.refreshDrawableState(); + } + + @Override + public void onCommitCompletion(CompletionInfo text) { + EditText.this.onCommitCompletion(text); + } + + @Override + public void onCommitCorrection(CorrectionInfo info) { + EditText.this.onCommitCorrection(info); + } + + @Override + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + return EditText.this.onCreateInputConnection(outAttrs); + } + + @Override + public void onEditorAction(int actionCode) { + EditText.this.onEditorAction(actionCode); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return EditText.this.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { + return EditText.this.onKeyMultiple(keyCode, repeatCount, event); + } + + @Override + public boolean onKeyPreIme(int keyCode, KeyEvent event) { + return EditText.this.onKeyPreIme(keyCode, event); + } + + @Override + public boolean onKeyShortcut(int keyCode, KeyEvent event) { + return EditText.this.onKeyShortcut(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + return EditText.this.onKeyUp(keyCode, event); + } + + @Override + protected void onSelectionChanged(int selStart, int selEnd) { + EditText.this.onSelectionChanged(selStart, selEnd); + } + + @Override + protected CharSequence convertSelectionToString(Object selectedItem) { + return EditText.this.convertSelectionToString(selectedItem); + } + + @Override + protected void performFiltering(CharSequence text, int keyCode) { + EditText.this.performFiltering(text, keyCode); + } + + @Override + protected void replaceText(CharSequence text) { + EditText.this.replaceText(text); + } + + @Override + protected Filter getFilter() { + return EditText.this.getFilter(); + } + + @Override + public void onFilterComplete(int count) { + EditText.this.onFilterComplete(count); + } + + void superOnCommitCompletion(CompletionInfo text) { + super.onCommitCompletion(text); + } + + void superOnCommitCorrection(CorrectionInfo info) { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) + super.onCommitCorrection(info); + } + + InputConnection superOnCreateInputConnection(EditorInfo outAttrs) { + return super.onCreateInputConnection(outAttrs); + } + + void superOnEditorAction(int actionCode) { + super.onEditorAction(actionCode); + } + + boolean superOnKeyDown(int keyCode, KeyEvent event) { + return super.onKeyDown(keyCode, event); + } + + boolean superOnKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { + return super.onKeyMultiple(keyCode, repeatCount, event); + } + + boolean superOnKeyPreIme(int keyCode, KeyEvent event) { + return super.onKeyPreIme(keyCode, event); + } + + boolean superOnKeyShortcut(int keyCode, KeyEvent event) { + return super.onKeyShortcut(keyCode, event); + } + + boolean superOnKeyUp(int keyCode, KeyEvent event) { + return super.onKeyUp(keyCode, event); + } + + void superOnFilterComplete(int count) { + super.onFilterComplete(count); + } + + CharSequence superConvertSelectionToString(Object selectedItem) { + return super.convertSelectionToString(selectedItem); + } + + void superPerformFiltering(CharSequence text, int keyCode) { + super.performFiltering(text, keyCode); + } + + void superReplaceText(CharSequence text) { + super.replaceText(text); + } + + Filter superGetFilter() { + return super.getFilter(); + } + + void superOnSelectionChanged(int selStart, int selEnd) { + super.onSelectionChanged(selStart, selEnd); + } + } + + private class InternalMultiAutoCompleteTextView extends MultiAutoCompleteTextView{ + + public InternalMultiAutoCompleteTextView(Context context) { + super(context); + } + + public InternalMultiAutoCompleteTextView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public InternalMultiAutoCompleteTextView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + public void setTextAppearance(int resId) { + ViewUtil.applyTextAppearance(this, resId); + } + + @Override + public void setTextAppearance(Context context, int resId) { + ViewUtil.applyTextAppearance(this, resId); + } + + @Override + public void refreshDrawableState() { + super.refreshDrawableState(); + + if(mLabelView != null) + mLabelView.refreshDrawableState(); + + if(mSupportView != null) + mSupportView.refreshDrawableState(); + } + + @Override + public void onCommitCompletion(CompletionInfo text) { + EditText.this.onCommitCompletion(text); + } + + @Override + public void onCommitCorrection(CorrectionInfo info) { + EditText.this.onCommitCorrection(info); + } + + @Override + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + return EditText.this.onCreateInputConnection(outAttrs); + } + + @Override + public void onEditorAction(int actionCode) { + EditText.this.onEditorAction(actionCode); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return EditText.this.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { + return EditText.this.onKeyMultiple(keyCode, repeatCount, event); + } + + @Override + public boolean onKeyPreIme(int keyCode, KeyEvent event) { + return EditText.this.onKeyPreIme(keyCode, event); + } + + @Override + public boolean onKeyShortcut(int keyCode, KeyEvent event) { + return EditText.this.onKeyShortcut(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + return EditText.this.onKeyUp(keyCode, event); + } + + @Override + protected void onSelectionChanged(int selStart, int selEnd) { + EditText.this.onSelectionChanged(selStart, selEnd); + } + + @Override + public void onFilterComplete(int count) { + EditText.this.onFilterComplete(count); + } + + @Override + protected CharSequence convertSelectionToString(Object selectedItem) { + return EditText.this.convertSelectionToString(selectedItem); + } + + @Override + protected void performFiltering(CharSequence text, int keyCode) { + EditText.this.performFiltering(text, keyCode); + } + + @Override + protected void replaceText(CharSequence text) { + EditText.this.replaceText(text); + } + + @Override + protected Filter getFilter() { + return EditText.this.getFilter(); + } + + @Override + protected void performFiltering(CharSequence text, int start, int end, int keyCode){ + EditText.this.performFiltering(text, start, end, keyCode); + } + + void superOnCommitCompletion(CompletionInfo text) { + super.onCommitCompletion(text); + } + + void superOnCommitCorrection(CorrectionInfo info) { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) + super.onCommitCorrection(info); + } + + InputConnection superOnCreateInputConnection(EditorInfo outAttrs) { + return super.onCreateInputConnection(outAttrs); + } + + void superOnEditorAction(int actionCode) { + super.onEditorAction(actionCode); + } + + boolean superOnKeyDown(int keyCode, KeyEvent event) { + return super.onKeyDown(keyCode, event); + } + + boolean superOnKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { + return super.onKeyMultiple(keyCode, repeatCount, event); + } + + boolean superOnKeyPreIme(int keyCode, KeyEvent event) { + return super.onKeyPreIme(keyCode, event); + } + + boolean superOnKeyShortcut(int keyCode, KeyEvent event) { + return super.onKeyShortcut(keyCode, event); + } + + boolean superOnKeyUp(int keyCode, KeyEvent event) { + return super.onKeyUp(keyCode, event); + } + + void superOnFilterComplete(int count) { + super.onFilterComplete(count); + } + + CharSequence superConvertSelectionToString(Object selectedItem) { + return super.convertSelectionToString(selectedItem); + } + + void superPerformFiltering(CharSequence text, int keyCode) { + super.performFiltering(text, keyCode); + } + + void superReplaceText(CharSequence text) { + super.replaceText(text); + } + + Filter superGetFilter() { + return super.getFilter(); + } + + void superPerformFiltering(CharSequence text, int start, int end, int keyCode){ + super.performFiltering(text, start, end, keyCode); + } + + void superOnSelectionChanged(int selStart, int selEnd) { + super.onSelectionChanged(selStart, selEnd); + } + } +} diff --git a/material/src/main/java/com/rey/material/widget/FloatingActionButton.java b/material/src/main/java/com/rey/material/widget/FloatingActionButton.java new file mode 100644 index 0000000..6500149 --- /dev/null +++ b/material/src/main/java/com/rey/material/widget/FloatingActionButton.java @@ -0,0 +1,664 @@ +package com.rey.material.widget; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.util.Log; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AnimationUtils; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; +import android.widget.FrameLayout; +import android.widget.RelativeLayout; + +import com.rey.material.R; +import com.rey.material.util.ThemeManager; +import com.rey.material.drawable.LineMorphingDrawable; +import com.rey.material.drawable.OvalShadowDrawable; +import com.rey.material.drawable.RippleDrawable; +import com.rey.material.util.RippleManager; +import com.rey.material.util.ThemeUtil; +import com.rey.material.util.ViewUtil; + +@SuppressWarnings("unused") +public class FloatingActionButton extends View implements ThemeManager.OnThemeChangedListener { + + private OvalShadowDrawable mBackground; + private Drawable mIcon; + private Drawable mPrevIcon; + private int mAnimDuration = -1; + private Interpolator mInterpolator; + private SwitchIconAnimator mSwitchIconAnimator; + private int mIconSize = -1; + + private RippleManager mRippleManager; + protected int mStyleId; + protected int mCurrentStyle = ThemeManager.THEME_UNDEFINED; + + public static FloatingActionButton make(Context context, int resId) { + return new FloatingActionButton(context, null, resId); + } + + public FloatingActionButton(Context context) { + super(context); + + init(context, null, 0, 0); + } + + public FloatingActionButton(Context context, AttributeSet attrs) { + super(context, attrs); + + init(context, attrs, 0, 0); + } + + public FloatingActionButton(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + init(context, attrs, defStyleAttr, 0); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public FloatingActionButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + init(context, attrs, defStyleAttr, defStyleRes); + } + + protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + setClickable(true); + mSwitchIconAnimator = new SwitchIconAnimator(); + applyStyle(context, attrs, defStyleAttr, defStyleRes); + + if (!isInEditMode()) + mStyleId = ThemeManager.getStyleId(context, attrs, defStyleAttr, defStyleRes); + } + + public void applyStyle(int resId) { + ViewUtil.applyStyle(this, resId); + applyStyle(getContext(), null, 0, resId); + } + + protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FloatingActionButton, defStyleAttr, defStyleRes); + + int radius = -1; + int elevation = -1; + ColorStateList bgColor = null; + int bgAnimDuration = -1; + int iconSrc = 0; + int iconLineMorphing = 0; + + for (int i = 0, count = a.getIndexCount(); i < count; i++) { + int attr = a.getIndex(i); + + if (attr == R.styleable.FloatingActionButton_fab_radius) + radius = a.getDimensionPixelSize(attr, 0); + else if (attr == R.styleable.FloatingActionButton_fab_elevation) + elevation = a.getDimensionPixelSize(attr, 0); + else if (attr == R.styleable.FloatingActionButton_fab_backgroundColor) + bgColor = a.getColorStateList(attr); + else if (attr == R.styleable.FloatingActionButton_fab_backgroundAnimDuration) + bgAnimDuration = a.getInteger(attr, 0); + else if (attr == R.styleable.FloatingActionButton_fab_iconSrc) + iconSrc = a.getResourceId(attr, 0); + else if (attr == R.styleable.FloatingActionButton_fab_iconLineMorphing) + iconLineMorphing = a.getResourceId(attr, 0); + else if (attr == R.styleable.FloatingActionButton_fab_iconSize) + mIconSize = a.getDimensionPixelSize(attr, 0); + else if (attr == R.styleable.FloatingActionButton_fab_animDuration) + mAnimDuration = a.getInteger(attr, 0); + else if (attr == R.styleable.FloatingActionButton_fab_interpolator) { + int resId = a.getResourceId(R.styleable.FloatingActionButton_fab_interpolator, 0); + if (resId != 0) + mInterpolator = AnimationUtils.loadInterpolator(context, resId); + } + } + + a.recycle(); + + if (mIconSize < 0) + mIconSize = ThemeUtil.dpToPx(context, 24); + + if (mAnimDuration < 0) + mAnimDuration = context.getResources().getInteger(android.R.integer.config_mediumAnimTime); + + if (mInterpolator == null) + mInterpolator = new DecelerateInterpolator(); + + if (mBackground == null) { + if (radius < 0) + radius = ThemeUtil.dpToPx(context, 28); + + if (elevation < 0) + elevation = ThemeUtil.dpToPx(context, 4); + + if (bgColor == null) + bgColor = ColorStateList.valueOf(ThemeUtil.colorAccent(context, 0)); + + if (bgAnimDuration < 0) + bgAnimDuration = 0; + + mBackground = new OvalShadowDrawable(radius, bgColor, elevation, elevation, bgAnimDuration); + mBackground.setInEditMode(isInEditMode()); + mBackground.setBounds(0, 0, getWidth(), getHeight()); + mBackground.setCallback(this); + } else { + if (radius >= 0) + mBackground.setRadius(radius); + + if (bgColor != null) + mBackground.setColor(bgColor); + + if (elevation >= 0) + mBackground.setShadow(elevation, elevation); + + if (bgAnimDuration >= 0) + mBackground.setAnimationDuration(bgAnimDuration); + } + + if (iconLineMorphing != 0) + setIcon(new LineMorphingDrawable.Builder(context, iconLineMorphing).build(), false); + else if (iconSrc != 0) { + setIcon(getDrawable(iconSrc), false); + } + + getRippleManager().onCreate(this, context, attrs, defStyleAttr, defStyleRes); + Drawable background = getBackground(); + if (background != null && background instanceof RippleDrawable) { + RippleDrawable drawable = (RippleDrawable) background; + drawable.setBackgroundDrawable(null); + drawable.setMask(RippleDrawable.Mask.TYPE_OVAL, 0, 0, 0, 0, (int) mBackground.getPaddingLeft(), (int) mBackground.getPaddingTop(), (int) mBackground.getPaddingRight(), (int) mBackground.getPaddingBottom()); + } + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private Drawable getDrawable(int iconSrc) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + return getContext().getResources().getDrawable(iconSrc, null); + } + //noinspection deprecation + return getContext().getResources().getDrawable(iconSrc); + } + + + @Override + public void onThemeChanged(ThemeManager.OnThemeChangedEvent event) { + int style = ThemeManager.getInstance().getCurrentStyle(mStyleId); + if (mCurrentStyle != style) { + mCurrentStyle = style; + applyStyle(mCurrentStyle); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (mStyleId != 0) { + ThemeManager.getInstance().registerOnThemeChangedListener(this); + onThemeChanged(null); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + RippleManager.cancelRipple(this); + if (mStyleId != 0) + ThemeManager.getInstance().unregisterOnThemeChangedListener(this); + } + + + /** + * @return The radius of the button. + */ + public int getRadius() { + return mBackground.getRadius(); + } + + /** + * Set radius of the button. + * + * @param radius The radius in pixel. + */ + public void setRadius(int radius) { + if (mBackground.setRadius(radius)) + requestLayout(); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public float getElevation() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + return super.getElevation(); + + return mBackground.getShadowSize(); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public void setElevation(float elevation) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + super.setElevation(elevation); + else if (mBackground.setShadow(elevation, elevation)) + requestLayout(); + } + + /** + * @return The line state of LineMorphingDrawable that is used as this button's icon. + */ + public int getLineMorphingState() { + if (mIcon != null && mIcon instanceof LineMorphingDrawable) + return ((LineMorphingDrawable) mIcon).getLineState(); + + return -1; + } + + /** + * Set the line state of LineMorphingDrawable that is used as this button's icon. + * + * @param state The line state. + * @param animation Indicate should show animation when switch line state or not. + */ + public void setLineMorphingState(int state, boolean animation) { + if (mIcon != null && mIcon instanceof LineMorphingDrawable) + ((LineMorphingDrawable) mIcon).switchLineState(state, animation); + } + + /** + * @return The background color of this button. + */ + public ColorStateList getBackgroundColor() { + return mBackground.getColor(); + } + + /** + * @return The drawable is used as this button's icon. + */ + public Drawable getIcon() { + return mIcon; + } + + /** + * Set the drawable that is used as this button's icon. + * + * @param icon The drawable. + * @param animation Indicate should show animation when switch drawable or not. + */ + public void setIcon(Drawable icon, boolean animation) { + if (icon == null) + return; + + if (animation) { + mSwitchIconAnimator.startAnimation(icon); + invalidate(); + } else { + if (mIcon != null) { + mIcon.setCallback(null); + unscheduleDrawable(mIcon); + } + + mIcon = icon; + float half = mIconSize / 2f; + mIcon.setBounds((int) (mBackground.getCenterX() - half), (int) (mBackground.getCenterY() - half), (int) (mBackground.getCenterX() + half), (int) (mBackground.getCenterY() + half)); + mIcon.setCallback(this); + invalidate(); + } + } + + public void setBackgroundColor(ColorStateList color) { + mBackground.setColor(color); + invalidate(); + } + + @Override + public void setBackgroundColor(int color) { + mBackground.setColor(color); + invalidate(); + } + + /** + * Show this button at the specific location. If this button isn't attached to any parent view yet, + * it will be add to activity's root view. If not, it will just update the location. + * + * @param activity The activity that this button will be attached to. + * @param x The x value of anchor point. + * @param y The y value of anchor point. + * @param gravity The gravity apply with this button. + * @see Gravity + */ + public void show(Activity activity, int x, int y, int gravity) { + if (getParent() == null) { + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(mBackground.getIntrinsicWidth(), mBackground.getIntrinsicHeight()); + updateParams(x, y, gravity, params); + + activity.getWindow().addContentView(this, params); + } else + updateLocation(x, y, gravity); + } + + /** + * Show this button at the specific location. If this button isn't attached to any parent view yet, + * it will be add to activity's root view. If not, it will just update the location. + * + * @param parent The parent view. Should be {@link FrameLayout} or {@link RelativeLayout} + * @param x The x value of anchor point. + * @param y The y value of anchor point. + * @param gravity The gravity apply with this button. + * @see Gravity + */ + public void show(ViewGroup parent, int x, int y, int gravity) { + if (getParent() == null) { + ViewGroup.LayoutParams params = parent.generateLayoutParams(null); + params.width = mBackground.getIntrinsicWidth(); + params.height = mBackground.getIntrinsicHeight(); + updateParams(x, y, gravity, params); + + parent.addView(this, params); + } else + updateLocation(x, y, gravity); + } + + /** + * Update the location of this button. This method only work if it's already attached to a parent view. + * + * @param x The x value of anchor point. + * @param y The y value of anchor point. + * @param gravity The gravity apply with this button. + * @see Gravity + */ + public void updateLocation(int x, int y, int gravity) { + if (getParent() != null) + updateParams(x, y, gravity, getLayoutParams()); + else + Log.v(FloatingActionButton.class.getSimpleName(), "updateLocation() is called without parent"); + } + + private void updateParams(int x, int y, int gravity, ViewGroup.LayoutParams params) { + int horizontalGravity = gravity & Gravity.HORIZONTAL_GRAVITY_MASK; + + switch (horizontalGravity) { + case Gravity.START: + setLeftMargin(params, (int) (x - mBackground.getPaddingLeft())); + break; + case Gravity.CENTER_HORIZONTAL: + setLeftMargin(params, (int) (x - mBackground.getCenterX())); + break; + case Gravity.END: + setLeftMargin(params, (int) (x - mBackground.getPaddingLeft() - mBackground.getRadius() * 2)); + break; + default: + setLeftMargin(params, (int) (x - mBackground.getPaddingLeft())); + break; + } + + int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; + + switch (verticalGravity) { + case Gravity.TOP: + setTopMargin(params, (int) (y - mBackground.getPaddingTop())); + break; + case Gravity.CENTER_VERTICAL: + setTopMargin(params, (int) (y - mBackground.getCenterY())); + break; + case Gravity.BOTTOM: + setTopMargin(params, (int) (y - mBackground.getPaddingTop() - mBackground.getRadius() * 2)); + break; + default: + setTopMargin(params, (int) (y - mBackground.getPaddingTop())); + break; + } + + setLayoutParams(params); + } + + private void setLeftMargin(ViewGroup.LayoutParams params, int value) { + if (params instanceof FrameLayout.LayoutParams) + ((FrameLayout.LayoutParams) params).leftMargin = value; + else if (params instanceof RelativeLayout.LayoutParams) + ((RelativeLayout.LayoutParams) params).leftMargin = value; + else + Log.v(FloatingActionButton.class.getSimpleName(), "cannot recognize LayoutParams: " + params); + } + + private void setTopMargin(ViewGroup.LayoutParams params, int value) { + if (params instanceof FrameLayout.LayoutParams) + ((FrameLayout.LayoutParams) params).topMargin = value; + else if (params instanceof RelativeLayout.LayoutParams) + ((RelativeLayout.LayoutParams) params).topMargin = value; + else + Log.v(FloatingActionButton.class.getSimpleName(), "cannot recognize LayoutParams: " + params); + } + + /** + * Remove this button from parent view. + */ + public void dismiss() { + if (getParent() != null) + ((ViewGroup) getParent()).removeView(this); + } + + @Override + protected boolean verifyDrawable(Drawable who) { + return super.verifyDrawable(who) || mBackground == who || mIcon == who || mPrevIcon == who; + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + if (mBackground != null) + mBackground.setState(getDrawableState()); + if (mIcon != null) + mIcon.setState(getDrawableState()); + if (mPrevIcon != null) + mPrevIcon.setState(getDrawableState()); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + setMeasuredDimension(mBackground.getIntrinsicWidth(), mBackground.getIntrinsicHeight()); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + mBackground.setBounds(0, 0, w, h); + + if (mIcon != null) { + float half = mIconSize / 2f; + mIcon.setBounds((int) (mBackground.getCenterX() - half), (int) (mBackground.getCenterY() - half), (int) (mBackground.getCenterX() + half), (int) (mBackground.getCenterY() + half)); + } + + if (mPrevIcon != null) { + float half = mIconSize / 2f; + mPrevIcon.setBounds((int) (mBackground.getCenterX() - half), (int) (mBackground.getCenterY() - half), (int) (mBackground.getCenterX() + half), (int) (mBackground.getCenterY() + half)); + } + } + + @Override + public void draw(@NonNull Canvas canvas) { + mBackground.draw(canvas); + super.draw(canvas); + if (mPrevIcon != null) + mPrevIcon.draw(canvas); + if (mIcon != null) + mIcon.draw(canvas); + } + + protected RippleManager getRippleManager() { + if (mRippleManager == null) { + synchronized (RippleManager.class) { + if (mRippleManager == null) + mRippleManager = new RippleManager(); + } + } + + return mRippleManager; + } + + @Override + public void setOnClickListener(OnClickListener l) { + RippleManager rippleManager = getRippleManager(); + if (l == rippleManager) + super.setOnClickListener(l); + else { + rippleManager.setOnClickListener(l); + setOnClickListener(rippleManager); + } + } + + @Override + public boolean onTouchEvent(@NonNull MotionEvent event) { + int action = event.getActionMasked(); + + if (action == MotionEvent.ACTION_DOWN && !mBackground.isPointerOver(event.getX(), event.getY())) + return false; + + boolean result = super.onTouchEvent(event); + return getRippleManager().onTouchEvent(this, event) || result; + } + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + + SavedState ss = new SavedState(superState); + + ss.state = getLineMorphingState(); + return ss; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + + super.onRestoreInstanceState(ss.getSuperState()); + if (ss.state >= 0) + setLineMorphingState(ss.state, false); + requestLayout(); + } + + static class SavedState extends BaseSavedState { + int state; + + /** + * Constructor called from {@link Slider#onSaveInstanceState()} + */ + SavedState(Parcelable superState) { + super(superState); + } + + /** + * Constructor called from {@link #CREATOR} + */ + private SavedState(Parcel in) { + super(in); + state = in.readInt(); + } + + @Override + public void writeToParcel(@NonNull Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeInt(state); + } + + @Override + public String toString() { + return "FloatingActionButton.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " state=" + state + "}"; + } + + public static final Creator CREATOR + = new Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + class SwitchIconAnimator implements Runnable { + + boolean mRunning = false; + long mStartTime; + + public void resetAnimation() { + mStartTime = SystemClock.uptimeMillis(); + mIcon.setAlpha(0); + mPrevIcon.setAlpha(255); + } + + public boolean startAnimation(Drawable icon) { + if (mIcon == icon) + return false; + + mPrevIcon = mIcon; + mIcon = icon; + float half = mIconSize / 2f; + mIcon.setBounds((int) (mBackground.getCenterX() - half), (int) (mBackground.getCenterY() - half), (int) (mBackground.getCenterX() + half), (int) (mBackground.getCenterY() + half)); + mIcon.setCallback(FloatingActionButton.this); + + if (getHandler() != null) { + resetAnimation(); + mRunning = true; + getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + } else { + mPrevIcon.setCallback(null); + unscheduleDrawable(mPrevIcon); + mPrevIcon = null; + } + + invalidate(); + return true; + } + + public void stopAnimation() { + mRunning = false; + mPrevIcon.setCallback(null); + unscheduleDrawable(mPrevIcon); + mPrevIcon = null; + mIcon.setAlpha(255); + if (getHandler() != null) + getHandler().removeCallbacks(this); + invalidate(); + } + + @Override + public void run() { + long curTime = SystemClock.uptimeMillis(); + float progress = Math.min(1f, (float) (curTime - mStartTime) / mAnimDuration); + float value = mInterpolator.getInterpolation(progress); + + mIcon.setAlpha(Math.round(255 * value)); + mPrevIcon.setAlpha(Math.round(255 * (1f - value))); + + if (progress == 1f) + stopAnimation(); + + if (mRunning) { + if (getHandler() != null) + getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + else + stopAnimation(); + } + + invalidate(); + } + + } +} diff --git a/material/src/main/java/com/rey/material/widget/FrameLayout.java b/material/src/main/java/com/rey/material/widget/FrameLayout.java new file mode 100644 index 0000000..f6883cb --- /dev/null +++ b/material/src/main/java/com/rey/material/widget/FrameLayout.java @@ -0,0 +1,126 @@ +package com.rey.material.widget; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import com.rey.material.util.ThemeManager; +import com.rey.material.drawable.RippleDrawable; +import com.rey.material.util.RippleManager; +import com.rey.material.util.ViewUtil; + +public class FrameLayout extends android.widget.FrameLayout implements ThemeManager.OnThemeChangedListener{ + + private RippleManager mRippleManager; + + protected int mStyleId; + protected int mCurrentStyle = ThemeManager.THEME_UNDEFINED; + + public FrameLayout(Context context) { + super(context); + + init(context, null, 0, 0); + } + + public FrameLayout(Context context, AttributeSet attrs) { + super(context, attrs); + + init(context, attrs, 0, 0); + } + + public FrameLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + init(context, attrs, defStyleAttr, 0); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public FrameLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + init(context, attrs, defStyleAttr, defStyleRes); + } + + protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + applyStyle(context, attrs, defStyleAttr, defStyleRes); + if(!isInEditMode()) + mStyleId = ThemeManager.getStyleId(context, attrs, defStyleAttr, defStyleRes); + } + + public void applyStyle(int resId){ + ViewUtil.applyStyle(this, resId); + applyStyle(getContext(), null, 0, resId); + } + + protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + getRippleManager().onCreate(this, context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public void onThemeChanged(ThemeManager.OnThemeChangedEvent event) { + int style = ThemeManager.getInstance().getCurrentStyle(mStyleId); + if(mCurrentStyle != style){ + mCurrentStyle = style; + applyStyle(mCurrentStyle); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if(mStyleId != 0) { + ThemeManager.getInstance().registerOnThemeChangedListener(this); + onThemeChanged(null); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + RippleManager.cancelRipple(this); + if(mStyleId != 0) + ThemeManager.getInstance().unregisterOnThemeChangedListener(this); + } + + @Override + public void setBackgroundDrawable(Drawable drawable) { + Drawable background = getBackground(); + if(background instanceof RippleDrawable && !(drawable instanceof RippleDrawable)) + ((RippleDrawable) background).setBackgroundDrawable(drawable); + else + super.setBackgroundDrawable(drawable); + } + + protected RippleManager getRippleManager(){ + if(mRippleManager == null){ + synchronized (RippleManager.class){ + if(mRippleManager == null) + mRippleManager = new RippleManager(); + } + } + + return mRippleManager; + } + + @Override + public void setOnClickListener(OnClickListener l) { + RippleManager rippleManager = getRippleManager(); + if (l == rippleManager) + super.setOnClickListener(l); + else { + rippleManager.setOnClickListener(l); + setOnClickListener(rippleManager); + } + } + + @Override + public boolean onTouchEvent(@NonNull MotionEvent event) { + boolean result = super.onTouchEvent(event); + return getRippleManager().onTouchEvent(this, event) || result; + } + +} diff --git a/material/src/main/java/com/rey/material/widget/ImageButton.java b/material/src/main/java/com/rey/material/widget/ImageButton.java new file mode 100644 index 0000000..97af751 --- /dev/null +++ b/material/src/main/java/com/rey/material/widget/ImageButton.java @@ -0,0 +1,125 @@ +package com.rey.material.widget; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import com.rey.material.util.ThemeManager; +import com.rey.material.drawable.RippleDrawable; +import com.rey.material.util.RippleManager; +import com.rey.material.util.ViewUtil; + +public class ImageButton extends android.widget.ImageButton implements ThemeManager.OnThemeChangedListener{ + + private RippleManager mRippleManager; + protected int mStyleId; + protected int mCurrentStyle = ThemeManager.THEME_UNDEFINED; + + public ImageButton(Context context) { + super(context); + + init(context, null, 0, 0); + } + + public ImageButton(Context context, AttributeSet attrs) { + super(context, attrs); + + init(context, attrs, 0, 0); + } + + public ImageButton(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + init(context, attrs, defStyleAttr, 0); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public ImageButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + init(context, attrs, defStyleAttr, defStyleRes); + } + + protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + applyStyle(context, attrs, defStyleAttr, defStyleRes); + if(!isInEditMode()) + mStyleId = ThemeManager.getStyleId(context, attrs, defStyleAttr, defStyleRes); + } + + public void applyStyle(int resId){ + ViewUtil.applyStyle(this, resId); + applyStyle(getContext(), null, 0, resId); + } + + protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + getRippleManager().onCreate(this, context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public void onThemeChanged(ThemeManager.OnThemeChangedEvent event) { + int style = ThemeManager.getInstance().getCurrentStyle(mStyleId); + if(mCurrentStyle != style){ + mCurrentStyle = style; + applyStyle(mCurrentStyle); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if(mStyleId != 0) { + ThemeManager.getInstance().registerOnThemeChangedListener(this); + onThemeChanged(null); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + RippleManager.cancelRipple(this); + if(mStyleId != 0) + ThemeManager.getInstance().unregisterOnThemeChangedListener(this); + } + + @Override + public void setBackground(Drawable drawable) { + Drawable background = getBackground(); + if(background instanceof RippleDrawable && !(drawable instanceof RippleDrawable)) + ((RippleDrawable) background).setBackgroundDrawable(drawable); + else + super.setBackground(drawable); + } + + protected RippleManager getRippleManager(){ + if(mRippleManager == null){ + synchronized (RippleManager.class){ + if(mRippleManager == null) + mRippleManager = new RippleManager(); + } + } + + return mRippleManager; + } + + @Override + public void setOnClickListener(OnClickListener l) { + RippleManager rippleManager = getRippleManager(); + if (l == rippleManager) + super.setOnClickListener(l); + else { + rippleManager.setOnClickListener(l); + setOnClickListener(rippleManager); + } + } + + @Override + public boolean onTouchEvent(@NonNull MotionEvent event) { + boolean result = super.onTouchEvent(event); + return getRippleManager().onTouchEvent(this, event) || result; + } + +} diff --git a/material/src/main/java/com/rey/material/widget/LinearLayout.java b/material/src/main/java/com/rey/material/widget/LinearLayout.java new file mode 100644 index 0000000..e1ca3a9 --- /dev/null +++ b/material/src/main/java/com/rey/material/widget/LinearLayout.java @@ -0,0 +1,127 @@ +package com.rey.material.widget; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import com.rey.material.util.ThemeManager; +import com.rey.material.drawable.RippleDrawable; +import com.rey.material.util.RippleManager; +import com.rey.material.util.ViewUtil; + +public class LinearLayout extends android.widget.LinearLayout implements ThemeManager.OnThemeChangedListener{ + + private RippleManager mRippleManager; + + protected int mStyleId; + protected int mCurrentStyle = ThemeManager.THEME_UNDEFINED; + + public LinearLayout(Context context) { + super(context); + + init(context, null, 0, 0); + } + + public LinearLayout(Context context, AttributeSet attrs) { + super(context, attrs); + + init(context, attrs, 0, 0); + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public LinearLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + init(context, attrs, defStyleAttr, 0); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public LinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + init(context, attrs, defStyleAttr, defStyleRes); + } + + protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + applyStyle(context, attrs, defStyleAttr, defStyleRes); + if(!isInEditMode()) + mStyleId = ThemeManager.getStyleId(context, attrs, defStyleAttr, defStyleRes); + } + + public void applyStyle(int resId){ + ViewUtil.applyStyle(this, resId); + applyStyle(getContext(), null, 0, resId); + } + + protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + getRippleManager().onCreate(this, context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public void onThemeChanged(ThemeManager.OnThemeChangedEvent event) { + int style = ThemeManager.getInstance().getCurrentStyle(mStyleId); + if(mCurrentStyle != style){ + mCurrentStyle = style; + applyStyle(mCurrentStyle); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if(mStyleId != 0) { + ThemeManager.getInstance().registerOnThemeChangedListener(this); + onThemeChanged(null); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + RippleManager.cancelRipple(this); + if(mStyleId != 0) + ThemeManager.getInstance().unregisterOnThemeChangedListener(this); + } + + @Override + public void setBackground(Drawable drawable) { + Drawable background = getBackground(); + if(background instanceof RippleDrawable && !(drawable instanceof RippleDrawable)) + ((RippleDrawable) background).setBackgroundDrawable(drawable); + else + super.setBackground(drawable); + } + + protected RippleManager getRippleManager(){ + if(mRippleManager == null){ + synchronized (RippleManager.class){ + if(mRippleManager == null) + mRippleManager = new RippleManager(); + } + } + + return mRippleManager; + } + + @Override + public void setOnClickListener(OnClickListener l) { + RippleManager rippleManager = getRippleManager(); + if (l == rippleManager) + super.setOnClickListener(l); + else { + rippleManager.setOnClickListener(l); + setOnClickListener(rippleManager); + } + } + + @Override + public boolean onTouchEvent(@NonNull MotionEvent event) { + boolean result = super.onTouchEvent(event); + return getRippleManager().onTouchEvent(this, event) || result; + } + +} diff --git a/material/src/main/java/com/rey/material/widget/ListPopupWindow.java b/material/src/main/java/com/rey/material/widget/ListPopupWindow.java new file mode 100644 index 0000000..d5bdc2f --- /dev/null +++ b/material/src/main/java/com/rey/material/widget/ListPopupWindow.java @@ -0,0 +1,1823 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rey.material.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.database.DataSetObserver; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Handler; +import android.os.SystemClock; +import android.support.v4.text.TextUtilsCompat; +import android.support.v4.view.MotionEventCompat; +import android.support.v4.view.ViewPropertyAnimatorCompat; +import android.support.v4.widget.ListViewAutoScrollHelper; +import android.support.v4.widget.PopupWindowCompat; +import android.util.AttributeSet; +import android.util.Log; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.MeasureSpec; +import android.view.View.OnTouchListener; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.ViewTreeObserver; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.AbsListView; +import android.widget.AdapterView; +import android.widget.LinearLayout; +import android.widget.ListAdapter; + +import com.rey.material.R; + +import java.lang.reflect.Method; +import java.util.Locale; + +/** + * This is a copy of android.support.v7.widget.ListPopupWindow. + * Just change DropDownListView's parent class to com.rey.material.widget.ListView to support + * RippleEffect in child view. + * + * @see android.widget.ListPopupWindow + */ +public class ListPopupWindow { + private static final String TAG = "ListPopupWindow"; + private static final boolean DEBUG = false; + + /** + * This value controls the length of time that the user + * must leave a pointer down without scrolling to expand + * the autocomplete dropdown list to cover the IME. + */ + private static final int EXPAND_LIST_TIMEOUT = 250; + + private static Method sClipToWindowEnabledMethod; + + static { + try { + sClipToWindowEnabledMethod = android.widget.PopupWindow.class.getDeclaredMethod( + "setClipToScreenEnabled", boolean.class); + } catch (NoSuchMethodException e) { + Log.i(TAG, "Could not find method setClipToScreenEnabled() on PopupWindow. Oh well."); + } + } + + private Context mContext; + private PopupWindow mPopup; + private ListAdapter mAdapter; + private DropDownListView mDropDownList; + + private int mDropDownHeight = ViewGroup.LayoutParams.WRAP_CONTENT; + private int mDropDownWidth = ViewGroup.LayoutParams.WRAP_CONTENT; + private int mDropDownHorizontalOffset; + private int mDropDownVerticalOffset; + private boolean mDropDownVerticalOffsetSet; + + private int mItemAnimationId; + private int mItemAnimationOffset; + + private int mDropDownGravity = Gravity.NO_GRAVITY; + + private boolean mDropDownAlwaysVisible = false; + private boolean mForceIgnoreOutsideTouch = false; + int mListItemExpandMaximum = Integer.MAX_VALUE; + + private View mPromptView; + private int mPromptPosition = POSITION_PROMPT_ABOVE; + + private DataSetObserver mObserver; + + private View mDropDownAnchorView; + + private Drawable mDropDownListHighlight; + + private AdapterView.OnItemClickListener mItemClickListener; + private AdapterView.OnItemSelectedListener mItemSelectedListener; + + private final ResizePopupRunnable mResizePopupRunnable = new ResizePopupRunnable(); + private final PopupTouchInterceptor mTouchInterceptor = new PopupTouchInterceptor(); + private final PopupScrollListener mScrollListener = new PopupScrollListener(); + private final ListSelectorHider mHideSelector = new ListSelectorHider(); + private Runnable mShowDropDownRunnable; + + private Handler mHandler = new Handler(); + + private Rect mTempRect = new Rect(); + + private boolean mModal; + + private int mLayoutDirection; + + /** + * The provided prompt view should appear above list content. + * + * @see #setPromptPosition(int) + * @see #getPromptPosition() + * @see #setPromptView(View) + */ + public static final int POSITION_PROMPT_ABOVE = 0; + + /** + * The provided prompt view should appear below list content. + * + * @see #setPromptPosition(int) + * @see #getPromptPosition() + * @see #setPromptView(View) + */ + public static final int POSITION_PROMPT_BELOW = 1; + + /** + * Alias for {@link ViewGroup.LayoutParams#MATCH_PARENT}. + * If used to specify a popup width, the popup will match the width of the anchor view. + * If used to specify a popup height, the popup will fill available space. + */ + public static final int MATCH_PARENT = ViewGroup.LayoutParams.MATCH_PARENT; + + /** + * Alias for {@link ViewGroup.LayoutParams#WRAP_CONTENT}. + * If used to specify a popup width, the popup will use the width of its content. + */ + public static final int WRAP_CONTENT = ViewGroup.LayoutParams.WRAP_CONTENT; + + /** + * Mode for {@link #setInputMethodMode(int)}: the requirements for the + * input method should be based on the focusability of the popup. That is + * if it is focusable than it needs to work with the input method, else + * it doesn't. + */ + public static final int INPUT_METHOD_FROM_FOCUSABLE = PopupWindow.INPUT_METHOD_FROM_FOCUSABLE; + + /** + * Mode for {@link #setInputMethodMode(int)}: this popup always needs to + * work with an input method, regardless of whether it is focusable. This + * means that it will always be displayed so that the user can also operate + * the input method while it is shown. + */ + public static final int INPUT_METHOD_NEEDED = PopupWindow.INPUT_METHOD_NEEDED; + + /** + * Mode for {@link #setInputMethodMode(int)}: this popup never needs to + * work with an input method, regardless of whether it is focusable. This + * means that it will always be displayed to use as much space on the + * screen as needed, regardless of whether this covers the input method. + */ + public static final int INPUT_METHOD_NOT_NEEDED = PopupWindow.INPUT_METHOD_NOT_NEEDED; + + /** + * Create a new, empty popup window capable of displaying items from a ListAdapter. + * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}. + * + * @param context Context used for contained views. + */ + public ListPopupWindow(Context context) { + this(context, null, R.attr.listPopupWindowStyle, 0); + } + + /** + * Create a new, empty popup window capable of displaying items from a ListAdapter. + * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}. + * + * @param context Context used for contained views. + * @param attrs Attributes from inflating parent views used to style the popup. + */ + public ListPopupWindow(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.listPopupWindowStyle, 0); + } + + /** + * Create a new, empty popup window capable of displaying items from a ListAdapter. + * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}. + * + * @param context Context used for contained views. + * @param attrs Attributes from inflating parent views used to style the popup. + * @param defStyleAttr Default style attribute to use for popup content. + */ + public ListPopupWindow(Context context, AttributeSet attrs, int defStyleAttr){ + this(context, attrs, defStyleAttr, 0); + } + + /** + * Create a new, empty popup window capable of displaying items from a ListAdapter. + * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}. + * + * @param context Context used for contained views. + * @param attrs Attributes from inflating parent views used to style the popup. + * @param defStyleAttr Default style attribute to use for popup content. + * @param defStyleRes Default style to use for popup content. + */ + public ListPopupWindow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + mContext = context; + + final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ListPopupWindow, + defStyleAttr, defStyleRes); + mDropDownHorizontalOffset = a.getDimensionPixelOffset( + R.styleable.ListPopupWindow_android_dropDownHorizontalOffset, 0); + mDropDownVerticalOffset = a.getDimensionPixelOffset( + R.styleable.ListPopupWindow_android_dropDownVerticalOffset, 0); + if (mDropDownVerticalOffset != 0) { + mDropDownVerticalOffsetSet = true; + } + a.recycle(); + + mPopup = new PopupWindow(context, attrs, defStyleAttr); + mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); + + // Set the default layout direction to match the default locale one + final Locale locale = mContext.getResources().getConfiguration().locale; + mLayoutDirection = TextUtilsCompat.getLayoutDirectionFromLocale(locale); + } + + public void setItemAnimation(int id){ + mItemAnimationId = id; + } + + public void setItemAnimationOffset(int offset){ + mItemAnimationOffset = offset; + } + + public void setBackgroundDrawable(Drawable background){ + mPopup.setBackgroundDrawable(background); + } + + public Drawable getBackground(){ + return mPopup.getBackground(); + } + + /** + * Sets the adapter that provides the data and the views to represent the data + * in this popup window. + * + * @param adapter The adapter to use to create this window's content. + */ + public void setAdapter(ListAdapter adapter) { + if (mObserver == null) { + mObserver = new PopupDataSetObserver(); + } else if (mAdapter != null) { + mAdapter.unregisterDataSetObserver(mObserver); + } + mAdapter = adapter; + if (mAdapter != null) { + adapter.registerDataSetObserver(mObserver); + } + + if (mDropDownList != null) { + mDropDownList.setAdapter(mAdapter); + } + } + + /** + * Set where the optional prompt view should appear. The default is + * {@link #POSITION_PROMPT_ABOVE}. + * + * @param position A position constant declaring where the prompt should be displayed. + * + * @see #POSITION_PROMPT_ABOVE + * @see #POSITION_PROMPT_BELOW + */ + public void setPromptPosition(int position) { + mPromptPosition = position; + } + + /** + * @return Where the optional prompt view should appear. + * + * @see #POSITION_PROMPT_ABOVE + * @see #POSITION_PROMPT_BELOW + */ + public int getPromptPosition() { + return mPromptPosition; + } + + /** + * Set whether this window should be modal when shown. + * + *

If a popup window is modal, it will receive all touch and key input. + * If the user touches outside the popup window's content area the popup window + * will be dismissed. + * + * @param modal {@code true} if the popup window should be modal, {@code false} otherwise. + */ + public void setModal(boolean modal) { + mModal = modal; + mPopup.setFocusable(modal); + } + + /** + * Returns whether the popup window will be modal when shown. + * + * @return {@code true} if the popup window will be modal, {@code false} otherwise. + */ + public boolean isModal() { + return mModal; + } + + /** + * Forces outside touches to be ignored. Normally if {@link #isDropDownAlwaysVisible()} is + * false, we allow outside touch to dismiss the dropdown. If this is set to true, then we + * ignore outside touch even when the drop down is not set to always visible. + * + * @hide Used only by AutoCompleteTextView to handle some internal special cases. + */ + public void setForceIgnoreOutsideTouch(boolean forceIgnoreOutsideTouch) { + mForceIgnoreOutsideTouch = forceIgnoreOutsideTouch; + } + + /** + * Sets whether the drop-down should remain visible under certain conditions. + * + * The drop-down will occupy the entire screen below {@link #getAnchorView} regardless + * of the size or content of the list. {@link #getBackground()} will fill any space + * that is not used by the list. + * + * @param dropDownAlwaysVisible Whether to keep the drop-down visible. + * + * @hide Only used by AutoCompleteTextView under special conditions. + */ + public void setDropDownAlwaysVisible(boolean dropDownAlwaysVisible) { + mDropDownAlwaysVisible = dropDownAlwaysVisible; + } + + /** + * @return Whether the drop-down is visible under special conditions. + * + * @hide Only used by AutoCompleteTextView under special conditions. + */ + public boolean isDropDownAlwaysVisible() { + return mDropDownAlwaysVisible; + } + + /** + * Sets the operating mode for the soft input area. + * + * @param mode The desired mode, see + * {@link android.view.WindowManager.LayoutParams#softInputMode} + * for the full list + * + * @see android.view.WindowManager.LayoutParams#softInputMode + * @see #getSoftInputMode() + */ + public void setSoftInputMode(int mode) { + mPopup.setSoftInputMode(mode); + } + + /** + * Returns the current value in {@link #setSoftInputMode(int)}. + * + * @see #setSoftInputMode(int) + * @see android.view.WindowManager.LayoutParams#softInputMode + */ + public int getSoftInputMode() { + return mPopup.getSoftInputMode(); + } + + /** + * Sets a drawable to use as the list item selector. + * + * @param selector List selector drawable to use in the popup. + */ + public void setListSelector(Drawable selector) { + mDropDownListHighlight = selector; + } + + /** + * Set an animation style to use when the popup window is shown or dismissed. + * + * @param animationStyle Animation style to use. + */ + public void setAnimationStyle(int animationStyle) { + mPopup.setAnimationStyle(animationStyle); + } + + /** + * Returns the animation style that will be used when the popup window is shown or dismissed. + * + * @return Animation style that will be used. + */ + public int getAnimationStyle() { + return mPopup.getAnimationStyle(); + } + + /** + * Returns the view that will be used to anchor this popup. + * + * @return The popup's anchor view + */ + public View getAnchorView() { + return mDropDownAnchorView; + } + + /** + * Sets the popup's anchor view. This popup will always be positioned relative to the anchor + * view when shown. + * + * @param anchor The view to use as an anchor. + */ + public void setAnchorView(View anchor) { + mDropDownAnchorView = anchor; + } + + /** + * @return The horizontal offset of the popup from its anchor in pixels. + */ + public int getHorizontalOffset() { + return mDropDownHorizontalOffset; + } + + /** + * Set the horizontal offset of this popup from its anchor view in pixels. + * + * @param offset The horizontal offset of the popup from its anchor. + */ + public void setHorizontalOffset(int offset) { + mDropDownHorizontalOffset = offset; + } + + /** + * @return The vertical offset of the popup from its anchor in pixels. + */ + public int getVerticalOffset() { + if (!mDropDownVerticalOffsetSet) { + return 0; + } + return mDropDownVerticalOffset; + } + + /** + * Set the vertical offset of this popup from its anchor view in pixels. + * + * @param offset The vertical offset of the popup from its anchor. + */ + public void setVerticalOffset(int offset) { + mDropDownVerticalOffset = offset; + mDropDownVerticalOffsetSet = true; + } + + /** + * Set the gravity of the dropdown list. This is commonly used to + * set gravity to START or END for alignment with the anchor. + * + * @param gravity Gravity value to use + */ + public void setDropDownGravity(int gravity) { + mDropDownGravity = gravity; + } + + /** + * @return The width of the popup window in pixels. + */ + public int getWidth() { + return mDropDownWidth; + } + + /** + * Sets the width of the popup window in pixels. Can also be {@link #MATCH_PARENT} + * or {@link #WRAP_CONTENT}. + * + * @param width Width of the popup window. + */ + public void setWidth(int width) { + mDropDownWidth = width; + } + + /** + * Sets the width of the popup window by the size of its content. The final width may be + * larger to accommodate styled window dressing. + * + * @param width Desired width of content in pixels. + */ + public void setContentWidth(int width) { + Drawable popupBackground = mPopup.getBackground(); + if (popupBackground != null) { + popupBackground.getPadding(mTempRect); + mDropDownWidth = mTempRect.left + mTempRect.right + width; + } else { + setWidth(width); + } + } + + /** + * @return The height of the popup window in pixels. + */ + public int getHeight() { + return mDropDownHeight; + } + + /** + * Sets the height of the popup window in pixels. Can also be {@link #MATCH_PARENT}. + * + * @param height Height of the popup window. + */ + public void setHeight(int height) { + mDropDownHeight = height; + } + + /** + * Sets a listener to receive events when a list item is clicked. + * + * @param clickListener Listener to register + * + * @see ListView#setOnItemClickListener(AdapterView.OnItemClickListener) + */ + public void setOnItemClickListener(AdapterView.OnItemClickListener clickListener) { + mItemClickListener = clickListener; + } + + /** + * Sets a listener to receive events when a list item is selected. + * + * @param selectedListener Listener to register. + * + * @see ListView#setOnItemSelectedListener(AdapterView.OnItemSelectedListener) + */ + public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener selectedListener) { + mItemSelectedListener = selectedListener; + } + + /** + * Set a view to act as a user prompt for this popup window. Where the prompt view will appear + * is controlled by {@link #setPromptPosition(int)}. + * + * @param prompt View to use as an informational prompt. + */ + public void setPromptView(View prompt) { + boolean showing = isShowing(); + if (showing) { + removePromptView(); + } + mPromptView = prompt; + if (showing) { + show(); + } + } + + /** + * Post a {@link #show()} call to the UI thread. + */ + public void postShow() { + mHandler.post(mShowDropDownRunnable); + } + + /** + * Show the popup list. If the list is already showing, this method + * will recalculate the popup's size and position. + */ + public void show() { + int height = buildDropDown(); + + int widthSpec = 0; + int heightSpec = 0; + + boolean noInputMethod = isInputMethodNotNeeded(); + + if (mPopup.isShowing()) { + if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) { + // The call to PopupWindow's update method below can accept -1 for any + // value you do not want to update. + widthSpec = -1; + } else if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { + widthSpec = getAnchorView().getWidth(); + } else { + widthSpec = mDropDownWidth; + } + + if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { + // The call to PopupWindow's update method below can accept -1 for any + // value you do not want to update. + heightSpec = noInputMethod ? height : ViewGroup.LayoutParams.MATCH_PARENT; + if (noInputMethod) { + mPopup.setWindowLayoutMode( + mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ? + ViewGroup.LayoutParams.MATCH_PARENT : 0, 0); + } else { + mPopup.setWindowLayoutMode( + mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ? + ViewGroup.LayoutParams.MATCH_PARENT : 0, + ViewGroup.LayoutParams.MATCH_PARENT); + } + } else if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { + heightSpec = height; + } else { + heightSpec = mDropDownHeight; + } + + mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible); + + mPopup.update(getAnchorView(), mDropDownHorizontalOffset, + mDropDownVerticalOffset, widthSpec, heightSpec); + } else { + if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) { + widthSpec = ViewGroup.LayoutParams.MATCH_PARENT; + } else { + if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { + mPopup.setWidth(getAnchorView().getWidth()); + } else { + mPopup.setWidth(mDropDownWidth); + } + } + + if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { + heightSpec = ViewGroup.LayoutParams.MATCH_PARENT; + } else { + if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { + mPopup.setHeight(height); + } else { + mPopup.setHeight(mDropDownHeight); + } + } + + mPopup.setWindowLayoutMode(widthSpec, heightSpec); + setPopupClipToScreenEnabled(true); + + // use outside touchable to dismiss drop down when touching outside of it, so + // only set this if the dropdown is not always visible + mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible); + mPopup.setTouchInterceptor(mTouchInterceptor); + PopupWindowCompat.showAsDropDown(mPopup, getAnchorView(), mDropDownHorizontalOffset, + mDropDownVerticalOffset, mDropDownGravity); + mDropDownList.setSelection(ListView.INVALID_POSITION); + + if (!mModal || mDropDownList.isInTouchMode()) { + clearListSelection(); + } + if (!mModal) { + mHandler.post(mHideSelector); + } + + // show item animation + if(mItemAnimationId != 0) + mPopup.getContentView().getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { + + @Override + public boolean onPreDraw() { + mPopup.getContentView().getViewTreeObserver().removeOnPreDrawListener(this); + for(int i = 0, count = mDropDownList.getChildCount(); i < count; i ++){ + View v = mDropDownList.getChildAt(i); + + Animation anim = AnimationUtils.loadAnimation(mContext, mItemAnimationId); + anim.setStartOffset(mItemAnimationOffset * i); + v.startAnimation(anim); + } + return false; + } + + }); + } + } + + /** + * Dismiss the popup window. + */ + public void dismiss() { + mPopup.dismiss(); + removePromptView(); + mPopup.setContentView(null); + mDropDownList = null; + mHandler.removeCallbacks(mResizePopupRunnable); + } + + /** + * Set a listener to receive a callback when the popup is dismissed. + * + * @param listener Listener that will be notified when the popup is dismissed. + */ + public void setOnDismissListener(PopupWindow.OnDismissListener listener) { + mPopup.setOnDismissListener(listener); + } + + private void removePromptView() { + if (mPromptView != null) { + final ViewParent parent = mPromptView.getParent(); + if (parent instanceof ViewGroup) { + final ViewGroup group = (ViewGroup) parent; + group.removeView(mPromptView); + } + } + } + + /** + * Control how the popup operates with an input method: one of + * {@link #INPUT_METHOD_FROM_FOCUSABLE}, {@link #INPUT_METHOD_NEEDED}, + * or {@link #INPUT_METHOD_NOT_NEEDED}. + * + *

If the popup is showing, calling this method will take effect only + * the next time the popup is shown or through a manual call to the {@link #show()} + * method.

+ * + * @see #getInputMethodMode() + * @see #show() + */ + public void setInputMethodMode(int mode) { + mPopup.setInputMethodMode(mode); + } + + /** + * Return the current value in {@link #setInputMethodMode(int)}. + * + * @see #setInputMethodMode(int) + */ + public int getInputMethodMode() { + return mPopup.getInputMethodMode(); + } + + /** + * Set the selected position of the list. + * Only valid when {@link #isShowing()} == {@code true}. + * + * @param position List position to set as selected. + */ + public void setSelection(int position) { + DropDownListView list = mDropDownList; + if (isShowing() && list != null) { + list.mListSelectionHidden = false; + list.setSelection(position); + + if (Build.VERSION.SDK_INT >= 11) { + if (list.getChoiceMode() != ListView.CHOICE_MODE_NONE) { + list.setItemChecked(position, true); + } + } + } + } + + /** + * Clear any current list selection. + * Only valid when {@link #isShowing()} == {@code true}. + */ + public void clearListSelection() { + final DropDownListView list = mDropDownList; + if (list != null) { + // WARNING: Please read the comment where mListSelectionHidden is declared + list.mListSelectionHidden = true; + //list.hideSelector(); + list.requestLayout(); + } + } + + /** + * @return {@code true} if the popup is currently showing, {@code false} otherwise. + */ + public boolean isShowing() { + return mPopup.isShowing(); + } + + /** + * @return {@code true} if this popup is configured to assume the user does not need + * to interact with the IME while it is showing, {@code false} otherwise. + */ + public boolean isInputMethodNotNeeded() { + return mPopup.getInputMethodMode() == INPUT_METHOD_NOT_NEEDED; + } + + /** + * Perform an item click operation on the specified list adapter position. + * + * @param position Adapter position for performing the click + * @return true if the click action could be performed, false if not. + * (e.g. if the popup was not showing, this method would return false.) + */ + public boolean performItemClick(int position) { + if (isShowing()) { + if (mItemClickListener != null) { + final DropDownListView list = mDropDownList; + final View child = list.getChildAt(position - list.getFirstVisiblePosition()); + final ListAdapter adapter = list.getAdapter(); + mItemClickListener.onItemClick(list, child, position, adapter.getItemId(position)); + } + return true; + } + return false; + } + + /** + * @return The currently selected item or null if the popup is not showing. + */ + public Object getSelectedItem() { + if (!isShowing()) { + return null; + } + return mDropDownList.getSelectedItem(); + } + + /** + * @return The position of the currently selected item or {@link ListView#INVALID_POSITION} + * if {@link #isShowing()} == {@code false}. + * + * @see ListView#getSelectedItemPosition() + */ + public int getSelectedItemPosition() { + if (!isShowing()) { + return ListView.INVALID_POSITION; + } + return mDropDownList.getSelectedItemPosition(); + } + + /** + * @return The ID of the currently selected item or {@link ListView#INVALID_ROW_ID} + * if {@link #isShowing()} == {@code false}. + * + * @see ListView#getSelectedItemId() + */ + public long getSelectedItemId() { + if (!isShowing()) { + return ListView.INVALID_ROW_ID; + } + return mDropDownList.getSelectedItemId(); + } + + /** + * @return The View for the currently selected item or null if + * {@link #isShowing()} == {@code false}. + * + * @see ListView#getSelectedView() + */ + public View getSelectedView() { + if (!isShowing()) { + return null; + } + return mDropDownList.getSelectedView(); + } + + /** + * @return The {@link ListView} displayed within the popup window. + * Only valid when {@link #isShowing()} == {@code true}. + */ + public ListView getListView() { + return mDropDownList; + } + + public PopupWindow getPopup(){ + return mPopup; + } + + /** + * The maximum number of list items that can be visible and still have + * the list expand when touched. + * + * @param max Max number of items that can be visible and still allow the list to expand. + */ + void setListItemExpandMax(int max) { + mListItemExpandMaximum = max; + } + + /** + * Filter key down events. By forwarding key down events to this function, + * views using non-modal ListPopupWindow can have it handle key selection of items. + * + * @param keyCode keyCode param passed to the host view's onKeyDown + * @param event event param passed to the host view's onKeyDown + * @return true if the event was handled, false if it was ignored. + * + * @see #setModal(boolean) + */ + public boolean onKeyDown(int keyCode, KeyEvent event) { + // when the drop down is shown, we drive it directly + if (isShowing()) { + // the key events are forwarded to the list in the drop down view + // note that ListView handles space but we don't want that to happen + // also if selection is not currently in the drop down, then don't + // let center or enter presses go there since that would cause it + // to select one of its items + if (keyCode != KeyEvent.KEYCODE_SPACE + && (mDropDownList.getSelectedItemPosition() >= 0 + || !isConfirmKey(keyCode))) { + int curIndex = mDropDownList.getSelectedItemPosition(); + boolean consumed; + + final boolean below = !mPopup.isAboveAnchor(); + + final ListAdapter adapter = mAdapter; + + boolean allEnabled; + int firstItem = Integer.MAX_VALUE; + int lastItem = Integer.MIN_VALUE; + + if (adapter != null) { + allEnabled = adapter.areAllItemsEnabled(); + firstItem = allEnabled ? 0 : + mDropDownList.lookForSelectablePosition(0, true); + lastItem = allEnabled ? adapter.getCount() - 1 : + mDropDownList.lookForSelectablePosition(adapter.getCount() - 1, false); + } + + if ((below && keyCode == KeyEvent.KEYCODE_DPAD_UP && curIndex <= firstItem) || + (!below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN && curIndex >= lastItem)) { + // When the selection is at the top, we block the key + // event to prevent focus from moving. + clearListSelection(); + mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); + show(); + return true; + } else { + // WARNING: Please read the comment where mListSelectionHidden + // is declared + mDropDownList.mListSelectionHidden = false; + } + + consumed = mDropDownList.onKeyDown(keyCode, event); + if (DEBUG) Log.v(TAG, "Key down: code=" + keyCode + " list consumed=" + consumed); + + if (consumed) { + // If it handled the key event, then the user is + // navigating in the list, so we should put it in front. + mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); + // Here's a little trick we need to do to make sure that + // the list view is actually showing its focus indicator, + // by ensuring it has focus and getting its window out + // of touch mode. + mDropDownList.requestFocusFromTouch(); + show(); + + switch (keyCode) { + // avoid passing the focus from the text view to the + // next component + case KeyEvent.KEYCODE_ENTER: + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_DPAD_DOWN: + case KeyEvent.KEYCODE_DPAD_UP: + return true; + } + } else { + if (below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { + // when the selection is at the bottom, we block the + // event to avoid going to the next focusable widget + if (curIndex == lastItem) { + return true; + } + } else if (!below && keyCode == KeyEvent.KEYCODE_DPAD_UP && + curIndex == firstItem) { + return true; + } + } + } + } + + return false; + } + + /** + * Filter key down events. By forwarding key up events to this function, + * views using non-modal ListPopupWindow can have it handle key selection of items. + * + * @param keyCode keyCode param passed to the host view's onKeyUp + * @param event event param passed to the host view's onKeyUp + * @return true if the event was handled, false if it was ignored. + * + * @see #setModal(boolean) + */ + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (isShowing() && mDropDownList.getSelectedItemPosition() >= 0) { + boolean consumed = mDropDownList.onKeyUp(keyCode, event); + if (consumed && isConfirmKey(keyCode)) { + // if the list accepts the key events and the key event was a click, the text view + // gets the selected item from the drop down as its content + dismiss(); + } + return consumed; + } + return false; + } + + /** + * Filter pre-IME key events. By forwarding {@link View#onKeyPreIme(int, KeyEvent)} + * events to this function, views using ListPopupWindow can have it dismiss the popup + * when the back key is pressed. + * + * @param keyCode keyCode param passed to the host view's onKeyPreIme + * @param event event param passed to the host view's onKeyPreIme + * @return true if the event was handled, false if it was ignored. + * + * @see #setModal(boolean) + */ + public boolean onKeyPreIme(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK && isShowing()) { + // special case for the back key, we do not even try to send it + // to the drop down list but instead, consume it immediately + final View anchorView = mDropDownAnchorView; + if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { + KeyEvent.DispatcherState state = anchorView.getKeyDispatcherState(); + if (state != null) { + state.startTracking(event, this); + } + return true; + } else if (event.getAction() == KeyEvent.ACTION_UP) { + KeyEvent.DispatcherState state = anchorView.getKeyDispatcherState(); + if (state != null) { + state.handleUpEvent(event); + } + if (event.isTracking() && !event.isCanceled()) { + dismiss(); + return true; + } + } + } + return false; + } + + /** + * Returns an {@link OnTouchListener} that can be added to the source view + * to implement drag-to-open behavior. Generally, the source view should be + * the same view that was passed to {@link #setAnchorView}. + *

+ * When the listener is set on a view, touching that view and dragging + * outside of its bounds will open the popup window. Lifting will select the + * currently touched list item. + *

+ * Example usage: + *

+     * ListPopupWindow myPopup = new ListPopupWindow(context);
+     * myPopup.setAnchor(myAnchor);
+     * OnTouchListener dragListener = myPopup.createDragToOpenListener(myAnchor);
+     * myAnchor.setOnTouchListener(dragListener);
+     * 
+ * + * @param src the view on which the resulting listener will be set + * @return a touch listener that controls drag-to-open behavior + */ + public OnTouchListener createDragToOpenListener(View src) { + return new ForwardingListener(src) { + @Override + public ListPopupWindow getPopup() { + return ListPopupWindow.this; + } + }; + } + + private int getSystemBarHeight(String resourceName) { + int height = 0; + int resourceId = mContext.getResources().getIdentifier(resourceName, "dimen", "android"); + if (resourceId > 0) { + height = mContext.getResources().getDimensionPixelSize(resourceId); + } + return height; + } + + /** + *

Builds the popup window's content and returns the height the popup + * should have. Returns -1 when the content already exists.

+ * + * @return the content's height or -1 if content already exists + */ + private int buildDropDown() { + int otherHeights = 0; + + if (mDropDownList == null) { + ViewGroup dropDownView; + Context context = mContext; + + /** + * This Runnable exists for the sole purpose of checking if the view layout has got + * completed and if so call showDropDown to display the drop down. This is used to show + * the drop down as soon as possible after user opens up the search dialog, without + * waiting for the normal UI pipeline to do it's job which is slower than this method. + */ + mShowDropDownRunnable = new Runnable() { + public void run() { + // View layout should be all done before displaying the drop down. + View view = getAnchorView(); + if (view != null && view.getWindowToken() != null) { + show(); + } + } + }; + + mDropDownList = new DropDownListView(context, !mModal); + if (mDropDownListHighlight != null) { + mDropDownList.setSelector(mDropDownListHighlight); + } + mDropDownList.setAdapter(mAdapter); + mDropDownList.setOnItemClickListener(mItemClickListener); + mDropDownList.setFocusable(true); + mDropDownList.setFocusableInTouchMode(true); + mDropDownList.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + public void onItemSelected(AdapterView parent, View view, + int position, long id) { + + if (position != -1) { + DropDownListView dropDownList = mDropDownList; + + if (dropDownList != null) { + dropDownList.mListSelectionHidden = false; + } + } + } + + public void onNothingSelected(AdapterView parent) { + } + }); + mDropDownList.setOnScrollListener(mScrollListener); + + if (mItemSelectedListener != null) { + mDropDownList.setOnItemSelectedListener(mItemSelectedListener); + } + + dropDownView = mDropDownList; + + View hintView = mPromptView; + if (hintView != null) { + // if a hint has been specified, we accomodate more space for it and + // add a text view in the drop down menu, at the bottom of the list + LinearLayout hintContainer = new LinearLayout(context); + hintContainer.setOrientation(LinearLayout.VERTICAL); + + LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, 0, 1.0f + ); + + switch (mPromptPosition) { + case POSITION_PROMPT_BELOW: + hintContainer.addView(dropDownView, hintParams); + hintContainer.addView(hintView); + break; + + case POSITION_PROMPT_ABOVE: + hintContainer.addView(hintView); + hintContainer.addView(dropDownView, hintParams); + break; + + default: + Log.e(TAG, "Invalid hint position " + mPromptPosition); + break; + } + + // measure the hint's height to find how much more vertical space + // we need to add to the drop down's height + int widthSpec = MeasureSpec.makeMeasureSpec(mDropDownWidth, MeasureSpec.AT_MOST); + int heightSpec = MeasureSpec.UNSPECIFIED; + hintView.measure(widthSpec, heightSpec); + + hintParams = (LinearLayout.LayoutParams) hintView.getLayoutParams(); + otherHeights = hintView.getMeasuredHeight() + hintParams.topMargin + + hintParams.bottomMargin; + + dropDownView = hintContainer; + } + + mPopup.setContentView(dropDownView); + + } else { + final View view = mPromptView; + if (view != null) { + LinearLayout.LayoutParams hintParams = + (LinearLayout.LayoutParams) view.getLayoutParams(); + otherHeights = view.getMeasuredHeight() + hintParams.topMargin + + hintParams.bottomMargin; + } + } + + // getMaxAvailableHeight() subtracts the padding, so we put it back + // to get the available height for the whole window + int padding = 0; + Drawable background = mPopup.getBackground(); + if (background != null) { + background.getPadding(mTempRect); + padding = mTempRect.top + mTempRect.bottom; + + // If we don't have an explicit vertical offset, determine one from the window + // background so that content will line up. + if (!mDropDownVerticalOffsetSet) { + mDropDownVerticalOffset = -mTempRect.top; + } + } else { + mTempRect.setEmpty(); + } + + int systemBarsReservedSpace = 0; + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + // getMaxAvailableHeight() on Lollipop seems to ignore the system bars. + systemBarsReservedSpace = Math.max( + getSystemBarHeight("status_bar_height"), + getSystemBarHeight("navigation_bar_height") + ); + } + + // Max height available on the screen for a popup. + boolean ignoreBottomDecorations = + mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED; + final int maxHeight = mPopup.getMaxAvailableHeight( + getAnchorView(), mDropDownVerticalOffset /*, ignoreBottomDecorations*/) + - systemBarsReservedSpace; + + if (mDropDownAlwaysVisible || mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { + return maxHeight + padding; + } + + final int childWidthSpec; + switch (mDropDownWidth) { + case ViewGroup.LayoutParams.WRAP_CONTENT: + childWidthSpec = MeasureSpec.makeMeasureSpec( + mContext.getResources().getDisplayMetrics().widthPixels - + (mTempRect.left + mTempRect.right), + MeasureSpec.AT_MOST); + break; + case ViewGroup.LayoutParams.MATCH_PARENT: + childWidthSpec = MeasureSpec.makeMeasureSpec( + mContext.getResources().getDisplayMetrics().widthPixels - + (mTempRect.left + mTempRect.right), + MeasureSpec.EXACTLY); + break; + default: + childWidthSpec = MeasureSpec.makeMeasureSpec(mDropDownWidth, MeasureSpec.EXACTLY); + break; + } + + final int listContent = mDropDownList.measureHeightOfChildrenCompat(childWidthSpec, + 0, DropDownListView.NO_POSITION, maxHeight - otherHeights, -1); + // add padding only if the list has items in it, that way we don't show + // the popup if it is not needed + if (listContent > 0) otherHeights += padding; + + return listContent + otherHeights; + } + + /** + * Abstract class that forwards touch events to a {@link ListPopupWindow}. + * + * @hide + */ + public static abstract class ForwardingListener implements OnTouchListener { + /** Scaled touch slop, used for detecting movement outside bounds. */ + private final float mScaledTouchSlop; + + /** Timeout before disallowing intercept on the source's parent. */ + private final int mTapTimeout; + /** Timeout before accepting a long-press to start forwarding. */ + private final int mLongPressTimeout; + + /** Source view from which events are forwarded. */ + private final View mSrc; + + /** Runnable used to prevent conflicts with scrolling parents. */ + private Runnable mDisallowIntercept; + /** Runnable used to trigger forwarding on long-press. */ + private Runnable mTriggerLongPress; + + /** Whether this listener is currently forwarding touch events. */ + private boolean mForwarding; + /** + * Whether forwarding was initiated by a long-press. If so, we won't + * force the window to dismiss when the touch stream ends. + */ + private boolean mWasLongPress; + + /** The id of the first pointer down in the current event stream. */ + private int mActivePointerId; + + /** + * Temporary Matrix instance + */ + private final int[] mTmpLocation = new int[2]; + + public ForwardingListener(View src) { + mSrc = src; + mScaledTouchSlop = ViewConfiguration.get(src.getContext()).getScaledTouchSlop(); + mTapTimeout = ViewConfiguration.getTapTimeout(); + // Use a medium-press timeout. Halfway between tap and long-press. + mLongPressTimeout = (mTapTimeout + ViewConfiguration.getLongPressTimeout()) / 2; + } + + /** + * Returns the popup to which this listener is forwarding events. + *

+ * Override this to return the correct popup. If the popup is displayed + * asynchronously, you may also need to override + * {@link #onForwardingStopped} to prevent premature cancelation of + * forwarding. + * + * @return the popup to which this listener is forwarding events + */ + public abstract ListPopupWindow getPopup(); + + @Override + public boolean onTouch(View v, MotionEvent event) { + final boolean wasForwarding = mForwarding; + final boolean forwarding; + if (wasForwarding) { + if (mWasLongPress) { + // If we started forwarding as a result of a long-press, + // just silently stop forwarding events so that the window + // stays open. + forwarding = onTouchForwarded(event); + } else { + forwarding = onTouchForwarded(event) || !onForwardingStopped(); + } + } else { + forwarding = onTouchObserved(event) && onForwardingStarted(); + + if (forwarding) { + // Make sure we cancel any ongoing source event stream. + final long now = SystemClock.uptimeMillis(); + final MotionEvent e = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, + 0.0f, 0.0f, 0); + mSrc.onTouchEvent(e); + e.recycle(); + } + } + + mForwarding = forwarding; + return forwarding || wasForwarding; + } + + /** + * Called when forwarding would like to start.

By default, this will show the popup + * returned by {@link #getPopup()}. It may be overridden to perform another action, like + * clicking the source view or preparing the popup before showing it. + * + * @return true to start forwarding, false otherwise + */ + protected boolean onForwardingStarted() { + final ListPopupWindow popup = getPopup(); + if (popup != null && !popup.isShowing()) { + popup.show(); + } + return true; + } + + /** + * Called when forwarding would like to stop.

By default, this will dismiss the popup + * returned by {@link #getPopup()}. It may be overridden to perform some other action. + * + * @return true to stop forwarding, false otherwise + */ + protected boolean onForwardingStopped() { + final ListPopupWindow popup = getPopup(); + if (popup != null && popup.isShowing()) { + popup.dismiss(); + } + return true; + } + + /** + * Observes motion events and determines when to start forwarding. + * + * @param srcEvent motion event in source view coordinates + * @return true to start forwarding motion events, false otherwise + */ + private boolean onTouchObserved(MotionEvent srcEvent) { + final View src = mSrc; + if (!src.isEnabled()) { + return false; + } + + final int actionMasked = MotionEventCompat.getActionMasked(srcEvent); + switch (actionMasked) { + case MotionEvent.ACTION_DOWN: + mActivePointerId = srcEvent.getPointerId(0); + mWasLongPress = false; + + if (mDisallowIntercept == null) { + mDisallowIntercept = new DisallowIntercept(); + } + src.postDelayed(mDisallowIntercept, mTapTimeout); + if (mTriggerLongPress == null) { + mTriggerLongPress = new TriggerLongPress(); + } + src.postDelayed(mTriggerLongPress, mLongPressTimeout); + break; + case MotionEvent.ACTION_MOVE: + final int activePointerIndex = srcEvent.findPointerIndex(mActivePointerId); + if (activePointerIndex >= 0) { + final float x = srcEvent.getX(activePointerIndex); + final float y = srcEvent.getY(activePointerIndex); + if (!pointInView(src, x, y, mScaledTouchSlop)) { + clearCallbacks(); + + // Don't let the parent intercept our events. + src.getParent().requestDisallowInterceptTouchEvent(true); + return true; + } + } + break; + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + clearCallbacks(); + break; + } + + return false; + } + + private void clearCallbacks() { + if (mTriggerLongPress != null) { + mSrc.removeCallbacks(mTriggerLongPress); + } + + if (mDisallowIntercept != null) { + mSrc.removeCallbacks(mDisallowIntercept); + } + } + + private void onLongPress() { + clearCallbacks(); + + final View src = mSrc; + if (!src.isEnabled()) { + return; + } + + if (!onForwardingStarted()) { + return; + } + + // Don't let the parent intercept our events. + mSrc.getParent().requestDisallowInterceptTouchEvent(true); + + // Make sure we cancel any ongoing source event stream. + final long now = SystemClock.uptimeMillis(); + final MotionEvent e = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0); + mSrc.onTouchEvent(e); + e.recycle(); + + mForwarding = true; + mWasLongPress = true; + } + + /** + * Handled forwarded motion events and determines when to stop forwarding. + * + * @param srcEvent motion event in source view coordinates + * @return true to continue forwarding motion events, false to cancel + */ + private boolean onTouchForwarded(MotionEvent srcEvent) { + final View src = mSrc; + final ListPopupWindow popup = getPopup(); + if (popup == null || !popup.isShowing()) { + return false; + } + + final DropDownListView dst = popup.mDropDownList; + if (dst == null || !dst.isShown()) { + return false; + } + + // Convert event to destination-local coordinates. + final MotionEvent dstEvent = MotionEvent.obtainNoHistory(srcEvent); + toGlobalMotionEvent(src, dstEvent); + toLocalMotionEvent(dst, dstEvent); + + // Forward converted event to destination view, then recycle it. + final boolean handled = dst.onForwardedEvent(dstEvent, mActivePointerId); + dstEvent.recycle(); + + // Always cancel forwarding when the touch stream ends. + final int action = MotionEventCompat.getActionMasked(srcEvent); + final boolean keepForwarding = action != MotionEvent.ACTION_UP + && action != MotionEvent.ACTION_CANCEL; + + return handled && keepForwarding; + } + + private static boolean pointInView(View view, float localX, float localY, float slop) { + return localX >= -slop && localY >= -slop && + localX < ((view.getRight() - view.getLeft()) + slop) && + localY < ((view.getBottom() - view.getTop()) + slop); + } + + /** + * Emulates View.toLocalMotionEvent(). This implementation does not handle transformations + * (scaleX, scaleY, etc). + */ + private boolean toLocalMotionEvent(View view, MotionEvent event) { + final int[] loc = mTmpLocation; + view.getLocationOnScreen(loc); + event.offsetLocation(-loc[0], -loc[1]); + return true; + } + + /** + * Emulates View.toGlobalMotionEvent(). This implementation does not handle transformations + * (scaleX, scaleY, etc). + */ + private boolean toGlobalMotionEvent(View view, MotionEvent event) { + final int[] loc = mTmpLocation; + view.getLocationOnScreen(loc); + event.offsetLocation(loc[0], loc[1]); + return true; + } + + private class DisallowIntercept implements Runnable { + @Override + public void run() { + final ViewParent parent = mSrc.getParent(); + parent.requestDisallowInterceptTouchEvent(true); + } + } + + private class TriggerLongPress implements Runnable { + @Override + public void run() { + onLongPress(); + } + } + } + + /** + *

Wrapper class for a ListView. This wrapper can hijack the focus to + * make sure the list uses the appropriate drawables and states when + * displayed on screen within a drop down. The focus is never actually + * passed to the drop down in this mode; the list only looks focused.

+ */ + private static class DropDownListView extends ListView { + + /* + * WARNING: This is a workaround for a touch mode issue. + * + * Touch mode is propagated lazily to windows. This causes problems in + * the following scenario: + * - Type something in the AutoCompleteTextView and get some results + * - Move down with the d-pad to select an item in the list + * - Move up with the d-pad until the selection disappears + * - Type more text in the AutoCompleteTextView *using the soft keyboard* + * and get new results; you are now in touch mode + * - The selection comes back on the first item in the list, even though + * the list is supposed to be in touch mode + * + * Using the soft keyboard triggers the touch mode change but that change + * is propagated to our window only after the first list layout, therefore + * after the list attempts to resurrect the selection. + * + * The trick to work around this issue is to pretend the list is in touch + * mode when we know that the selection should not appear, that is when + * we know the user moved the selection away from the list. + * + * This boolean is set to true whenever we explicitly hide the list's + * selection and reset to false whenever we know the user moved the + * selection back to the list. + * + * When this boolean is true, isInTouchMode() returns true, otherwise it + * returns super.isInTouchMode(). + */ + private boolean mListSelectionHidden; + + /** + * True if this wrapper should fake focus. + */ + private boolean mHijackFocus; + + /** Whether to force drawing of the pressed state selector. */ + private boolean mDrawsInPressedState; + + /** Current drag-to-open click animation, if any. */ + private ViewPropertyAnimatorCompat mClickAnimation; + + /** Helper for drag-to-open auto scrolling. */ + private ListViewAutoScrollHelper mScrollHelper; + + /** + *

Creates a new list view wrapper.

+ * + * @param context this view's context + */ + public DropDownListView(Context context, boolean hijackFocus) { + super(context, null, R.attr.dropDownListViewStyle); + mHijackFocus = hijackFocus; + setCacheColorHint(0); // Transparent, since the background drawable could be anything. + } + + /** + * Handles forwarded events. + * + * @param activePointerId id of the pointer that activated forwarding + * @return whether the event was handled + */ + public boolean onForwardedEvent(MotionEvent event, int activePointerId) { + boolean handledEvent = true; + boolean clearPressedItem = false; + + final int actionMasked = MotionEventCompat.getActionMasked(event); + switch (actionMasked) { + case MotionEvent.ACTION_CANCEL: + handledEvent = false; + break; + case MotionEvent.ACTION_UP: + handledEvent = false; + // $FALL-THROUGH$ + case MotionEvent.ACTION_MOVE: + final int activeIndex = event.findPointerIndex(activePointerId); + if (activeIndex < 0) { + handledEvent = false; + break; + } + + final int x = (int) event.getX(activeIndex); + final int y = (int) event.getY(activeIndex); + final int position = pointToPosition(x, y); + if (position == INVALID_POSITION) { + clearPressedItem = true; + break; + } + + final View child = getChildAt(position - getFirstVisiblePosition()); + setPressedItem(child, position, x, y); + handledEvent = true; + + if (actionMasked == MotionEvent.ACTION_UP) { + clickPressedItem(child, position); + } + break; + } + + // Failure to handle the event cancels forwarding. + if (!handledEvent || clearPressedItem) { + clearPressedItem(); + } + + // Manage automatic scrolling. + if (handledEvent) { + if (mScrollHelper == null) { + mScrollHelper = new ListViewAutoScrollHelper(this); + } + mScrollHelper.setEnabled(true); + mScrollHelper.onTouch(this, event); + } else if (mScrollHelper != null) { + mScrollHelper.setEnabled(false); + } + + return handledEvent; + } + + /** + * Starts an alpha animation on the selector. When the animation ends, + * the list performs a click on the item. + */ + private void clickPressedItem(final View child, final int position) { + final long id = getItemIdAtPosition(position); + performItemClick(child, position, id); + } + + private void clearPressedItem() { + mDrawsInPressedState = false; + setPressed(false); + // This will call through to updateSelectorState() + drawableStateChanged(); + + if (mClickAnimation != null) { + mClickAnimation.cancel(); + mClickAnimation = null; + } + } + + private void setPressedItem(View child, int position, float x, float y) { + mDrawsInPressedState = true; + + // Ordering is essential. First update the pressed state and layout + // the children. This will ensure the selector actually gets drawn. + setPressed(true); + layoutChildren(); + + // Ensure that keyboard focus starts from the last touched position. + setSelection(position); + positionSelectorLikeTouchCompat(position, child, x, y); + + // This needs some explanation. We need to disable the selector for this next call + // due to the way that ListViewCompat works. Otherwise both ListView and ListViewCompat + // will draw the selector and bad things happen. + setSelectorEnabled(false); + + // Refresh the drawable state to reflect the new pressed state, + // which will also update the selector state. + refreshDrawableState(); + } + + @Override + protected boolean touchModeDrawsInPressedStateCompat() { + return mDrawsInPressedState || super.touchModeDrawsInPressedStateCompat(); + } + + @Override + public boolean isInTouchMode() { + // WARNING: Please read the comment where mListSelectionHidden is declared + return (mHijackFocus && mListSelectionHidden) || super.isInTouchMode(); + } + + /** + *

Returns the focus state in the drop down.

+ * + * @return true always if hijacking focus + */ + @Override + public boolean hasWindowFocus() { + return mHijackFocus || super.hasWindowFocus(); + } + + /** + *

Returns the focus state in the drop down.

+ * + * @return true always if hijacking focus + */ + @Override + public boolean isFocused() { + return mHijackFocus || super.isFocused(); + } + + /** + *

Returns the focus state in the drop down.

+ * + * @return true always if hijacking focus + */ + @Override + public boolean hasFocus() { + return mHijackFocus || super.hasFocus(); + } + + } + + private class PopupDataSetObserver extends DataSetObserver { + @Override + public void onChanged() { + if (isShowing()) { + // Resize the popup to fit new content + show(); + } + } + + @Override + public void onInvalidated() { + dismiss(); + } + } + + private class ListSelectorHider implements Runnable { + public void run() { + clearListSelection(); + } + } + + private class ResizePopupRunnable implements Runnable { + public void run() { + if (mDropDownList != null && mDropDownList.getCount() > mDropDownList.getChildCount() && + mDropDownList.getChildCount() <= mListItemExpandMaximum) { + mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); + show(); + } + } + } + + private class PopupTouchInterceptor implements OnTouchListener { + public boolean onTouch(View v, MotionEvent event) { + final int action = event.getAction(); + final int x = (int) event.getX(); + final int y = (int) event.getY(); + + if (action == MotionEvent.ACTION_DOWN && + mPopup != null && mPopup.isShowing() && + (x >= 0 && x < mPopup.getWidth() && y >= 0 && y < mPopup.getHeight())) { + mHandler.postDelayed(mResizePopupRunnable, EXPAND_LIST_TIMEOUT); + } else if (action == MotionEvent.ACTION_UP) { + mHandler.removeCallbacks(mResizePopupRunnable); + } + return false; + } + } + + private class PopupScrollListener implements ListView.OnScrollListener { + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, + int totalItemCount) { + + } + + public void onScrollStateChanged(AbsListView view, int scrollState) { + if (scrollState == SCROLL_STATE_TOUCH_SCROLL && + !isInputMethodNotNeeded() && mPopup.getContentView() != null) { + mHandler.removeCallbacks(mResizePopupRunnable); + mResizePopupRunnable.run(); + } + } + } + + private static boolean isConfirmKey(int keyCode) { + return keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_DPAD_CENTER; + } + + private void setPopupClipToScreenEnabled(boolean clip) { + if (sClipToWindowEnabledMethod != null) { + try { + sClipToWindowEnabledMethod.invoke(mPopup, clip); + } catch (Exception e) { + Log.i(TAG, "Could not call setClipToScreenEnabled() on PopupWindow. Oh well."); + } + } else if(clip && Build.VERSION.SDK_INT >= Build.VERSION_CODES.CUPCAKE) { + mPopup.setClippingEnabled(false); + } + } + +} \ No newline at end of file diff --git a/material/src/main/java/com/rey/material/widget/ListView.java b/material/src/main/java/com/rey/material/widget/ListView.java new file mode 100644 index 0000000..6d0e244 --- /dev/null +++ b/material/src/main/java/com/rey/material/widget/ListView.java @@ -0,0 +1,99 @@ +package com.rey.material.widget; + +import android.content.Context; +import android.support.v7.widget.ListViewCompat; +import android.util.AttributeSet; +import android.view.View; + +import com.rey.material.util.ThemeManager; +import com.rey.material.util.RippleManager; +import com.rey.material.util.ViewUtil; + +public class ListView extends ListViewCompat implements ThemeManager.OnThemeChangedListener{ + + private RecyclerListener mRecyclerListener; + + protected int mStyleId; + protected int mCurrentStyle = ThemeManager.THEME_UNDEFINED; + + public ListView(Context context) { + super(context); + + init(context, null, 0, 0); + } + + public ListView(Context context, AttributeSet attrs) { + super(context, attrs); + + init(context, attrs, 0, 0); + } + + public ListView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + init(context, attrs, defStyleAttr, 0); + } + + public ListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr); + + init(context, attrs, defStyleAttr, defStyleRes); + } + + protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + + super.setRecyclerListener(new RecyclerListener() { + + @Override + public void onMovedToScrapHeap(View view) { + RippleManager.cancelRipple(view); + + if(mRecyclerListener != null) + mRecyclerListener.onMovedToScrapHeap(view); + } + + }); + + if(!isInEditMode()) + mStyleId = ThemeManager.getStyleId(context, attrs, defStyleAttr, defStyleRes); + } + + public void applyStyle(int resId){ + ViewUtil.applyStyle(this, resId); + applyStyle(getContext(), null, 0, resId); + } + + protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + } + + @Override + public void onThemeChanged(ThemeManager.OnThemeChangedEvent event) { + int style = ThemeManager.getInstance().getCurrentStyle(mStyleId); + if(mCurrentStyle != style){ + mCurrentStyle = style; + applyStyle(mCurrentStyle); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if(mStyleId != 0) { + ThemeManager.getInstance().registerOnThemeChangedListener(this); + onThemeChanged(null); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if(mStyleId != 0) + ThemeManager.getInstance().unregisterOnThemeChangedListener(this); + } + + @Override + public void setRecyclerListener(RecyclerListener listener) { + mRecyclerListener = listener; + } + +} diff --git a/material/src/main/java/com/rey/material/widget/MaterialRippleLayout.java b/material/src/main/java/com/rey/material/widget/MaterialRippleLayout.java new file mode 100644 index 0000000..3efefc0 --- /dev/null +++ b/material/src/main/java/com/rey/material/widget/MaterialRippleLayout.java @@ -0,0 +1,816 @@ +/* + * Copyright (C) 2014 Balys Valentukevicius + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rey.material.widget; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.util.AttributeSet; +import android.util.Property; +import android.util.TypedValue; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.LinearInterpolator; +import android.widget.AdapterView; +import android.widget.FrameLayout; + +import com.rey.material.R; + +import static android.view.GestureDetector.SimpleOnGestureListener; +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; + +public class MaterialRippleLayout extends FrameLayout { + + private static final int DEFAULT_DURATION = 350; + private static final int DEFAULT_FADE_DURATION = 75; + private static final float DEFAULT_DIAMETER_DP = 35; + private static final float DEFAULT_ALPHA = 0.2f; + private static final int DEFAULT_COLOR = Color.parseColor("#afadad"); + private static final int DEFAULT_BACKGROUND = Color.TRANSPARENT; + private static final boolean DEFAULT_HOVER = true; + private static final boolean DEFAULT_DELAY_CLICK = true; + private static final boolean DEFAULT_PERSISTENT = false; + private static final boolean DEFAULT_SEARCH_ADAPTER = false; + private static final boolean DEFAULT_RIPPLE_OVERLAY = false; + private static final int DEFAULT_ROUNDED_CORNERS = 0; + + private static final int FADE_EXTRA_DELAY = 50; + private static final long HOVER_DURATION = 2500; + + private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Rect bounds = new Rect(); + + private int rippleColor; + private boolean rippleOverlay; + private boolean rippleHover; + private int rippleDiameter; + private int rippleDuration; + private int rippleAlpha; + private boolean rippleDelayClick; + private int rippleFadeDuration; + private boolean ripplePersistent; + private Drawable rippleBackground; + private boolean rippleInAdapter; + private float rippleRoundedCorners; + + private float radius; + + private AdapterView parentAdapter; + private View childView; + + private AnimatorSet rippleAnimator; + private ObjectAnimator hoverAnimator; + + private Point currentCoords = new Point(); + private Point previousCoords = new Point(); + + private int layerType; + + private boolean eventCancelled; + private boolean prepressed; + private int positionInAdapter; + + private GestureDetector gestureDetector; + private PerformClickEvent pendingClickEvent; + private PressedEvent pendingPressEvent; + + public static RippleBuilder on(View view) { + return new RippleBuilder(view); + } + + public MaterialRippleLayout(Context context) { + this(context, null, 0); + } + + public MaterialRippleLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public MaterialRippleLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + setWillNotDraw(false); + gestureDetector = new GestureDetector(context, longClickListener); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MaterialRippleLayout); + rippleColor = a.getColor(R.styleable.MaterialRippleLayout_mrl_rippleColor, DEFAULT_COLOR); + rippleDiameter = a.getDimensionPixelSize( + R.styleable.MaterialRippleLayout_mrl_rippleDimension, + (int) dpToPx(getResources(), DEFAULT_DIAMETER_DP) + ); + rippleOverlay = a.getBoolean(R.styleable.MaterialRippleLayout_mrl_rippleOverlay, DEFAULT_RIPPLE_OVERLAY); + rippleHover = a.getBoolean(R.styleable.MaterialRippleLayout_mrl_rippleHover, DEFAULT_HOVER); + rippleDuration = a.getInt(R.styleable.MaterialRippleLayout_mrl_rippleDuration, DEFAULT_DURATION); + rippleAlpha = (int) (255 * a.getFloat(R.styleable.MaterialRippleLayout_mrl_rippleAlpha, DEFAULT_ALPHA)); + rippleDelayClick = a.getBoolean(R.styleable.MaterialRippleLayout_mrl_rippleDelayClick, DEFAULT_DELAY_CLICK); + rippleFadeDuration = a.getInteger(R.styleable.MaterialRippleLayout_mrl_rippleFadeDuration, DEFAULT_FADE_DURATION); + rippleBackground = new ColorDrawable(a.getColor(R.styleable.MaterialRippleLayout_mrl_rippleBackground, DEFAULT_BACKGROUND)); + ripplePersistent = a.getBoolean(R.styleable.MaterialRippleLayout_mrl_ripplePersistent, DEFAULT_PERSISTENT); + rippleInAdapter = a.getBoolean(R.styleable.MaterialRippleLayout_mrl_rippleInAdapter, DEFAULT_SEARCH_ADAPTER); + rippleRoundedCorners = a.getDimensionPixelSize(R.styleable.MaterialRippleLayout_mrl_rippleRoundedCorners, DEFAULT_ROUNDED_CORNERS); + + a.recycle(); + + paint.setColor(rippleColor); + paint.setAlpha(rippleAlpha); + + enableClipPathSupportIfNecessary(); + } + + + @SuppressWarnings("unchecked") + public T getChildView() { + return (T) childView; + } + + @Override + public final void addView(View child, int index, ViewGroup.LayoutParams params) { + if (getChildCount() > 0) { + throw new IllegalStateException("MaterialRippleLayout can host only one child"); + } + //noinspection unchecked + childView = child; + super.addView(child, index, params); + } + + @Override + public void setOnClickListener(OnClickListener onClickListener) { + if (childView == null) { + throw new IllegalStateException("MaterialRippleLayout must have a child view to handle clicks"); + } + childView.setOnClickListener(onClickListener); + } + + @Override + public void setOnLongClickListener(OnLongClickListener onClickListener) { + if (childView == null) { + throw new IllegalStateException("MaterialRippleLayout must have a child view to handle clicks"); + } + childView.setOnLongClickListener(onClickListener); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + return !findClickableViewInChild(childView, (int) event.getX(), (int) event.getY()); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + boolean superOnTouchEvent = super.onTouchEvent(event); + + if (!isEnabled() || !childView.isEnabled()) return superOnTouchEvent; + + boolean isEventInBounds = bounds.contains((int) event.getX(), (int) event.getY()); + + if (isEventInBounds) { + previousCoords.set(currentCoords.x, currentCoords.y); + currentCoords.set((int) event.getX(), (int) event.getY()); + } + + boolean gestureResult = gestureDetector.onTouchEvent(event); + if (gestureResult || hasPerformedLongPress) { + return true; + } else { + int action = event.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_UP: + pendingClickEvent = new PerformClickEvent(); + + if (prepressed) { + childView.setPressed(true); + postDelayed( + new Runnable() { + @Override + public void run() { + childView.setPressed(false); + } + }, ViewConfiguration.getPressedStateDuration()); + } + + if (isEventInBounds) { + startRipple(pendingClickEvent); + } else if (!rippleHover) { + setRadius(0); + } + if (!rippleDelayClick && isEventInBounds) { + pendingClickEvent.run(); + } + cancelPressedEvent(); + break; + case MotionEvent.ACTION_DOWN: + setPositionInAdapter(); + eventCancelled = false; + pendingPressEvent = new PressedEvent(event); + if (isInScrollingContainer()) { + cancelPressedEvent(); + prepressed = true; + postDelayed(pendingPressEvent, ViewConfiguration.getTapTimeout()); + } else { + pendingPressEvent.run(); + } + break; + case MotionEvent.ACTION_CANCEL: + if (rippleInAdapter) { + // dont use current coords in adapter since they tend to jump drastically on scroll + currentCoords.set(previousCoords.x, previousCoords.y); + previousCoords = new Point(); + } + childView.onTouchEvent(event); + if (rippleHover) { + if (!prepressed) { + startRipple(null); + } + } else { + childView.setPressed(false); + } + cancelPressedEvent(); + break; + case MotionEvent.ACTION_MOVE: + if (rippleHover) { + if (isEventInBounds && !eventCancelled) { + invalidate(); + } else if (!isEventInBounds) { + startRipple(null); + } + } + + if (!isEventInBounds) { + cancelPressedEvent(); + if (hoverAnimator != null) { + hoverAnimator.cancel(); + } + childView.onTouchEvent(event); + eventCancelled = true; + } + break; + } + return true; + } + } + + private void cancelPressedEvent() { + if (pendingPressEvent != null) { + removeCallbacks(pendingPressEvent); + prepressed = false; + } + } + + private boolean hasPerformedLongPress; + private SimpleOnGestureListener longClickListener = new SimpleOnGestureListener() { + public void onLongPress(MotionEvent e) { + hasPerformedLongPress = childView.performLongClick(); + if (hasPerformedLongPress) { + if (rippleHover) { + startRipple(null); + } + cancelPressedEvent(); + } + } + + @Override + public boolean onDown(MotionEvent e) { + hasPerformedLongPress = false; + return super.onDown(e); + } + }; + + private void startHover() { + if (eventCancelled) return; + + if (hoverAnimator != null) { + hoverAnimator.cancel(); + } + final float radius = (float) (Math.sqrt(Math.pow(getWidth(), 2) + Math.pow(getHeight(), 2)) * 1.2f); + hoverAnimator = ObjectAnimator.ofFloat(this, radiusProperty, rippleDiameter, radius) + .setDuration(HOVER_DURATION); + hoverAnimator.setInterpolator(new LinearInterpolator()); + hoverAnimator.start(); + } + + private void startRipple(final Runnable animationEndRunnable) { + if (eventCancelled) return; + + float endRadius = getEndRadius(); + + cancelAnimations(); + + rippleAnimator = new AnimatorSet(); + rippleAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (!ripplePersistent) { + setRadius(0); + setRippleAlpha(rippleAlpha); + } + if (animationEndRunnable != null && rippleDelayClick) { + animationEndRunnable.run(); + } + childView.setPressed(false); + } + }); + + ObjectAnimator ripple = ObjectAnimator.ofFloat(this, radiusProperty, radius, endRadius); + ripple.setDuration(rippleDuration); + ripple.setInterpolator(new DecelerateInterpolator()); + ObjectAnimator fade = ObjectAnimator.ofInt(this, circleAlphaProperty, rippleAlpha, 0); + fade.setDuration(rippleFadeDuration); + fade.setInterpolator(new AccelerateInterpolator()); + fade.setStartDelay(rippleDuration - rippleFadeDuration - FADE_EXTRA_DELAY); + + if (ripplePersistent) { + rippleAnimator.play(ripple); + } else if (getRadius() > endRadius) { + fade.setStartDelay(0); + rippleAnimator.play(fade); + } else { + rippleAnimator.playTogether(ripple, fade); + } + rippleAnimator.start(); + } + + private void cancelAnimations() { + if (rippleAnimator != null) { + rippleAnimator.cancel(); + rippleAnimator.removeAllListeners(); + } + + if (hoverAnimator != null) { + hoverAnimator.cancel(); + } + } + + private float getEndRadius() { + final int width = getWidth(); + final int height = getHeight(); + + final int halfWidth = width / 2; + final int halfHeight = height / 2; + + final float radiusX = halfWidth > currentCoords.x ? width - currentCoords.x : currentCoords.x; + final float radiusY = halfHeight > currentCoords.y ? height - currentCoords.y : currentCoords.y; + + return (float) Math.sqrt(Math.pow(radiusX, 2) + Math.pow(radiusY, 2)) * 1.2f; + } + + private boolean isInScrollingContainer() { + ViewParent p = getParent(); + while (p != null && p instanceof ViewGroup) { + if (((ViewGroup) p).shouldDelayChildPressedState()) { + return true; + } + p = p.getParent(); + } + return false; + } + + private AdapterView findParentAdapterView() { + if (parentAdapter != null) { + return parentAdapter; + } + ViewParent current = getParent(); + while (true) { + if (current instanceof AdapterView) { + parentAdapter = (AdapterView) current; + return parentAdapter; + } else { + try { + current = current.getParent(); + } catch (NullPointerException npe) { + throw new RuntimeException("Could not find a parent AdapterView"); + } + } + } + } + + private void setPositionInAdapter() { + if (rippleInAdapter) { + positionInAdapter = findParentAdapterView().getPositionForView(MaterialRippleLayout.this); + } + } + + private boolean adapterPositionChanged() { + if (rippleInAdapter) { + int newPosition = findParentAdapterView().getPositionForView(MaterialRippleLayout.this); + final boolean changed = newPosition != positionInAdapter; + positionInAdapter = newPosition; + if (changed) { + cancelPressedEvent(); + cancelAnimations(); + childView.setPressed(false); + setRadius(0); + } + return changed; + } + return false; + } + + private boolean findClickableViewInChild(View view, int x, int y) { + if (view instanceof ViewGroup) { + ViewGroup viewGroup = (ViewGroup) view; + for (int i = 0; i < viewGroup.getChildCount(); i++) { + View child = viewGroup.getChildAt(i); + final Rect rect = new Rect(); + child.getHitRect(rect); + + final boolean contains = rect.contains(x, y); + if (contains) { + return findClickableViewInChild(child, x - rect.left, y - rect.top); + } + } + } else if (view != childView) { + return (view.isEnabled() && (view.isClickable() || view.isLongClickable() || view.isFocusableInTouchMode())); + } + + return view.isFocusableInTouchMode(); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + bounds.set(0, 0, w, h); + rippleBackground.setBounds(bounds); + } + + @Override + public boolean isInEditMode() { + return true; + } + + /* + * Drawing + */ + @Override + public void draw(Canvas canvas) { + final boolean positionChanged = adapterPositionChanged(); + if (rippleOverlay) { + if (!positionChanged) { + rippleBackground.draw(canvas); + } + super.draw(canvas); + if (!positionChanged) { + if (rippleRoundedCorners != 0) { + Path clipPath = new Path(); + RectF rect = new RectF(0, 0, canvas.getWidth(), canvas.getHeight()); + clipPath.addRoundRect(rect, rippleRoundedCorners, rippleRoundedCorners, Path.Direction.CW); + canvas.clipPath(clipPath); + } + canvas.drawCircle(currentCoords.x, currentCoords.y, radius, paint); + } + } else { + if (!positionChanged) { + rippleBackground.draw(canvas); + canvas.drawCircle(currentCoords.x, currentCoords.y, radius, paint); + } + super.draw(canvas); + } + } + + /* + * Animations + */ + private Property radiusProperty + = new Property(Float.class, "radius") { + @Override + public Float get(MaterialRippleLayout object) { + return object.getRadius(); + } + + @Override + public void set(MaterialRippleLayout object, Float value) { + object.setRadius(value); + } + }; + + private float getRadius() { + return radius; + } + + + public void setRadius(float radius) { + this.radius = radius; + invalidate(); + } + + private Property circleAlphaProperty + = new Property(Integer.class, "rippleAlpha") { + @Override + public Integer get(MaterialRippleLayout object) { + return object.getRippleAlpha(); + } + + @Override + public void set(MaterialRippleLayout object, Integer value) { + object.setRippleAlpha(value); + } + }; + + public int getRippleAlpha() { + return paint.getAlpha(); + } + + public void setRippleAlpha(Integer rippleAlpha) { + paint.setAlpha(rippleAlpha); + invalidate(); + } + + /* + * Accessor + */ + public void setRippleColor(int rippleColor) { + this.rippleColor = rippleColor; + paint.setColor(rippleColor); + paint.setAlpha(rippleAlpha); + invalidate(); + } + + public void setRippleOverlay(boolean rippleOverlay) { + this.rippleOverlay = rippleOverlay; + } + + public void setRippleDiameter(int rippleDiameter) { + this.rippleDiameter = rippleDiameter; + } + + public void setRippleDuration(int rippleDuration) { + this.rippleDuration = rippleDuration; + } + + public void setRippleBackground(int color) { + rippleBackground = new ColorDrawable(color); + rippleBackground.setBounds(bounds); + invalidate(); + } + + public void setRippleHover(boolean rippleHover) { + this.rippleHover = rippleHover; + } + + public void setRippleDelayClick(boolean rippleDelayClick) { + this.rippleDelayClick = rippleDelayClick; + } + + public void setRippleFadeDuration(int rippleFadeDuration) { + this.rippleFadeDuration = rippleFadeDuration; + } + + public void setRipplePersistent(boolean ripplePersistent) { + this.ripplePersistent = ripplePersistent; + } + + public void setRippleInAdapter(boolean rippleInAdapter) { + this.rippleInAdapter = rippleInAdapter; + } + + public void setRippleRoundedCorners(int rippleRoundedCorner) { + this.rippleRoundedCorners = rippleRoundedCorner; + enableClipPathSupportIfNecessary(); + } + + public void setDefaultRippleAlpha(int alpha) { + this.rippleAlpha = alpha; + paint.setAlpha(alpha); + invalidate(); + } + + public void performRipple() { + currentCoords = new Point(getWidth() / 2, getHeight() / 2); + startRipple(null); + } + + public void performRipple(Point anchor) { + currentCoords = new Point(anchor.x, anchor.y); + startRipple(null); + } + + /** + * {@link Canvas#clipPath(Path)} is not supported in hardware accelerated layers + * before API 18. Use software layer instead + *

+ * https://developer.android.com/guide/topics/graphics/hardware-accel.html#unsupported + */ + private void enableClipPathSupportIfNecessary() { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR1) { + if (rippleRoundedCorners != 0) { + layerType = getLayerType(); + setLayerType(LAYER_TYPE_SOFTWARE, null); + } else { + setLayerType(layerType, null); + } + } + } + + /* + * Helper + */ + private class PerformClickEvent implements Runnable { + + @Override + public void run() { + if (hasPerformedLongPress) return; + + // if parent is an AdapterView, try to call its ItemClickListener + if (getParent() instanceof AdapterView) { + clickAdapterView((AdapterView) getParent()); + } else if (rippleInAdapter) { + // find adapter view + clickAdapterView(findParentAdapterView()); + } else { + // otherwise, just perform click on child + childView.performClick(); + } + } + + private void clickAdapterView(AdapterView parent) { + final int position = parent.getPositionForView(MaterialRippleLayout.this); + final long itemId = parent.getAdapter() != null + ? parent.getAdapter().getItemId(position) + : 0; + if (position != AdapterView.INVALID_POSITION) { + parent.performItemClick(MaterialRippleLayout.this, position, itemId); + } + } + } + + private final class PressedEvent implements Runnable { + + private final MotionEvent event; + + public PressedEvent(MotionEvent event) { + this.event = event; + } + + @Override + public void run() { + prepressed = false; + childView.setLongClickable(false);//prevent the child's long click,let's the ripple layout call it's performLongClick + childView.onTouchEvent(event); + childView.setPressed(true); + if (rippleHover) { + startHover(); + } + } + } + + static float dpToPx(Resources resources, float dp) { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.getDisplayMetrics()); + } + + /* + * Builder + */ + + public static class RippleBuilder { + + private final Context context; + private final View child; + + private int rippleColor = DEFAULT_COLOR; + private boolean rippleOverlay = DEFAULT_RIPPLE_OVERLAY; + private boolean rippleHover = DEFAULT_HOVER; + private float rippleDiameter = DEFAULT_DIAMETER_DP; + private int rippleDuration = DEFAULT_DURATION; + private float rippleAlpha = DEFAULT_ALPHA; + private boolean rippleDelayClick = DEFAULT_DELAY_CLICK; + private int rippleFadeDuration = DEFAULT_FADE_DURATION; + private boolean ripplePersistent = DEFAULT_PERSISTENT; + private int rippleBackground = DEFAULT_BACKGROUND; + private boolean rippleSearchAdapter = DEFAULT_SEARCH_ADAPTER; + private float rippleRoundedCorner = DEFAULT_ROUNDED_CORNERS; + + public RippleBuilder(View child) { + this.child = child; + this.context = child.getContext(); + } + + public RippleBuilder rippleColor(int color) { + this.rippleColor = color; + return this; + } + + public RippleBuilder rippleOverlay(boolean overlay) { + this.rippleOverlay = overlay; + return this; + } + + public RippleBuilder rippleHover(boolean hover) { + this.rippleHover = hover; + return this; + } + + public RippleBuilder rippleDiameterDp(int diameterDp) { + this.rippleDiameter = diameterDp; + return this; + } + + public RippleBuilder rippleDuration(int duration) { + this.rippleDuration = duration; + return this; + } + + public RippleBuilder rippleAlpha(float alpha) { + this.rippleAlpha = 255 * alpha; + return this; + } + + public RippleBuilder rippleDelayClick(boolean delayClick) { + this.rippleDelayClick = delayClick; + return this; + } + + public RippleBuilder rippleFadeDuration(int fadeDuration) { + this.rippleFadeDuration = fadeDuration; + return this; + } + + public RippleBuilder ripplePersistent(boolean persistent) { + this.ripplePersistent = persistent; + return this; + } + + public RippleBuilder rippleBackground(int color) { + this.rippleBackground = color; + return this; + } + + public RippleBuilder rippleInAdapter(boolean inAdapter) { + this.rippleSearchAdapter = inAdapter; + return this; + } + + public RippleBuilder rippleRoundedCorners(int radiusDp) { + this.rippleRoundedCorner = radiusDp; + return this; + } + + public MaterialRippleLayout create() { + MaterialRippleLayout layout = new MaterialRippleLayout(context); + layout.setRippleColor(rippleColor); + layout.setDefaultRippleAlpha((int) rippleAlpha); + layout.setRippleDelayClick(rippleDelayClick); + layout.setRippleDiameter((int) dpToPx(context.getResources(), rippleDiameter)); + layout.setRippleDuration(rippleDuration); + layout.setRippleFadeDuration(rippleFadeDuration); + layout.setRippleHover(rippleHover); + layout.setRipplePersistent(ripplePersistent); + layout.setRippleOverlay(rippleOverlay); + layout.setRippleBackground(rippleBackground); + layout.setRippleInAdapter(rippleSearchAdapter); + layout.setRippleRoundedCorners((int) dpToPx(context.getResources(), rippleRoundedCorner)); + + ViewGroup.LayoutParams params = child.getLayoutParams(); + ViewGroup parent = (ViewGroup) child.getParent(); + int index = 0; + + if (parent != null && parent instanceof MaterialRippleLayout) { + throw new IllegalStateException("MaterialRippleLayout could not be created: parent of the view already is a MaterialRippleLayout"); + } + + if (parent != null) { + index = parent.indexOfChild(child); + parent.removeView(child); + } + + layout.addView(child, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); + + if (parent != null) { + parent.addView(layout, index, params); + } + + return layout; + } + } +} diff --git a/material/src/main/java/com/rey/material/widget/PopupWindow.java b/material/src/main/java/com/rey/material/widget/PopupWindow.java new file mode 100644 index 0000000..17bbc24 --- /dev/null +++ b/material/src/main/java/com/rey/material/widget/PopupWindow.java @@ -0,0 +1,52 @@ +package com.rey.material.widget; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Build; +import android.util.AttributeSet; +import android.view.View; + +import com.rey.material.R; + +public class PopupWindow extends android.widget.PopupWindow { + + private final boolean mOverlapAnchor; + + public PopupWindow(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PopupWindow, defStyleAttr, 0); + mOverlapAnchor = a.getBoolean(R.styleable.PopupWindow_overlapAnchor, false); + a.recycle(); + + } + + @Override + public void showAsDropDown(View anchor, int xoff, int yoff) { + if (Build.VERSION.SDK_INT < 21 && mOverlapAnchor) { + // If we're pre-L, emulate overlapAnchor by modifying the yOff + yoff -= anchor.getHeight(); + } + super.showAsDropDown(anchor, xoff, yoff); + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + @Override + public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) { + if (Build.VERSION.SDK_INT < 21 && mOverlapAnchor) { + // If we're pre-L, emulate overlapAnchor by modifying the yOff + yoff -= anchor.getHeight(); + } + super.showAsDropDown(anchor, xoff, yoff, gravity); + } + + @Override + public void update(View anchor, int xoff, int yoff, int width, int height) { + if (Build.VERSION.SDK_INT < 21 && mOverlapAnchor) { + // If we're pre-L, emulate overlapAnchor by modifying the yOff + yoff -= anchor.getHeight(); + } + super.update(anchor, xoff, yoff, width, height); + } +} diff --git a/material/src/main/java/com/rey/material/widget/ProgressView.java b/material/src/main/java/com/rey/material/widget/ProgressView.java new file mode 100644 index 0000000..919db98 --- /dev/null +++ b/material/src/main/java/com/rey/material/widget/ProgressView.java @@ -0,0 +1,255 @@ +package com.rey.material.widget; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.view.View; + +import com.rey.material.R; +import com.rey.material.util.ThemeManager; +import com.rey.material.drawable.CircularProgressDrawable; +import com.rey.material.drawable.LinearProgressDrawable; +import com.rey.material.util.ViewUtil; + +public class ProgressView extends View implements ThemeManager.OnThemeChangedListener{ + + protected int mStyleId; + protected int mCurrentStyle = ThemeManager.THEME_UNDEFINED; + + private boolean mAutostart = false; + private boolean mCircular = true; + private int mProgressId; + + public static final int MODE_DETERMINATE = 0; + public static final int MODE_INDETERMINATE = 1; + public static final int MODE_BUFFER = 2; + public static final int MODE_QUERY = 3; + + + private Drawable mProgressDrawable; + + public ProgressView(Context context) { + super(context); + + init(context, null, 0, 0); + } + + public ProgressView(Context context, AttributeSet attrs) { + super(context, attrs); + + init(context, attrs, 0, 0); + } + + public ProgressView(Context context, AttributeSet attrs, int defStyleAttr){ + super(context, attrs, defStyleAttr); + + init(context, attrs, defStyleAttr, 0); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public ProgressView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + init(context, attrs, defStyleAttr, defStyleRes); + } + + protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + applyStyle(context, attrs, defStyleAttr, defStyleRes); + if(!isInEditMode()) + mStyleId = ThemeManager.getStyleId(context, attrs, defStyleAttr, defStyleRes); + } + + public void applyStyle(int resId){ + ViewUtil.applyStyle(this, resId); + applyStyle(getContext(), null, 0, resId); + } + + private boolean needCreateProgress(boolean circular){ + if(mProgressDrawable == null) + return true; + + if(circular) + return !(mProgressDrawable instanceof CircularProgressDrawable); + else + return !(mProgressDrawable instanceof LinearProgressDrawable); + } + + protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ProgressView, defStyleAttr, defStyleRes); + + int progressId = 0; + int progressMode = -1; + float progress = -1; + float secondaryProgress = -1; + + for(int i = 0, count = a.getIndexCount(); i < count; i++){ + int attr = a.getIndex(i); + + if(attr == R.styleable.ProgressView_pv_autostart) + mAutostart = a.getBoolean(attr, false); + else if(attr == R.styleable.ProgressView_pv_circular) + mCircular = a.getBoolean(attr, true); + else if(attr == R.styleable.ProgressView_pv_progressStyle) + progressId = a.getResourceId(attr, 0); + else if(attr == R.styleable.ProgressView_pv_progressMode) + progressMode = a.getInteger(attr, 0); + else if(attr == R.styleable.ProgressView_pv_progress) + progress = a.getFloat(attr, 0); + else if(attr == R.styleable.ProgressView_pv_secondaryProgress) + secondaryProgress = a.getFloat(attr, 0); + } + + a.recycle(); + + boolean needStart = false; + + if(needCreateProgress(mCircular)){ + mProgressId = progressId; + if(mProgressId == 0) + mProgressId = mCircular ? R.style.Material_Drawable_CircularProgress : R.style.Material_Drawable_LinearProgress; + + needStart = mProgressDrawable != null && ((Animatable)mProgressDrawable).isRunning(); + mProgressDrawable = mCircular ? new CircularProgressDrawable.Builder(context, mProgressId).build() : new LinearProgressDrawable.Builder(context, mProgressId).build(); + ViewUtil.setBackground(this, mProgressDrawable); + } + else if(mProgressId != progressId){ + mProgressId = progressId; + if(mProgressDrawable instanceof CircularProgressDrawable) + ((CircularProgressDrawable) mProgressDrawable).applyStyle(context, mProgressId); + else + ((LinearProgressDrawable)mProgressDrawable).applyStyle(context, mProgressId); + } + + if(progressMode >= 0) { + if(mProgressDrawable instanceof CircularProgressDrawable) + ((CircularProgressDrawable) mProgressDrawable).setProgressMode(progressMode); + else + ((LinearProgressDrawable)mProgressDrawable).setProgressMode(progressMode); + } + + if(progress >= 0) + setProgress(progress); + + if(secondaryProgress >= 0) + setSecondaryProgress(secondaryProgress); + + if(needStart) + start(); + } + + @Override + public void onThemeChanged(ThemeManager.OnThemeChangedEvent event) { + int style = ThemeManager.getInstance().getCurrentStyle(mStyleId); + if(mCurrentStyle != style){ + mCurrentStyle = style; + applyStyle(mCurrentStyle); + } + } + + @Override + protected void onVisibilityChanged(@NonNull View changedView, int visibility) { + super.onVisibilityChanged(changedView, visibility); + + if(changedView != this) + return; + + if (mAutostart) { + if (visibility == GONE || visibility == INVISIBLE) + stop(); + else + start(); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if(getVisibility() == View.VISIBLE && mAutostart) + start(); + if(mStyleId != 0) { + ThemeManager.getInstance().registerOnThemeChangedListener(this); + onThemeChanged(null); + } + } + + @Override + protected void onDetachedFromWindow() { + if (mAutostart) + stop(); + + super.onDetachedFromWindow(); + if(mStyleId != 0) + ThemeManager.getInstance().unregisterOnThemeChangedListener(this); + } + + public int getProgressMode(){ + if(mCircular) + return ((CircularProgressDrawable)mProgressDrawable).getProgressMode(); + else + return ((LinearProgressDrawable)mProgressDrawable).getProgressMode(); + } + + /** + * @return The current progress of this view in [0..1] range. + */ + public float getProgress(){ + if(mCircular) + return ((CircularProgressDrawable)mProgressDrawable).getProgress(); + else + return ((LinearProgressDrawable)mProgressDrawable).getProgress(); + } + + /** + * @return The current secondary progress of this view in [0..1] range. + */ + public float getSecondaryProgress(){ + if(mCircular) + return ((CircularProgressDrawable)mProgressDrawable).getSecondaryProgress(); + else + return ((LinearProgressDrawable)mProgressDrawable).getSecondaryProgress(); + } + + /** + * Set the current progress of this view. + * @param percent The progress value in [0..1] range. + */ + public void setProgress(float percent){ + if(mCircular) + ((CircularProgressDrawable)mProgressDrawable).setProgress(percent); + else + ((LinearProgressDrawable)mProgressDrawable).setProgress(percent); + } + + /** + * Set the current secondary progress of this view. + * @param percent The progress value in [0..1] range. + */ + public void setSecondaryProgress(float percent){ + if(mCircular) + ((CircularProgressDrawable)mProgressDrawable).setSecondaryProgress(percent); + else + ((LinearProgressDrawable)mProgressDrawable).setSecondaryProgress(percent); + } + + /** + * Start showing progress. + */ + public void start(){ + if(mProgressDrawable != null) + ((Animatable)mProgressDrawable).start(); + } + + /** + * Stop showing progress. + */ + public void stop(){ + if(mProgressDrawable != null) + ((Animatable)mProgressDrawable).stop(); + } + +} diff --git a/material/src/main/java/com/rey/material/widget/RadioButton.java b/material/src/main/java/com/rey/material/widget/RadioButton.java new file mode 100644 index 0000000..d40c88e --- /dev/null +++ b/material/src/main/java/com/rey/material/widget/RadioButton.java @@ -0,0 +1,61 @@ +package com.rey.material.widget; + +import android.content.Context; +import android.util.AttributeSet; + +import com.rey.material.drawable.RadioButtonDrawable; + +public class RadioButton extends CompoundButton { + + public RadioButton(Context context) { + super(context); + } + + public RadioButton(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public RadioButton(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public RadioButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + super.applyStyle(context, attrs, defStyleAttr, defStyleRes); + + RadioButtonDrawable drawable = new RadioButtonDrawable.Builder(context, attrs, defStyleAttr, defStyleRes).build(); + drawable.setInEditMode(isInEditMode()); + drawable.setAnimEnable(false); + setButtonDrawable(drawable); + drawable.setAnimEnable(true); + } + + @Override + public void toggle() { + // we override to prevent toggle when the radio is already + // checked (as opposed to check boxes widgets) + if (!isChecked()) { + super.toggle(); + } + } + + /** + * Change the checked state of this button immediately without showing animation. + * @param checked The checked state. + */ + public void setCheckedImmediately(boolean checked){ + if(getButtonDrawable() instanceof RadioButtonDrawable){ + RadioButtonDrawable drawable = (RadioButtonDrawable)getButtonDrawable(); + drawable.setAnimEnable(false); + setChecked(checked); + drawable.setAnimEnable(true); + } + else + setChecked(checked); + } + +} diff --git a/material/src/main/java/com/rey/material/widget/Slider.java b/material/src/main/java/com/rey/material/widget/Slider.java new file mode 100644 index 0000000..65739e4 --- /dev/null +++ b/material/src/main/java/com/rey/material/widget/Slider.java @@ -0,0 +1,1251 @@ +package com.rey.material.widget; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.animation.AnimationUtils; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + +import com.rey.material.R; +import com.rey.material.util.ThemeManager; +import com.rey.material.drawable.RippleDrawable; +import com.rey.material.util.ColorUtil; +import com.rey.material.util.RippleManager; +import com.rey.material.util.ThemeUtil; +import com.rey.material.util.TypefaceUtil; +import com.rey.material.util.ViewUtil; + +/** + * Created by Ret on 3/18/2015. + */ +public class Slider extends View implements ThemeManager.OnThemeChangedListener{ + + private RippleManager mRippleManager; + protected int mStyleId; + protected int mCurrentStyle = ThemeManager.THEME_UNDEFINED; + + private Paint mPaint; + private RectF mDrawRect; + private RectF mTempRect; + private Path mLeftTrackPath; + private Path mRightTrackPath; + private Path mMarkPath; + + private int mMinValue = 0; + private int mMaxValue = 100; + private int mStepValue = 1; + + private boolean mDiscreteMode = false; + + private int mPrimaryColor; + private int mSecondaryColor; + private int mTrackSize = -1; + private Paint.Cap mTrackCap = Paint.Cap.BUTT; + private int mThumbBorderSize = -1; + private int mThumbRadius = -1; + private int mThumbFocusRadius = -1; + private float mThumbPosition = -1; + private Typeface mTypeface = Typeface.DEFAULT; + private int mTextSize = -1; + private int mTextColor = 0xFFFFFFFF; + private int mGravity = Gravity.CENTER; + private int mTravelAnimationDuration = -1; + private int mTransformAnimationDuration = -1; + private Interpolator mInterpolator; + private int mBaselineOffset; + + private int mTouchSlop; + private PointF mMemoPoint; + private boolean mIsDragging; + private float mThumbCurrentRadius; + private float mThumbFillPercent; + private boolean mAlwaysFillThumb = false; + private int mTextHeight; + private int mMemoValue; + private String mValueText; + + private ThumbRadiusAnimator mThumbRadiusAnimator; + private ThumbStrokeAnimator mThumbStrokeAnimator; + private ThumbMoveAnimator mThumbMoveAnimator; + + private boolean mIsRtl = false; + + /** + * Interface definition for a callback to be invoked when thumb's position changed. + */ + public interface OnPositionChangeListener{ + /** + * Called when thumb's position changed. + * + * @param view The view fire this event. + * @param fromUser Indicate the change is from user touch event or not. + * @param oldPos The old position of thumb. + * @param newPos The new position of thumb. + * @param oldValue The old value. + * @param newValue The new value. + */ + public void onPositionChanged(Slider view, boolean fromUser, float oldPos, float newPos, int oldValue, int newValue); + } + + private OnPositionChangeListener mOnPositionChangeListener; + + public interface ValueDescriptionProvider{ + + public String getDescription(int value); + + } + + private ValueDescriptionProvider mValueDescriptionProvider; + + public Slider(Context context) { + super(context); + + init(context, null, 0, 0); + } + + public Slider(Context context, AttributeSet attrs) { + super(context, attrs); + + init(context, attrs, 0, 0); + } + + public Slider(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + init(context, attrs, defStyleAttr, 0); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public Slider(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + init(context, attrs, defStyleAttr, defStyleRes); + } + + protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + + //default color + mPrimaryColor = ThemeUtil.colorControlActivated(context, 0xFF000000); + mSecondaryColor = ThemeUtil.colorControlNormal(context, 0xFF000000); + + mDrawRect = new RectF(); + mTempRect = new RectF(); + mLeftTrackPath = new Path(); + mRightTrackPath = new Path(); + + mThumbRadiusAnimator = new ThumbRadiusAnimator(); + mThumbStrokeAnimator = new ThumbStrokeAnimator(); + mThumbMoveAnimator = new ThumbMoveAnimator(); + + mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + mMemoPoint = new PointF(); + + applyStyle(context, attrs, defStyleAttr, defStyleRes); + + if(!isInEditMode()) + mStyleId = ThemeManager.getStyleId(context, attrs, defStyleAttr, defStyleRes); + } + + public void applyStyle(int resId){ + ViewUtil.applyStyle(this, resId); + applyStyle(getContext(), null, 0, resId); + } + + protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + getRippleManager().onCreate(this, context, attrs, defStyleAttr, defStyleRes); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Slider, defStyleAttr, defStyleRes); + int minValue = getMinValue(); + int maxValue = getMaxValue(); + boolean valueRangeDefined = false; + int value = -1; + boolean valueDefined = false; + String familyName = null; + int style = Typeface.NORMAL; + boolean textStyleDefined = false; + for(int i = 0, count = a.getIndexCount(); i < count; i++){ + int attr = a.getIndex(i); + if(attr == R.styleable.Slider_sl_discreteMode) + mDiscreteMode = a.getBoolean(attr, false); + else if(attr == R.styleable.Slider_sl_primaryColor) + mPrimaryColor = a.getColor(attr, 0); + else if(attr == R.styleable.Slider_sl_secondaryColor) + mSecondaryColor = a.getColor(attr, 0); + else if(attr == R.styleable.Slider_sl_trackSize) + mTrackSize = a.getDimensionPixelSize(attr, 0); + else if(attr == R.styleable.Slider_sl_trackCap) { + int cap = a.getInteger(attr, 0); + if(cap == 0) + mTrackCap = Paint.Cap.BUTT; + else if(cap == 1) + mTrackCap = Paint.Cap.ROUND; + else + mTrackCap = Paint.Cap.SQUARE; + } + else if(attr == R.styleable.Slider_sl_thumbBorderSize) + mThumbBorderSize = a.getDimensionPixelSize(attr, 0); + else if(attr == R.styleable.Slider_sl_thumbRadius) + mThumbRadius = a.getDimensionPixelSize(attr, 0); + else if(attr == R.styleable.Slider_sl_thumbFocusRadius) + mThumbFocusRadius = a.getDimensionPixelSize(attr, 0); + else if(attr == R.styleable.Slider_sl_travelAnimDuration) { + mTravelAnimationDuration = a.getInteger(attr, 0); + mTransformAnimationDuration = mTravelAnimationDuration; + } + else if(attr == R.styleable.Slider_sl_alwaysFillThumb) { + mAlwaysFillThumb = a.getBoolean(R.styleable.Slider_sl_alwaysFillThumb, false); + } + else if(attr == R.styleable.Slider_sl_interpolator){ + int resId = a.getResourceId(R.styleable.Slider_sl_interpolator, 0); + mInterpolator = AnimationUtils.loadInterpolator(context, resId); + } + else if(attr == R.styleable.Slider_android_gravity) + mGravity = a.getInteger(attr, 0); + else if(attr == R.styleable.Slider_sl_minValue) { + minValue = a.getInteger(attr, 0); + valueRangeDefined = true; + } + else if(attr == R.styleable.Slider_sl_maxValue) { + maxValue = a.getInteger(attr, 0); + valueRangeDefined = true; + } + else if(attr == R.styleable.Slider_sl_stepValue) + mStepValue = a.getInteger(attr, 0); + else if(attr == R.styleable.Slider_sl_value) { + value = a.getInteger(attr, 0); + valueDefined = true; + } + else if(attr == R.styleable.Slider_sl_fontFamily) { + familyName = a.getString(attr); + textStyleDefined = true; + } + else if(attr == R.styleable.Slider_sl_textStyle) { + style = a.getInteger(attr, 0); + textStyleDefined = true; + } + else if(attr == R.styleable.Slider_sl_textColor) + mTextColor = a.getColor(attr, 0); + else if(attr == R.styleable.Slider_sl_textSize) + mTextSize = a.getDimensionPixelSize(attr, 0); + else if(attr == R.styleable.Slider_android_enabled) + setEnabled(a.getBoolean(attr, true)); + else if(attr == R.styleable.Slider_sl_baselineOffset) + mBaselineOffset = a.getDimensionPixelOffset(attr, 0); + } + + a.recycle(); + + if(mTrackSize < 0) + mTrackSize = ThemeUtil.dpToPx(context, 2); + + if(mThumbBorderSize < 0) + mThumbBorderSize = ThemeUtil.dpToPx(context, 2); + + if(mThumbRadius < 0) + mThumbRadius = ThemeUtil.dpToPx(context, 10); + + if(mThumbFocusRadius < 0) + mThumbFocusRadius = ThemeUtil.dpToPx(context, 14); + + if(mTravelAnimationDuration < 0){ + mTravelAnimationDuration = context.getResources().getInteger(android.R.integer.config_mediumAnimTime); + mTransformAnimationDuration = mTravelAnimationDuration; + } + + if(mInterpolator == null) + mInterpolator = new DecelerateInterpolator(); + + if(valueRangeDefined) + setValueRange(minValue, maxValue, false); + + if(valueDefined) + setValue(value, false); + else if(mThumbPosition < 0) + setValue(mMinValue, false); + + if(textStyleDefined) + mTypeface = TypefaceUtil.load(context, familyName, style); + + if(mTextSize < 0) + mTextSize = context.getResources().getDimensionPixelOffset(R.dimen.abc_text_size_small_material); + + mPaint.setTextSize(mTextSize); + mPaint.setTextAlign(Paint.Align.CENTER); + mPaint.setTypeface(mTypeface); + + measureText(); + invalidate(); + } + + @Override + public void onThemeChanged(ThemeManager.OnThemeChangedEvent event) { + int style = ThemeManager.getInstance().getCurrentStyle(mStyleId); + if(mCurrentStyle != style){ + mCurrentStyle = style; + applyStyle(mCurrentStyle); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if(mStyleId != 0) { + ThemeManager.getInstance().registerOnThemeChangedListener(this); + onThemeChanged(null); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + RippleManager.cancelRipple(this); + if(mStyleId != 0) + ThemeManager.getInstance().unregisterOnThemeChangedListener(this); + } + + private void measureText(){ + if(mValueText == null) + return; + + Rect temp = new Rect(); + mPaint.setTextSize(mTextSize); + float width = mPaint.measureText(mValueText); + float maxWidth = (float)(mThumbRadius * Math.sqrt(2) * 2 - ThemeUtil.dpToPx(getContext(), 8)); + if(width > maxWidth){ + float textSize = mTextSize * maxWidth / width; + mPaint.setTextSize(textSize); + } + + mPaint.getTextBounds(mValueText, 0, mValueText.length(), temp); + mTextHeight = temp.height(); + } + + private String getValueText(){ + int value = getValue(); + if(mValueText == null || mMemoValue != value){ + mMemoValue = value; + mValueText = mValueDescriptionProvider == null ? String.valueOf(mMemoValue) : mValueDescriptionProvider.getDescription(mMemoValue); + measureText(); + } + + return mValueText; + } + + /** + * @return The minimum selectable value. + */ + public int getMinValue(){ + return mMinValue; + } + + /** + * @return The maximum selectable value. + */ + public int getMaxValue(){ + return mMaxValue; + } + + /** + * @return The step value. + */ + public int getStepValue(){ + return mStepValue; + } + + /** + * Set the randge of selectable value. + * @param min The minimum selectable value. + * @param max The maximum selectable value. + * @param animation Indicate that should show animation when thumb's current position changed. + */ + public void setValueRange(int min, int max, boolean animation){ + if(max < min || (min == mMinValue && max == mMaxValue)) + return; + + float oldValue = getExactValue(); + float oldPosition = getPosition(); + mMinValue = min; + mMaxValue = max; + + setValue(oldValue, animation); + if(mOnPositionChangeListener != null && oldPosition == getPosition() && oldValue != getExactValue()) + mOnPositionChangeListener.onPositionChanged(this, false, oldPosition, oldPosition, Math.round(oldValue), getValue()); + } + + /** + * @return The selected value. + */ + public int getValue(){ + return Math.round(getExactValue()); + } + + /** + * @return The exact selected value. + */ + public float getExactValue(){ + return (mMaxValue - mMinValue) * getPosition() + mMinValue; + } + + /** + * @return The current position of thumb in [0..1] range. + */ + public float getPosition(){ + return mThumbMoveAnimator.isRunning() ? mThumbMoveAnimator.getPosition() : mThumbPosition; + } + + /** + * Set current position of thumb. + * @param pos The position in [0..1] range. + * @param animation Indicate that should show animation when change thumb's position. + */ + public void setPosition(float pos, boolean animation){ + setPosition(pos, animation, animation, false); + } + + private void setPosition(float pos, boolean moveAnimation, boolean transformAnimation, boolean fromUser){ + boolean change = getPosition() != pos; + int oldValue = getValue(); + float oldPos = getPosition(); + + if(!moveAnimation || !mThumbMoveAnimator.startAnimation(pos)){ + mThumbPosition = pos; + + if(transformAnimation) { + if(!mIsDragging) + mThumbRadiusAnimator.startAnimation(mThumbRadius); + mThumbStrokeAnimator.startAnimation(pos == 0 ? 0 : 1); + } + else{ + mThumbCurrentRadius = mThumbRadius; + mThumbFillPercent = (mAlwaysFillThumb || mThumbPosition != 0) ? 1 : 0; + invalidate(); + } + } + + int newValue = getValue(); + float newPos = getPosition(); + + if(change && mOnPositionChangeListener != null) + mOnPositionChangeListener.onPositionChanged(this, fromUser, oldPos, newPos, oldValue, newValue); + } + + /** + * Changes the primary color and invalidates the view to force a redraw. + * @param color New color to assign to mPrimaryColor. + */ + public void setPrimaryColor(int color) { + mPrimaryColor = color; + invalidate(); + } + + /** + * Changes the secondary color and invalidates the view to force a redraw. + * @param color New color to assign to mSecondaryColor. + */ + public void setSecondaryColor(int color) { + mSecondaryColor = color; + invalidate(); + } + + /** + * Set if we want the thumb to always be filled. + * @param alwaysFillThumb Do we want it to always be filled. + */ + public void setAlwaysFillThumb(boolean alwaysFillThumb) { + mAlwaysFillThumb = alwaysFillThumb; + } + + /** + * Set the selected value of this Slider. + * @param value The selected value. + * @param animation Indicate that should show animation when change thumb's position. + */ + public void setValue(float value, boolean animation){ + value = Math.min(mMaxValue, Math.max(value, mMinValue)); + setPosition((value - mMinValue) / (mMaxValue - mMinValue), animation); + } + + /** + * Set a listener will be called when thumb's position changed. + * @param listener The {@link OnPositionChangeListener} will be called. + */ + public void setOnPositionChangeListener(OnPositionChangeListener listener){ + mOnPositionChangeListener = listener; + } + + public void setValueDescriptionProvider(ValueDescriptionProvider provider){ + mValueDescriptionProvider = provider; + } + + @Override + public void setBackgroundDrawable(Drawable drawable) { + Drawable background = getBackground(); + if(background instanceof RippleDrawable && !(drawable instanceof RippleDrawable)) + ((RippleDrawable) background).setBackgroundDrawable(drawable); + else + super.setBackgroundDrawable(drawable); + } + + protected RippleManager getRippleManager(){ + if(mRippleManager == null){ + synchronized (RippleManager.class){ + if(mRippleManager == null) + mRippleManager = new RippleManager(); + } + } + + return mRippleManager; + } + + @Override + public void setOnClickListener(OnClickListener l) { + RippleManager rippleManager = getRippleManager(); + if (l == rippleManager) + super.setOnClickListener(l); + else { + rippleManager.setOnClickListener(l); + setOnClickListener(rippleManager); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + + switch (widthMode) { + case MeasureSpec.UNSPECIFIED: + widthSize = getSuggestedMinimumWidth(); + break; + case MeasureSpec.AT_MOST: + widthSize = Math.min(widthSize, getSuggestedMinimumWidth()); + break; + } + + switch (heightMode) { + case MeasureSpec.UNSPECIFIED: + heightSize = getSuggestedMinimumHeight(); + break; + case MeasureSpec.AT_MOST: + heightSize = Math.min(heightSize, getSuggestedMinimumHeight()); + break; + } + + setMeasuredDimension(widthSize, heightSize); + } + + @Override + public int getSuggestedMinimumWidth() { + return (mDiscreteMode ? (int)(mThumbRadius * Math.sqrt(2)) : mThumbFocusRadius) * 4 + getPaddingLeft() + getPaddingRight(); + } + + @Override + public int getSuggestedMinimumHeight() { + return (mDiscreteMode ? (int)(mThumbRadius * (4 + Math.sqrt(2))) : mThumbFocusRadius * 2) + getPaddingTop() + getPaddingBottom(); + } + + @Override + public void onRtlPropertiesChanged(int layoutDirection) { + boolean rtl = layoutDirection == LAYOUT_DIRECTION_RTL; + if(mIsRtl != rtl) { + mIsRtl = rtl; + invalidate(); + } + } + + @Override + public int getBaseline() { + int align = mGravity & Gravity.VERTICAL_GRAVITY_MASK; + int baseline; + + if(mDiscreteMode){ + int fullHeight = (int)(mThumbRadius * (4 + Math.sqrt(2))); + int height = mThumbRadius * 2; + switch (align) { + case Gravity.TOP: + baseline = Math.max(getPaddingTop(), fullHeight - height) + mThumbRadius; + break; + case Gravity.BOTTOM: + baseline = getMeasuredHeight() - getPaddingBottom(); + break; + default: + baseline = Math.round(Math.max((getMeasuredHeight() - height) / 2f, fullHeight - height) + mThumbRadius); + break; + } + } + else{ + int height = mThumbFocusRadius * 2; + switch (align) { + case Gravity.TOP: + baseline = getPaddingTop() + mThumbFocusRadius; + break; + case Gravity.BOTTOM: + baseline = getMeasuredHeight() - getPaddingBottom(); + break; + default: + baseline = Math.round((getMeasuredHeight() - height) / 2f + mThumbFocusRadius); + break; + } + } + + return baseline + mBaselineOffset; + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + mDrawRect.left = getPaddingLeft() + mThumbRadius; + mDrawRect.right = w - getPaddingRight() - mThumbRadius; + + int align = mGravity & Gravity.VERTICAL_GRAVITY_MASK; + + if(mDiscreteMode){ + int fullHeight = (int)(mThumbRadius * (4 + Math.sqrt(2))); + int height = mThumbRadius * 2; + switch (align) { + case Gravity.TOP: + mDrawRect.top = Math.max(getPaddingTop(), fullHeight - height); + mDrawRect.bottom = mDrawRect.top + height; + break; + case Gravity.BOTTOM: + mDrawRect.bottom = h - getPaddingBottom(); + mDrawRect.top = mDrawRect.bottom - height; + break; + default: + mDrawRect.top = Math.max((h - height) / 2f, fullHeight - height); + mDrawRect.bottom = mDrawRect.top + height; + break; + } + } + else{ + int height = mThumbFocusRadius * 2; + switch (align) { + case Gravity.TOP: + mDrawRect.top = getPaddingTop(); + mDrawRect.bottom = mDrawRect.top + height; + break; + case Gravity.BOTTOM: + mDrawRect.bottom = h - getPaddingBottom(); + mDrawRect.top = mDrawRect.bottom - height; + break; + default: + mDrawRect.top = (h - height) / 2f; + mDrawRect.bottom = mDrawRect.top + height; + break; + } + } + } + + private boolean isThumbHit(float x, float y, float radius){ + float cx = mDrawRect.width() * mThumbPosition + mDrawRect.left; + float cy = mDrawRect.centerY(); + + return x >= cx - radius && x <= cx + radius && y >= cy - radius && y < cy + radius; + } + + private double distance(float x1, float y1, float x2, float y2){ + return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2)); + } + + private float correctPosition(float position){ + if(!mDiscreteMode) + return position; + + int totalOffset = mMaxValue - mMinValue; + int valueOffset = Math.round(totalOffset * position); + int stepOffset = valueOffset / mStepValue; + int lowerValue = stepOffset * mStepValue; + int higherValue = Math.min(totalOffset, (stepOffset + 1) * mStepValue); + + if(valueOffset - lowerValue < higherValue - valueOffset) + position = lowerValue / (float)totalOffset; + else + position = higherValue / (float)totalOffset; + + return position; + } + + @Override + public boolean onTouchEvent(@NonNull MotionEvent event) { + super.onTouchEvent(event); + getRippleManager().onTouchEvent(this, event); + + if(!isEnabled()) + return false; + + float x = event.getX(); + float y = event.getY(); + if(mIsRtl) + x = 2 * mDrawRect.centerX() - x; + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + mIsDragging = isThumbHit(x, y, mThumbRadius) && !mThumbMoveAnimator.isRunning(); + mMemoPoint.set(x, y); + if(mIsDragging) { + mThumbRadiusAnimator.startAnimation(mDiscreteMode ? 0 : mThumbFocusRadius); + + if(getParent() != null) + getParent().requestDisallowInterceptTouchEvent(true); + } + break; + case MotionEvent.ACTION_MOVE: + if(mIsDragging) { + if(mDiscreteMode) { + float position = correctPosition(Math.min(1f, Math.max(0f, (x - mDrawRect.left) / mDrawRect.width()))); + setPosition(position, true, true, true); + } + else{ + float offset = (x - mMemoPoint.x) / mDrawRect.width(); + float position = Math.min(1f, Math.max(0f, mThumbPosition + offset)); + setPosition(position, false, true, true); + mMemoPoint.x = x; + invalidate(); + } + } + break; + case MotionEvent.ACTION_UP: + if(mIsDragging) { + mIsDragging = false; + setPosition(getPosition(), true, true, true); + + if(getParent() != null) + getParent().requestDisallowInterceptTouchEvent(false); + } + else if(distance(mMemoPoint.x, mMemoPoint.y, x, y) <= mTouchSlop){ + float position = correctPosition(Math.min(1f, Math.max(0f, (x - mDrawRect.left) / mDrawRect.width()))); + setPosition(position, true, true, true); + } + break; + case MotionEvent.ACTION_CANCEL: + if(mIsDragging) { + mIsDragging = false; + setPosition(getPosition(), true, true, true); + + if(getParent() != null) + getParent().requestDisallowInterceptTouchEvent(false); + } + break; + } + + return true; + } + + private void getTrackPath(float x, float y, float radius){ + float halfStroke = mTrackSize / 2f; + + mLeftTrackPath.reset(); + mRightTrackPath.reset(); + + if(radius - 1f < halfStroke){ + if(mTrackCap != Paint.Cap.ROUND){ + if(x > mDrawRect.left){ + mLeftTrackPath.moveTo(mDrawRect.left, y - halfStroke); + mLeftTrackPath.lineTo(x, y - halfStroke); + mLeftTrackPath.lineTo(x, y + halfStroke); + mLeftTrackPath.lineTo(mDrawRect.left, y + halfStroke); + mLeftTrackPath.close(); + } + + if(x < mDrawRect.right){ + mRightTrackPath.moveTo(mDrawRect.right, y + halfStroke); + mRightTrackPath.lineTo(x, y + halfStroke); + mRightTrackPath.lineTo(x, y - halfStroke); + mRightTrackPath.lineTo(mDrawRect.right, y - halfStroke); + mRightTrackPath.close(); + } + } + else{ + if(x > mDrawRect.left){ + mTempRect.set(mDrawRect.left, y - halfStroke, mDrawRect.left + mTrackSize, y + halfStroke); + mLeftTrackPath.arcTo(mTempRect, 90, 180); + mLeftTrackPath.lineTo(x, y - halfStroke); + mLeftTrackPath.lineTo(x, y + halfStroke); + mLeftTrackPath.close(); + } + + if(x < mDrawRect.right){ + mTempRect.set(mDrawRect.right - mTrackSize, y - halfStroke, mDrawRect.right, y + halfStroke); + mRightTrackPath.arcTo(mTempRect, 270, 180); + mRightTrackPath.lineTo(x, y + halfStroke); + mRightTrackPath.lineTo(x, y - halfStroke); + mRightTrackPath.close(); + } + } + } + else{ + if(mTrackCap != Paint.Cap.ROUND){ + mTempRect.set(x - radius + 1f, y - radius + 1f, x + radius - 1f, y + radius - 1f); + float angle = (float)(Math.asin(halfStroke / (radius - 1f)) / Math.PI * 180); + + if(x - radius > mDrawRect.left){ + mLeftTrackPath.moveTo(mDrawRect.left, y - halfStroke); + mLeftTrackPath.arcTo(mTempRect, 180 + angle, -angle * 2); + mLeftTrackPath.lineTo(mDrawRect.left, y + halfStroke); + mLeftTrackPath.close(); + } + + if(x + radius < mDrawRect.right){ + mRightTrackPath.moveTo(mDrawRect.right, y - halfStroke); + mRightTrackPath.arcTo(mTempRect, -angle, angle * 2); + mRightTrackPath.lineTo(mDrawRect.right, y + halfStroke); + mRightTrackPath.close(); + } + } + else{ + float angle = (float)(Math.asin(halfStroke / (radius - 1f)) / Math.PI * 180); + + if(x - radius > mDrawRect.left){ + float angle2 = (float)(Math.acos(Math.max(0f, (mDrawRect.left + halfStroke - x + radius) / halfStroke)) / Math.PI * 180); + + mTempRect.set(mDrawRect.left, y - halfStroke, mDrawRect.left + mTrackSize, y + halfStroke); + mLeftTrackPath.arcTo(mTempRect, 180 - angle2, angle2 * 2); + + mTempRect.set(x - radius + 1f, y - radius + 1f, x + radius - 1f, y + radius - 1f); + mLeftTrackPath.arcTo(mTempRect, 180 + angle, -angle * 2); + mLeftTrackPath.close(); + } + + if(x + radius < mDrawRect.right){ + float angle2 = (float)Math.acos(Math.max(0f, (x + radius - mDrawRect.right + halfStroke) / halfStroke)); + mRightTrackPath.moveTo((float) (mDrawRect.right - halfStroke + Math.cos(angle2) * halfStroke), (float) (y + Math.sin(angle2) * halfStroke)); + + angle2 = (float)(angle2 / Math.PI * 180); + mTempRect.set(mDrawRect.right - mTrackSize, y - halfStroke, mDrawRect.right, y + halfStroke); + mRightTrackPath.arcTo(mTempRect, angle2, -angle2 * 2); + + mTempRect.set(x - radius + 1f, y - radius + 1f, x + radius - 1f, y + radius - 1f); + mRightTrackPath.arcTo(mTempRect, -angle, angle * 2); + mRightTrackPath.close(); + } + } + } + } + + private Path getMarkPath(Path path, float cx, float cy, float radius, float factor){ + if(path == null) + path = new Path(); + else + path.reset(); + + float x1 = cx - radius; + float y1 = cy; + float x2 = cx + radius; + float y2 = cy; + float x3 = cx; + float y3 = cy + radius; + + float nCx = cx; + float nCy = cy - radius * factor; + + // calculate first arc + float angle = (float)(Math.atan2(y2 - nCy, x2 - nCx) * 180 / Math.PI); + float nRadius = (float)distance(nCx, nCy, x1, y1); + mTempRect.set(nCx - nRadius, nCy - nRadius, nCx + nRadius, nCy + nRadius); + path.moveTo(x1, y1); + path.arcTo(mTempRect, 180 - angle, 180 + angle * 2); + + if(factor > 0.9f) + path.lineTo(x3, y3); + else{ + // find center point for second arc + float x4 = (x2 + x3) / 2; + float y4 = (y2 + y3) / 2; + + double d1 = distance(x2, y2, x4, y4); + double d2 = d1 / Math.tan(Math.PI * (1f - factor) / 4); + + nCx = (float)(x4 - Math.cos(Math.PI / 4) * d2); + nCy = (float)(y4 - Math.sin(Math.PI / 4) * d2); + + // calculate second arc + angle = (float)(Math.atan2(y2 - nCy, x2 - nCx) * 180 / Math.PI); + float angle2 = (float)(Math.atan2(y3 - nCy, x3 - nCx) * 180 / Math.PI); + nRadius = (float)distance(nCx, nCy, x2, y2); + mTempRect.set(nCx - nRadius, nCy - nRadius, nCx + nRadius, nCy + nRadius); + path.arcTo(mTempRect, angle, angle2 - angle); + + // calculate third arc + nCx = cx * 2 - nCx; + angle = (float)(Math.atan2(y3 - nCy, x3 - nCx) * 180 / Math.PI); + angle2 = (float)(Math.atan2(y1 - nCy, x1 - nCx) * 180 / Math.PI); + mTempRect.set(nCx - nRadius, nCy - nRadius, nCx + nRadius, nCy + nRadius); + path.arcTo(mTempRect, angle + (float)Math.PI / 4, angle2 - angle); + } + + path.close(); + + return path; + } + + @Override + public void draw(@NonNull Canvas canvas) { + super.draw(canvas); + + float x = mDrawRect.width() * mThumbPosition + mDrawRect.left; + if(mIsRtl) + x = 2 * mDrawRect.centerX() - x; + float y = mDrawRect.centerY(); + int filledPrimaryColor = ColorUtil.getMiddleColor(mSecondaryColor, isEnabled() ? mPrimaryColor : mSecondaryColor, mThumbFillPercent); + + getTrackPath(x, y, mThumbCurrentRadius); + mPaint.setStyle(Paint.Style.FILL); + mPaint.setColor(mIsRtl ? filledPrimaryColor : mSecondaryColor); + canvas.drawPath(mRightTrackPath, mPaint); + mPaint.setColor(mIsRtl ? mSecondaryColor : filledPrimaryColor); + canvas.drawPath(mLeftTrackPath, mPaint); + + mPaint.setColor(filledPrimaryColor); + if(mDiscreteMode){ + float factor = 1f - mThumbCurrentRadius / mThumbRadius; + + if(factor > 0){ + mMarkPath = getMarkPath(mMarkPath, x, y, mThumbRadius, factor); + mPaint.setStyle(Paint.Style.FILL); + int saveCount = canvas.save(); + canvas.translate(0, -mThumbRadius * 2 * factor); + canvas.drawPath(mMarkPath, mPaint); + mPaint.setColor(ColorUtil.getColor(mTextColor, factor)); + canvas.drawText(getValueText(), x, y + mTextHeight / 2f - mThumbRadius * factor, mPaint); + canvas.restoreToCount(saveCount); + } + + float radius = isEnabled() ? mThumbCurrentRadius : mThumbCurrentRadius - mThumbBorderSize; + if(radius > 0) { + mPaint.setColor(filledPrimaryColor); + canvas.drawCircle(x, y, radius, mPaint); + } + } + else{ + float radius = isEnabled() ? mThumbCurrentRadius : mThumbCurrentRadius - mThumbBorderSize; + if(mThumbFillPercent == 1) + mPaint.setStyle(Paint.Style.FILL); + else{ + float strokeWidth = (radius - mThumbBorderSize) * mThumbFillPercent + mThumbBorderSize; + radius = radius - strokeWidth / 2f; + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setStrokeWidth(strokeWidth); + } + canvas.drawCircle(x, y, radius, mPaint); + } + } + + class ThumbRadiusAnimator implements Runnable{ + + boolean mRunning = false; + long mStartTime; + float mStartRadius; + int mRadius; + + public void resetAnimation(){ + mStartTime = SystemClock.uptimeMillis(); + mStartRadius = mThumbCurrentRadius; + } + + public boolean startAnimation(int radius) { + if(mThumbCurrentRadius == radius) + return false; + + mRadius = radius; + + if(getHandler() != null){ + resetAnimation(); + mRunning = true; + getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + invalidate(); + return true; + } + else { + mThumbCurrentRadius = mRadius; + invalidate(); + return false; + } + } + + public void stopAnimation() { + mRunning = false; + mThumbCurrentRadius = mRadius; + if(getHandler() != null) + getHandler().removeCallbacks(this); + invalidate(); + } + + @Override + public void run() { + long curTime = SystemClock.uptimeMillis(); + float progress = Math.min(1f, (float)(curTime - mStartTime) / mTransformAnimationDuration); + float value = mInterpolator.getInterpolation(progress); + + mThumbCurrentRadius = (mRadius - mStartRadius) * value + mStartRadius; + + if(progress == 1f) + stopAnimation(); + + if(mRunning) { + if(getHandler() != null) + getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + else + stopAnimation(); + } + + invalidate(); + } + + } + + class ThumbStrokeAnimator implements Runnable{ + + boolean mRunning = false; + long mStartTime; + float mStartFillPercent; + int mFillPercent; + + public void resetAnimation(){ + mStartTime = SystemClock.uptimeMillis(); + mStartFillPercent = mThumbFillPercent; + } + + public boolean startAnimation(int fillPercent) { + if(mThumbFillPercent == fillPercent) + return false; + + mFillPercent = fillPercent; + + if(getHandler() != null){ + resetAnimation(); + mRunning = true; + getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + invalidate(); + return true; + } + else { + mThumbFillPercent = mAlwaysFillThumb ? 1 : mFillPercent; + invalidate(); + return false; + } + } + + public void stopAnimation() { + mRunning = false; + mThumbFillPercent = mAlwaysFillThumb ? 1 : mFillPercent; + if(getHandler() != null) + getHandler().removeCallbacks(this); + invalidate(); + } + + @Override + public void run() { + long curTime = SystemClock.uptimeMillis(); + float progress = Math.min(1f, (float)(curTime - mStartTime) / mTransformAnimationDuration); + float value = mInterpolator.getInterpolation(progress); + + mThumbFillPercent = mAlwaysFillThumb ? 1 : ((mFillPercent - mStartFillPercent) * value + mStartFillPercent); + + if(progress == 1f) + stopAnimation(); + + if(mRunning) { + if(getHandler() != null) + getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + else + stopAnimation(); + } + + invalidate(); + } + + } + + class ThumbMoveAnimator implements Runnable{ + + boolean mRunning = false; + long mStartTime; + float mStartFillPercent; + float mStartRadius; + float mStartPosition; + float mPosition; + float mFillPercent; + int mDuration; + + public boolean isRunning(){ + return mRunning; + } + + public float getPosition(){ + return mPosition; + } + + public void resetAnimation(){ + mStartTime = SystemClock.uptimeMillis(); + mStartPosition = mThumbPosition; + mStartFillPercent = mThumbFillPercent; + mStartRadius = mThumbCurrentRadius; + mFillPercent = mPosition == 0 ? 0 : 1; + mDuration = mDiscreteMode && !mIsDragging ? mTransformAnimationDuration * 2 + mTravelAnimationDuration : mTravelAnimationDuration; + } + + public boolean startAnimation(float position) { + if(mThumbPosition == position) + return false; + + mPosition = position; + + if(getHandler() != null){ + resetAnimation(); + mRunning = true; + getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + invalidate(); + return true; + } + else { + mThumbPosition = position; + invalidate(); + return false; + } + } + + public void stopAnimation() { + mRunning = false; + mThumbCurrentRadius = mDiscreteMode && mIsDragging ? 0 : mThumbRadius; + mThumbFillPercent = mAlwaysFillThumb ? 1 : mFillPercent; + mThumbPosition = mPosition; + if(getHandler() != null) + getHandler().removeCallbacks(this); + invalidate(); + } + + @Override + public void run() { + long curTime = SystemClock.uptimeMillis(); + float progress = Math.min(1f, (float)(curTime - mStartTime) / mDuration); + float value = mInterpolator.getInterpolation(progress); + + if(mDiscreteMode){ + if(mIsDragging) { + mThumbPosition = (mPosition - mStartPosition) * value + mStartPosition; + mThumbFillPercent = mAlwaysFillThumb ? 1 : ((mFillPercent - mStartFillPercent) * value + mStartFillPercent); + } + else{ + float p1 = (float)mTravelAnimationDuration / mDuration; + float p2 = (float)(mTravelAnimationDuration + mTransformAnimationDuration)/ mDuration; + if(progress < p1) { + value = mInterpolator.getInterpolation(progress / p1); + mThumbCurrentRadius = mStartRadius * (1f - value); + mThumbPosition = (mPosition - mStartPosition) * value + mStartPosition; + mThumbFillPercent = mAlwaysFillThumb ? 1 : ((mFillPercent - mStartFillPercent) * value + mStartFillPercent); + } + else if(progress > p2){ + mThumbCurrentRadius = mThumbRadius * (progress - p2) / (1 - p2); + } + } + } + else{ + mThumbPosition = (mPosition - mStartPosition) * value + mStartPosition; + mThumbFillPercent = mAlwaysFillThumb ? 1 : ((mFillPercent - mStartFillPercent) * value + mStartFillPercent); + + if(progress < 0.2) + mThumbCurrentRadius = Math.max(mThumbRadius + mThumbBorderSize * progress * 5, mThumbCurrentRadius); + else if(progress >= 0.8) + mThumbCurrentRadius = mThumbRadius + mThumbBorderSize * (5f - progress * 5); + } + + + if(progress == 1f) + stopAnimation(); + + if(mRunning) { + if(getHandler() != null) + getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + else + stopAnimation(); + } + + invalidate(); + } + + } + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + + SavedState ss = new SavedState(superState); + + ss.position = getPosition(); + return ss; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + + super.onRestoreInstanceState(ss.getSuperState()); + setPosition(ss.position, false); + requestLayout(); + } + + static class SavedState extends BaseSavedState { + float position; + + /** + * Constructor called from {@link Slider#onSaveInstanceState()} + */ + SavedState(Parcelable superState) { + super(superState); + } + + /** + * Constructor called from {@link #CREATOR} + */ + private SavedState(Parcel in) { + super(in); + position = in.readFloat(); + } + + @Override + public void writeToParcel(@NonNull Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeFloat(position); + } + + @Override + public String toString() { + return "Slider.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " pos=" + position + "}"; + } + + public static final Creator CREATOR + = new Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } +} diff --git a/material/src/main/java/com/rey/material/widget/SnackBar.java b/material/src/main/java/com/rey/material/widget/SnackBar.java new file mode 100644 index 0000000..920fe93 --- /dev/null +++ b/material/src/main/java/com/rey/material/widget/SnackBar.java @@ -0,0 +1,976 @@ +package com.rey.material.widget; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.text.TextUtils; +import android.text.TextUtils.TruncateAt; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; + +import com.rey.material.R; +import com.rey.material.util.ThemeManager; +import com.rey.material.drawable.RippleDrawable; +import com.rey.material.util.ThemeUtil; +import com.rey.material.util.ViewUtil; + +@SuppressWarnings("unused") +public class SnackBar extends FrameLayout implements ThemeManager.OnThemeChangedListener{ + + private TextView mText; + private Button mAction; + + public static final int MATCH_PARENT = ViewGroup.LayoutParams.MATCH_PARENT; + public static final int WRAP_CONTENT = ViewGroup.LayoutParams.WRAP_CONTENT; + + private BackgroundDrawable mBackground; + private int mMarginStart; + private int mMarginBottom; + private int mWidth; + private int mHeight; + private int mMaxHeight; + private int mMinHeight; + private long mDuration; + private int mActionId; + private boolean mRemoveOnDismiss; + + private Animation mInAnimation; + private Animation mOutAnimation; + + private Runnable mDismissRunnable = new Runnable() { + @Override + public void run() { + dismiss(); + } + }; + + private int mState = STATE_DISMISSED; + + /** + * Indicate this SnackBar is already dismissed. + */ + public static final int STATE_DISMISSED = 0; + /** + * Indicate this SnackBar is already shown. + */ + public static final int STATE_SHOWN = 1; + /** + * Indicate this SnackBar is being shown. + */ + public static final int STATE_SHOWING = 2; + /** + * Indicate this SnackBar is being dismissed. + */ + public static final int STATE_DISMISSING = 3; + + private boolean mIsRtl; + + /** + * Interface definition for a callback to be invoked when action button is clicked. + */ + public interface OnActionClickListener{ + + /** + * Called when action button is clicked. + * @param sb The SnackBar fire this event. + * @param actionId The ActionId of this SnackBar. + */ + void onActionClick(SnackBar sb, int actionId); + } + + private OnActionClickListener mActionClickListener; + + /** + * Interface definition for a callback to be invoked when SnackBar's state is changed. + */ + public interface OnStateChangeListener{ + + /** + * Called when SnackBar's state is changed. + * @param sb The SnackBar fire this event. + * @param oldState The old state of SnackBar. + * @param newState The new state of SnackBar. + */ + void onStateChange(SnackBar sb, int oldState, int newState); + } + + private OnStateChangeListener mStateChangeListener; + + public static SnackBar make(Context context){ + return new SnackBar(context); + } + + public SnackBar(Context context){ + super(context); + } + + public SnackBar(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public SnackBar(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public SnackBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + mWidth = MATCH_PARENT; + mHeight = WRAP_CONTENT; + mDuration = -1; + mIsRtl = false; + + mText = new TextView(context); + mText.setSingleLine(true); + mText.setGravity(Gravity.START | Gravity.CENTER_VERTICAL); + addView(mText, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); + + mAction = new Button(context); + mAction.setBackgroundResource(0); + mAction.setGravity(Gravity.CENTER); + mAction.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + if (mActionClickListener != null) + mActionClickListener.onActionClick(SnackBar.this, mActionId); + + dismiss(); + } + + }); + addView(mAction, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); + + mBackground = new BackgroundDrawable(); + mBackground.setColor(0xFF323232); + ViewUtil.setBackground(this, mBackground); + setClickable(true); + + super.init(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + super.applyStyle(context, attrs, defStyleAttr, defStyleRes); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SnackBar, defStyleAttr, defStyleRes); + + int horizontalPadding = -1; + int verticalPadding = -1; + int textSize = -1; + int textColor = 0; + boolean textColorDefined = false; + int textAppearance = 0; + int actionTextSize = -1; + ColorStateList actionTextColor = null; + int actionTextAppearance = 0; + + for(int i = 0, count = a.getIndexCount(); i < count; i++){ + int attr = a.getIndex(i); + if(attr == R.styleable.SnackBar_sb_backgroundColor) + backgroundColor(a.getColor(attr, 0)); + else if(attr == R.styleable.SnackBar_sb_backgroundCornerRadius) + backgroundRadius(a.getDimensionPixelSize(attr, 0)); + else if(attr == R.styleable.SnackBar_sb_horizontalPadding) + horizontalPadding = a.getDimensionPixelSize(attr, 0); + else if(attr == R.styleable.SnackBar_sb_verticalPadding) + verticalPadding = a.getDimensionPixelSize(attr, 0); + else if(attr == R.styleable.SnackBar_sb_width){ + if(ThemeUtil.getType(a, attr) == TypedValue.TYPE_INT_DEC) + width(a.getInteger(attr, 0)); + else + width(a.getDimensionPixelSize(attr, 0)); + } + else if(attr == R.styleable.SnackBar_sb_height){ + if(ThemeUtil.getType(a, attr) == TypedValue.TYPE_INT_DEC) + height(a.getInteger(attr, 0)); + else + height(a.getDimensionPixelSize(attr, 0)); + } + else if(attr == R.styleable.SnackBar_sb_minWidth) + minWidth(a.getDimensionPixelSize(attr, 0)); + else if(attr == R.styleable.SnackBar_sb_maxWidth) + maxWidth(a.getDimensionPixelSize(attr, 0)); + else if(attr == R.styleable.SnackBar_sb_minHeight) + minHeight(a.getDimensionPixelSize(attr, 0)); + else if(attr == R.styleable.SnackBar_sb_maxHeight) + maxHeight(a.getDimensionPixelSize(attr, 0)); + else if(attr == R.styleable.SnackBar_sb_marginStart) + marginStart(a.getDimensionPixelSize(attr, 0)); + else if(attr == R.styleable.SnackBar_sb_marginBottom) + marginBottom(a.getDimensionPixelSize(attr, 0)); + else if(attr == R.styleable.SnackBar_sb_textSize) + textSize = a.getDimensionPixelSize(attr, 0); + else if(attr == R.styleable.SnackBar_sb_textColor) { + textColor = a.getColor(attr, 0); + textColorDefined = true; + } + else if(attr == R.styleable.SnackBar_sb_textAppearance) + textAppearance = a.getResourceId(attr, 0); + else if(attr == R.styleable.SnackBar_sb_text) + text(a.getString(attr)); + else if(attr == R.styleable.SnackBar_sb_singleLine) + singleLine(a.getBoolean(attr, true)); + else if(attr == R.styleable.SnackBar_sb_maxLines) + maxLines(a.getInteger(attr, 0)); + else if(attr == R.styleable.SnackBar_sb_lines) + lines(a.getInteger(attr, 0)); + else if(attr == R.styleable.SnackBar_sb_ellipsize){ + int ellipsize = a.getInteger(attr, 0); + switch (ellipsize) { + case 1: + ellipsize(TruncateAt.START); + break; + case 2: + ellipsize(TruncateAt.MIDDLE); + break; + case 3: + ellipsize(TruncateAt.END); + break; + case 4: + ellipsize(TruncateAt.MARQUEE); + break; + default: + ellipsize(TruncateAt.END); + break; + } + } + else if(attr == R.styleable.SnackBar_sb_actionTextSize) + actionTextSize = a.getDimensionPixelSize(attr, 0); + else if(attr == R.styleable.SnackBar_sb_actionTextColor) + actionTextColor = a.getColorStateList(attr); + else if(attr == R.styleable.SnackBar_sb_actionTextAppearance) + actionTextAppearance = a.getResourceId(attr, 0); + else if(attr == R.styleable.SnackBar_sb_actionText) + actionText(a.getString(attr)); + else if(attr == R.styleable.SnackBar_sb_actionRipple) + actionRipple(a.getResourceId(attr, 0)); + else if(attr == R.styleable.SnackBar_sb_duration) + duration(a.getInteger(attr, 0)); + else if(attr == R.styleable.SnackBar_sb_removeOnDismiss) + removeOnDismiss(a.getBoolean(attr, true)); + else if(attr == R.styleable.SnackBar_sb_inAnimation) + animationIn(AnimationUtils.loadAnimation(getContext(), a.getResourceId(attr, 0))); + else if(attr == R.styleable.SnackBar_sb_outAnimation) + animationOut(AnimationUtils.loadAnimation(getContext(), a.getResourceId(attr, 0))); + } + + a.recycle(); + + if(horizontalPadding >= 0 || verticalPadding >= 0) + padding(horizontalPadding >= 0 ? horizontalPadding : mText.getPaddingLeft(), verticalPadding >= 0 ? verticalPadding : mText.getPaddingTop()); + + if(textAppearance != 0) + textAppearance(textAppearance); + if(textSize >= 0) + textSize(textSize); + if(textColorDefined) + textColor(textColor); + + if(textAppearance != 0) + actionTextAppearance(actionTextAppearance); + if(actionTextSize >= 0) + actionTextSize(actionTextSize); + if(actionTextColor != null) + actionTextColor(actionTextColor); + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + @Override + public void onRtlPropertiesChanged(int layoutDirection) { + boolean rtl = layoutDirection == LAYOUT_DIRECTION_RTL; + if(mIsRtl != rtl) { + mIsRtl = rtl; + + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1){ + mText.setTextDirection((mIsRtl ? TEXT_DIRECTION_RTL : TEXT_DIRECTION_LTR)); + mAction.setTextDirection((mIsRtl ? TEXT_DIRECTION_RTL : TEXT_DIRECTION_LTR)); + } + + requestLayout(); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int width; + int height; + + if(mAction.getVisibility() == View.VISIBLE){ + mAction.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), heightMeasureSpec); + int padding = mIsRtl ? mText.getPaddingLeft() : mText.getPaddingRight(); + mText.measure(MeasureSpec.makeMeasureSpec(widthSize - (mAction.getMeasuredWidth() - padding), widthMode), heightMeasureSpec); + width = mText.getMeasuredWidth() + mAction.getMeasuredWidth() - padding; + } + else{ + mText.measure(MeasureSpec.makeMeasureSpec(widthSize, widthMode), heightMeasureSpec); + width = mText.getMeasuredWidth(); + } + + height = Math.max(mText.getMeasuredHeight(), mAction.getMeasuredHeight()); + + switch (widthMode) { + case MeasureSpec.AT_MOST: + width = Math.min(widthSize, width); + break; + case MeasureSpec.EXACTLY: + width = widthSize; + break; + } + + switch (heightMode) { + case MeasureSpec.AT_MOST: + height = Math.min(heightSize, height); + break; + case MeasureSpec.EXACTLY: + height = heightSize; + break; + } + + if(mMaxHeight > 0) + height = Math.min(mMaxHeight, height); + + if(mMinHeight > 0) + height = Math.max(mMinHeight, height); + + setMeasuredDimension(width, height); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + int childLeft = getPaddingLeft(); + int childRight = r - l - getPaddingRight(); + int childTop = getPaddingTop(); + int childBottom = b - t - getPaddingBottom(); + + if(mAction.getVisibility() == View.VISIBLE){ + if(mIsRtl) { + mAction.layout(childLeft, childTop, childLeft + mAction.getMeasuredWidth(), childBottom); + childLeft += mAction.getMeasuredWidth() - mText.getPaddingLeft(); + } + else { + mAction.layout(childRight - mAction.getMeasuredWidth(), childTop, childRight, childBottom); + childRight -= mAction.getMeasuredWidth() - mText.getPaddingRight(); + } + } + + mText.layout(childLeft, childTop, childRight, childBottom); + } + + /** + * Set the text that this SnackBar is to display. + * @param text The text is displayed. + * @return This SnackBar for chaining methods. + */ + public SnackBar text(CharSequence text){ + mText.setText(text); + return this; + } + + /** + * Set the text that this SnackBar is to display. + * @param id The resourceId of text is displayed. + * @return This SnackBar for chaining methods. + */ + public SnackBar text(int id){ + return text(getContext().getResources().getString(id)); + } + + /** + * Set the text color. + * @param color The color of text. + * @return This SnackBar for chaining methods. + */ + public SnackBar textColor(int color){ + mText.setTextColor(color); + return this; + } + + /** + * Set the text size to the given value, interpreted as "scaled pixel" units. + * @param size The size of text. + * @return This SnackBar for chaining methods. + */ + public SnackBar textSize(float size){ + mText.setTextSize(TypedValue.COMPLEX_UNIT_SP, size); + return this; + } + + /** + * Sets the text color, size, style from the specified TextAppearance resource. + * @param resId The resourceId value. + * @return This SnackBar for chaining methods. + */ + public SnackBar textAppearance(int resId){ + if(resId != 0) + mText.setTextAppearance(getContext(), resId); + return this; + } + + /** + * Causes words in the text that are longer than the view is wide to be ellipsized instead of broken in the middle. + * @param at a + * @return This SnackBar for chaining methods. + */ + public SnackBar ellipsize(TruncateAt at){ + mText.setEllipsize(at); + return this; + } + + /** + * Sets the text will be single-line or not. + * @param b f + * @return This SnackBar for chaining methods. + */ + public SnackBar singleLine(boolean b){ + mText.setSingleLine(b); + return this; + } + + /** + * Makes the text at most this many lines tall. + * @param lines The maximum line value. + * @return This SnackBar for chaining methods. + */ + public SnackBar maxLines(int lines){ + mText.setMaxLines(lines); + return this; + } + + /** + * Makes the text exactly this many lines tall. + * @param lines The line number. + * @return This SnackBar for chaining methods. + */ + public SnackBar lines(int lines){ + mText.setLines(lines); + return this; + } + + /** + * Set the actionId of this SnackBar. Used to determine the current action of this SnackBar. + * @param id The actionId value. + * @return This SnackBar for chaining methods. + */ + public SnackBar actionId(int id){ + mActionId = id; + return this; + } + + /** + * Set the text that the ActionButton is to display. + * @param text If null, then the ActionButton will be hidden. + * @return This SnackBar for chaining methods. + */ + public SnackBar actionText(CharSequence text){ + if(TextUtils.isEmpty(text)) + mAction.setVisibility(View.INVISIBLE); + else{ + mAction.setVisibility(View.VISIBLE); + mAction.setText(text); + } + return this; + } + + /** + * Set the text that the ActionButton is to display. + * @param id If 0, then the ActionButton will be hidden. + * @return This SnackBar for chaining methods. + */ + public SnackBar actionText(int id){ + if(id == 0) + return actionText(null); + + return actionText(getContext().getResources().getString(id)); + } + + /** + * Set the text color of the ActionButton for all states. + * @param color The color of text. + * @return This SnackBar for chaining methods. + */ + public SnackBar actionTextColor(int color){ + mAction.setTextColor(color); + return this; + } + + /** + * Set the text color of the ActionButton. + * @param colors c + * @return This SnackBar for chaining methods. + */ + public SnackBar actionTextColor(ColorStateList colors){ + mAction.setTextColor(colors); + return this; + } + + /** + * Sets the text color, size, style of the ActionButton from the specified TextAppearance resource. + * @param resId The resourceId value. + * @return This SnackBar for chaining methods. + */ + public SnackBar actionTextAppearance(int resId){ + if(resId != 0) + mAction.setTextAppearance(resId); + return this; + } + + /** + * Set the text size of the ActionButton to the given value, interpreted as "scaled pixel" units. + * @param size The size of text. + * @return This SnackBar for chaining methods. + */ + public SnackBar actionTextSize(float size){ + mAction.setTextSize(TypedValue.COMPLEX_UNIT_SP, size); + return this; + } + + /** + * Set the style of RippleEffect of the ActionButton. + * @param resId The resourceId of RippleEffect. + * @return This SnackBar for chaining methods. + */ + public SnackBar actionRipple(int resId){ + if(resId != 0) + ViewUtil.setBackground(mAction, new RippleDrawable.Builder(getContext(), resId).build()); + return this; + } + + /** + * Set the duration this SnackBar will be shown before dismissing. + * @param duration If 0, then the SnackBar will not be dismissed until {@link #dismiss() dismiss()} is called. + * @return This SnackBar for chaining methods. + */ + public SnackBar duration(long duration){ + mDuration = duration; + return this; + } + + /** + * Set the background color of this SnackBar. + * @param color The color of background. + * @return This SnackBar for chaining methods. + */ + public SnackBar backgroundColor(int color){ + mBackground.setColor(color); + return this; + } + + /** + * Set the background's corner radius of this SnackBar. + * @param radius The corner radius. + * @return This SnackBar for chaining methods. + */ + public SnackBar backgroundRadius(int radius){ + mBackground.setRadius(radius); + return this; + } + + /** + * Set the horizontal padding between this SnackBar and it's text and button. + * @param padding p + * @return This SnackBar for chaining methods. + */ + public SnackBar horizontalPadding(int padding){ + mText.setPadding(padding, mText.getPaddingTop(), padding, mText.getPaddingBottom()); + mAction.setPadding(padding, mAction.getPaddingTop(), padding, mAction.getPaddingBottom()); + return this; + } + + /** + * Set the vertical padding between this SnackBar and it's text and button. + * @param padding p + * @return This SnackBar for chaining methods. + */ + public SnackBar verticalPadding(int padding){ + mText.setPadding(mText.getPaddingLeft(), padding, mText.getPaddingRight(), padding); + mAction.setPadding(mAction.getPaddingLeft(), padding, mAction.getPaddingRight(), padding); + return this; + } + + /** + * Set the padding between this SnackBar and it's text and button. + * @param horizontalPadding The horizontal padding. + * @param verticalPadding The vertical padding. + * @return This SnackBar for chaining methods. + */ + public SnackBar padding(int horizontalPadding, int verticalPadding){ + mText.setPadding(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding); + mAction.setPadding(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding); + return this; + } + + /** + * Makes this SnackBar exactly this many pixels wide. + * @param width The width value in pixels. + * @return This SnackBar for chaining methods. + */ + public SnackBar width(int width){ + mWidth = width; + return this; + } + + /** + * Makes this SnackBar at least this many pixels wide + * @param width The minimum width value in pixels. + * @return This SnackBar for chaining methods. + */ + public SnackBar minWidth(int width){ + mText.setMinWidth(width); + return this; + } + + /** + * Makes this SnackBar at most this many pixels wide + * @param width The maximum width value in pixels. + * @return This SnackBar for chaining methods. + */ + public SnackBar maxWidth(int width){ + mText.setMaxWidth(width); + return this; + } + + /** + * Makes this SnackBar exactly this many pixels tall. + * @param height The height value in pixels. + * @return This SnackBar for chaining methods. + */ + public SnackBar height(int height){ + mHeight = height; + return this; + } + + /** + * Makes this SnackBar at most this many pixels tall + * @param height The maximum height value in pixels. + * @return This SnackBar for chaining methods. + */ + public SnackBar maxHeight(int height){ + mMaxHeight = height; + return this; + } + + /** + * Makes this SnackBar at least this many pixels tall + * @param height The maximum height value in pixels. + * @return This SnackBar for chaining methods. + */ + public SnackBar minHeight(int height){ + mMinHeight = height; + return this; + } + + /** + * Set the start margin between this SnackBar and it's parent. + * @param size s + * @return This SnackBar for chaining methods. + */ + public SnackBar marginStart(int size){ + mMarginStart = size; + return this; + } + + /** + * Set the bottom margin between this SnackBar and it's parent. + * @param size s + * @return This SnackBar for chaining methods. + */ + public SnackBar marginBottom(int size){ + mMarginBottom = size; + return this; + } + + /** + * Set the listener will be called when the ActionButton is clicked. + * @param listener The {@link OnActionClickListener} will be called. + * @return This SnackBar for chaining methods. + */ + public SnackBar actionClickListener(OnActionClickListener listener){ + mActionClickListener = listener; + return this; + } + + /** + * Set the listener will be called when this SnackBar's state is changed. + * @param listener The {@link OnStateChangeListener} will be called. + * @return This SnackBar for chaining methods. + */ + public SnackBar stateChangeListener(OnStateChangeListener listener){ + mStateChangeListener = listener; + return this; + } + + /** + * Set the animation will be shown when SnackBar enter screen. + * @param anim The animation. + * @return This SnackBar for chaining methods. + */ + public SnackBar animationIn(Animation anim){ + mInAnimation = anim; + return this; + } + + /** + * Set the animation will be shown when SnackBar exit screen. + * @param anim The animation. + * @return This SnackBar for chaining methods. + */ + public SnackBar animationOut(Animation anim){ + mOutAnimation = anim; + return this; + } + + /** + * Indicate that this SnackBar should remove itself from parent view after being dismissed. + * @param b b + * @return This SnackBar for chaining methods. + */ + public SnackBar removeOnDismiss(boolean b){ + mRemoveOnDismiss = b; + return this; + } + + /** + * Show this SnackBar. It will auto attach to the activity's root view. + */ + public void show(Activity activity){ + show((ViewGroup)activity.getWindow().findViewById(Window.ID_ANDROID_CONTENT)); + } + + /** + * Show this SnackBar. It will auto attach to the parent view. + * @param parent Must be {@link android.widget.FrameLayout} or {@link android.widget.RelativeLayout} + */ + public void show(ViewGroup parent){ + if(mState == STATE_SHOWING || mState == STATE_DISMISSING) + return; + + if(getParent() != parent) { + if(getParent() != null) + ((ViewGroup) getParent()).removeView(this); + + parent.addView(this); + } + + show(); + } + + /** + * Show this SnackBar. + * Make sure it already attached to a parent view or this method will do nothing. + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public void show(){ + ViewGroup parent = (ViewGroup)getParent(); + if(parent == null || mState == STATE_SHOWING || mState == STATE_DISMISSING) + return; + + if(parent instanceof android.widget.FrameLayout){ + LayoutParams params = (LayoutParams)getLayoutParams(); + + params.width = mWidth; + params.height = mHeight; + params.gravity = Gravity.START | Gravity.BOTTOM; + if(mIsRtl) + params.rightMargin = mMarginStart; + else + params.leftMargin = mMarginStart; + params.bottomMargin = mMarginBottom; + + setLayoutParams(params); + } + else if(parent instanceof android.widget.RelativeLayout){ + android.widget.RelativeLayout.LayoutParams params = (android.widget.RelativeLayout.LayoutParams)getLayoutParams(); + + params.width = mWidth; + params.height = mHeight; + params.addRule(android.widget.RelativeLayout.ALIGN_PARENT_BOTTOM); + params.addRule(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 ? android.widget.RelativeLayout.ALIGN_PARENT_START : android.widget.RelativeLayout.ALIGN_PARENT_LEFT); + if(mIsRtl) + params.rightMargin = mMarginStart; + else + params.leftMargin = mMarginStart; + params.bottomMargin = mMarginBottom; + + setLayoutParams(params); + } + + if(mInAnimation != null && mState != STATE_SHOWN){ + mInAnimation.cancel(); + mInAnimation.reset(); + mInAnimation.setAnimationListener(new Animation.AnimationListener() { + + @Override + public void onAnimationStart(Animation animation) { + setState(STATE_SHOWING); + setVisibility(View.VISIBLE); + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + + @Override + public void onAnimationEnd(Animation animation) { + setState(STATE_SHOWN); + startTimer(); + } + }); + clearAnimation(); + startAnimation(mInAnimation); + } + else { + setVisibility(View.VISIBLE); + setState(STATE_SHOWN); + startTimer(); + } + } + + private void startTimer(){ + removeCallbacks(mDismissRunnable); + if(mDuration > 0) + postDelayed(mDismissRunnable, mDuration); + } + + /** + * Dismiss this SnackBar. It must be in {@link #STATE_SHOWN} to be dismissed. + */ + public void dismiss(){ + if(mState != STATE_SHOWN) + return; + + removeCallbacks(mDismissRunnable); + + if(mOutAnimation != null){ + mOutAnimation.cancel(); + mOutAnimation.reset(); + mOutAnimation.setAnimationListener(new Animation.AnimationListener() { + + @Override + public void onAnimationStart(Animation animation) { + setState(STATE_DISMISSING); + } + + @Override + public void onAnimationRepeat(Animation animation) {} + + @Override + public void onAnimationEnd(Animation animation) { + if(mRemoveOnDismiss && getParent() != null && getParent() instanceof ViewGroup) + ((ViewGroup)getParent()).removeView(SnackBar.this); + + setState(STATE_DISMISSED); + setVisibility(View.GONE); + } + }); + clearAnimation(); + startAnimation(mOutAnimation); + } + else{ + if(mRemoveOnDismiss && getParent() != null && getParent() instanceof ViewGroup) + ((ViewGroup)getParent()).removeView(this); + + setState(STATE_DISMISSED); + setVisibility(View.GONE); + } + + } + + /** + * Get the current state of this SnackBar. + * @return The current state of this SnackBar. Can be {@link #STATE_DISMISSED}, {@link #STATE_DISMISSING}, {@link #STATE_SHOWING} or {@link #STATE_SHOWN}. + */ + public int getState(){ + return mState; + } + + private void setState(int state){ + if(mState != state){ + int oldState = mState; + mState = state; + if(mStateChangeListener != null) + mStateChangeListener.onStateChange(this, oldState, mState); + } + } + + private class BackgroundDrawable extends Drawable{ + + private int mBackgroundColor; + private int mBackgroundRadius; + + private Paint mPaint; + private RectF mRect; + + public BackgroundDrawable(){ + mPaint = new Paint(); + mPaint.setAntiAlias(true); + mPaint.setStyle(Paint.Style.FILL); + + mRect = new RectF(); + } + + public void setColor(int color){ + if(mBackgroundColor != color){ + mBackgroundColor = color; + mPaint.setColor(mBackgroundColor); + invalidateSelf(); + } + } + + public void setRadius(int radius){ + if(mBackgroundRadius != radius){ + mBackgroundRadius = radius; + invalidateSelf(); + } + } + + @Override + protected void onBoundsChange(Rect bounds) { + mRect.set(bounds); + } + + @Override + public void draw(Canvas canvas) { + canvas.drawRoundRect(mRect, mBackgroundRadius, mBackgroundRadius, mPaint); + } + + @Override + public void setAlpha(int alpha) { + mPaint.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter cf) { + mPaint.setColorFilter(cf); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + } +} diff --git a/material/src/main/java/com/rey/material/widget/Spinner.java b/material/src/main/java/com/rey/material/widget/Spinner.java new file mode 100644 index 0000000..7c37afb --- /dev/null +++ b/material/src/main/java/com/rey/material/widget/Spinner.java @@ -0,0 +1,1294 @@ +package com.rey.material.widget; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.database.DataSetObserver; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.NonNull; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.SparseArray; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; +import android.widget.AdapterView; +import android.widget.ListAdapter; +import android.widget.SpinnerAdapter; + +import com.rey.material.R; +import com.rey.material.util.ThemeManager; +import com.rey.material.drawable.ArrowDrawable; +import com.rey.material.drawable.DividerDrawable; +import com.rey.material.util.ThemeUtil; + +public class Spinner extends FrameLayout implements ThemeManager.OnThemeChangedListener{ + + private static final int MAX_ITEMS_MEASURED = 15; + + private static final int INVALID_POSITION = -1; + + /** + * Interface definition for a callback to be invoked when a item's view is clicked. + */ + public interface OnItemClickListener{ + /** + * Called when a item's view is clicked. + * @param parent The Spinner view. + * @param view The item view. + * @param position The position of item. + * @param id The id of item. + * @return false will make the Spinner doesn't select this item. + */ + boolean onItemClick(Spinner parent, View view, int position, long id); + } + + /** + * Interface definition for a callback to be invoked when an item is selected. + */ + public interface OnItemSelectedListener{ + /** + * Called when an item is selected. + * @param parent The Spinner view. + * @param view The item view. + * @param position The position of item. + * @param id The id of item. + */ + void onItemSelected(Spinner parent, View view, int position, long id); + } + + private boolean mLabelEnable; + private TextView mLabelView; + + private SpinnerAdapter mAdapter; + private OnItemClickListener mOnItemClickListener; + private OnItemSelectedListener mOnItemSelectedListener; + + private int mMinWidth; + private int mMinHeight; + + private DropdownPopup mPopup; + private int mDropDownWidth; + + private ArrowDrawable mArrowDrawable; + private int mArrowSize; + private int mArrowPadding; + private boolean mArrowAnimSwitchMode; + + private DividerDrawable mDividerDrawable; + private int mDividerHeight; + private int mDividerPadding; + + private int mGravity; + private boolean mDisableChildrenWhenDisabled; + + private int mSelectedPosition; + + private RecycleBin mRecycler = new RecycleBin(); + + private Rect mTempRect = new Rect(); + + private DropDownAdapter mTempAdapter; + + private SpinnerDataSetObserver mDataSetObserver = new SpinnerDataSetObserver(); + + private boolean mIsRtl; + + public Spinner(Context context) { + super(context, null, R.attr.listPopupWindowStyle); + } + + public Spinner(Context context, AttributeSet attrs) { + super(context, attrs, R.attr.listPopupWindowStyle); + } + + public Spinner(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public Spinner(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + mLabelEnable = false; + mDropDownWidth = LayoutParams.WRAP_CONTENT; + mArrowAnimSwitchMode = false; + mGravity = Gravity.CENTER; + mDisableChildrenWhenDisabled = false; + mSelectedPosition = INVALID_POSITION; + mIsRtl = false; + + setWillNotDraw(false); + + mPopup = new DropdownPopup(context, attrs, defStyleAttr, defStyleRes); + mPopup.setModal(true); + + if(isInEditMode()) + applyStyle(R.style.Material_Widget_Spinner); + + setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + showPopup(); + } + }); + + super.init(context, attrs, defStyleAttr, defStyleRes); + } + + private android.widget.TextView getLabelView(){ + if(mLabelView == null){ + mLabelView = new TextView(getContext()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) + mLabelView.setTextDirection(mIsRtl ? TEXT_DIRECTION_RTL : TEXT_DIRECTION_LTR); + mLabelView.setSingleLine(true); + mLabelView.setDuplicateParentStateEnabled(true); + } + + return mLabelView; + } + + @Override + protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + super.applyStyle(context, attrs, defStyleAttr, defStyleRes); + + removeAllViews(); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Spinner, defStyleAttr, defStyleRes); + + int arrowAnimDuration = -1; + ColorStateList arrowColor = null; + Interpolator arrowInterpolator = null; + boolean arrowClockwise = true; + int dividerAnimDuration = -1; + ColorStateList dividerColor = null; + ColorStateList labelTextColor = null; + int labelTextSize = -1; + + for(int i = 0, count = a.getIndexCount(); i < count; i++){ + int attr = a.getIndex(i); + + if(attr == R.styleable.Spinner_spn_labelEnable) + mLabelEnable = a.getBoolean(attr, false); + else if(attr == R.styleable.Spinner_spn_labelPadding) + getLabelView().setPadding(0, 0, 0, a.getDimensionPixelSize(attr, 0)); + else if (attr == R.styleable.Spinner_spn_labelTextSize) + labelTextSize = a.getDimensionPixelSize(attr, 0); + else if(attr == R.styleable.Spinner_spn_labelTextColor) + labelTextColor = a.getColorStateList(attr); + else if(attr == R.styleable.Spinner_spn_labelTextAppearance) + getLabelView().setTextAppearance(context, a.getResourceId(attr, 0)); + else if(attr == R.styleable.Spinner_spn_labelEllipsize){ + int labelEllipsize = a.getInteger(attr, 0); + switch (labelEllipsize) { + case 1: + getLabelView().setEllipsize(TextUtils.TruncateAt.START); + break; + case 2: + getLabelView().setEllipsize(TextUtils.TruncateAt.MIDDLE); + break; + case 3: + getLabelView().setEllipsize(TextUtils.TruncateAt.END); + break; + case 4: + getLabelView().setEllipsize(TextUtils.TruncateAt.MARQUEE); + break; + default: + getLabelView().setEllipsize(TextUtils.TruncateAt.END); + break; + } + } + else if(attr == R.styleable.Spinner_spn_label) + getLabelView().setText(a.getString(attr)); + else if(attr == R.styleable.Spinner_android_gravity) + mGravity = a.getInt(attr, 0); + else if(attr == R.styleable.Spinner_android_minWidth) + setMinimumWidth(a.getDimensionPixelOffset(attr, 0)); + else if(attr == R.styleable.Spinner_android_minHeight) + setMinimumHeight(a.getDimensionPixelOffset(attr, 0)); + else if(attr == R.styleable.Spinner_android_dropDownWidth) + mDropDownWidth = a.getLayoutDimension(attr, LayoutParams.WRAP_CONTENT); + else if(attr == R.styleable.Spinner_android_popupBackground) + mPopup.setBackgroundDrawable(a.getDrawable(attr)); + else if(attr == R.styleable.Spinner_android_prompt) + mPopup.setPromptText(a.getString(attr)); + else if(attr == R.styleable.Spinner_spn_popupItemAnimation) + mPopup.setItemAnimation(a.getResourceId(attr, 0)); + else if(attr == R.styleable.Spinner_spn_popupItemAnimOffset) + mPopup.setItemAnimationOffset(a.getInteger(attr, 0)); + else if(attr == R.styleable.Spinner_spn_disableChildrenWhenDisabled) + mDisableChildrenWhenDisabled = a.getBoolean(attr, false); + else if(attr == R.styleable.Spinner_spn_arrowSwitchMode) + mArrowAnimSwitchMode = a.getBoolean(attr, false); + else if(attr == R.styleable.Spinner_spn_arrowAnimDuration) + arrowAnimDuration = a.getInteger(attr, 0); + else if(attr == R.styleable.Spinner_spn_arrowSize) + mArrowSize = a.getDimensionPixelSize(attr, 0); + else if(attr == R.styleable.Spinner_spn_arrowPadding) + mArrowPadding = a.getDimensionPixelSize(attr, 0); + else if(attr == R.styleable.Spinner_spn_arrowColor) + arrowColor = a.getColorStateList(attr); + else if(attr == R.styleable.Spinner_spn_arrowInterpolator){ + int resId = a.getResourceId(attr, 0); + arrowInterpolator = AnimationUtils.loadInterpolator(context, resId); + } + else if(attr == R.styleable.Spinner_spn_arrowAnimClockwise) + arrowClockwise = a.getBoolean(attr, true); + else if(attr == R.styleable.Spinner_spn_dividerHeight) + mDividerHeight = a.getDimensionPixelOffset(attr, 0); + else if(attr == R.styleable.Spinner_spn_dividerPadding) + mDividerPadding = a.getDimensionPixelOffset(attr, 0); + else if(attr == R.styleable.Spinner_spn_dividerAnimDuration) + dividerAnimDuration = a.getInteger(attr, 0); + else if(attr == R.styleable.Spinner_spn_dividerColor) + dividerColor = a.getColorStateList(attr); + } + + a.recycle(); + + if(labelTextColor != null) + getLabelView().setTextColor(labelTextColor); + + if(labelTextSize >= 0) + getLabelView().setTextSize(TypedValue.COMPLEX_UNIT_PX, labelTextSize); + + if(mLabelEnable) + addView(getLabelView(), 0, new ViewGroup.LayoutParams(LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + + if(mArrowSize > 0){ + if(mArrowDrawable == null){ + if(arrowColor == null) + arrowColor = ColorStateList.valueOf(ThemeUtil.colorControlNormal(context, 0xFF000000)); + + if(arrowAnimDuration < 0) + arrowAnimDuration = 0; + + mArrowDrawable = new ArrowDrawable(ArrowDrawable.MODE_DOWN, mArrowSize, arrowColor, arrowAnimDuration, arrowInterpolator, arrowClockwise); + mArrowDrawable.setCallback(this); + } + else{ + mArrowDrawable.setArrowSize(mArrowSize); + mArrowDrawable.setClockwise(arrowClockwise); + + if(arrowColor != null) + mArrowDrawable.setColor(arrowColor); + + if(arrowAnimDuration >= 0) + mArrowDrawable.setAnimationDuration(arrowAnimDuration); + + if(arrowInterpolator != null) + mArrowDrawable.setInterpolator(arrowInterpolator); + } + } + else if(mArrowDrawable != null){ + mArrowDrawable.setCallback(null); + mArrowDrawable = null; + } + + if(mDividerHeight > 0){ + if(mDividerDrawable == null){ + if(dividerAnimDuration < 0) + dividerAnimDuration = 0; + + if(dividerColor == null){ + int[][] states = new int[][]{ + new int[]{-android.R.attr.state_pressed}, + new int[]{android.R.attr.state_pressed, android.R.attr.state_enabled}, + }; + int[] colors = new int[]{ + ThemeUtil.colorControlNormal(context, 0xFF000000), + ThemeUtil.colorControlActivated(context, 0xFF000000), + }; + + dividerColor = new ColorStateList(states, colors); + } + + mDividerDrawable = new DividerDrawable(mDividerHeight, dividerColor, dividerAnimDuration); + mDividerDrawable.setCallback(this); + } + else{ + mDividerDrawable.setDividerHeight(mDividerHeight); + + if(dividerColor != null) + mDividerDrawable.setColor(dividerColor); + + if(dividerAnimDuration >= 0) + mDividerDrawable.setAnimationDuration(dividerAnimDuration); + } + } + else if(mDividerDrawable != null){ + mDividerDrawable.setCallback(null); + mDividerDrawable = null; + } + + if (mTempAdapter != null) { + mPopup.setAdapter(mTempAdapter); + mTempAdapter = null; + } + + if(mAdapter != null) + setAdapter(mAdapter); + + if(isInEditMode()){ + TextView tv = new TextView(context, attrs, defStyleAttr); + tv.setText("Item 1"); + super.addView(tv); + } + + requestLayout(); + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + @Override + public void onRtlPropertiesChanged(int layoutDirection) { + boolean rtl = layoutDirection == LAYOUT_DIRECTION_RTL; + if(mIsRtl != rtl) { + mIsRtl = rtl; + + if(mLabelView != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) + mLabelView.setTextDirection(mIsRtl ? TEXT_DIRECTION_RTL : TEXT_DIRECTION_LTR); + + requestLayout(); + } + } + + /** + * @return The selected item's view. + */ + public View getSelectedView() { + View v = getChildAt(getChildCount() - 1); + return v == mLabelView ? null : v; + } + + /** + * Set the selected position of this Spinner. + * @param position The selected position. + */ + public void setSelection(int position) { + if(mAdapter != null) + position = Math.max(0, Math.min(position, mAdapter.getCount() - 1)); + + if(mSelectedPosition != position){ + mSelectedPosition = position; + + if(mOnItemSelectedListener != null) + mOnItemSelectedListener.onItemSelected(this, getSelectedView(), position, mAdapter == null ? -1 : mAdapter.getItemId(position)); + + onDataInvalidated(); + } + } + + /** + * @return The selected posiiton. + */ + public int getSelectedItemPosition(){ + return mSelectedPosition; + } + + /** + * @return The selected item. + */ + public Object getSelectedItem(){ + return mAdapter == null ? null : mAdapter.getItem(mSelectedPosition); + } + + /** + * @return The adapter back this Spinner. + */ + public SpinnerAdapter getAdapter() { + return mAdapter; + } + + /** + * Set an adapter for this Spinner. + * @param adapter + */ + public void setAdapter(SpinnerAdapter adapter) { + if(mAdapter != null) + mAdapter.unregisterDataSetObserver(mDataSetObserver); + + mRecycler.clear(); + + mAdapter = adapter; + mAdapter.registerDataSetObserver(mDataSetObserver); + onDataChanged(); + + if (mPopup != null) + mPopup.setAdapter(new DropDownAdapter(adapter)); + else + mTempAdapter = new DropDownAdapter(adapter); + } + + /** + * Set the background drawable for the spinner's popup window of choices. + * + * @param background Background drawable + * + * @attr ref android.R.styleable#Spinner_popupBackground + */ + public void setPopupBackgroundDrawable(Drawable background) { + mPopup.setBackgroundDrawable(background); + } + + /** + * Set the background drawable for the spinner's popup window of choices. + * + * @param resId Resource ID of a background drawable + * + * @attr ref android.R.styleable#Spinner_popupBackground + */ + public void setPopupBackgroundResource(int resId) { + setPopupBackgroundDrawable(getContext().getDrawable(resId)); + } + + /** + * Get the background drawable for the spinner's popup window of choices. + * + * @return background Background drawable + * + * @attr ref android.R.styleable#Spinner_popupBackground + */ + public Drawable getPopupBackground() { + return mPopup.getBackground(); + } + + /** + * Set a vertical offset in pixels for the spinner's popup window of choices. + * + * @param pixels Vertical offset in pixels + * + * @attr ref android.R.styleable#ListPopupWindow_dropDownVerticalOffset + */ + public void setDropDownVerticalOffset(int pixels) { + mPopup.setVerticalOffset(pixels); + } + + /** + * Get the configured vertical offset in pixels for the spinner's popup window of choices. + * + * @return Vertical offset in pixels + * + * @attr ref android.R.styleable#ListPopupWindow_dropDownVerticalOffset + */ + public int getDropDownVerticalOffset() { + return mPopup.getVerticalOffset(); + } + + /** + * Set a horizontal offset in pixels for the spinner's popup window of choices. + * + * @param pixels Horizontal offset in pixels + * + * @attr ref android.R.styleable#ListPopupWindow_dropDownHorizontalOffset + */ + public void setDropDownHorizontalOffset(int pixels) { + mPopup.setHorizontalOffset(pixels); + } + + /** + * Get the configured horizontal offset in pixels for the spinner's popup window of choices. + * + * @return Horizontal offset in pixels + * + * @attr ref android.R.styleable#ListPopupWindow_dropDownHorizontalOffset + */ + public int getDropDownHorizontalOffset() { + return mPopup.getHorizontalOffset(); + } + + /** + * Set the width of the spinner's popup window of choices in pixels. This value + * may also be set to {@link ViewGroup.LayoutParams#MATCH_PARENT} + * to match the width of the Spinner itself, or + * {@link ViewGroup.LayoutParams#WRAP_CONTENT} to wrap to the measured size + * of contained dropdown list items. + * + * @param pixels Width in pixels, WRAP_CONTENT, or MATCH_PARENT + * + * @attr ref android.R.styleable#Spinner_dropDownWidth + */ + public void setDropDownWidth(int pixels) { + mDropDownWidth = pixels; + } + + /** + * Get the configured width of the spinner's popup window of choices in pixels. + * The returned value may also be {@link ViewGroup.LayoutParams#MATCH_PARENT} + * meaning the popup window will match the width of the Spinner itself, or + * {@link ViewGroup.LayoutParams#WRAP_CONTENT} to wrap to the measured size + * of contained dropdown list items. + * + * @return Width in pixels, WRAP_CONTENT, or MATCH_PARENT + * + * @attr ref android.R.styleable#Spinner_dropDownWidth + */ + public int getDropDownWidth() { + return mDropDownWidth; + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + if (mDisableChildrenWhenDisabled) { + final int count = getChildCount(); + for (int i = 0; i < count; i++) + getChildAt(i).setEnabled(enabled); + } + } + + @Override + public void setMinimumHeight(int minHeight) { + mMinHeight = minHeight; + super.setMinimumHeight(minHeight); + } + + @Override + public void setMinimumWidth(int minWidth) { + mMinWidth = minWidth; + super.setMinimumWidth(minWidth); + } + + /** + * Describes how the selected item view is positioned. + * + * @param gravity See {@link Gravity} + * + * @attr ref android.R.styleable#Spinner_gravity + */ + public void setGravity(int gravity) { + if (mGravity != gravity) { + if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == 0) + gravity |= Gravity.START; + mGravity = gravity; + requestLayout(); + } + } + + @Override + public int getBaseline() { + View child = getSelectedView(); + + if (child != null) { + final int childBaseline = child.getBaseline(); + return childBaseline >= 0 ? child.getTop() + childBaseline : -1; + } + + return -1; + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (mPopup != null && mPopup.isShowing()) + mPopup.dismiss(); + } + + /** + * Set a listener that will be called when a item's view is clicked. + * @param l The {@link OnItemClickListener} will be called. + */ + public void setOnItemClickListener(OnItemClickListener l) { + mOnItemClickListener = l; + } + + /** + * Set a listener that will be called when an item is selected. + * @param l The {@link OnItemSelectedListener} will be called. + */ + public void setOnItemSelectedListener(OnItemSelectedListener l) { + mOnItemSelectedListener = l; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + return true; + } + + @Override + protected boolean verifyDrawable(Drawable who) { + return super.verifyDrawable(who) || mArrowDrawable == who || mDividerDrawable == who; + } + + private int getArrowDrawableWidth(){ + return mArrowDrawable != null ? mArrowSize + mArrowPadding * 2 : 0; + } + + private int getDividerDrawableHeight(){ + return mDividerHeight > 0 ? mDividerHeight + mDividerPadding : 0; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + int paddingHorizontal = getPaddingLeft() + getPaddingRight() + getArrowDrawableWidth(); + int paddingVertical = getPaddingTop() + getPaddingBottom() + getDividerDrawableHeight(); + + int labelWidth = 0; + int labelHeight = 0; + if(mLabelView != null && mLabelView.getLayoutParams() != null){ + mLabelView.measure(MeasureSpec.makeMeasureSpec(widthSize - paddingHorizontal, widthMode), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + labelWidth = mLabelView.getMeasuredWidth(); + labelHeight = mLabelView.getMeasuredHeight(); + } + + int width = 0; + int height = 0; + + View v = getSelectedView(); + if(v != null){ + int ws; + int hs; + ViewGroup.LayoutParams params = v.getLayoutParams(); + switch (params.width){ + case ViewGroup.LayoutParams.WRAP_CONTENT: + ws = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + break; + case ViewGroup.LayoutParams.MATCH_PARENT: + ws = MeasureSpec.makeMeasureSpec(widthSize - paddingHorizontal, widthMode); + break; + default: + ws = MeasureSpec.makeMeasureSpec(params.width, MeasureSpec.EXACTLY); + break; + } + switch (params.height){ + case ViewGroup.LayoutParams.WRAP_CONTENT: + hs = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + break; + case ViewGroup.LayoutParams.MATCH_PARENT: + hs = MeasureSpec.makeMeasureSpec(heightSize - paddingVertical - labelHeight, heightMode); + break; + default: + hs = MeasureSpec.makeMeasureSpec(params.height, MeasureSpec.EXACTLY); + break; + } + + v.measure(ws, hs); + width = v.getMeasuredWidth(); + height = v.getMeasuredHeight(); + } + + width = Math.max(mMinWidth, Math.max(labelWidth, width) + paddingHorizontal); + height = Math.max(mMinHeight, height + labelHeight + paddingVertical); + + switch (widthMode){ + case MeasureSpec.AT_MOST: + width = Math.min(widthSize, width); + break; + case MeasureSpec.EXACTLY: + width = widthSize; + break; + } + + switch (heightMode){ + case MeasureSpec.AT_MOST: + height = Math.min(heightSize, height); + break; + case MeasureSpec.EXACTLY: + height = heightSize; + break; + } + + setMeasuredDimension(width, height); + + if(v != null){ + ViewGroup.LayoutParams params = v.getLayoutParams(); + int viewWidth; + int viewHeight; + switch (params.width){ + case ViewGroup.LayoutParams.WRAP_CONTENT: + viewWidth = v.getMeasuredWidth(); + break; + case ViewGroup.LayoutParams.MATCH_PARENT: + viewWidth = width - paddingHorizontal; + break; + default: + viewWidth = params.width; + break; + } + switch (params.height){ + case ViewGroup.LayoutParams.WRAP_CONTENT: + viewHeight = v.getMeasuredHeight(); + break; + case ViewGroup.LayoutParams.MATCH_PARENT: + viewHeight = height - labelHeight - paddingVertical; + break; + default: + viewHeight = params.height; + break; + } + + if(v.getMeasuredWidth() != viewWidth || v.getMeasuredHeight() != viewHeight) + v.measure(MeasureSpec.makeMeasureSpec(viewWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(viewHeight, MeasureSpec.EXACTLY)); + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + int w = r - l; + int h = b - t; + int arrowWidth = getArrowDrawableWidth(); + + if(mArrowDrawable != null) { + int top = getPaddingTop() + (mLabelView == null ? 0 : mLabelView.getMeasuredHeight()); + int bottom = h - getDividerDrawableHeight() - getPaddingBottom(); + if(mIsRtl) + mArrowDrawable.setBounds(getPaddingLeft(), top, getPaddingLeft() + arrowWidth, bottom); + else + mArrowDrawable.setBounds(getWidth() - getPaddingRight() - arrowWidth, top, getWidth() - getPaddingRight(), bottom); + } + + if(mDividerDrawable != null) + mDividerDrawable.setBounds(getPaddingLeft(), h - mDividerHeight - getPaddingBottom(), w - getPaddingRight(), h - getPaddingBottom()); + + int childLeft = mIsRtl ? (getPaddingLeft() + arrowWidth) : getPaddingLeft(); + int childRight = mIsRtl ? (w - getPaddingRight()) : (w - getPaddingRight() - arrowWidth); + int childTop = getPaddingTop(); + int childBottom = h - getPaddingBottom(); + + if(mLabelView != null){ + if(mIsRtl) + mLabelView.layout(childRight - mLabelView.getMeasuredWidth(), childTop, childRight, childTop + mLabelView.getMeasuredHeight()); + else + mLabelView.layout(childLeft, childTop, childLeft + mLabelView.getMeasuredWidth(), childTop + mLabelView.getMeasuredHeight()); + childTop += mLabelView.getMeasuredHeight(); + } + + View v = getSelectedView(); + if(v != null){ + int x, y; + + int horizontalGravity = mGravity & Gravity.HORIZONTAL_GRAVITY_MASK; + if(horizontalGravity == Gravity.START) + horizontalGravity = mIsRtl ? Gravity.RIGHT : Gravity.LEFT; + else if(horizontalGravity == Gravity.END) + horizontalGravity = mIsRtl ? Gravity.LEFT : Gravity.RIGHT; + + switch (horizontalGravity) { + case Gravity.LEFT: + x = childLeft; + break; + case Gravity.CENTER_HORIZONTAL: + x = (childRight - childLeft - v.getMeasuredWidth()) / 2 + childLeft; + break; + case Gravity.RIGHT: + x = childRight - v.getMeasuredWidth(); + break; + default: + x = (childRight - childLeft - v.getMeasuredWidth()) / 2 + childLeft; + break; + } + + int verticalGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; + + switch (verticalGravity) { + case Gravity.TOP: + y = childTop; + break; + case Gravity.CENTER_VERTICAL: + y = (childBottom - childTop - v.getMeasuredHeight()) / 2 + childTop; + break; + case Gravity.BOTTOM: + y = childBottom - v.getMeasuredHeight(); + break; + default: + y = (childBottom - childTop - v.getMeasuredHeight()) / 2 + childTop; + break; + } + + v.layout(x, y, x + v.getMeasuredWidth(), y + v.getMeasuredHeight()); + } + } + + @Override + public void draw(@NonNull Canvas canvas) { + super.draw(canvas); + if(mDividerDrawable != null) + mDividerDrawable.draw(canvas); + if(mArrowDrawable != null) + mArrowDrawable.draw(canvas); + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + if(mArrowDrawable != null) + mArrowDrawable.setState(getDrawableState()); + if(mDividerDrawable != null) + mDividerDrawable.setState(getDrawableState()); + } + + public boolean performItemClick(View view, int position, long id) { + if (mOnItemClickListener != null) { +// playSoundEffect(SoundEffectConstants.CLICK); +// if (view != null) +// view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); + + if(mOnItemClickListener.onItemClick(this, view, position, id)) + setSelection(position); + + return true; + } + else + setSelection(position); + + return false; + } + + private void onDataChanged(){ + if(mSelectedPosition == INVALID_POSITION) + setSelection(0); + else if(mSelectedPosition < mAdapter.getCount()) + onDataInvalidated(); + else + setSelection(mAdapter.getCount() - 1); + } + + private void onDataInvalidated(){ + if(mAdapter == null) + return; + + if(mLabelView == null) + removeAllViews(); + else + for(int i = getChildCount() - 1; i > 0; i--) + removeViewAt(i); + + int type = mAdapter.getItemViewType(mSelectedPosition); + View v = mAdapter.getView(mSelectedPosition, mRecycler.get(type), this); + v.setFocusable(false); + v.setClickable(false); + + if(v.getParent() != null) + ((ViewGroup)v.getParent()).removeView(v); + + super.addView(v); + + mRecycler.put(type, v); + } + + private void showPopup(){ + if (!mPopup.isShowing()){ + mPopup.show(); + final ListView lv = mPopup.getListView(); + if(lv != null){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) + lv.setChoiceMode(ListView.CHOICE_MODE_SINGLE); + lv.setSelection(getSelectedItemPosition()); + if(mArrowDrawable != null && mArrowAnimSwitchMode) + lv.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + lv.getViewTreeObserver().removeOnPreDrawListener(this); + mArrowDrawable.setMode(ArrowDrawable.MODE_UP, true); + return true; + } + }); + } + + } + } + + private void onPopupDismissed(){ + if(mArrowDrawable != null) + mArrowDrawable.setMode(ArrowDrawable.MODE_DOWN, true); + } + + private int measureContentWidth(SpinnerAdapter adapter, Drawable background) { + if (adapter == null) + return 0; + + int width = 0; + View itemView = null; + int itemType = 0; + + final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + + // Make sure the number of items we'll measure is capped. If it's a huge data set + // with wildly varying sizes, oh well. + int start = Math.max(0, getSelectedItemPosition()); + final int end = Math.min(adapter.getCount(), start + MAX_ITEMS_MEASURED); + final int count = end - start; + start = Math.max(0, start - (MAX_ITEMS_MEASURED - count)); + for (int i = start; i < end; i++) { + final int positionType = adapter.getItemViewType(i); + if (positionType != itemType) { + itemType = positionType; + itemView = null; + } + itemView = adapter.getView(i, itemView, null); + if (itemView.getLayoutParams() == null) + itemView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); + + itemView.measure(widthMeasureSpec, heightMeasureSpec); + width = Math.max(width, itemView.getMeasuredWidth()); + } + + // Add background padding to measured width + if (background != null) { + background.getPadding(mTempRect); + width += mTempRect.left + mTempRect.right; + } + + return width; + } + + static class SavedState extends BaseSavedState { + + int position; + boolean showDropdown; + + SavedState(Parcelable superState) { + super(superState); + } + + /** + * Constructor called from {@link #CREATOR} + */ + SavedState(Parcel in) { + super(in); + position = in.readInt(); + showDropdown = in.readByte() != 0; + } + + @Override + public void writeToParcel(@NonNull Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeInt(position); + out.writeByte((byte) (showDropdown ? 1 : 0)); + } + + @Override + public String toString() { + return "AbsSpinner.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " position=" + position + + " showDropdown=" + showDropdown + "}"; + } + + public static final Creator CREATOR + = new Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + @Override + public Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState ss = new SavedState(superState); + ss.position = getSelectedItemPosition(); + ss.showDropdown = mPopup != null && mPopup.isShowing(); + return ss; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + + super.onRestoreInstanceState(ss.getSuperState()); + + setSelection(ss.position); + + if (ss.showDropdown) { + ViewTreeObserver vto = getViewTreeObserver(); + if (vto != null) { + final ViewTreeObserver.OnGlobalLayoutListener listener = new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + showPopup(); + final ViewTreeObserver vto = getViewTreeObserver(); + if (vto != null) + vto.removeGlobalOnLayoutListener(this); + } + }; + vto.addOnGlobalLayoutListener(listener); + } + } + } + + private class SpinnerDataSetObserver extends DataSetObserver{ + + @Override + public void onChanged() { + onDataChanged(); + } + + @Override + public void onInvalidated() { + onDataInvalidated(); + } + + } + + private class RecycleBin { + private final SparseArray mScrapHeap = new SparseArray<>(); + + public void put(int type, View v) { + mScrapHeap.put(type, v); + } + + View get(int type) { + View result = mScrapHeap.get(type); + if (result != null) + mScrapHeap.delete(type); + + return result; + } + + void clear() { + final SparseArray scrapHeap = mScrapHeap; + scrapHeap.clear(); + } + } + + private static class DropDownAdapter implements ListAdapter, SpinnerAdapter, OnClickListener { + + private SpinnerAdapter mAdapter; + + private ListAdapter mListAdapter; + + private AdapterView.OnItemClickListener mOnItemClickListener; + + /** + *

Creates a new ListAdapter wrapper for the specified adapter.

+ * + * @param adapter the Adapter to transform into a ListAdapter + */ + public DropDownAdapter(SpinnerAdapter adapter) { + this.mAdapter = adapter; + if (adapter instanceof ListAdapter) + this.mListAdapter = (ListAdapter) adapter; + } + + public void setOnItemClickListener(AdapterView.OnItemClickListener listener){ + mOnItemClickListener = listener; + } + + @Override + public void onClick(View v) { + int position = (Integer) v.getTag(); + if(mOnItemClickListener != null) + mOnItemClickListener.onItemClick(null, v, position, 0); + } + + public int getCount() { + return mAdapter == null ? 0 : mAdapter.getCount(); + } + + public Object getItem(int position) { + return mAdapter == null ? null : mAdapter.getItem(position); + } + + public long getItemId(int position) { + return mAdapter == null ? -1 : mAdapter.getItemId(position); + } + + public View getView(int position, View convertView, ViewGroup parent) { + View v = getDropDownView(position, convertView, parent); + v.setOnClickListener(this); + v.setTag(position); + return v; + } + + public View getDropDownView(int position, View convertView, ViewGroup parent) { + return (mAdapter == null) ? null : mAdapter.getDropDownView(position, convertView, parent); + } + + public boolean hasStableIds() { + return mAdapter != null && mAdapter.hasStableIds(); + } + + /** + * If the wrapped SpinnerAdapter is also a ListAdapter, delegate this call. Otherwise, + * return true. + */ + public boolean areAllItemsEnabled() { + final ListAdapter adapter = mListAdapter; + return adapter == null || adapter.areAllItemsEnabled(); + } + + /** + * If the wrapped SpinnerAdapter is also a ListAdapter, delegate this call. Otherwise, + * return true. + */ + public boolean isEnabled(int position) { + final ListAdapter adapter = mListAdapter; + return adapter == null || adapter.isEnabled(position); + } + + public int getItemViewType(int position) { + final ListAdapter adapter = mListAdapter; + if (adapter != null) + return adapter.getItemViewType(position); + else + return 0; + } + + public int getViewTypeCount() { + final ListAdapter adapter = mListAdapter; + if (adapter != null) + return adapter.getViewTypeCount(); + else + return 1; + } + + public boolean isEmpty() { + return getCount() == 0; + } + + @Override + public void registerDataSetObserver(DataSetObserver observer) { + if (mAdapter != null) + mAdapter.registerDataSetObserver(observer); + } + + @Override + public void unregisterDataSetObserver(DataSetObserver observer) { + if (mAdapter != null) + mAdapter.unregisterDataSetObserver(observer); + } + } + + private class DropdownPopup extends ListPopupWindow { + + private CharSequence mHintText; + + private DropDownAdapter mAdapter; + + private ViewTreeObserver.OnGlobalLayoutListener layoutListener = new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + computeContentWidth(); + + // Use super.show here to update; we don't want to move the selected + // position or adjust other things that would be reset otherwise. + DropdownPopup.super.show(); + } + }; + + public DropdownPopup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + setAnchorView(Spinner.this); + setModal(true); + setPromptPosition(POSITION_PROMPT_ABOVE); + + setOnDismissListener(new PopupWindow.OnDismissListener() { + + @SuppressWarnings("deprecation") + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + @Override + public void onDismiss() { + final ViewTreeObserver vto = getViewTreeObserver(); + if (vto != null) { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) + vto.removeOnGlobalLayoutListener(layoutListener); + else + vto.removeGlobalOnLayoutListener(layoutListener); + } + onPopupDismissed(); + } + + }); + } + + @Override + public void setAdapter(ListAdapter adapter) { + super.setAdapter(adapter); + mAdapter = (DropDownAdapter)adapter; + mAdapter.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View v, int position, long id) { + Spinner.this.performItemClick(v, position, mAdapter.getItemId(position)); + dismiss(); + } + }); + } + + public CharSequence getHintText() { + return mHintText; + } + + public void setPromptText(CharSequence hintText) { + mHintText = hintText; + } + + void computeContentWidth() { + final Drawable background = getBackground(); + int hOffset = 0; + if (background != null) { + background.getPadding(mTempRect); + hOffset = mIsRtl ? mTempRect.right : -mTempRect.left; + } else + mTempRect.left = mTempRect.right = 0; + + final int spinnerPaddingLeft = Spinner.this.getPaddingLeft(); + final int spinnerPaddingRight = Spinner.this.getPaddingRight(); + final int spinnerWidth = Spinner.this.getWidth(); + + if (mDropDownWidth == WRAP_CONTENT) { + int contentWidth = measureContentWidth((SpinnerAdapter) mAdapter, getBackground()); + final int contentWidthLimit = getContext().getResources().getDisplayMetrics().widthPixels - mTempRect.left - mTempRect.right; + if (contentWidth > contentWidthLimit) + contentWidth = contentWidthLimit; + + setContentWidth(Math.max(contentWidth, spinnerWidth - spinnerPaddingLeft - spinnerPaddingRight)); + } else if (mDropDownWidth == MATCH_PARENT) + setContentWidth(spinnerWidth - spinnerPaddingLeft - spinnerPaddingRight); + else + setContentWidth(mDropDownWidth); + + if (mIsRtl) + hOffset += spinnerWidth - spinnerPaddingRight - getWidth(); + else + hOffset += spinnerPaddingLeft; + + setHorizontalOffset(hOffset); + } + + public void show() { + final boolean wasShowing = isShowing(); + + computeContentWidth(); + setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED); + super.show(); + + if (wasShowing) { + // Skip setting up the layout/dismiss listener below. If we were previously + // showing it will still stick around. + return; + } + + // Make sure we hide if our anchor goes away. + // TODO: This might be appropriate to push all the way down to PopupWindow, + // but it may have other side effects to investigate first. (Text editing handles, etc.) + final ViewTreeObserver vto = getViewTreeObserver(); + if (vto != null) + vto.addOnGlobalLayoutListener(layoutListener); + } + } + +} diff --git a/material/src/main/java/com/rey/material/widget/Switch.java b/material/src/main/java/com/rey/material/widget/Switch.java new file mode 100644 index 0000000..ca63ce8 --- /dev/null +++ b/material/src/main/java/com/rey/material/widget/Switch.java @@ -0,0 +1,690 @@ +package com.rey.material.widget; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.RadialGradient; +import android.graphics.RectF; +import android.graphics.Shader; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.animation.AnimationUtils; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; +import android.widget.Checkable; + +import com.rey.material.R; +import com.rey.material.util.ThemeManager; +import com.rey.material.drawable.RippleDrawable; +import com.rey.material.util.ColorUtil; +import com.rey.material.util.RippleManager; +import com.rey.material.util.ThemeUtil; +import com.rey.material.util.ViewUtil; + +public class Switch extends View implements Checkable, ThemeManager.OnThemeChangedListener { + + private RippleManager mRippleManager; + protected int mStyleId; + protected int mCurrentStyle = ThemeManager.THEME_UNDEFINED; + + private boolean mRunning = false; + + private Paint mPaint; + private RectF mDrawRect; + private RectF mTempRect; + private Path mTrackPath; + + private int mTrackSize = -1; + private ColorStateList mTrackColors; + private Paint.Cap mTrackCap = Paint.Cap.ROUND; + private int mThumbRadius = -1; + private ColorStateList mThumbColors; + private float mThumbPosition; + private int mMaxAnimDuration = -1; + private Interpolator mInterpolator; + private int mGravity = Gravity.CENTER_VERTICAL; + + private boolean mChecked = false; + private float mMemoX; + + private float mStartX; + private float mFlingVelocity; + + private long mStartTime; + private int mAnimDuration; + private float mStartPosition; + + private int[] mTempStates = new int[2]; + + private int mShadowSize = -1; + private int mShadowOffset = -1; + private Path mShadowPath; + private Paint mShadowPaint; + + private static final int COLOR_SHADOW_START = 0x4C000000; + private static final int COLOR_SHADOW_END = 0x00000000; + + private boolean mIsRtl = false; + + /** + * Interface definition for a callback to be invoked when the checked state is changed. + */ + public interface OnCheckedChangeListener { + /** + * Called when the checked state is changed. + * + * @param view The Switch view. + * @param checked The checked state. + */ + void onCheckedChanged(Switch view, boolean checked); + } + + private OnCheckedChangeListener mOnCheckedChangeListener; + + public Switch(Context context) { + super(context); + + init(context, null, 0, 0); + } + + public Switch(Context context, AttributeSet attrs) { + super(context, attrs); + + init(context, attrs, 0, 0); + } + + public Switch(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + init(context, attrs, defStyleAttr, 0); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public Switch(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + init(context, attrs, defStyleAttr, defStyleRes); + } + + protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + + mDrawRect = new RectF(); + mTempRect = new RectF(); + mTrackPath = new Path(); + + mFlingVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity(); + + applyStyle(context, attrs, defStyleAttr, defStyleRes); + + if (!isInEditMode()) + mStyleId = ThemeManager.getStyleId(context, attrs, defStyleAttr, defStyleRes); + } + + public void applyStyle(int resId) { + ViewUtil.applyStyle(this, resId); + applyStyle(getContext(), null, 0, resId); + } + + protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + getRippleManager().onCreate(this, context, attrs, defStyleAttr, defStyleRes); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Switch, defStyleAttr, defStyleRes); + + for (int i = 0, count = a.getIndexCount(); i < count; i++) { + int attr = a.getIndex(i); + if (attr == R.styleable.Switch_sw_trackSize) + mTrackSize = a.getDimensionPixelSize(attr, 0); + else if (attr == R.styleable.Switch_sw_trackColor) + mTrackColors = a.getColorStateList(attr); + else if (attr == R.styleable.Switch_sw_trackCap) { + int cap = a.getInteger(attr, 0); + if (cap == 0) + mTrackCap = Paint.Cap.BUTT; + else if (cap == 1) + mTrackCap = Paint.Cap.ROUND; + else + mTrackCap = Paint.Cap.SQUARE; + } else if (attr == R.styleable.Switch_sw_thumbColor) + mThumbColors = a.getColorStateList(attr); + else if (attr == R.styleable.Switch_sw_thumbRadius) + mThumbRadius = a.getDimensionPixelSize(attr, 0); + else if (attr == R.styleable.Switch_sw_thumbElevation) { + mShadowSize = a.getDimensionPixelSize(attr, 0); + mShadowOffset = mShadowSize / 2; + } else if (attr == R.styleable.Switch_sw_animDuration) + mMaxAnimDuration = a.getInt(attr, 0); + else if (attr == R.styleable.Switch_android_gravity) + mGravity = a.getInt(attr, 0); + else if (attr == R.styleable.Switch_android_checked) + setCheckedImmediately(a.getBoolean(attr, mChecked)); + else if (attr == R.styleable.Switch_sw_interpolator) { + int resId = a.getResourceId(R.styleable.Switch_sw_interpolator, 0); + if (resId != 0) + mInterpolator = AnimationUtils.loadInterpolator(context, resId); + } + } + + a.recycle(); + + if (mTrackSize < 0) + mTrackSize = ThemeUtil.dpToPx(context, 2); + + if (mThumbRadius < 0) + mThumbRadius = ThemeUtil.dpToPx(context, 8); + + if (mShadowSize < 0) { + mShadowSize = ThemeUtil.dpToPx(context, 2); + mShadowOffset = mShadowSize / 2; + } + + if (mMaxAnimDuration < 0) + mMaxAnimDuration = context.getResources().getInteger(android.R.integer.config_mediumAnimTime); + + if (mInterpolator == null) + mInterpolator = new DecelerateInterpolator(); + + if (mTrackColors == null) { + int[][] states = new int[][]{ + new int[]{-android.R.attr.state_checked}, + new int[]{android.R.attr.state_checked}, + }; + int[] colors = new int[]{ + ColorUtil.getColor(ThemeUtil.colorControlNormal(context, 0xFF000000), 0.5f), + ColorUtil.getColor(ThemeUtil.colorControlActivated(context, 0xFF000000), 0.5f), + }; + + mTrackColors = new ColorStateList(states, colors); + } + + if (mThumbColors == null) { + int[][] states = new int[][]{ + new int[]{-android.R.attr.state_checked}, + new int[]{android.R.attr.state_checked}, + }; + int[] colors = new int[]{ + 0xFAFAFA, + ThemeUtil.colorControlActivated(context, 0xFF000000), + }; + + mThumbColors = new ColorStateList(states, colors); + } + + mPaint.setStrokeCap(mTrackCap); + buildShadow(); + invalidate(); + } + + @Override + public void onThemeChanged(ThemeManager.OnThemeChangedEvent event) { + int style = ThemeManager.getInstance().getCurrentStyle(mStyleId); + if (mCurrentStyle != style) { + mCurrentStyle = style; + applyStyle(mCurrentStyle); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (mStyleId != 0) { + ThemeManager.getInstance().registerOnThemeChangedListener(this); + onThemeChanged(null); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + RippleManager.cancelRipple(this); + if (mStyleId != 0) + ThemeManager.getInstance().unregisterOnThemeChangedListener(this); + } + + @Override + public void setBackground(Drawable drawable) { + Drawable background = getBackground(); + if (background instanceof RippleDrawable && !(drawable instanceof RippleDrawable)) + ((RippleDrawable) background).setBackgroundDrawable(drawable); + else + super.setBackground(drawable); + } + + protected RippleManager getRippleManager() { + if (mRippleManager == null) { + synchronized (RippleManager.class) { + if (mRippleManager == null) + mRippleManager = new RippleManager(); + } + } + + return mRippleManager; + } + + @Override + public void setOnClickListener(OnClickListener l) { + RippleManager rippleManager = getRippleManager(); + if (l == rippleManager) + super.setOnClickListener(l); + else { + rippleManager.setOnClickListener(l); + setOnClickListener(rippleManager); + } + } + + /** + * Set a listener will be called when the checked state is changed. + * + * @param listener The {@link OnCheckedChangeListener} will be called. + */ + public void setOnCheckedChangeListener(OnCheckedChangeListener listener) { + mOnCheckedChangeListener = listener; + } + + @Override + public void setChecked(boolean checked) { + if (mChecked != checked) { + mChecked = checked; + if (mOnCheckedChangeListener != null) + mOnCheckedChangeListener.onCheckedChanged(this, mChecked); + } + + float desPos = mChecked ? 1f : 0f; + + if (mThumbPosition != desPos) + startAnimation(); + } + + /** + * Change the checked state of this Switch immediately without showing animation. + * + * @param checked The checked state. + */ + public void setCheckedImmediately(boolean checked) { + if (mChecked != checked) { + mChecked = checked; + if (mOnCheckedChangeListener != null) + mOnCheckedChangeListener.onCheckedChanged(this, mChecked); + } + mThumbPosition = mChecked ? 1f : 0f; + invalidate(); + } + + @Override + public boolean isChecked() { + return mChecked; + } + + @Override + public void toggle() { + if (isEnabled()) + setChecked(!mChecked); + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + @Override + public void onRtlPropertiesChanged(int layoutDirection) { + boolean rtl = layoutDirection == LAYOUT_DIRECTION_RTL; + if (mIsRtl != rtl) { + mIsRtl = rtl; + invalidate(); + } + } + + @Override + public boolean onTouchEvent(@NonNull MotionEvent event) { + super.onTouchEvent(event); + getRippleManager().onTouchEvent(this, event); + + float x = event.getX(); + if (mIsRtl) + x = 2 * mDrawRect.centerX() - x; + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + if (getParent() != null) + getParent().requestDisallowInterceptTouchEvent(true); + mMemoX = x; + mStartX = mMemoX; + mStartTime = SystemClock.uptimeMillis(); + break; + case MotionEvent.ACTION_MOVE: + float offset = (x - mMemoX) / (mDrawRect.width() - mThumbRadius * 2); + mThumbPosition = Math.min(1f, Math.max(0f, mThumbPosition + offset)); + mMemoX = x; + invalidate(); + break; + case MotionEvent.ACTION_UP: + if (getParent() != null) + getParent().requestDisallowInterceptTouchEvent(false); + + float velocity = (x - mStartX) / (SystemClock.uptimeMillis() - mStartTime) * 1000; + if (Math.abs(velocity) >= mFlingVelocity) + setChecked(velocity > 0); + else if ((!mChecked && mThumbPosition < 0.1f) || (mChecked && mThumbPosition > 0.9f)) + toggle(); + else + setChecked(mThumbPosition > 0.5f); + break; + case MotionEvent.ACTION_CANCEL: + if (getParent() != null) + getParent().requestDisallowInterceptTouchEvent(false); + + setChecked(mThumbPosition > 0.5f); + break; + } + + return true; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + + switch (widthMode) { + case MeasureSpec.UNSPECIFIED: + widthSize = getSuggestedMinimumWidth(); + break; + case MeasureSpec.AT_MOST: + widthSize = Math.min(widthSize, getSuggestedMinimumWidth()); + break; + } + + switch (heightMode) { + case MeasureSpec.UNSPECIFIED: + heightSize = getSuggestedMinimumHeight(); + break; + case MeasureSpec.AT_MOST: + heightSize = Math.min(heightSize, getSuggestedMinimumHeight()); + break; + } + + setMeasuredDimension(widthSize, heightSize); + } + + @Override + public int getSuggestedMinimumWidth() { + return mThumbRadius * 4 + Math.max(mShadowSize, getPaddingLeft()) + Math.max(mShadowSize, getPaddingRight()); + } + + @Override + public int getSuggestedMinimumHeight() { + return mThumbRadius * 2 + Math.max(mShadowSize - mShadowOffset, getPaddingTop()) + Math.max(mShadowSize + mShadowOffset, getPaddingBottom()); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + mDrawRect.left = Math.max(mShadowSize, getPaddingLeft()); + mDrawRect.right = w - Math.max(mShadowSize, getPaddingRight()); + + int height = mThumbRadius * 2; + int align = mGravity & Gravity.VERTICAL_GRAVITY_MASK; + + switch (align) { + case Gravity.TOP: + mDrawRect.top = Math.max(mShadowSize - mShadowOffset, getPaddingTop()); + mDrawRect.bottom = mDrawRect.top + height; + break; + case Gravity.BOTTOM: + mDrawRect.bottom = h - Math.max(mShadowSize + mShadowOffset, getPaddingBottom()); + mDrawRect.top = mDrawRect.bottom - height; + break; + default: + mDrawRect.top = (h - height) / 2f; + mDrawRect.bottom = mDrawRect.top + height; + break; + } + } + + private int getTrackColor(boolean checked) { + mTempStates[0] = isEnabled() ? android.R.attr.state_enabled : -android.R.attr.state_enabled; + mTempStates[1] = checked ? android.R.attr.state_checked : -android.R.attr.state_checked; + + return mTrackColors.getColorForState(mTempStates, 0); + } + + private int getThumbColor(boolean checked) { + mTempStates[0] = isEnabled() ? android.R.attr.state_enabled : -android.R.attr.state_enabled; + mTempStates[1] = checked ? android.R.attr.state_checked : -android.R.attr.state_checked; + + return mThumbColors.getColorForState(mTempStates, 0); + } + + private void buildShadow() { + if (mShadowSize <= 0) + return; + + if (mShadowPaint == null) { + mShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); + mShadowPaint.setStyle(Paint.Style.FILL); + mShadowPaint.setDither(true); + } + float startRatio = (float) mThumbRadius / (mThumbRadius + mShadowSize + mShadowOffset); + mShadowPaint.setShader(new RadialGradient(0, 0, mThumbRadius + mShadowSize, + new int[]{COLOR_SHADOW_START, COLOR_SHADOW_START, COLOR_SHADOW_END}, + new float[]{0f, startRatio, 1f} + , Shader.TileMode.CLAMP)); + + if (mShadowPath == null) { + mShadowPath = new Path(); + mShadowPath.setFillType(Path.FillType.EVEN_ODD); + } else + mShadowPath.reset(); + float radius = mThumbRadius + mShadowSize; + mTempRect.set(-radius, -radius, radius, radius); + mShadowPath.addOval(mTempRect, Path.Direction.CW); + radius = mThumbRadius - 1; + mTempRect.set(-radius, -radius - mShadowOffset, radius, radius - mShadowOffset); + mShadowPath.addOval(mTempRect, Path.Direction.CW); + } + + private void getTrackPath(float x, float y, float radius) { + float halfStroke = mTrackSize / 2f; + + mTrackPath.reset(); + + if (mTrackCap != Paint.Cap.ROUND) { + mTempRect.set(x - radius + 1f, y - radius + 1f, x + radius - 1f, y + radius - 1f); + float angle = (float) (Math.asin(halfStroke / (radius - 1f)) / Math.PI * 180); + + if (x - radius > mDrawRect.left) { + mTrackPath.moveTo(mDrawRect.left, y - halfStroke); + mTrackPath.arcTo(mTempRect, 180 + angle, -angle * 2); + mTrackPath.lineTo(mDrawRect.left, y + halfStroke); + mTrackPath.close(); + } + + if (x + radius < mDrawRect.right) { + mTrackPath.moveTo(mDrawRect.right, y - halfStroke); + mTrackPath.arcTo(mTempRect, -angle, angle * 2); + mTrackPath.lineTo(mDrawRect.right, y + halfStroke); + mTrackPath.close(); + } + } else { + float angle = (float) (Math.asin(halfStroke / (radius - 1f)) / Math.PI * 180); + + if (x - radius > mDrawRect.left) { + float angle2 = (float) (Math.acos(Math.max(0f, (mDrawRect.left + halfStroke - x + radius) / halfStroke)) / Math.PI * 180); + + mTempRect.set(mDrawRect.left, y - halfStroke, mDrawRect.left + mTrackSize, y + halfStroke); + mTrackPath.arcTo(mTempRect, 180 - angle2, angle2 * 2); + + mTempRect.set(x - radius + 1f, y - radius + 1f, x + radius - 1f, y + radius - 1f); + mTrackPath.arcTo(mTempRect, 180 + angle, -angle * 2); + mTrackPath.close(); + } + + if (x + radius < mDrawRect.right) { + float angle2 = (float) Math.acos(Math.max(0f, (x + radius - mDrawRect.right + halfStroke) / halfStroke)); + mTrackPath.moveTo((float) (mDrawRect.right - halfStroke + Math.cos(angle2) * halfStroke), (float) (y + Math.sin(angle2) * halfStroke)); + + angle2 = (float) (angle2 / Math.PI * 180); + mTempRect.set(mDrawRect.right - mTrackSize, y - halfStroke, mDrawRect.right, y + halfStroke); + mTrackPath.arcTo(mTempRect, angle2, -angle2 * 2); + + mTempRect.set(x - radius + 1f, y - radius + 1f, x + radius - 1f, y + radius - 1f); + mTrackPath.arcTo(mTempRect, -angle, angle * 2); + mTrackPath.close(); + } + } + } + + @Override + public void draw(@NonNull Canvas canvas) { + super.draw(canvas); + + float x = (mDrawRect.width() - mThumbRadius * 2) * mThumbPosition + mDrawRect.left + mThumbRadius; + if (mIsRtl) + x = 2 * mDrawRect.centerX() - x; + float y = mDrawRect.centerY(); + + getTrackPath(x, y, mThumbRadius); + mPaint.setColor(ColorUtil.getMiddleColor(getTrackColor(false), getTrackColor(true), mThumbPosition)); + mPaint.setStyle(Paint.Style.FILL); + canvas.drawPath(mTrackPath, mPaint); + + if (mShadowSize > 0) { + int saveCount = canvas.save(); + canvas.translate(x, y + mShadowOffset); + canvas.drawPath(mShadowPath, mShadowPaint); + canvas.restoreToCount(saveCount); + } + + mPaint.setColor(ColorUtil.getMiddleColor(getThumbColor(false), getThumbColor(true), mThumbPosition)); + mPaint.setStyle(Paint.Style.FILL); + canvas.drawCircle(x, y, mThumbRadius, mPaint); + } + + private void resetAnimation() { + mStartTime = SystemClock.uptimeMillis(); + mStartPosition = mThumbPosition; + mAnimDuration = (int) (mMaxAnimDuration * (mChecked ? (1f - mStartPosition) : mStartPosition)); + } + + private void startAnimation() { + if (getHandler() != null) { + resetAnimation(); + mRunning = true; + getHandler().postAtTime(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + } else + mThumbPosition = mChecked ? 1f : 0f; + invalidate(); + } + + private void stopAnimation() { + mRunning = false; + mThumbPosition = mChecked ? 1f : 0f; + if (getHandler() != null) + getHandler().removeCallbacks(mUpdater); + invalidate(); + } + + private final Runnable mUpdater = new Runnable() { + + @Override + public void run() { + update(); + } + + }; + + private void update() { + long curTime = SystemClock.uptimeMillis(); + float progress = Math.min(1f, (float) (curTime - mStartTime) / mAnimDuration); + float value = mInterpolator.getInterpolation(progress); + + mThumbPosition = mChecked ? (mStartPosition * (1 - value) + value) : (mStartPosition * (1 - value)); + + if (progress == 1f) + stopAnimation(); + + if (mRunning) { + if (getHandler() != null) + getHandler().postAtTime(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + else + stopAnimation(); + } + + invalidate(); + } + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + + SavedState ss = new SavedState(superState); + + ss.checked = isChecked(); + return ss; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + + super.onRestoreInstanceState(ss.getSuperState()); + setChecked(ss.checked); + requestLayout(); + } + + static class SavedState extends BaseSavedState { + boolean checked; + + /** + * Constructor called from {@link Switch#onSaveInstanceState()} + */ + SavedState(Parcelable superState) { + super(superState); + } + + /** + * Constructor called from {@link #CREATOR} + */ + private SavedState(Parcel in) { + super(in); + checked = (Boolean) in.readValue(null); + } + + @Override + public void writeToParcel(@NonNull Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeValue(checked); + } + + @Override + public String toString() { + return "Switch.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " checked=" + checked + "}"; + } + + public static final Creator CREATOR + = new Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + +} diff --git a/material/src/main/java/com/rey/material/widget/TabIndicatorView.java b/material/src/main/java/com/rey/material/widget/TabIndicatorView.java new file mode 100644 index 0000000..9e12473 --- /dev/null +++ b/material/src/main/java/com/rey/material/widget/TabIndicatorView.java @@ -0,0 +1,768 @@ +package com.rey.material.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.support.annotation.NonNull; +import android.support.v4.view.ViewPager; +import android.support.v7.widget.DefaultItemAnimator; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Checkable; +import android.widget.ImageView; + +import com.rey.material.R; +import com.rey.material.util.ThemeManager; +import com.rey.material.drawable.RippleDrawable; +import com.rey.material.util.ThemeUtil; +import com.rey.material.util.ViewUtil; + +/** + * Created by Rey on 9/15/2015. + */ +public class TabIndicatorView extends RecyclerView implements ThemeManager.OnThemeChangedListener{ + + protected int mStyleId; + protected int mCurrentStyle = ThemeManager.THEME_UNDEFINED; + + private int mMode; + private int mTabPadding; + private int mTabRippleStyle; + private int mTextAppearance; + private boolean mTabSingleLine; + private boolean mCenterCurrentTab; + + private int mIndicatorOffset; + private int mIndicatorWidth; + private int mIndicatorHeight; + private boolean mIndicatorAtTop; + + private Paint mPaint; + + public static final int MODE_SCROLL = 0; + public static final int MODE_FIXED = 1; + + public static final int SCROLL_STATE_IDLE = 0; + public static final int SCROLL_STATE_DRAGGING = 1; + public static final int SCROLL_STATE_SETTLING = 2; + + private int mSelectedPosition; + private boolean mScrolling; + private boolean mIsRtl; + + private LayoutManager mLayoutManager; + private Adapter mAdapter; + private TabIndicatorFactory mFactory; + + private Runnable mTabAnimSelector; + + private boolean mScrollingToCenter = false; + + public TabIndicatorView(Context context) { + super(context); + + init(context, null, 0, 0); + } + + public TabIndicatorView(Context context, AttributeSet attrs) { + super(context, attrs); + + init(context, attrs, 0, 0); + } + + public TabIndicatorView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + init(context, attrs, defStyleAttr, 0); + } + + protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + setHorizontalScrollBarEnabled(false); + + mTabPadding = -1; + mTabSingleLine = true; + mCenterCurrentTab = false; + mIndicatorHeight = -1; + mIndicatorAtTop = false; + mScrolling = false; + mIsRtl = false; + + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mPaint.setStyle(Paint.Style.FILL); + mPaint.setColor(ThemeUtil.colorAccent(context, 0xFFFFFFFF)); + + mAdapter = new Adapter(); + setAdapter(mAdapter); + mLayoutManager = new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, mIsRtl); + setLayoutManager(mLayoutManager); + setItemAnimator(new DefaultItemAnimator()); + addOnScrollListener(new OnScrollListener() { + + @Override + public void onScrollStateChanged(RecyclerView recyclerView, int newState) { + if (newState == RecyclerView.SCROLL_STATE_IDLE) { + updateIndicator(mLayoutManager.findViewByPosition(mSelectedPosition)); + } + } + + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + updateIndicator(mLayoutManager.findViewByPosition(mSelectedPosition)); + } + + }); + + applyStyle(context, attrs, defStyleAttr, defStyleRes); + + if(!isInEditMode()) + mStyleId = ThemeManager.getStyleId(context, attrs, defStyleAttr, defStyleRes); + } + + public void applyStyle(int resId){ + ViewUtil.applyStyle(this, resId); + applyStyle(getContext(), null, 0, resId); + } + + protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabPageIndicator, defStyleAttr, defStyleRes); + + int tabPadding = -1; + int textAppearance = 0; + int mode = -1; + int rippleStyle = 0; + boolean tabSingleLine = false; + boolean singleLineDefined = false; + + for(int i = 0, count = a.getIndexCount(); i < count; i++){ + int attr = a.getIndex(i); + if(attr == R.styleable.TabPageIndicator_tpi_tabPadding) + tabPadding = a.getDimensionPixelSize(attr, 0); + else if(attr == R.styleable.TabPageIndicator_tpi_tabRipple) + rippleStyle = a.getResourceId(attr, 0); + else if(attr == R.styleable.TabPageIndicator_tpi_indicatorColor) + mPaint.setColor(a.getColor(attr, 0)); + else if(attr == R.styleable.TabPageIndicator_tpi_indicatorHeight) + mIndicatorHeight = a.getDimensionPixelSize(attr, 0); + else if(attr == R.styleable.TabPageIndicator_tpi_indicatorAtTop) + mIndicatorAtTop = a.getBoolean(attr, true); + else if(attr == R.styleable.TabPageIndicator_tpi_tabSingleLine) { + tabSingleLine = a.getBoolean(attr, true); + singleLineDefined = true; + } + else if(attr == R.styleable.TabPageIndicator_tpi_centerCurrentTab) + mCenterCurrentTab = a.getBoolean(attr, true); + else if(attr == R.styleable.TabPageIndicator_android_textAppearance) + textAppearance = a.getResourceId(attr, 0); + else if(attr == R.styleable.TabPageIndicator_tpi_mode) + mode = a.getInteger(attr, 0); + } + + a.recycle(); + + if(mIndicatorHeight < 0) + mIndicatorHeight = ThemeUtil.dpToPx(context, 2); + + boolean shouldNotify = false; + + if(tabPadding >= 0 && mTabPadding != tabPadding){ + mTabPadding = tabPadding; + shouldNotify = true; + } + + if(singleLineDefined && mTabSingleLine != tabSingleLine){ + mTabSingleLine = tabSingleLine; + shouldNotify = true; + } + + if(mode >= 0 && mMode != mode){ + mMode = mode; + mAdapter.setFixedWidth(0, 0); + shouldNotify = true; + } + + if(textAppearance != 0 && mTextAppearance != textAppearance){ + mTextAppearance = textAppearance; + shouldNotify = true; + } + + if(rippleStyle != 0 && rippleStyle != mTabRippleStyle){ + mTabRippleStyle = rippleStyle; + shouldNotify = true; + } + + if(shouldNotify) + mAdapter.notifyItemRangeChanged(0, mAdapter.getItemCount()); + + invalidate(); + } + + public void setTabIndicatorFactory(TabIndicatorFactory factory){ + mFactory = factory; + mAdapter.setFactory(factory); + } + + private void animateToTab(final int position) { + if(position < 0 || position >= mAdapter.getItemCount()) + return; + + if (mTabAnimSelector != null) + removeCallbacks(mTabAnimSelector); + + mTabAnimSelector = new Runnable() { + public void run() { + View v = mLayoutManager.findViewByPosition(position); + if(!mScrolling) + updateIndicator(v); + + smoothScrollToPosition(mSelectedPosition); + mTabAnimSelector = null; + } + }; + + post(mTabAnimSelector); + } + + private void updateIndicator(int offset, int width){ + mIndicatorOffset = offset; + mIndicatorWidth = width; + invalidate(); + } + + private void updateIndicator(View anchorView){ + if(anchorView != null) { + updateIndicator(anchorView.getLeft(), anchorView.getMeasuredWidth()); + ((Checkable)anchorView).setChecked(true); + } + else { + updateIndicator(getWidth(), 0); + } + } + + /** + * Set the current tab of this TabIndicatorView. + * @param position The position of current tab. + */ + public void setCurrentTab(int position) { + if(mSelectedPosition != position){ + View v = mLayoutManager.findViewByPosition(mSelectedPosition); + if(v != null) + ((Checkable)v).setChecked(false); + } + + mSelectedPosition = position; + View v = mLayoutManager.findViewByPosition(mSelectedPosition); + if(v != null) + ((Checkable)v).setChecked(true); + + animateToTab(position); + } + + @Override + public void onThemeChanged(ThemeManager.OnThemeChangedEvent event) { + int style = ThemeManager.getInstance().getCurrentStyle(mStyleId); + if(mCurrentStyle != style){ + mCurrentStyle = style; + applyStyle(mCurrentStyle); + } + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + // Re-post the selector we saved + if (mTabAnimSelector != null) + post(mTabAnimSelector); + + if(mStyleId != 0) { + ThemeManager.getInstance().registerOnThemeChangedListener(this); + onThemeChanged(null); + } + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (mTabAnimSelector != null) + removeCallbacks(mTabAnimSelector); + + if(mStyleId != 0) + ThemeManager.getInstance().unregisterOnThemeChangedListener(this); + } + + @Override + public void onRtlPropertiesChanged(int layoutDirection) { + boolean rtl = layoutDirection == LAYOUT_DIRECTION_RTL; + if(mIsRtl != rtl) { + mIsRtl = rtl; + mLayoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, mIsRtl); + setLayoutManager(mLayoutManager); + requestLayout(); + } + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + super.onMeasure(widthSpec, heightSpec); + + if(mMode == MODE_FIXED){ + int totalWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); + int count = mAdapter.getItemCount(); + if(count > 0) { + int width = totalWidth / count; + int lastWidth = totalWidth - width * (count - 1); + mAdapter.setFixedWidth(width, lastWidth); + } + else + mAdapter.setFixedWidth(totalWidth, totalWidth); + } + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + updateIndicator(mLayoutManager.findViewByPosition(mSelectedPosition)); + } + + @Override + public void draw(@NonNull Canvas canvas) { + super.draw(canvas); + + int x = mIndicatorOffset; + int y = mIndicatorAtTop ? 0 : getHeight() - mIndicatorHeight; + canvas.drawRect(x, y, x + mIndicatorWidth, y + mIndicatorHeight, mPaint); + + //TODO: handle it + +// if(isInEditMode()) +// canvas.drawRect(getPaddingLeft(), y, getPaddingLeft() + mTabContainer.getChildAt(0).getWidth(), y + mIndicatorHeight, mPaint); + } + + protected void onTabScrollStateChanged(int state){ + if(mCenterCurrentTab) { + if (state == SCROLL_STATE_IDLE) { + if (!mScrollingToCenter) { + View v = mLayoutManager.findViewByPosition(mSelectedPosition); + if (v != null) { + int viewCenter = (v.getLeft() + v.getRight()) / 2; + int parentCenter = (getLeft() + getPaddingLeft() + getRight() - getPaddingRight()) / 2; + int scrollNeeded = viewCenter - parentCenter; + if (scrollNeeded != 0) { + smoothScrollBy(scrollNeeded, 0); + mScrollingToCenter = true; + } + } + } + } + + if (state == SCROLL_STATE_DRAGGING || state == SCROLL_STATE_SETTLING) + mScrollingToCenter = false; + } + + if(state == ViewPager.SCROLL_STATE_IDLE){ + mScrolling = false; + View v = mLayoutManager.findViewByPosition(mSelectedPosition); + updateIndicator(v); + } + else + mScrolling = true; + } + + protected void onTabScrolled(int position, float positionOffset) { + View scrollView = mLayoutManager.findViewByPosition(position); + View nextView = mLayoutManager.findViewByPosition(position + 1); + + if(scrollView != null && nextView != null){ + int width_scroll = scrollView.getMeasuredWidth(); + int width_next = nextView.getMeasuredWidth(); + float distance = (width_scroll + width_next) / 2f; + + int width = (int)(width_scroll + (width_next - width_scroll) * positionOffset + 0.5f); + int offset = (int)(scrollView.getLeft() + width_scroll / 2f + distance * positionOffset - width / 2f + 0.5f); + updateIndicator(offset, width); + } + } + + protected void onTabSelected(int position){ + setCurrentTab(position); + } + + public static abstract class TabIndicatorFactory { + + private TabIndicatorView mView; + + /** + * Get the number of tab indicators. + * @return + */ + public abstract int getTabIndicatorCount(); + + /** + * Check if the tab indicator at specific position is icon or text. + * @param position The position of tab indicator. + * @return + */ + public abstract boolean isIconTabIndicator(int position); + + /** + * Get the icon for tab indicator at specific position. + * @param position The position of tab indicator. + * @return + */ + public abstract Drawable getIcon(int position); + + /** + * Get the text for tab indicator at specific position. + * @param position The position of tab indicator. + * @return + */ + public abstract CharSequence getText(int position); + + /** + * Get the current selected tab. + * @return + */ + public abstract int getCurrentTabIndicator(); + + /** + * Notify the selected tab indicator has changed. Your layout should be updated to reflect the changes of TabIndicatorView. + * @param position The position of selected tab indicator. + */ + public abstract void onTabIndicatorSelected(int position); + + protected void setTabIndicatorView(TabIndicatorView view){ + mView = view; + } + + /** + * Notify the scroll state of your tab layout has changed, and the TabIndicatorView should update to reflect the changes. + * @param state The new scroll state. + * @see TabIndicatorView#SCROLL_STATE_IDLE + * @see TabIndicatorView#SCROLL_STATE_DRAGGING + * @see TabIndicatorView#SCROLL_STATE_SETTLING + */ + public final void notifyTabScrollStateChanged(int state){ + mView.onTabScrollStateChanged(state); + } + + /** + * Notify the current tab is scrolled, and the TabIndicatorView should update to reflect the changes. + * + * @param position Position of the first left tab . + * @param positionOffset Value from [0, 1) indicating the offset from the page at position. + */ + public final void notifyTabScrolled(int position, float positionOffset) { + mView.onTabScrolled(position, positionOffset); + } + + /** + * Notify a new tab becomes selected, and the TabIndicatorView should update to reflect the changes. + * Animation is not necessarily complete. + * + * @param position Position of the new selected tab. + */ + public final void notifyTabSelected(int position){ + mView.onTabSelected(position); + } + + /** + * Notify tab's data set has changed, and the TabIndicatorView should update to reflect the changes. + */ + public final void notifyDataSetChanged(){ + mView.getAdapter().notifyDataSetChanged(); + } + + /** + * Notify the tab at specific position has beenchanged, and the TabIndicatorView should update to reflect the changes. + * @param position Position of the tab. + */ + public final void notifyTabChanged(int position) { + mView.getAdapter().notifyItemRangeChanged(position, 1); + } + + /** + * Notify the range of tab has been changed, and the TabIndicatorView should update to reflect the changes. + * @param positionStart The start position of range. + * @param itemCount The number of tabs. + */ + public final void notifyTabRangeChanged(int positionStart, int itemCount) { + mView.getAdapter().notifyItemRangeChanged(positionStart, itemCount); + } + + /** + * Notify the tab at specific position has been inserted, and the TabIndicatorView should update to reflect the changes. + * @param position Position of the tab. + */ + public final void notifyTabInserted(int position) { + mView.getAdapter().notifyItemRangeInserted(position, 1); + } + + /** + * Notify the tab at specific position has been moved, and the TabIndicatorView should update to reflect the changes. + * @param fromPosition The old position of the tab. + * @param toPosition The new position of the tab. + */ + public final void notifyTabMoved(int fromPosition, int toPosition) { + mView.getAdapter().notifyItemMoved(fromPosition, toPosition); + } + + /** + * Notify the range of tab has been inserted, and the TabIndicatorView should update to reflect the changes. + * @param positionStart The start position of range. + * @param itemCount The number of tabs. + */ + public final void notifyTabRangeInserted(int positionStart, int itemCount) { + mView.getAdapter().notifyItemRangeInserted(positionStart, itemCount); + } + + /** + * Notify the tab at specific position has been removed, and the TabIndicatorView should update to reflect the changes. + * @param position Position of the tab. + */ + public final void notifyTabRemoved(int position) { + mView.getAdapter().notifyItemRangeRemoved(position, 1); + } + + /** + * Notify the range of tab has been removed, and the TabIndicatorView should update to reflect the changes. + * @param positionStart The start position of range. + * @param itemCount The number of tabs. + */ + public final void notifyTabRangeRemoved(int positionStart, int itemCount) { + mView.getAdapter().notifyItemRangeRemoved(positionStart, itemCount); + } + } + + class Adapter extends RecyclerView.Adapter implements View.OnClickListener { + + TabIndicatorFactory mFactory; + + static final int TYPE_TEXT = 0; + static final int TYPE_ICON = 1; + + int mFixedWidth; + int mLastFixedWidth; + + public void setFactory(TabIndicatorFactory factory){ + if(mFactory != null) + mFactory.setTabIndicatorView(null); + + int prevCount = getItemCount(); + if(prevCount > 0) + notifyItemRangeRemoved(0, prevCount); + + mFactory = factory; + if(mFactory != null) + mFactory.setTabIndicatorView(TabIndicatorView.this); + int count = getItemCount(); + if(count > 0) + notifyItemRangeInserted(0, count); + + if(mFactory != null) + onTabSelected(mFactory.getCurrentTabIndicator()); + } + + public void setFixedWidth(int width, int lastWidth){ + if(mFixedWidth != width || mLastFixedWidth != lastWidth){ + mFixedWidth = width; + mLastFixedWidth = lastWidth; + + int count = getItemCount(); + if(count > 0) + notifyItemRangeChanged(0, count); + } + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View v = null; + switch (viewType){ + case TYPE_TEXT: + v = new CheckedTextView(parent.getContext()); + break; + case TYPE_ICON: + v = new ImageButton(parent.getContext()); + break; + } + + ViewHolder holder = new ViewHolder(v); + v.setTag(holder); + v.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)); + v.setOnClickListener(this); + + switch (viewType){ + case TYPE_TEXT: + holder.textView.setCheckMarkDrawable(null); + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) + holder.textView.setTextAlignment(TEXT_ALIGNMENT_GRAVITY); + holder.textView.setGravity(Gravity.CENTER); + holder.textView.setEllipsize(TextUtils.TruncateAt.END); + holder.textView.setSingleLine(true); + break; + case TYPE_ICON: + holder.iconView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); + break; + } + + return holder; + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + int viewType = getItemViewType(position); + + ViewGroup.LayoutParams params = holder.itemView.getLayoutParams(); + if(mFixedWidth > 0) + params.width = position == getItemCount() - 1 ? mLastFixedWidth : mFixedWidth; + else + params.width = ViewGroup.LayoutParams.WRAP_CONTENT; + holder.itemView.setLayoutParams(params); + + if(holder.padding != mTabPadding){ + holder.padding = mTabPadding; + holder.itemView.setPadding(mTabPadding, 0, mTabPadding, 0); + } + + if(holder.rippleStyle != mTabRippleStyle){ + holder.rippleStyle = mTabRippleStyle; + if(mTabRippleStyle > 0) + ViewUtil.setBackground(holder.itemView, new RippleDrawable.Builder(getContext(), mTabRippleStyle).build()); + } + + switch (viewType){ + case TYPE_TEXT: + if(holder.textAppearance != mTextAppearance) { + holder.textAppearance = mTextAppearance; + holder.textView.setTextAppearance(getContext(), mTextAppearance); + } + if(holder.singleLine != mTabSingleLine) { + holder.singleLine = mTabSingleLine; + if (mTabSingleLine) + holder.textView.setSingleLine(true); + else { + holder.textView.setSingleLine(false); + holder.textView.setMaxLines(2); + } + } + + holder.textView.setText(mFactory.getText(position)); + holder.textView.setChecked(position == mSelectedPosition); + break; + case TYPE_ICON: + holder.iconView.setImageDrawable(mFactory.getIcon(position)); + holder.iconView.setChecked(position == mSelectedPosition); + break; + } + } + + @Override + public int getItemViewType(int position) { + return mFactory.isIconTabIndicator(position) ? TYPE_ICON : TYPE_TEXT; + } + + @Override + public int getItemCount() { + return mFactory == null ? 0 : mFactory.getTabIndicatorCount(); + } + + @Override + public void onClick(View view) { + ViewHolder holder = (ViewHolder) view.getTag(); + mFactory.onTabIndicatorSelected(holder.getAdapterPosition()); + } + } + + class ViewHolder extends RecyclerView.ViewHolder{ + + CheckedTextView textView; + + CheckedImageView iconView; + + int rippleStyle = 0; + boolean singleLine = true; + int textAppearance = 0; + int padding = 0; + + public ViewHolder(View itemView) { + super(itemView); + if(itemView instanceof CheckedImageView) + iconView = (CheckedImageView)itemView; + else if(itemView instanceof CheckedTextView) + textView = (CheckedTextView)itemView; + } + + } + + public static class ViewPagerIndicatorFactory extends TabIndicatorFactory implements ViewPager.OnPageChangeListener { + + ViewPager mViewPager; + + public ViewPagerIndicatorFactory(ViewPager vp){ + mViewPager = vp; + mViewPager.addOnPageChangeListener(this); + } + + @Override + public int getTabIndicatorCount() { + return mViewPager.getAdapter().getCount(); + } + + @Override + public boolean isIconTabIndicator(int position) { + return false; + } + + @Override + public Drawable getIcon(int position) { + return null; + } + + @Override + public CharSequence getText(int position) { + return mViewPager.getAdapter().getPageTitle(position); + } + + @Override + public void onTabIndicatorSelected(int position) { + mViewPager.setCurrentItem(position, true); + } + + @Override + public int getCurrentTabIndicator() { + return mViewPager.getCurrentItem(); + } + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + notifyTabScrolled(position, positionOffset); + } + + @Override + public void onPageSelected(int position) { + notifyTabSelected(position); + } + + @Override + public void onPageScrollStateChanged(int state) { + switch (state){ + case ViewPager.SCROLL_STATE_IDLE: + notifyTabScrollStateChanged(SCROLL_STATE_IDLE); + break; + case ViewPager.SCROLL_STATE_DRAGGING: + notifyTabScrollStateChanged(SCROLL_STATE_DRAGGING); + break; + case ViewPager.SCROLL_STATE_SETTLING: + notifyTabScrollStateChanged(SCROLL_STATE_SETTLING); + break; + } + } + } + +} diff --git a/material/src/main/java/com/rey/material/widget/TabPageIndicator.java b/material/src/main/java/com/rey/material/widget/TabPageIndicator.java new file mode 100644 index 0000000..c9d0b55 --- /dev/null +++ b/material/src/main/java/com/rey/material/widget/TabPageIndicator.java @@ -0,0 +1,646 @@ +package com.rey.material.widget; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.TypedArray; +import android.database.DataSetObserver; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.os.Build; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.view.PagerAdapter; +import android.support.v4.view.ViewPager; +import android.text.TextUtils.TruncateAt; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.HorizontalScrollView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.rey.material.R; +import com.rey.material.util.ThemeManager; +import com.rey.material.drawable.RippleDrawable; +import com.rey.material.util.ThemeUtil; +import com.rey.material.util.ViewUtil; + +@TargetApi(Build.VERSION_CODES.JELLY_BEAN) +public class TabPageIndicator extends HorizontalScrollView implements ViewPager.OnPageChangeListener, View.OnClickListener, ThemeManager.OnThemeChangedListener{ + + protected int mStyleId; + protected int mCurrentStyle = ThemeManager.THEME_UNDEFINED; + + private TabContainerLayout mTabContainer; + private ViewPager mViewPager; + + private int mMode; + private int mTabPadding; + private int mTabRippleStyle; + private int mTextAppearance; + private boolean mTabSingleLine; + + private int mIndicatorOffset; + private int mIndicatorWidth; + private int mIndicatorHeight; + private boolean mIndicatorAtTop; + + private Paint mPaint; + + public static final int MODE_SCROLL = 0; + public static final int MODE_FIXED = 1; + + private int mSelectedPosition; + private boolean mScrolling; + private boolean mIsRtl; + + private Runnable mTabAnimSelector; + + private ViewPager.OnPageChangeListener mListener; + + private DataSetObserver mObserver = new DataSetObserver(){ + + @Override + public void onChanged() { + notifyDataSetChanged(); + } + + @Override + public void onInvalidated() { + notifyDataSetInvalidated(); + } + + }; + + public TabPageIndicator(Context context) { + super(context); + + init(context, null, 0, 0); + } + + public TabPageIndicator(Context context, AttributeSet attrs) { + super(context, attrs); + + init(context, attrs, 0, 0); + } + + public TabPageIndicator(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + init(context, attrs, defStyleAttr, 0); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public TabPageIndicator(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + init(context, attrs, defStyleAttr, defStyleRes); + } + + protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + setHorizontalScrollBarEnabled(false); + + mTabPadding = -1; + mTabSingleLine = true; + mIndicatorHeight = -1; + mIndicatorAtTop = false; + mScrolling = false; + mIsRtl = false; + + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mPaint.setStyle(Paint.Style.FILL); + mPaint.setColor(ThemeUtil.colorAccent(context, 0xFFFFFFFF)); + + mTabContainer = new TabContainerLayout(context); + + applyStyle(context, attrs, defStyleAttr, defStyleRes); + + if(isInEditMode()) + addTemporaryTab(); + + if(!isInEditMode()) + mStyleId = ThemeManager.getStyleId(context, attrs, defStyleAttr, defStyleRes); + } + + public void applyStyle(int resId){ + ViewUtil.applyStyle(this, resId); + applyStyle(getContext(), null, 0, resId); + } + + protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabPageIndicator, defStyleAttr, defStyleRes); + + int textAppearance = 0; + int mode = -1; + int rippleStyle = 0; + + for(int i = 0, count = a.getIndexCount(); i < count; i++){ + int attr = a.getIndex(i); + if(attr == R.styleable.TabPageIndicator_tpi_tabPadding) + mTabPadding = a.getDimensionPixelSize(attr, 0); + else if(attr == R.styleable.TabPageIndicator_tpi_tabRipple) + rippleStyle = a.getResourceId(attr, 0); + else if(attr == R.styleable.TabPageIndicator_tpi_indicatorColor) + mPaint.setColor(a.getColor(attr, 0)); + else if(attr == R.styleable.TabPageIndicator_tpi_indicatorHeight) + mIndicatorHeight = a.getDimensionPixelSize(attr, 0); + else if(attr == R.styleable.TabPageIndicator_tpi_indicatorAtTop) + mIndicatorAtTop = a.getBoolean(attr, true); + else if(attr == R.styleable.TabPageIndicator_tpi_tabSingleLine) + mTabSingleLine = a.getBoolean(attr, true); + else if(attr == R.styleable.TabPageIndicator_android_textAppearance) + textAppearance = a.getResourceId(attr, 0); + else if(attr == R.styleable.TabPageIndicator_tpi_mode) + mode = a.getInteger(attr, 0); + } + + a.recycle(); + + if(mTabPadding < 0) + mTabPadding = ThemeUtil.dpToPx(context, 12); + + if(mIndicatorHeight < 0) + mIndicatorHeight = ThemeUtil.dpToPx(context, 2); + + if(mode >= 0){ + if(mMode != mode || getChildCount() == 0){ + mMode = mode; + removeAllViews(); + if(mMode == MODE_SCROLL) { + addView(mTabContainer, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)); + setFillViewport(false); + } + else if(mMode == MODE_FIXED){ + addView(mTabContainer, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + setFillViewport(true); + } + } + } + + if(textAppearance != 0 && mTextAppearance != textAppearance){ + mTextAppearance = textAppearance; + for(int i = 0, count = mTabContainer.getChildCount(); i < count; i++){ + CheckedTextView tv = (CheckedTextView)mTabContainer.getChildAt(i); + tv.setTextAppearance(context, mTextAppearance); + } + } + + if(rippleStyle != 0 && rippleStyle != mTabRippleStyle){ + mTabRippleStyle = rippleStyle; + for(int i = 0, count = mTabContainer.getChildCount(); i < count; i++) + ViewUtil.setBackground(mTabContainer.getChildAt(i), new RippleDrawable.Builder(getContext(), mTabRippleStyle).build()); + } + + if(mViewPager != null) + notifyDataSetChanged(); + requestLayout(); + } + + @Override + public void onThemeChanged(ThemeManager.OnThemeChangedEvent event) { + int style = ThemeManager.getInstance().getCurrentStyle(mStyleId); + if(mCurrentStyle != style){ + mCurrentStyle = style; + applyStyle(mCurrentStyle); + } + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + // Re-post the selector we saved + if (mTabAnimSelector != null) + post(mTabAnimSelector); + + if(mStyleId != 0) { + ThemeManager.getInstance().registerOnThemeChangedListener(this); + onThemeChanged(null); + } + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (mTabAnimSelector != null) + removeCallbacks(mTabAnimSelector); + + if(mStyleId != 0) + ThemeManager.getInstance().unregisterOnThemeChangedListener(this); + } + + @Override + public void onRtlPropertiesChanged(int layoutDirection) { + boolean rtl = layoutDirection == LAYOUT_DIRECTION_RTL; + if(mIsRtl != rtl) { + mIsRtl = rtl; + invalidate(); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + int ws = widthMeasureSpec; + if(ws != MeasureSpec.UNSPECIFIED) + ws = MeasureSpec.makeMeasureSpec(widthSize - getPaddingLeft() - getPaddingRight(), widthMode); + + int hs = heightMeasureSpec; + if(heightMode != MeasureSpec.UNSPECIFIED) + hs = MeasureSpec.makeMeasureSpec(heightSize - getPaddingTop() - getPaddingBottom(), heightMode); + + mTabContainer.measure(ws, hs); + + int width = 0; + switch (widthMode){ + case MeasureSpec.UNSPECIFIED: + width = mTabContainer.getMeasuredWidth() + getPaddingLeft() + getPaddingRight(); + break; + case MeasureSpec.AT_MOST: + width = Math.min(mTabContainer.getMeasuredWidth() + getPaddingLeft() + getPaddingRight(), widthSize); + break; + case MeasureSpec.EXACTLY: + width = widthSize; + break; + } + + int height = 0; + switch (heightMode){ + case MeasureSpec.UNSPECIFIED: + height = mTabContainer.getMeasuredHeight() + getPaddingTop() + getPaddingBottom(); + break; + case MeasureSpec.AT_MOST: + height = Math.min(mTabContainer.getMeasuredHeight() + getPaddingTop() + getPaddingBottom(), heightSize); + break; + case MeasureSpec.EXACTLY: + height = heightSize; + break; + } + + if(mTabContainer.getMeasuredWidth() != width - getPaddingLeft() - getPaddingRight() || mTabContainer.getMeasuredHeight() != height - getPaddingTop() - getPaddingBottom()) + mTabContainer.measure(MeasureSpec.makeMeasureSpec(width - getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY)); + + setMeasuredDimension(width, height); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + TextView tv = getTabView(mSelectedPosition); + if(tv != null) + updateIndicator(tv.getLeft(), tv.getMeasuredWidth()); + } + + private CheckedTextView getTabView(int position){ + return (CheckedTextView)mTabContainer.getChildAt(position); + } + + private void animateToTab(final int position) { + if(getTabView(position) == null) + return; + + if (mTabAnimSelector != null) + removeCallbacks(mTabAnimSelector); + + mTabAnimSelector = new Runnable() { + public void run() { + CheckedTextView tv = getTabView(position); + if(tv != null) { + if (!mScrolling) + updateIndicator(tv.getLeft(), tv.getMeasuredWidth()); + + smoothScrollTo(tv.getLeft() - (getWidth() - tv.getWidth()) / 2 + getPaddingLeft(), 0); + } + mTabAnimSelector = null; + } + }; + + post(mTabAnimSelector); + } + + /** + * Set a listener will be called when the current page is changed. + * @param listener The {@link ViewPager.OnPageChangeListener} will be called. + */ + public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) { + mListener = listener; + } + + /** + * Set the ViewPager associate with this indicator view. + * @param view The ViewPager view. + */ + public void setViewPager(@Nullable ViewPager view) { + if (mViewPager == view) + return; + + if (mViewPager != null){ + mViewPager.removeOnPageChangeListener(this); + PagerAdapter adapter = mViewPager.getAdapter(); + if(adapter != null) + adapter.unregisterDataSetObserver(mObserver); + } + + mViewPager = view; + + if(mViewPager != null) { + PagerAdapter adapter = mViewPager.getAdapter(); + if (adapter == null) + throw new IllegalStateException("ViewPager does not have adapter instance."); + + adapter.registerDataSetObserver(mObserver); + + mViewPager.addOnPageChangeListener(this); + + notifyDataSetChanged(); + onPageSelected(mViewPager.getCurrentItem()); + } + else + mTabContainer.removeAllViews(); + } + + /** + * Set the ViewPager associate with this indicator view and the current position; + * @param view The ViewPager view. + * @param initialPosition The current position. + */ + public void setViewPager(ViewPager view, int initialPosition) { + setViewPager(view); + setCurrentItem(initialPosition); + } + + private void updateIndicator(int offset, int width){ + mIndicatorOffset = offset; + mIndicatorWidth = width; + invalidate(); + } + + @Override + public void draw(@NonNull Canvas canvas) { + super.draw(canvas); + + int x = mIndicatorOffset + getPaddingLeft(); + int y = mIndicatorAtTop ? 0 : getHeight() - mIndicatorHeight; + canvas.drawRect(x, y, x + mIndicatorWidth, y + mIndicatorHeight, mPaint); + + if(isInEditMode()) + canvas.drawRect(getPaddingLeft(), y, getPaddingLeft() + mTabContainer.getChildAt(0).getWidth(), y + mIndicatorHeight, mPaint); + } + + @Override + public void onPageScrollStateChanged(int state) { + if(state == ViewPager.SCROLL_STATE_IDLE){ + mScrolling = false; + TextView tv = getTabView(mSelectedPosition); + if(tv != null) { + updateIndicator(tv.getLeft(), tv.getMeasuredWidth()); + } + } + else + mScrolling = true; + + if (mListener != null) + mListener.onPageScrollStateChanged(state); + } + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + if (mListener != null) + mListener.onPageScrolled(position, positionOffset, positionOffsetPixels); + + CheckedTextView tv_scroll = getTabView(position); + CheckedTextView tv_next = getTabView(position + 1); + + if(tv_scroll != null && tv_next != null){ + int width_scroll = tv_scroll.getMeasuredWidth(); + int width_next = tv_next.getMeasuredWidth(); + float distance = (width_scroll + width_next) / 2f; + + int width = (int)(width_scroll + (width_next - width_scroll) * positionOffset + 0.5f); + int offset = (int)(tv_scroll.getLeft() + width_scroll / 2f + distance * positionOffset - width / 2f + 0.5f); + updateIndicator(offset, width); + } + } + + @Override + public void onPageSelected(int position) { + setCurrentItem(position); + if (mListener != null) + mListener.onPageSelected(position); + } + + @Override + public void onClick(View v) { + int position = (Integer)v.getTag(); + if(position == mSelectedPosition && mListener != null) + mListener.onPageSelected(position); + + mViewPager.setCurrentItem(position, true); + } + + /** + * Set the current page of this TabPageIndicator. + * @param position The position of current page. + */ + public void setCurrentItem(int position) { + if(mSelectedPosition != position){ + CheckedTextView tv = getTabView(mSelectedPosition); + if(tv != null) + tv.setChecked(false); + } + + mSelectedPosition = position; + CheckedTextView tv = getTabView(mSelectedPosition); + if(tv != null) + tv.setChecked(true); + + animateToTab(position); + } + + private void notifyDataSetChanged() { + mTabContainer.removeAllViews(); + + PagerAdapter adapter = mViewPager.getAdapter(); + final int count = adapter.getCount(); + + if (mSelectedPosition > count) + mSelectedPosition = count - 1; + + for (int i = 0; i < count; i++) { + CharSequence title = adapter.getPageTitle(i); + if (title == null) + title = "NULL"; + + CheckedTextView tv = new CheckedTextView(getContext()); + tv.setCheckMarkDrawable(null); + tv.setText(title); + tv.setGravity(Gravity.CENTER); + tv.setTextAppearance(getContext(), mTextAppearance); + if(mTabSingleLine) + tv.setSingleLine(true); + else { + tv.setSingleLine(false); + tv.setMaxLines(2); + } + tv.setEllipsize(TruncateAt.END); + tv.setOnClickListener(this); + tv.setTag(i); + if(mTabRippleStyle > 0) + ViewUtil.setBackground(tv, new RippleDrawable.Builder(getContext(), mTabRippleStyle).build()); + + tv.setPadding(mTabPadding, 0, mTabPadding, 0); + mTabContainer.addView(tv, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)); + } + + setCurrentItem(mSelectedPosition); + requestLayout(); + } + + private void notifyDataSetInvalidated() { + PagerAdapter adapter = mViewPager.getAdapter(); + final int count = adapter.getCount(); + for (int i = 0; i < count; i++) { + TextView tv = getTabView(i); + + if(tv != null) { + CharSequence title = adapter.getPageTitle(i); + if (title == null) + title = "NULL"; + + tv.setText(title); + } + } + + requestLayout(); + } + + private void addTemporaryTab(){ + for (int i = 0; i < 3; i++) { + CharSequence title = null; + if (i == 0) + title = "TAB ONE"; + else if (i == 1) + title = "TAB TWO"; + else if (i == 2) + title = "TAB THREE"; + + CheckedTextView tv = new CheckedTextView(getContext()); + tv.setCheckMarkDrawable(null); + tv.setText(title); + tv.setGravity(Gravity.CENTER); + tv.setTextAppearance(getContext(), mTextAppearance); + tv.setSingleLine(true); + tv.setEllipsize(TruncateAt.END); + tv.setTag(i); + tv.setChecked(i == 0); + if(mMode == MODE_SCROLL){ + tv.setPadding(mTabPadding, 0, mTabPadding, 0); + mTabContainer.addView(tv, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)); + } + else if(mMode == MODE_FIXED){ + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT); + params.weight = 1f; + mTabContainer.addView(tv, params); + } + } + } + + private class TabContainerLayout extends FrameLayout{ + + public TabContainerLayout(Context context) { + super(context); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + + int width = 0; + int height = 0; + + if(mMode == MODE_SCROLL){ + int ws = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + child.measure(ws, heightMeasureSpec); + width += child.getMeasuredWidth(); + height = Math.max(height, child.getMeasuredHeight()); + } + setMeasuredDimension(width, height); + } + else{ + if(widthMode != MeasureSpec.EXACTLY){ + int ws = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + child.measure(ws, heightMeasureSpec); + width += child.getMeasuredWidth(); + height = Math.max(height, child.getMeasuredHeight()); + } + + if(widthMode == MeasureSpec.UNSPECIFIED || width < widthSize) + setMeasuredDimension(widthSize, height); + else{ + int count = getChildCount(); + int childWidth = count == 0 ? 0 : widthSize / count; + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + if(i != count - 1) + child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY), heightMeasureSpec); + else + child.measure(MeasureSpec.makeMeasureSpec(widthSize - childWidth * (count - 1), MeasureSpec.EXACTLY), heightMeasureSpec); + } + setMeasuredDimension(widthSize, height); + } + } + else { + int count = getChildCount(); + int childWidth = count == 0 ? 0 : widthSize / count; + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + if(i != count - 1) + child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY), heightMeasureSpec); + else + child.measure(MeasureSpec.makeMeasureSpec(widthSize - childWidth * (count - 1), MeasureSpec.EXACTLY), heightMeasureSpec); + height = Math.max(height, child.getMeasuredHeight()); + } + setMeasuredDimension(widthSize, height); + } + } + + int hs = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if(child.getMeasuredHeight() != height) + child.measure(MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(), MeasureSpec.EXACTLY), hs); + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int childLeft = 0; + int childTop = 0; + int childRight = right - left; + int childBottom = bottom - top; + + if(mIsRtl) + for(int i = 0, count = getChildCount(); i < count; i++){ + View child = getChildAt(i); + child.layout(childRight - child.getMeasuredWidth(), childTop, childRight, childBottom); + childRight -= child.getMeasuredWidth(); + } + else + for(int i = 0, count = getChildCount(); i < count; i++){ + View child = getChildAt(i); + child.layout(childLeft, childTop, childLeft + child.getMeasuredWidth(), childBottom); + childLeft += child.getMeasuredWidth(); + } + } + } +} diff --git a/material/src/main/java/com/rey/material/widget/TextView.java b/material/src/main/java/com/rey/material/widget/TextView.java new file mode 100644 index 0000000..58e31a8 --- /dev/null +++ b/material/src/main/java/com/rey/material/widget/TextView.java @@ -0,0 +1,150 @@ +package com.rey.material.widget; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + +import com.rey.material.util.ThemeManager; +import com.rey.material.drawable.RippleDrawable; +import com.rey.material.util.RippleManager; +import com.rey.material.util.ViewUtil; + +public class TextView extends android.widget.TextView implements ThemeManager.OnThemeChangedListener{ + + private RippleManager mRippleManager; + protected int mStyleId; + protected int mCurrentStyle = ThemeManager.THEME_UNDEFINED; + + public interface OnSelectionChangedListener{ + void onSelectionChanged(View v, int selStart, int selEnd); + } + + private OnSelectionChangedListener mOnSelectionChangedListener; + + public TextView(Context context) { + super(context); + + init(context, null, 0, 0); + } + + public TextView(Context context, AttributeSet attrs) { + super(context, attrs); + + init(context, attrs, 0, 0); + } + + public TextView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + init(context, attrs, defStyleAttr, 0); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public TextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + init(context, attrs, defStyleAttr, defStyleRes); + } + + protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + ViewUtil.applyFont(this, attrs, defStyleAttr, defStyleRes); + applyStyle(context, attrs, defStyleAttr, defStyleRes); + if(!isInEditMode()) + mStyleId = ThemeManager.getStyleId(context, attrs, defStyleAttr, defStyleRes); + } + + public void applyStyle(int resId){ + ViewUtil.applyStyle(this, resId); + applyStyle(getContext(), null, 0, resId); + } + + protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + getRippleManager().onCreate(this, context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public void setTextAppearance(int resId) { + ViewUtil.applyTextAppearance(this, resId); + } + + + @Override + public void onThemeChanged(ThemeManager.OnThemeChangedEvent event) { + int style = ThemeManager.getInstance().getCurrentStyle(mStyleId); + if(mCurrentStyle != style){ + mCurrentStyle = style; + applyStyle(mCurrentStyle); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if(mStyleId != 0) { + ThemeManager.getInstance().registerOnThemeChangedListener(this); + onThemeChanged(null); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + RippleManager.cancelRipple(this); + if(mStyleId != 0) + ThemeManager.getInstance().unregisterOnThemeChangedListener(this); + } + + @Override + public void setBackground(Drawable drawable) { + Drawable background = getBackground(); + if(background instanceof RippleDrawable && !(drawable instanceof RippleDrawable)) + ((RippleDrawable) background).setBackgroundDrawable(drawable); + else + super.setBackground(drawable); + } + + protected RippleManager getRippleManager(){ + if(mRippleManager == null){ + synchronized (RippleManager.class){ + if(mRippleManager == null) + mRippleManager = new RippleManager(); + } + } + + return mRippleManager; + } + + @Override + public void setOnClickListener(OnClickListener l) { + RippleManager rippleManager = getRippleManager(); + if (l == rippleManager) + super.setOnClickListener(l); + else { + rippleManager.setOnClickListener(l); + setOnClickListener(rippleManager); + } + } + + @Override + public boolean onTouchEvent(@NonNull MotionEvent event) { + boolean result = super.onTouchEvent(event); + return getRippleManager().onTouchEvent(this, event) || result; + } + + public void setOnSelectionChangedListener(OnSelectionChangedListener listener){ + mOnSelectionChangedListener = listener; + } + + @Override + protected void onSelectionChanged(int selStart, int selEnd) { + super.onSelectionChanged(selStart, selEnd); + + if(mOnSelectionChangedListener != null) + mOnSelectionChangedListener.onSelectionChanged(this, selStart, selEnd); + } +} diff --git a/material/src/main/java/com/rey/material/widget/TimePicker.java b/material/src/main/java/com/rey/material/widget/TimePicker.java new file mode 100644 index 0000000..6227d08 --- /dev/null +++ b/material/src/main/java/com/rey/material/widget/TimePicker.java @@ -0,0 +1,895 @@ +package com.rey.material.widget; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; +import android.support.annotation.NonNull; +import android.text.format.DateFormat; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.animation.AnimationUtils; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + +import com.rey.material.R; +import com.rey.material.util.ThemeManager; +import com.rey.material.util.ColorUtil; +import com.rey.material.util.ThemeUtil; +import com.rey.material.util.TypefaceUtil; +import com.rey.material.util.ViewUtil; + +public class TimePicker extends View implements ThemeManager.OnThemeChangedListener{ + + protected int mStyleId; + protected int mCurrentStyle = ThemeManager.THEME_UNDEFINED; + + private int mBackgroundColor; + private int mSelectionColor; + private int mSelectionRadius = -1; + private int mTickSize = -1; + private Typeface mTypeface = Typeface.DEFAULT; + private int mTextSize = -1; + private int mTextColor = 0xFF000000; + private int mTextHighlightColor = 0xFFFFFFFF; + private boolean m24Hour = true; + + private int mAnimDuration = -1; + private Interpolator mInInterpolator; + private Interpolator mOutInterpolator; + private long mStartTime; + private float mAnimProgress; + private boolean mRunning; + + private Paint mPaint; + + private PointF mCenterPoint; + private float mOuterRadius; + private float mInnerRadius; + private float mSecondInnerRadius; + + private float[] mLocations = new float[72]; + private Rect mRect; + private String[] mTicks; + + private int mMode = MODE_HOUR; + + public static final int MODE_HOUR = 0; + public static final int MODE_MINUTE = 1; + + private int mHour = 0; + private int mMinute = 0; + + private boolean mEdited = false; + + /** + * Interface definition for a callback to be invoked when the selected time is changed. + */ + public interface OnTimeChangedListener{ + + /** + * Called when the select mode is changed + * @param mode The current mode. Can be {@link #MODE_HOUR} or {@link #MODE_MINUTE}. + */ + void onModeChanged(int mode); + + /** + * Called then the selected hour is changed. + * @param oldValue The old hour value. + * @param newValue The new hour value. + */ + void onHourChanged(int oldValue, int newValue); + + /** + * Called then the selected minute is changed. + * @param oldValue The old minute value. + * @param newValue The new minute value. + */ + void onMinuteChanged(int oldValue, int newValue); + } + + private OnTimeChangedListener mOnTimeChangedListener; + + public TimePicker(Context context) { + super(context); + + init(context, null, 0, 0); + } + + public TimePicker(Context context, AttributeSet attrs) { + super(context, attrs); + + init(context, attrs, 0, 0); + } + + public TimePicker(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + init(context, attrs, defStyleAttr, 0); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public TimePicker(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + init(context, attrs, defStyleAttr, defStyleRes); + } + + protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mRect = new Rect(); + + mBackgroundColor = ColorUtil.getColor(ThemeUtil.colorPrimary(context, 0xFF000000), 0.25f); + mSelectionColor = ThemeUtil.colorPrimary(context, 0xFF000000); + + initTickLabels(); + + setWillNotDraw(false); + applyStyle(context, attrs, defStyleAttr, defStyleRes); + + if(!isInEditMode()) + mStyleId = ThemeManager.getStyleId(context, attrs, defStyleAttr, defStyleRes); + } + + /** + * Init the localized label of ticks. The value of ticks in order: + * 1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", + * "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "0", + * "5", "10", "15", "20", "25", "30", "35", "40", "45", "50", "55", "0" + */ + private void initTickLabels(){ + String format = "%2d"; + mTicks = new String[36]; + for(int i = 0; i < 23; i++) + mTicks[i] = String.format(format, i + 1); + mTicks[23] = String.format(format, 0); + mTicks[35] = mTicks[23]; + for(int i = 24; i < 35; i++) + mTicks[i] = String.format(format, (i - 23) * 5); + } + + public void applyStyle(int styleId){ + ViewUtil.applyStyle(this, styleId); + applyStyle(getContext(), null, 0, styleId); + } + + protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TimePicker, defStyleAttr, defStyleRes); + + boolean hourDefined = false; + String familyName = null; + int style = -1; + + for(int i = 0, count = a.getIndexCount(); i < count; i++){ + int attr = a.getIndex(i); + + if(attr == R.styleable.TimePicker_tp_backgroundColor) + mBackgroundColor = a.getColor(attr, 0); + else if(attr == R.styleable.TimePicker_tp_selectionColor) + mSelectionColor = a.getColor(attr, 0); + else if(attr == R.styleable.TimePicker_tp_selectionRadius) + mSelectionRadius = a.getDimensionPixelOffset(attr, 0); + else if(attr == R.styleable.TimePicker_tp_tickSize) + mTickSize = a.getDimensionPixelOffset(attr, 0); + else if(attr == R.styleable.TimePicker_tp_textSize) + mTextSize = a.getDimensionPixelOffset(attr, 0); + else if(attr == R.styleable.TimePicker_tp_textColor) + mTextColor = a.getColor(attr, 0); + else if(attr == R.styleable.TimePicker_tp_textHighlightColor) + mTextHighlightColor = a.getColor(attr, 0); + else if(attr == R.styleable.TimePicker_tp_animDuration) + mAnimDuration = a.getInteger(attr, 0); + else if(attr == R.styleable.TimePicker_tp_inInterpolator) + mInInterpolator = AnimationUtils.loadInterpolator(context, a.getResourceId(attr, 0)); + else if(attr == R.styleable.TimePicker_tp_outInterpolator) + mOutInterpolator = AnimationUtils.loadInterpolator(context, a.getResourceId(attr, 0)); + else if(attr == R.styleable.TimePicker_tp_mode) + setMode(a.getInteger(attr, 0), false); + else if(attr == R.styleable.TimePicker_tp_24Hour) { + set24Hour(a.getBoolean(attr, false)); + hourDefined = true; + } + else if(attr == R.styleable.TimePicker_tp_hour) + setHour(a.getInteger(attr, 0)); + else if(attr == R.styleable.TimePicker_tp_minute) + setMinute(a.getInteger(attr, 0)); + else if(attr == R.styleable.TimePicker_tp_fontFamily) + familyName = a.getString(attr); + else if(attr == R.styleable.TimePicker_tp_textStyle) + style = a.getInteger(attr, 0); + } + + a.recycle(); + + if(mSelectionRadius < 0) + mSecondInnerRadius = ThemeUtil.dpToPx(context, 8); + + if(mTickSize < 0) + mTickSize = ThemeUtil.dpToPx(context, 1); + + if(mTextSize < 0) + mTextSize = context.getResources().getDimensionPixelOffset(R.dimen.abc_text_size_caption_material); + + if(mAnimDuration < 0) + mAnimDuration = context.getResources().getInteger(android.R.integer.config_mediumAnimTime); + + if(mInInterpolator == null) + mInInterpolator = new DecelerateInterpolator(); + + if(mOutInterpolator == null) + mOutInterpolator = new DecelerateInterpolator(); + + if(!hourDefined) + set24Hour(DateFormat.is24HourFormat(context)); + + if(familyName != null || style >= 0) + mTypeface = TypefaceUtil.load(context, familyName, style); + } + + @Override + public void onThemeChanged(ThemeManager.OnThemeChangedEvent event) { + int style = ThemeManager.getInstance().getCurrentStyle(mStyleId); + if(mCurrentStyle != style){ + mCurrentStyle = style; + applyStyle(mCurrentStyle); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if(mStyleId != 0) { + ThemeManager.getInstance().registerOnThemeChangedListener(this); + onThemeChanged(null); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if(mStyleId != 0) + ThemeManager.getInstance().unregisterOnThemeChangedListener(this); + } + + public int getBackgroundColor(){ + return mBackgroundColor; + } + + public int getSelectionColor(){ + return mSelectionColor; + } + + public Typeface getTypeface(){ + return mTypeface; + } + + public int getTextSize(){ + return mTextSize; + } + + public int getTextColor(){ + return mTextColor; + } + + public int getTextHighlightColor(){ + return mTextHighlightColor; + } + + public int getAnimDuration(){ + return mAnimDuration; + } + + public Interpolator getInInterpolator(){ + return mInInterpolator; + } + + public Interpolator getOutInterpolator(){ + return mOutInterpolator; + } + + /** + * @return The current select mode. Can be {@link #MODE_HOUR} or {@link #MODE_MINUTE}. + */ + public int getMode(){ + return mMode; + } + + /** + * @return The selected hour value. + */ + public int getHour(){ + return mHour; + } + + /** + * @return The selected minute value. + */ + public int getMinute(){ + return mMinute; + } + + /** + * @return this TimePicker use 24-hour format or not. + */ + public boolean is24Hour(){ + return m24Hour; + } + + /** + * Set the select mode of this TimePicker. + * @param mode The select mode. Can be {@link #MODE_HOUR} or {@link #MODE_MINUTE}. + * @param animation Indicate that should show animation when switch select mode or not. + */ + public void setMode(int mode, boolean animation){ + if(mMode != mode){ + mMode = mode; + + if(mOnTimeChangedListener != null) + mOnTimeChangedListener.onModeChanged(mMode); + + if(animation) + startAnimation(); + else + invalidate(); + } + } + + /** + * Set the selected hour value. + * @param hour The selected hour value. + */ + public void setHour(int hour){ + if(m24Hour) + hour = Math.max(hour, 0) % 24; + else + hour = Math.max(hour, 0) % 12; + + if(mHour != hour){ + int old = mHour; + mHour = hour; + + if(mOnTimeChangedListener != null) + mOnTimeChangedListener.onHourChanged(old, mHour); + + if(mMode == MODE_HOUR) + invalidate(); + } + } + + /** + * Set the selected minute value. + * @param minute The selected minute value. + */ + public void setMinute(int minute){ + minute = Math.min(Math.max(minute, 0), 59); + + if(mMinute != minute){ + int old = mMinute; + mMinute = minute; + + if(mOnTimeChangedListener != null) + mOnTimeChangedListener.onMinuteChanged(old, mMinute); + + if(mMode == MODE_MINUTE) + invalidate(); + } + } + + /** + * Set a listener will be called when the selected time is changed. + * @param listener The {@link OnTimeChangedListener} will be called. + */ + public void setOnTimeChangedListener(OnTimeChangedListener listener){ + mOnTimeChangedListener = listener; + } + + /** + * Set this TimePicker use 24-hour format or not. + * @param b b + */ + public void set24Hour(boolean b){ + if(m24Hour != b){ + m24Hour = b; + if(!m24Hour && mHour > 11) + setHour(mHour - 12); + calculateTextLocation(); + } + } + + private float getAngle(int value, int mode){ + switch (mode){ + case MODE_HOUR: + return (float)(-Math.PI / 2 + Math.PI / 6 * value); + case MODE_MINUTE: + return (float)(-Math.PI / 2 + Math.PI / 30 * value); + default: + return 0f; + } + } + + private int getSelectedTick(int value, int mode){ + switch (mode){ + case MODE_HOUR: + return value == 0 ? (m24Hour ? 23 : 11) : value - 1; + case MODE_MINUTE: + if(value % 5 == 0) + return (value == 0) ? 35 : (value / 5 + 23); + default: + return -1; + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int widthSize = (widthMode == MeasureSpec.UNSPECIFIED) ? mSelectionRadius * 12 : MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight(); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSize = (heightMode == MeasureSpec.UNSPECIFIED) ? mSelectionRadius * 12 : MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom(); + + int size = Math.min(widthSize, heightSize); + + int width = (widthMode == MeasureSpec.EXACTLY) ? widthSize : size; + int height = (heightMode == MeasureSpec.EXACTLY) ? heightSize : size; + + setMeasuredDimension(width + getPaddingLeft() + getPaddingRight(), height + getPaddingTop() + getPaddingBottom()); + } + + private void calculateTextLocation(){ + if(mCenterPoint == null) + return; + + double step = Math.PI / 6; + double angle = -Math.PI / 3; + float x, y; + + mPaint.setTextSize(mTextSize); + mPaint.setTypeface(mTypeface); + mPaint.setTextAlign(Paint.Align.CENTER); + + if(m24Hour){ + for(int i = 0; i < 12; i++){ + mPaint.getTextBounds(mTicks[i], 0, mTicks[i].length(), mRect); + if(i == 0) + mSecondInnerRadius = mInnerRadius - mSelectionRadius - mRect.height(); + + x = mCenterPoint.x + (float)Math.cos(angle) * mSecondInnerRadius; + y = mCenterPoint.y + (float)Math.sin(angle) * mSecondInnerRadius; + + mLocations[i * 2] = x; + mLocations[i * 2 + 1] = y + mRect.height() / 2f; + + angle += step; + } + + for(int i = 12; i < mTicks.length; i++){ + x = mCenterPoint.x + (float)Math.cos(angle) * mInnerRadius; + y = mCenterPoint.y + (float)Math.sin(angle) * mInnerRadius; + + mPaint.getTextBounds(mTicks[i], 0, mTicks[i].length(), mRect); + mLocations[i * 2] = x; + mLocations[i * 2 + 1] = y + mRect.height() / 2f; + + angle += step; + } + } + else{ + for(int i = 0; i < 12; i++){ + x = mCenterPoint.x + (float)Math.cos(angle) * mInnerRadius; + y = mCenterPoint.y + (float)Math.sin(angle) * mInnerRadius; + + mPaint.getTextBounds(mTicks[i], 0, mTicks[i].length(), mRect); + mLocations[i * 2] = x; + mLocations[i * 2 + 1] = y + mRect.height() / 2f; + + angle += step; + } + + for(int i = 24; i < mTicks.length; i++){ + x = mCenterPoint.x + (float)Math.cos(angle) * mInnerRadius; + y = mCenterPoint.y + (float)Math.sin(angle) * mInnerRadius; + + mPaint.getTextBounds(mTicks[i], 0, mTicks[i].length(), mRect); + mLocations[i * 2] = x; + mLocations[i * 2 + 1] = y + mRect.height() / 2f; + + angle += step; + } + } + + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + int left = getPaddingLeft(); + int top = getPaddingTop(); + int size = Math.min(w - getPaddingLeft() - getPaddingRight(), h - getPaddingTop() - getPaddingBottom()); + + if(mCenterPoint == null) + mCenterPoint = new PointF(); + + mOuterRadius = size / 2f; + mCenterPoint.set(left + mOuterRadius, top + mOuterRadius); + mInnerRadius = mOuterRadius - mSelectionRadius - ThemeUtil.dpToPx(getContext(), 4); + + calculateTextLocation(); + } + + private int getPointedValue(float x, float y, boolean isDown){ + float radius = (float) Math.sqrt(Math.pow(x - mCenterPoint.x, 2) + Math.pow(y - mCenterPoint.y, 2)); + if(isDown) { + if(mMode == MODE_HOUR && m24Hour){ + if (radius > mInnerRadius + mSelectionRadius || radius < mSecondInnerRadius - mSelectionRadius) + return -1; + } + else if (radius > mInnerRadius + mSelectionRadius || radius < mInnerRadius - mSelectionRadius) + return -1; + } + + float angle = (float)Math.atan2(y - mCenterPoint.y, x - mCenterPoint.x); + if(angle < 0) + angle += Math.PI * 2; + + if(mMode == MODE_HOUR){ + if(m24Hour){ + if(radius > mSecondInnerRadius + mSelectionRadius / 2){ + int value = (int) Math.round(angle * 6 / Math.PI) + 15; + if(value == 24) + return 0; + else if(value > 24) + return value - 12; + else + return value; + } + else{ + int value = (int) Math.round(angle * 6 / Math.PI) + 3; + return value > 12 ? value - 12 : value; + } + } + else { + int value = (int) Math.round(angle * 6 / Math.PI) + 3; + return value > 11 ? value - 12 : value; + } + } + else if(mMode == MODE_MINUTE){ + int value = (int)Math.round(angle * 30 / Math.PI) + 15; + return value > 59 ? value - 60 : value; + } + + return -1; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + switch (event.getAction()){ + case MotionEvent.ACTION_DOWN: + int value = getPointedValue(event.getX(), event.getY(), true); + if(value < 0) + return false; + else if(mMode == MODE_HOUR) + setHour(value); + else if(mMode == MODE_MINUTE) + setMinute(value); + mEdited = true; + return true; + case MotionEvent.ACTION_MOVE: + value = getPointedValue(event.getX(), event.getY(), false); + if(value < 0) + return true; + else if(mMode == MODE_HOUR) + setHour(value); + else if(mMode == MODE_MINUTE) + setMinute(value); + mEdited = true; + return true; + case MotionEvent.ACTION_UP: + if(mEdited && mMode == MODE_HOUR){ + setMode(MODE_MINUTE, true); + mEdited = false; + return true; + } + break; + case MotionEvent.ACTION_CANCEL: + mEdited = false; + break; + } + + return false; + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + + mPaint.setColor(mBackgroundColor); + mPaint.setStyle(Paint.Style.FILL); + canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, mOuterRadius, mPaint); + + if(!mRunning){ + float angle; + int selectedTick; + int start; + int length; + float radius; + + if(mMode == MODE_HOUR){ + angle = getAngle(mHour, MODE_HOUR); + selectedTick = getSelectedTick(mHour, MODE_HOUR); + start = 0; + length = m24Hour ? 24 : 12; + radius = m24Hour && selectedTick < 12 ? mSecondInnerRadius : mInnerRadius; + } + else{ + angle = getAngle(mMinute, MODE_MINUTE); + selectedTick = getSelectedTick(mMinute, MODE_MINUTE); + start = 24; + length = 12; + radius = mInnerRadius; + } + + mPaint.setColor(mSelectionColor); + float x = mCenterPoint.x + (float)Math.cos(angle) * radius; + float y = mCenterPoint.y + (float)Math.sin(angle) * radius; + canvas.drawCircle(x, y, mSelectionRadius, mPaint); + + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setStrokeWidth(mTickSize); + x -= (float)Math.cos(angle) * mSelectionRadius; + y -= (float)Math.sin(angle) * mSelectionRadius; + canvas.drawLine(mCenterPoint.x, mCenterPoint.y, x, y, mPaint); + + mPaint.setStyle(Paint.Style.FILL); + mPaint.setColor(mTextColor); + canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, mTickSize * 2, mPaint); + + mPaint.setTextSize(mTextSize); + mPaint.setTypeface(mTypeface); + mPaint.setTextAlign(Paint.Align.CENTER); + + int index; + for(int i = 0; i < length; i++) { + index = start + i; + mPaint.setColor(index == selectedTick ? mTextHighlightColor : mTextColor); + canvas.drawText(mTicks[index], mLocations[index * 2], mLocations[index * 2 + 1], mPaint); + } + } + else{ + float maxOffset = mOuterRadius - mInnerRadius + mTextSize / 2; + int textOutColor = ColorUtil.getColor(mTextColor, 1f - mAnimProgress); + int textHighlightOutColor= ColorUtil.getColor(mTextHighlightColor, 1f - mAnimProgress); + int textInColor = ColorUtil.getColor(mTextColor, mAnimProgress); + int textHighlightInColor= ColorUtil.getColor(mTextHighlightColor, mAnimProgress); + float outOffset; + float inOffset; + float outAngle; + float inAngle; + int outStart; + int inStart; + int outLength; + int inLength; + int outSelectedTick; + int inSelectedTick; + float outRadius; + float inRadius; + + if(mMode == MODE_MINUTE){ + outAngle = getAngle(mHour, MODE_HOUR); + inAngle = getAngle(mMinute, MODE_MINUTE); + outOffset = mOutInterpolator.getInterpolation(mAnimProgress) * maxOffset; + inOffset = (1f - mInInterpolator.getInterpolation(mAnimProgress)) * -maxOffset; + outSelectedTick = getSelectedTick(mHour, MODE_HOUR); + inSelectedTick = getSelectedTick(mMinute, MODE_MINUTE); + outStart = 0; + outLength = m24Hour ? 24 : 12; + outRadius = m24Hour && outSelectedTick < 12 ? mSecondInnerRadius : mInnerRadius; + inStart = 24; + inLength = 12; + inRadius = mInnerRadius; + } + else{ + outAngle = getAngle(mMinute, MODE_MINUTE); + inAngle = getAngle(mHour, MODE_HOUR); + outOffset = mOutInterpolator.getInterpolation(mAnimProgress) * -maxOffset; + inOffset = (1f - mInInterpolator.getInterpolation(mAnimProgress)) * maxOffset; + outSelectedTick = getSelectedTick(mMinute, MODE_MINUTE); + inSelectedTick = getSelectedTick(mHour, MODE_HOUR); + outStart = 24; + outLength = 12; + outRadius = mInnerRadius; + inStart = 0; + inLength = m24Hour ? 24 : 12; + inRadius = m24Hour && inSelectedTick < 12 ? mSecondInnerRadius : mInnerRadius; + } + + mPaint.setColor(ColorUtil.getColor(mSelectionColor, 1f - mAnimProgress)); + float x = mCenterPoint.x + (float)Math.cos(outAngle) * (outRadius + outOffset); + float y = mCenterPoint.y + (float)Math.sin(outAngle) * (outRadius + outOffset); + canvas.drawCircle(x, y, mSelectionRadius, mPaint); + + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setStrokeWidth(mTickSize); + x -= (float)Math.cos(outAngle) * mSelectionRadius; + y -= (float)Math.sin(outAngle) * mSelectionRadius; + canvas.drawLine(mCenterPoint.x, mCenterPoint.y, x, y, mPaint); + + mPaint.setStyle(Paint.Style.FILL); + mPaint.setColor(ColorUtil.getColor(mSelectionColor, mAnimProgress)); + x = mCenterPoint.x + (float)Math.cos(inAngle) * (inRadius + inOffset); + y = mCenterPoint.y + (float)Math.sin(inAngle) * (inRadius + inOffset); + canvas.drawCircle(x, y, mSelectionRadius, mPaint); + + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setStrokeWidth(mTickSize); + x -= (float)Math.cos(inAngle) * mSelectionRadius; + y -= (float)Math.sin(inAngle) * mSelectionRadius; + canvas.drawLine(mCenterPoint.x, mCenterPoint.y, x, y, mPaint); + + mPaint.setStyle(Paint.Style.FILL); + mPaint.setColor(mTextColor); + canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, mTickSize * 2, mPaint); + + mPaint.setTextSize(mTextSize); + mPaint.setTypeface(mTypeface); + mPaint.setTextAlign(Paint.Align.CENTER); + + double step = Math.PI / 6; + double angle = -Math.PI / 3; + int index; + + for(int i = 0; i < outLength; i++){ + index = i + outStart; + x = mLocations[index * 2] + (float)Math.cos(angle) * outOffset; + y = mLocations[index * 2 + 1] + (float)Math.sin(angle) * outOffset; + mPaint.setColor(index == outSelectedTick ? textHighlightOutColor : textOutColor); + canvas.drawText(mTicks[index], x, y, mPaint); + angle += step; + } + + for(int i = 0; i < inLength; i++){ + index = i + inStart; + x = mLocations[index * 2] + (float)Math.cos(angle) * inOffset; + y = mLocations[index * 2 + 1] + (float)Math.sin(angle) * inOffset; + mPaint.setColor(index == inSelectedTick ? textHighlightInColor : textInColor); + canvas.drawText(mTicks[index], x, y, mPaint); + angle += step; + } + } + } + + private void resetAnimation(){ + mStartTime = SystemClock.uptimeMillis(); + mAnimProgress = 0f; + } + + private void startAnimation() { + if(getHandler() != null){ + resetAnimation(); + mRunning = true; + getHandler().postAtTime(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + } + + invalidate(); + } + + private void stopAnimation() { + mRunning = false; + mAnimProgress = 1f; + if(getHandler() != null) + getHandler().removeCallbacks(mUpdater); + invalidate(); + } + + private final Runnable mUpdater = new Runnable() { + + @Override + public void run() { + update(); + } + + }; + + private void update(){ + long curTime = SystemClock.uptimeMillis(); + mAnimProgress = Math.min(1f, (float)(curTime - mStartTime) / mAnimDuration); + + if(mAnimProgress == 1f) + stopAnimation(); + + if(mRunning) { + if(getHandler() != null) + getHandler().postAtTime(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + else + stopAnimation(); + } + + invalidate(); + } + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + + SavedState ss = new SavedState(superState); + + ss.mode = mMode; + ss.hour = mHour; + ss.minute = mMinute; + ss.is24Hour = m24Hour; + + return ss; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + set24Hour(ss.is24Hour); + setMode(ss.mode, false); + setHour(ss.hour); + setMinute(ss.minute); + } + + static class SavedState extends BaseSavedState { + int mode; + int hour; + int minute; + boolean is24Hour; + + /** + * Constructor called from {@link Switch#onSaveInstanceState()} + */ + SavedState(Parcelable superState) { + super(superState); + } + + /** + * Constructor called from {@link #CREATOR} + */ + private SavedState(Parcel in) { + super(in); + mode = in.readInt(); + hour = in.readInt(); + minute = in.readInt(); + is24Hour = in.readInt() == 1; + } + + @Override + public void writeToParcel(@NonNull Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeValue(mode); + out.writeValue(hour); + out.writeValue(minute); + out.writeValue(is24Hour ? 1 : 0); + } + + @Override + public String toString() { + return "TimePicker.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " mode=" + mode + + " hour=" + hour + + " minute=" + minute + + "24hour=" + is24Hour + "}"; + } + + public static final Creator CREATOR + = new Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + +} diff --git a/material/src/main/java/com/rey/material/widget/YearPicker.java b/material/src/main/java/com/rey/material/widget/YearPicker.java new file mode 100644 index 0000000..26c7676 --- /dev/null +++ b/material/src/main/java/com/rey/material/widget/YearPicker.java @@ -0,0 +1,474 @@ +package com.rey.material.widget; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Typeface; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AnimationUtils; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; +import android.widget.BaseAdapter; + +import com.rey.material.R; +import com.rey.material.drawable.BlankDrawable; +import com.rey.material.util.ThemeUtil; +import com.rey.material.util.TypefaceUtil; + +import java.util.Calendar; + +public class YearPicker extends ListView { + + private YearAdapter mAdapter; + + private int mTextSize; + private int mItemHeight; + private int mSelectionColor; + private int mAnimDuration; + private Interpolator mInInterpolator; + private Interpolator mOutInterpolator; + private Typeface mTypeface; + + private int mItemRealHeight; + private int mPadding; + private int mPositionShift; + private int mDistanceShift; + + private Paint mPaint; + + /** + * Interface definition for a callback to be invoked when the selected year is changed. + */ + public interface OnYearChangedListener{ + + /** + * Called then the selected year is changed. + * @param oldValue The old year value. + * @param newValue The new year value. + */ + void onYearChanged(int oldValue, int newValue); + + } + + private OnYearChangedListener mOnYearChangedListener; + + private static final int[][] STATES = new int[][]{ + new int[]{-android.R.attr.state_checked}, + new int[]{android.R.attr.state_checked}, + }; + + private int[] mTextColors = new int[]{0xFF000000, 0xFFFFFFFF}; + + private static final String YEAR_FORMAT = "%4d"; + + public YearPicker(Context context) { + super(context); + } + + public YearPicker(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public YearPicker(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public YearPicker(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + mTextSize = -1; + mItemHeight = -1; + mAnimDuration = -1; + mTypeface = Typeface.DEFAULT; + mItemRealHeight = -1; + + setWillNotDraw(false); + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mPaint.setStyle(Paint.Style.FILL); + + mAdapter = new YearAdapter(); + setAdapter(mAdapter); + setScrollBarStyle(SCROLLBARS_OUTSIDE_OVERLAY); + setSelector(BlankDrawable.getInstance()); + setDividerHeight(0); + setCacheColorHint(Color.TRANSPARENT); + setClipToPadding(false); + + mPadding = ThemeUtil.dpToPx(context, 4); + + mSelectionColor = ThemeUtil.colorPrimary(context, 0xFF000000); + + super.init(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + super.applyStyle(context, attrs, defStyleAttr, defStyleRes); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.YearPicker, defStyleAttr, defStyleRes); + + int year = -1; + int yearMin = -1; + int yearMax = -1; + String familyName = null; + int style = -1; + + for(int i = 0, count = a.getIndexCount(); i < count; i++){ + int attr = a.getIndex(i); + + if(attr == R.styleable.YearPicker_dp_yearTextSize) + mTextSize = a.getDimensionPixelSize(attr, 0); + else if(attr == R.styleable.YearPicker_dp_year) + year = a.getInteger(attr, 0); + else if(attr == R.styleable.YearPicker_dp_yearMin) + yearMin = a.getInteger(attr, 0); + else if(attr == R.styleable.YearPicker_dp_yearMax) + yearMax = a.getInteger(attr, 0); + else if(attr == R.styleable.YearPicker_dp_yearItemHeight) + mItemHeight = a.getDimensionPixelSize(attr, 0); + else if(attr == R.styleable.YearPicker_dp_textColor) + mTextColors[0] = a.getColor(attr, 0); + else if(attr == R.styleable.YearPicker_dp_textHighlightColor) + mTextColors[1] = a.getColor(attr, 0); + else if(attr == R.styleable.YearPicker_dp_selectionColor) + mSelectionColor = a.getColor(attr, 0); + else if(attr == R.styleable.YearPicker_dp_animDuration) + mAnimDuration = a.getInteger(attr, 0); + else if(attr == R.styleable.YearPicker_dp_inInterpolator) + mInInterpolator = AnimationUtils.loadInterpolator(context, a.getResourceId(attr, 0)); + else if(attr == R.styleable.YearPicker_dp_outInterpolator) + mOutInterpolator = AnimationUtils.loadInterpolator(context, a.getResourceId(attr, 0)); + else if(attr == R.styleable.YearPicker_dp_fontFamily) + familyName = a.getString(attr); + else if(attr == R.styleable.YearPicker_dp_textStyle) + style = a.getInteger(attr, 0); + } + + a.recycle(); + + if(mTextSize < 0) + mTextSize = context.getResources().getDimensionPixelOffset(R.dimen.abc_text_size_title_material); + + if(mItemHeight < 0) + mItemHeight = ThemeUtil.dpToPx(context, 48); + + if(mAnimDuration < 0) + mAnimDuration = context.getResources().getInteger(android.R.integer.config_mediumAnimTime); + + if(mInInterpolator == null) + mInInterpolator = new DecelerateInterpolator(); + + if(mOutInterpolator == null) + mOutInterpolator = new DecelerateInterpolator(); + + if(familyName != null || style >= 0) + mTypeface = TypefaceUtil.load(context, familyName, style); + + if(yearMin >= 0 || yearMax >= 0){ + if(yearMin < 0) + yearMin = mAdapter.getMinYear(); + + if(yearMax < 0) + yearMax = mAdapter.getMaxYear(); + + if(yearMax < yearMin) + yearMax = Integer.MAX_VALUE; + + setYearRange(yearMin, yearMax); + } + + if(mAdapter.getYear() < 0 && year < 0){ + Calendar cal = Calendar.getInstance(); + year = cal.get(Calendar.YEAR); + } + + if(year >= 0){ + year = Math.max(yearMin, Math.min(yearMax, year)); + setYear(year); + } + + mAdapter.notifyDataSetChanged(); + requestLayout(); + } + + /** + * Set the range of selectable year value. + * @param min The minimum selectable year value. + * @param max The maximum selectable year value. + */ + public void setYearRange(int min, int max){ + mAdapter.setYearRange(min, max); + } + + /** + * Jump to a specific year. + * @param year y + */ + public void goTo(int year){ + int position = mAdapter.positionOfYear(year) - mPositionShift; + int offset = mDistanceShift; + if(position < 0){ + position = 0; + offset = 0; + } + postSetSelectionFromTop(position, offset); + } + + public void postSetSelectionFromTop(final int position, final int offset) { + post(new Runnable() { + @Override + public void run() { + setSelectionFromTop(position, offset); + requestLayout(); + } + }); + } + + /** + * Set the selected year. + * @param year The selected year value. + */ + public void setYear(int year){ + if(mAdapter.getYear() == year) + return; + + mAdapter.setYear(year); + goTo(year); + } + + /** + * @return The selected year value. + */ + public int getYear(){ + return mAdapter.getYear(); + } + + /** + * Set a listener will be called when the selected year value is changed. + * @param listener The {@link OnYearChangedListener} will be called. + */ + public void setOnYearChangedListener(OnYearChangedListener listener){ + mOnYearChangedListener = listener; + } + + private void measureItemHeight(){ + if(mItemRealHeight > 0) + return; + + mPaint.setTextSize(mTextSize); + mItemRealHeight = Math.max(Math.round(mPaint.measureText("9999", 0, 4)) + mPadding * 2, mItemHeight); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + measureItemHeight(); + + if(heightMode != MeasureSpec.EXACTLY){ + if(heightMode == MeasureSpec.AT_MOST){ + int num = Math.min(mAdapter.getCount(), heightSize / mItemRealHeight); + if(num >= 3) + heightSize = mItemRealHeight * (num % 2 == 0 ? num - 1 : num); + } + else + heightSize = mItemRealHeight * mAdapter.getCount(); + heightMeasureSpec = MeasureSpec.makeMeasureSpec(heightSize + getPaddingTop() + getPaddingBottom(), MeasureSpec.EXACTLY); + } + + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + @Override + protected void onSizeChanged(int w, int h, int oldWidth, int oldHigh) { + float shift = (h / (float)mItemRealHeight - 1) / 2; + mPositionShift = (int)Math.floor(shift); + mPositionShift = shift > mPositionShift ? mPositionShift + 1 : mPositionShift; + mDistanceShift = (int)((shift - mPositionShift) * mItemRealHeight) - getPaddingTop(); + goTo(mAdapter.getYear()); + } + + private class YearAdapter extends BaseAdapter implements OnClickListener{ + + private int mMinYear = 1990; + private int mMaxYear = Integer.MAX_VALUE - 1; + private int mCurYear = -1; + + public YearAdapter(){} + + public int getMinYear(){ + return mMinYear; + } + + public int getMaxYear(){ + return mMaxYear; + } + + public void setYearRange(int min, int max){ + if(mMinYear != min || mMaxYear != max){ + mMinYear = min; + mMaxYear = max; + notifyDataSetChanged(); + } + } + + public int positionOfYear(int year){ + return year - mMinYear; + } + + @Override + public int getCount(){ + return mMaxYear - mMinYear + 1; + } + + @Override + public Object getItem(int position){ + return mMinYear + position; + } + + @Override + public long getItemId(int position) { + return 0; + } + + public void setYear(int year){ + if(mCurYear != year){ + int old = mCurYear; + mCurYear = year; + + CircleCheckedTextView child = (CircleCheckedTextView) YearPicker.this.getChildAt(positionOfYear(old) - YearPicker.this.getFirstVisiblePosition()); + if(child != null) + child.setChecked(false); + + child = (CircleCheckedTextView) YearPicker.this.getChildAt(positionOfYear(mCurYear) - YearPicker.this.getFirstVisiblePosition()); + if(child != null) + child.setChecked(true); + + if(mOnYearChangedListener != null) + mOnYearChangedListener.onYearChanged(old, mCurYear); + } + } + + public int getYear(){ + return mCurYear; + } + + @Override + public void onClick(View v) { + setYear((Integer)v.getTag()); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + CircleCheckedTextView v = (CircleCheckedTextView)convertView; + if(v == null){ + v = new CircleCheckedTextView(getContext()); + v.setGravity(Gravity.CENTER); + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) + v.setTextAlignment(TEXT_ALIGNMENT_CENTER); + v.setMinHeight(mItemRealHeight); + v.setMaxHeight(mItemRealHeight); + v.setAnimDuration(mAnimDuration); + v.setInterpolator(mInInterpolator, mOutInterpolator); + v.setBackgroundColor(mSelectionColor); + v.setTypeface(mTypeface); + v.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize); + v.setTextColor(new ColorStateList(STATES, mTextColors)); + v.setOnClickListener(this); + } + + int year = (Integer)getItem(position); + v.setTag(year); + v.setText(String.format(YEAR_FORMAT, year)); + v.setCheckedImmediately(year == mCurYear); + return v; + } + } + + @Override + public Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + + SavedState ss = new SavedState(superState); + + ss.yearMin = mAdapter.getMinYear(); + ss.yearMax = mAdapter.getMaxYear(); + ss.year = mAdapter.getYear(); + + return ss; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + setYearRange(ss.yearMin, ss.yearMax); + setYear(ss.year); + } + + static class SavedState extends BaseSavedState { + int yearMin; + int yearMax; + int year; + + /** + * Constructor called from {@link Switch#onSaveInstanceState()} + */ + SavedState(Parcelable superState) { + super(superState); + } + + /** + * Constructor called from {@link #CREATOR} + */ + private SavedState(Parcel in) { + super(in); + yearMin = in.readInt(); + yearMax = in.readInt(); + year = in.readInt(); + } + + @Override + public void writeToParcel(@NonNull Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeValue(yearMin); + out.writeValue(yearMax); + out.writeValue(year); + } + + @Override + public String toString() { + return "YearPicker.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " yearMin=" + yearMin + + " yearMax=" + yearMax + + " year=" + year + "}"; + } + + public static final Creator CREATOR + = new Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + +} diff --git a/material/src/main/res/anim/anim_scale_in.xml b/material/src/main/res/anim/anim_scale_in.xml new file mode 100644 index 0000000..65b2ac9 --- /dev/null +++ b/material/src/main/res/anim/anim_scale_in.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/material/src/main/res/anim/anim_scale_out.xml b/material/src/main/res/anim/anim_scale_out.xml new file mode 100644 index 0000000..50df398 --- /dev/null +++ b/material/src/main/res/anim/anim_scale_out.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/material/src/main/res/drawable/selector.xml b/material/src/main/res/drawable/selector.xml new file mode 100644 index 0000000..88f81ce --- /dev/null +++ b/material/src/main/res/drawable/selector.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/material/src/main/res/values/attrs.xml b/material/src/main/res/values/attrs.xml new file mode 100644 index 0000000..6ef6b29 --- /dev/null +++ b/material/src/main/res/values/attrs.xml @@ -0,0 +1,682 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/material/src/main/res/values/colors.xml b/material/src/main/res/values/colors.xml new file mode 100644 index 0000000..7594b7a --- /dev/null +++ b/material/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #00FFFFFF + \ No newline at end of file diff --git a/material/src/main/res/values/drawables.xml b/material/src/main/res/values/drawables.xml new file mode 100644 index 0000000..25a8a3b --- /dev/null +++ b/material/src/main/res/values/drawables.xml @@ -0,0 +1,5 @@ + + + #00FFFFFF + #100F0000 + \ No newline at end of file diff --git a/material/src/main/res/values/styles.xml b/material/src/main/res/values/styles.xml new file mode 100644 index 0000000..cefb601 --- /dev/null +++ b/material/src/main/res/values/styles.xml @@ -0,0 +1,580 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/material/src/main/res/xml/nav_states.xml b/material/src/main/res/xml/nav_states.xml new file mode 100644 index 0000000..1afc63e --- /dev/null +++ b/material/src/main/res/xml/nav_states.xml @@ -0,0 +1,43 @@ + + + + + + 0 + 0.1 + 1 + 0.1 + 0 + 0.5 + 1 + 0.5 + 0 + 0.9 + 1 + 0.9 + + + + + + 0.5 + 0 + 1 + 0.5 + 0 + 0.5 + 1 + 0.5 + 0.5 + 1 + 1 + 0.5 + + + + 0 + 2 + + + + diff --git a/material/src/test/java/com/rey/material/ExampleUnitTest.java b/material/src/test/java/com/rey/material/ExampleUnitTest.java new file mode 100644 index 0000000..64a6ebe --- /dev/null +++ b/material/src/test/java/com/rey/material/ExampleUnitTest.java @@ -0,0 +1,15 @@ +package com.rey.material; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * To work on unit tests, switch the Test Artifact in the Build Variants view. + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() throws Exception { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/mdpreference/.gitignore b/mdpreference/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/mdpreference/.gitignore @@ -0,0 +1 @@ +/build diff --git a/mdpreference/build.gradle b/mdpreference/build.gradle new file mode 100644 index 0000000..fb78d58 --- /dev/null +++ b/mdpreference/build.gradle @@ -0,0 +1,28 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 23 + buildToolsVersion '23.0.2' + + defaultConfig { + minSdkVersion 16 + targetSdkVersion 23 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_7 + targetCompatibility JavaVersion.VERSION_1_7 + } +} + +dependencies { + compile fileTree(include: ['*.jar'], dir: 'libs') + compile project(':material') +} diff --git a/mdpreference/proguard-rules.pro b/mdpreference/proguard-rules.pro new file mode 100644 index 0000000..f02a51c --- /dev/null +++ b/mdpreference/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in D:\Program Files\adt\sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/mdpreference/src/androidTest/java/io/github/xhinliang/mdpreference/ApplicationTest.java b/mdpreference/src/androidTest/java/io/github/xhinliang/mdpreference/ApplicationTest.java new file mode 100644 index 0000000..f702d9f --- /dev/null +++ b/mdpreference/src/androidTest/java/io/github/xhinliang/mdpreference/ApplicationTest.java @@ -0,0 +1,13 @@ +package io.github.xhinliang.mdpreference; + +import android.app.Application; +import android.test.ApplicationTestCase; + +/** + * Testing Fundamentals + */ +public class ApplicationTest extends ApplicationTestCase { + public ApplicationTest() { + super(Application.class); + } +} \ No newline at end of file diff --git a/mdpreference/src/main/AndroidManifest.xml b/mdpreference/src/main/AndroidManifest.xml new file mode 100644 index 0000000..708faaa --- /dev/null +++ b/mdpreference/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mdpreference/src/main/assets/fonts/Roboto-Medium.ttf b/mdpreference/src/main/assets/fonts/Roboto-Medium.ttf new file mode 100644 index 0000000..a3c1a1f Binary files /dev/null and b/mdpreference/src/main/assets/fonts/Roboto-Medium.ttf differ diff --git a/mdpreference/src/main/assets/fonts/Roboto-Regular.ttf b/mdpreference/src/main/assets/fonts/Roboto-Regular.ttf new file mode 100644 index 0000000..0e58508 Binary files /dev/null and b/mdpreference/src/main/assets/fonts/Roboto-Regular.ttf differ diff --git a/mdpreference/src/main/java/io/github/xhinliang/mdpreference/CheckBoxPreference.java b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/CheckBoxPreference.java new file mode 100644 index 0000000..700d8b0 --- /dev/null +++ b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/CheckBoxPreference.java @@ -0,0 +1,42 @@ +package io.github.xhinliang.mdpreference; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; + +import com.rey.material.widget.CheckBox; + +public class CheckBoxPreference extends TwoStatePreference { + public CheckBoxPreference(Context context) { + super(context); + init(context, null, 0, 0); + } + + public CheckBoxPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs, 0, 0); + } + + public CheckBoxPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs, defStyleAttr, 0); + } + + public CheckBoxPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context, attrs, defStyleAttr, defStyleRes); + } + + protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super.init(context, attrs, defStyleAttr, defStyleRes); + setWidgetLayoutResource(R.layout.mp_checkbox_preference); + } + + @Override + protected void onBindView(View view) { + super.onBindView(view); + CheckBox checkBox = (CheckBox) view.findViewById(R.id.checkable); + checkBox.setCheckedImmediately(isChecked()); + syncSummaryView(); + } +} diff --git a/mdpreference/src/main/java/io/github/xhinliang/mdpreference/DialogPreference.java b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/DialogPreference.java new file mode 100644 index 0000000..6d62daa --- /dev/null +++ b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/DialogPreference.java @@ -0,0 +1,179 @@ +package io.github.xhinliang.mdpreference; + +import android.content.Context; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.StringRes; +import android.util.AttributeSet; +import android.view.Window; +import android.view.WindowManager; + +import com.rey.material.dialog.Dialog; + +/** + * Created by xhinliang on 16-2-23. + * DialogPreference + */ +public abstract class DialogPreference extends Preference { + + protected CharSequence mDialogTitle; + protected CharSequence mDialogMessage; + protected CharSequence mPositiveButtonText; + protected CharSequence mNegativeButtonText; + + protected Dialog mDialog; + + public DialogPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context); + } + + private void init(Context context) { + mDialogTitle = getTitle(); + mPositiveButtonText = context.getText(R.string.confirm); + mNegativeButtonText = context.getText(R.string.cancel); + } + + public DialogPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context); + } + + public DialogPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public DialogPreference(Context context) { + super(context); + init(context); + } + + public void setDialogTitle(CharSequence dialogTitle) { + mDialogTitle = dialogTitle; + } + + public void setDialogTitle(int dialogTitleResId) { + setDialogTitle(getContext().getString(dialogTitleResId)); + } + + public CharSequence getDialogTitle() { + return mDialogTitle; + } + + public void setDialogMessage(CharSequence dialogMessage) { + mDialogMessage = dialogMessage; + } + + public CharSequence getDialogMessage() { + return mDialogMessage; + } + + public void setPositiveButtonText(CharSequence positiveButtonText) { + mPositiveButtonText = positiveButtonText; + } + + public void setPositiveButtonText(@StringRes int positiveButtonTextResId) { + setPositiveButtonText(getContext().getString(positiveButtonTextResId)); + } + + public void setNegativeButtonText(CharSequence negativeButtonText) { + mNegativeButtonText = negativeButtonText; + } + + public void setNegativeButtonText(@StringRes int negativeButtonTextResId) { + setNegativeButtonText(getContext().getString(negativeButtonTextResId)); + } + + @Override + protected void onClick() { + if (mDialog != null && mDialog.isShowing()) return; + showDialog(null); + } + + protected abstract void onShowDialog(Bundle state); + + private void showDialog(Bundle bundle){ + onShowDialog(bundle); + if (needInputMethod()) { + requestInputMethod(mDialog); + } + } + + protected boolean needInputMethod() { + return false; + } + + private void requestInputMethod(android.app.Dialog dialog) { + Window window = dialog.getWindow(); + window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); + } + + public Dialog getDialog() { + return mDialog; + } + + + @Override + protected Parcelable onSaveInstanceState() { + final Parcelable superState = super.onSaveInstanceState(); + if (mDialog == null || !mDialog.isShowing()) { + return superState; + } + + final DialogSavedState myState = new DialogSavedState(superState); + myState.isDialogShowing = true; + myState.dialogBundle = mDialog.onSaveInstanceState(); + return myState; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + if (state == null || !state.getClass().equals(DialogSavedState.class)) { + // Didn't save state for us in onSaveInstanceState + super.onRestoreInstanceState(state); + return; + } + + DialogSavedState myState = (DialogSavedState) state; + super.onRestoreInstanceState(myState.getSuperState()); + if (myState.isDialogShowing) { + showDialog(myState.dialogBundle); + } + } + + protected static class DialogSavedState extends BaseSavedState { + boolean isDialogShowing; + Bundle dialogBundle; + + public DialogSavedState(Parcel source) { + super(source); + isDialogShowing = source.readInt() == 1; + dialogBundle = source.readBundle(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(isDialogShowing ? 1 : 0); + dest.writeBundle(dialogBundle); + } + + public DialogSavedState(Parcelable superState) { + super(superState); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public DialogSavedState createFromParcel(Parcel in) { + return new DialogSavedState(in); + } + + public DialogSavedState[] newArray(int size) { + return new DialogSavedState[size]; + } + }; + } + +} diff --git a/mdpreference/src/main/java/io/github/xhinliang/mdpreference/EditTextPreference.java b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/EditTextPreference.java new file mode 100644 index 0000000..9713d87 --- /dev/null +++ b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/EditTextPreference.java @@ -0,0 +1,152 @@ +package io.github.xhinliang.mdpreference; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.AttributeSet; + +import com.rey.material.dialog.SimpleDialog; +import com.rey.material.widget.EditText; + +/** + * Created by xhinliang on 16-2-29. + * sf + */ +public class EditTextPreference extends DialogPreference { + public EditTextPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public EditTextPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public EditTextPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public EditTextPreference(Context context) { + super(context); + } + + + private String mText; + + public void setText(String text) { + final boolean wasBlocking = shouldDisableDependents(); + mText = text; + persistString(text); + final boolean isBlocking = shouldDisableDependents(); + if (isBlocking != wasBlocking) { + notifyDependencyChange(isBlocking); + } + notifyChanged(); + } + + @Override + public CharSequence getSummary() { + return mText == null ? super.getSummary() : mText; + } + + public String getText() { + return mText; + } + + @Override + protected void onSetInitialValue(boolean restoreValue, Object defaultValue) { + setText(restoreValue ? getPersistedString(mText) : (String) defaultValue); + } + + @Override + protected void onShowDialog(Bundle state) { + com.rey.material.dialog.Dialog.Builder mBuilder = new SimpleDialog.Builder() + .title(mDialogTitle) + .contentView(R.layout.mp_edittext) + .positiveAction(mPositiveButtonText, new com.rey.material.dialog.Dialog.Action1() { + @Override + public void onAction(com.rey.material.dialog.Dialog dialog) { + String value = ((com.rey.material.widget.EditText) dialog.findViewById(R.id.custom_et)).getText().toString(); + if (callChangeListener(value)) { + setText(value); + } + } + }) + .negativeAction(mNegativeButtonText, null); + + final Dialog dialog = mDialog = mBuilder.build(getContext()); + EditText editText = (com.rey.material.widget.EditText) dialog.findViewById(R.id.custom_et); + editText.setText(getSummary()); + editText.setSelection(getSummary().length()); + if (state != null) { + dialog.onRestoreInstanceState(state); + } + dialog.show(); + } + + @Override + public boolean shouldDisableDependents() { + return TextUtils.isEmpty(mText) || super.shouldDisableDependents(); + } + + @Override + protected boolean needInputMethod() { + return true; + } + + @Override + protected Parcelable onSaveInstanceState() { + final Parcelable superState = super.onSaveInstanceState(); + if (isPersistent()) { + return superState; + } + final SavedState myState = new SavedState(superState); + myState.text = getText(); + return myState; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + if (state == null || !state.getClass().equals(SavedState.class)) { + super.onRestoreInstanceState(state); + return; + } + SavedState myState = (SavedState) state; + super.onRestoreInstanceState(myState.getSuperState()); + setText(myState.text); + } + + + private static class SavedState extends BaseSavedState { + String text; + + public SavedState(Parcel source) { + super(source); + text = source.readString(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeString(text); + } + + public SavedState(Parcelable superState) { + super(superState); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + + } +} diff --git a/mdpreference/src/main/java/io/github/xhinliang/mdpreference/ListPreference.java b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/ListPreference.java new file mode 100644 index 0000000..57a46b7 --- /dev/null +++ b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/ListPreference.java @@ -0,0 +1,216 @@ +package io.github.xhinliang.mdpreference; + +import android.app.Dialog; +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.ArrayRes; +import android.text.TextUtils; +import android.util.AttributeSet; + +import com.rey.material.dialog.SimpleDialog; + +/** + * Created by xhinliang on 16-2-23. + * List + */ +public class ListPreference extends DialogPreference { + + private CharSequence[] mEntries; + private CharSequence[] mEntryValues; + private String mValue; + private String mFormat; + private int mClickedDialogEntryIndex; + private boolean mValueSet; + + public ListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context, attrs, defStyleAttr, defStyleRes); + } + + public ListPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs, defStyleAttr, 0); + } + + public ListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs, 0, 0); + } + + public ListPreference(Context context) { + super(context); + init(context, null, 0, 0); + } + + private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.list_preference, defStyleAttr, defStyleRes); + mEntries = a.getTextArray(R.styleable.list_preference_entry_arr); + mEntryValues = a.getTextArray(R.styleable.list_preference_value_arr); + mFormat = a.getString(R.styleable.list_preference_format_str); + a.recycle(); + } + + @Override + protected void onShowDialog(Bundle state) { + if (mEntries == null || mEntryValues == null) { + throw new IllegalStateException( + "ListPreference requires an entries array and an entryValues array."); + } + + mClickedDialogEntryIndex = getValueIndex(); + com.rey.material.dialog.Dialog.Builder mBuilder = new SimpleDialog.Builder() + .items(mEntries, mClickedDialogEntryIndex) + .title(mDialogTitle) + .positiveAction(mPositiveButtonText, new com.rey.material.dialog.Dialog.Action1() { + @Override + public void onAction(com.rey.material.dialog.Dialog dialog) { + SimpleDialog simpleDialog = (SimpleDialog) dialog; + mClickedDialogEntryIndex = simpleDialog.getSelectedIndex(); + String value = mEntryValues[mClickedDialogEntryIndex].toString(); + if (callChangeListener(value)) { + setValue(value); + } + } + }) + .negativeAction(mNegativeButtonText, null); + final Dialog dialog = mDialog = mBuilder.build(getContext()); + if (state != null) { + dialog.onRestoreInstanceState(state); + } + dialog.show(); + } + + + public void setEntries(CharSequence[] entries) { + mEntries = entries; + } + + public void setEntries(@ArrayRes int entriesResId) { + setEntries(getContext().getResources().getTextArray(entriesResId)); + } + + public CharSequence[] getEntries() { + return mEntries; + } + + public void setEntryValues(CharSequence[] entryValues) { + mEntryValues = entryValues; + } + + public void setEntryValues(@ArrayRes int entryValuesResId) { + setEntryValues(getContext().getResources().getTextArray(entryValuesResId)); + } + + public CharSequence[] getEntryValues() { + return mEntryValues; + } + + public void setValue(String value) { + final boolean changed = !TextUtils.equals(mValue, value); + if (changed || !mValueSet) { + mValue = value; + mValueSet = true; + persistString(value); + if (changed) { + notifyChanged(); + } + } + } + + @Override + public CharSequence getSummary() { + final CharSequence entry = getEntry(); + if (mFormat == null || entry == null) { + return super.getSummary(); + } else { + return String.format(mFormat, entry); + } + } + + public String getValue() { + return mValue; + } + + public CharSequence getEntry() { + int index = getValueIndex(); + return index >= 0 && mEntries != null ? mEntries[index] : null; + } + + public int findIndexOfValue(String value) { + if (value != null && mEntryValues != null) { + for (int i = mEntryValues.length - 1; i >= 0; i--) { + if (mEntryValues[i].equals(value)) { + return i; + } + } + } + return -1; + } + + private int getValueIndex() { + return findIndexOfValue(mValue); + } + + @Override + protected Object onGetDefaultValue(TypedArray a, int index) { + return a.getString(index); + } + + @Override + protected void onSetInitialValue(boolean restoreValue, Object defaultValue) { + setValue(restoreValue ? getPersistedString(mValue) : (String) defaultValue); + } + + @Override + protected Parcelable onSaveInstanceState() { + final Parcelable superState = super.onSaveInstanceState(); + if (isPersistent()) { + return superState; + } + final SavedState myState = new SavedState(superState); + myState.value = getValue(); + return myState; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + if (state == null || !state.getClass().equals(SavedState.class)) { + super.onRestoreInstanceState(state); + return; + } + SavedState myState = (SavedState) state; + super.onRestoreInstanceState(myState.getSuperState()); + setValue(myState.value); + } + + private static class SavedState extends BaseSavedState { + String value; + public SavedState(Parcel source) { + super(source); + value = source.readString(); + } + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeString(value); + } + + public SavedState(Parcelable superState) { + super(superState); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } +} diff --git a/mdpreference/src/main/java/io/github/xhinliang/mdpreference/MultiSelectListPreference.java b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/MultiSelectListPreference.java new file mode 100644 index 0000000..6ffc2cc --- /dev/null +++ b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/MultiSelectListPreference.java @@ -0,0 +1,228 @@ +package io.github.xhinliang.mdpreference; + +import android.app.Dialog; +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.ArrayRes; +import android.util.AttributeSet; + +import com.rey.material.dialog.SimpleDialog; + +import java.util.HashSet; +import java.util.Set; + +public class MultiSelectListPreference extends DialogPreference { + + private CharSequence[] mEntries; + private CharSequence[] mEntryValues; + private boolean mValueSet; + private int selects; + + private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.list_preference, defStyleAttr, defStyleRes); + mEntries = a.getTextArray(R.styleable.list_preference_entry_arr); + mEntryValues = a.getTextArray(R.styleable.list_preference_value_arr); + a.recycle(); + } + + public MultiSelectListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context, attrs, defStyleAttr, defStyleRes); + } + + public MultiSelectListPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs, defStyleAttr, 0); + } + + public MultiSelectListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs, 0, 0); + } + + public MultiSelectListPreference(Context context) { + super(context); + init(context, null, 0, 0); + } + + + @Override + protected void onShowDialog(Bundle state) { + if (mEntries == null || mEntryValues == null) { + throw new IllegalStateException( + "MultiSelectListPreference requires an entries array and an entryValues array."); + } + com.rey.material.dialog.Dialog.Builder mBuilder = new SimpleDialog.Builder() + .multiChoiceItems(mEntries, getIndexes()) + .title(mDialogTitle) + .positiveAction(mPositiveButtonText, new com.rey.material.dialog.Dialog.Action1() { + @Override + public void onAction(com.rey.material.dialog.Dialog dialog) { + SimpleDialog simpleDialog = (SimpleDialog) dialog; + int[] indexes = simpleDialog.getSelectedIndexes(); + if (callChangeListener(indexes)) { + setIndexes(indexes); + } + } + }) + .negativeAction(mNegativeButtonText, null); + final Dialog dialog = mDialog = mBuilder.build(getContext()); + if (state != null) { + dialog.onRestoreInstanceState(state); + } + dialog.show(); + } + + public void setEntries(CharSequence[] entries) { + mEntries = entries; + } + + public void setEntries(@ArrayRes int entriesResId) { + setEntries(getContext().getResources().getTextArray(entriesResId)); + } + + public CharSequence[] getEntries() { + return mEntries; + } + + public void setEntryValues(CharSequence[] entryValues) { + mEntryValues = entryValues; + } + + public void setEntryValues(@ArrayRes int entryValuesResId) { + setEntryValues(getContext().getResources().getTextArray(entryValuesResId)); + } + + public CharSequence[] getEntryValues() { + return mEntryValues; + } + + public void setSelects(int indexes) { + if (indexes != selects || !mValueSet) { + selects = indexes; + mValueSet = true; + persistInt(selects); + if (indexes != selects) { + notifyChanged(); + } + } + } + + public void setIndexes(int[] indexes) { + setSelects(getBit(indexes)); + } + + private int getBit(int[] indexes) { + int selected = 0x0; + for (int item : indexes) { + int temp = 1; + temp <<= item; + selected |= temp; + } + return selected; + } + + private Set getArray(int bit) { + Set set = new HashSet<>(); + int temp = 1; + for (int i = 0; i < 32; ++i) { + if ((temp & bit) == temp) { + set.add(i); + } + temp <<= 1; + } + return set; + } + + + public int getSelects() { + return selects; + } + + public Set getSelectIndexes() { + return getArray(getSelects()); + } + + public int[] getIndexes() { + Set set = getSelectIndexes(); + int[] result = new int[set.size()]; + Integer[] array = new Integer[set.size()]; + set.toArray(array); + for (int i = 0; i < result.length; ++i) { + result[i] = array[i]; + } + return result; + } + + public Set getValue() { + return getArray(selects); + } + + + @Override + protected Object onGetDefaultValue(TypedArray a, int index) { + return a.getString(index); + } + + @Override + protected void onSetInitialValue(boolean restoreValue, Object defaultValue) { + setSelects(restoreValue ? getPersistedInt(0) : 0); + } + + @Override + protected Parcelable onSaveInstanceState() { + final Parcelable superState = super.onSaveInstanceState(); + if (isPersistent()) { + return superState; + } + final SavedState myState = new SavedState(superState); + super.onRestoreInstanceState(myState.getSuperState()); + myState.value = getSelects(); + return myState; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + if (state == null || !state.getClass().equals(SavedState.class)) { + super.onRestoreInstanceState(state); + return; + } + SavedState myState = (SavedState) state; + super.onRestoreInstanceState(myState.getSuperState()); + setSelects(myState.value); + } + + + private static class SavedState extends BaseSavedState { + int value; + + public SavedState(Parcel source) { + super(source); + value = source.readInt(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(value); + } + + public SavedState(Parcelable superState) { + super(superState); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } +} diff --git a/mdpreference/src/main/java/io/github/xhinliang/mdpreference/Preference.java b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/Preference.java new file mode 100644 index 0000000..b515f73 --- /dev/null +++ b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/Preference.java @@ -0,0 +1,111 @@ +package io.github.xhinliang.mdpreference; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import static android.content.Context.LAYOUT_INFLATER_SERVICE; +import static android.os.Build.VERSION_CODES.LOLLIPOP; +import static android.text.TextUtils.isEmpty; +import static android.view.View.GONE; +import static android.view.View.VISIBLE; + +public class Preference extends android.preference.Preference { + + protected TextView titleText; + protected TextView summaryText; + + protected ImageView imageView; + protected View imageFrame; + + private int iconResId; + private Drawable icon; + + public Preference(Context context) { + super(context); + init(context, null, 0, 0); + } + + public Preference(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs, 0, 0); + } + + public Preference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs, defStyleAttr, 0); + } + + @TargetApi(LOLLIPOP) + public Preference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context, attrs, defStyleAttr, defStyleRes); + } + + private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + TypedArray typedArray = context + .obtainStyledAttributes(attrs, new int[]{android.R.attr.icon}, defStyleAttr, defStyleRes); + iconResId = typedArray.getResourceId(0, 0); + typedArray.recycle(); + } + + @Override + protected View onCreateView(ViewGroup parent) { + LayoutInflater layoutInflater = + (LayoutInflater) getContext().getSystemService(LAYOUT_INFLATER_SERVICE); + View layout = layoutInflater.inflate(R.layout.mp_preference, parent, false); + + ViewGroup widgetFrame = (ViewGroup) layout.findViewById(R.id.widget_frame); + int widgetLayoutResId = getWidgetLayoutResource(); + if (widgetLayoutResId != 0) { + layoutInflater.inflate(widgetLayoutResId, widgetFrame); + } + widgetFrame.setVisibility(widgetLayoutResId != 0 ? VISIBLE : GONE); + + return layout; + } + + @Override + protected void onBindView(View view) { + CharSequence title = getTitle(); + titleText = (TextView) view.findViewById(R.id.title); + titleText.setText(title); + titleText.setVisibility(!isEmpty(title) ? VISIBLE : GONE); + titleText.setTypeface(Typefaces.getRobotoRegular(getContext())); + + CharSequence summary = getSummary(); + summaryText = (TextView) view.findViewById(R.id.summary); + summaryText.setText(summary); + summaryText.setVisibility(!isEmpty(summary) ? VISIBLE : GONE); + summaryText.setTypeface(Typefaces.getRobotoRegular(getContext())); + + if (icon == null && iconResId > 0) { + icon = getContext().getResources().getDrawable(iconResId); + } + imageView = (ImageView) view.findViewById(R.id.icon); + imageView.setImageDrawable(icon); + imageView.setVisibility(icon != null ? VISIBLE : GONE); + + imageFrame = view.findViewById(R.id.icon_frame); + imageFrame.setVisibility(icon != null ? VISIBLE : GONE); + } + + @Override + public void setIcon(int iconResId) { + super.setIcon(iconResId); + this.iconResId = iconResId; + } + + @Override + public void setIcon(Drawable icon) { + super.setIcon(icon); + this.icon = icon; + } +} diff --git a/mdpreference/src/main/java/io/github/xhinliang/mdpreference/PreferenceCategory.java b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/PreferenceCategory.java new file mode 100644 index 0000000..1a609b1 --- /dev/null +++ b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/PreferenceCategory.java @@ -0,0 +1,65 @@ +package io.github.xhinliang.mdpreference; + +import android.annotation.TargetApi; +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import static android.content.Context.LAYOUT_INFLATER_SERVICE; +import static android.os.Build.VERSION_CODES.LOLLIPOP; +import static android.text.TextUtils.isEmpty; +import static android.view.View.GONE; +import static android.view.View.VISIBLE; + +public class PreferenceCategory extends android.preference.PreferenceCategory { + + private int accentColor; + + public PreferenceCategory(Context context) { + super(context); + init(); + } + + public PreferenceCategory(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public PreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + @TargetApi(LOLLIPOP) + public PreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + + private void init() { + accentColor = ThemeUtils.resolveAccentColor(getContext()); + } + + @Override + protected View onCreateView(ViewGroup parent) { + super.onCreateView(parent); + LayoutInflater layoutInflater = + (LayoutInflater) getContext().getSystemService(LAYOUT_INFLATER_SERVICE); + return layoutInflater.inflate(R.layout.mp_preference_category, parent, false); + } + + @Override + protected void onBindView(View view) { + super.onBindView(view); + + CharSequence title = getTitle(); + TextView titleView = (TextView) view.findViewById(R.id.title); + titleView.setText(title); + titleView.setTextColor(accentColor); + titleView.setVisibility(!isEmpty(title) ? VISIBLE : GONE); + titleView.setTypeface(Typefaces.getRobotoMedium(getContext())); + } +} diff --git a/mdpreference/src/main/java/io/github/xhinliang/mdpreference/PreferenceFragment.java b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/PreferenceFragment.java new file mode 100644 index 0000000..e1b41fc --- /dev/null +++ b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/PreferenceFragment.java @@ -0,0 +1,20 @@ +package io.github.xhinliang.mdpreference; + +import android.os.Bundle; +import android.view.View; +import android.widget.ListView; + +public class PreferenceFragment extends android.preference.PreferenceFragment { + + private static final int PADDING_LEFT_RIGHT = 0; + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + ListView listView = (ListView) view.findViewById(android.R.id.list); + listView.setPadding(PADDING_LEFT_RIGHT, 0, PADDING_LEFT_RIGHT, 0); + listView.setHorizontalScrollBarEnabled(false); + listView.setVerticalScrollBarEnabled(false); + listView.setFooterDividersEnabled(false); + } +} diff --git a/mdpreference/src/main/java/io/github/xhinliang/mdpreference/SwitchPreference.java b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/SwitchPreference.java new file mode 100644 index 0000000..e1757da --- /dev/null +++ b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/SwitchPreference.java @@ -0,0 +1,43 @@ +package io.github.xhinliang.mdpreference; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.Checkable; + +public class SwitchPreference extends TwoStatePreference { + + public SwitchPreference(Context context) { + super(context); + init(context, null, 0, 0); + } + + public SwitchPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs, 0, 0); + } + + public SwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs, defStyleAttr, 0); + } + + public SwitchPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super.init(context, attrs, defStyleAttr, defStyleRes); + setWidgetLayoutResource(R.layout.mp_switch_preference); + } + + @Override + protected void onBindView(View view) { + super.onBindView(view); + Checkable checkable = (Checkable) view.findViewById(R.id.checkable); + checkable.setChecked(isChecked()); + syncSummaryView(); + } +} diff --git a/mdpreference/src/main/java/io/github/xhinliang/mdpreference/ThemeUtils.java b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/ThemeUtils.java new file mode 100644 index 0000000..016f459 --- /dev/null +++ b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/ThemeUtils.java @@ -0,0 +1,42 @@ +package io.github.xhinliang.mdpreference; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Resources.Theme; +import android.content.res.TypedArray; + +import static android.graphics.Color.parseColor; +import static android.os.Build.VERSION.SDK_INT; +import static android.os.Build.VERSION_CODES.LOLLIPOP; + +final class ThemeUtils { + + // material_deep_teal_500 + static final int FALLBACK_COLOR = parseColor("#009688"); + + private ThemeUtils() { + // no instances + } + + static boolean isAtLeastL() { + return SDK_INT >= LOLLIPOP; + } + + @TargetApi(LOLLIPOP) + static int resolveAccentColor(Context context) { + Theme theme = context.getTheme(); + + // on Lollipop, grab system colorAccent attribute + // pre-Lollipop, grab AppCompat colorAccent attribute + // finally, check for custom mp_colorAccent attribute + int attr = isAtLeastL() ? android.R.attr.colorAccent : R.attr.colorAccent; + TypedArray typedArray = theme.obtainStyledAttributes(new int[]{attr, R.attr.mp_colorAccent}); + + int accentColor = typedArray.getColor(0, FALLBACK_COLOR); + accentColor = typedArray.getColor(1, accentColor); + typedArray.recycle(); + + return accentColor; + } + +} diff --git a/mdpreference/src/main/java/io/github/xhinliang/mdpreference/TwoStatePreference.java b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/TwoStatePreference.java new file mode 100644 index 0000000..aa1137c --- /dev/null +++ b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/TwoStatePreference.java @@ -0,0 +1,270 @@ +package io.github.xhinliang.mdpreference; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.TypedArray; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; + +@SuppressWarnings("unused") +public abstract class TwoStatePreference extends Preference { + + private static final String TAG = "TwoStatePreference"; + + private CharSequence summaryOn; + private CharSequence summaryOff; + private boolean isChecked; + private boolean isCheckedSet; + private boolean disableDependentsState; + + public TwoStatePreference(Context context) { + super(context); + } + + public TwoStatePreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public TwoStatePreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public TwoStatePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @SuppressWarnings("ResourceType") + protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + TypedArray typedArray = context.obtainStyledAttributes(attrs, new int[]{ + android.R.attr.summaryOn, android.R.attr.summaryOff, android.R.attr.disableDependentsState + }, defStyleAttr, defStyleRes); + setSummaryOn(typedArray.getString(0)); + setSummaryOff(typedArray.getString(1)); + setDisableDependentsState(typedArray.getBoolean(2, false)); + typedArray.recycle(); + } + + + @Override + protected void onClick() { + super.onClick(); + boolean newValue = !isChecked(); + if (callChangeListener(newValue)) { + setChecked(newValue); + } + } + + /** + * Set the checked state and saves it to the {@link SharedPreferences}. + * + * @param checked The checked state. + */ + public void setChecked(boolean checked) { + // Always persist/notify the first time; don't assume the field's default of false. + boolean changed = isChecked != checked; + if (changed || !isCheckedSet) { + isChecked = checked; + isCheckedSet = true; + persistBoolean(checked); + if (changed) { + notifyDependencyChange(shouldDisableDependents()); + notifyChanged(); + } + } + } + + + /** + * Returns the checked state. + * + * @return The checked state. + */ + public boolean isChecked() { + return isChecked; + } + + @Override + public boolean shouldDisableDependents() { + boolean shouldDisable = disableDependentsState == isChecked; + return shouldDisable || super.shouldDisableDependents(); + } + + /** + * Sets the summary to be shown when checked. + * + * @param summary The summary to be shown when checked. + */ + public void setSummaryOn(CharSequence summary) { + summaryOn = summary; + if (isChecked()) { + notifyChanged(); + } + } + + /** + * @param summaryResId The summary as a resource. + * @see #setSummaryOn(CharSequence) + */ + public void setSummaryOn(int summaryResId) { + setSummaryOn(getContext().getString(summaryResId)); + } + + /** + * Returns the summary to be shown when checked. + * + * @return The summary. + */ + public CharSequence getSummaryOn() { + return summaryOn; + } + + /** + * Sets the summary to be shown when unchecked. + * + * @param summary The summary to be shown when unchecked. + */ + public void setSummaryOff(CharSequence summary) { + summaryOff = summary; + if (!isChecked()) { + notifyChanged(); + } + } + + /** + * @param summaryResId The summary as a resource. + * @see #setSummaryOff(CharSequence) + */ + public void setSummaryOff(int summaryResId) { + setSummaryOff(getContext().getString(summaryResId)); + } + + /** + * Returns the summary to be shown when unchecked. + * + * @return The summary. + */ + public CharSequence getSummaryOff() { + return summaryOff; + } + + /** + * Returns whether dependents are disabled when this preference is on ({@code true}) + * or when this preference is off ({@code false}). + * + * @return Whether dependents are disabled when this preference is on ({@code true}) + * or when this preference is off ({@code false}). + */ + public boolean getDisableDependentsState() { + return disableDependentsState; + } + + /** + * Sets whether dependents are disabled when this preference is on ({@code true}) + * or when this preference is off ({@code false}). + * + * @param disableDependentsState The preference state that should disable dependents. + */ + public void setDisableDependentsState(boolean disableDependentsState) { + this.disableDependentsState = disableDependentsState; + } + + @Override + protected Object onGetDefaultValue(TypedArray a, int index) { + return a.getBoolean(index, false); + } + + @Override + protected void onSetInitialValue(boolean restoreValue, Object defaultValue) { + setChecked(restoreValue ? getPersistedBoolean(isChecked) : (Boolean) defaultValue); + } + + /** + * Sync a summary view contained within view's sub hierarchy with the correct summary text. + */ + void syncSummaryView() { + Log.d(TAG, "syncSummaryView"); + // Sync the summary view + boolean useDefaultSummary = true; + if (isChecked && !TextUtils.isEmpty(summaryOn)) { + summaryText.setText(summaryOn); + useDefaultSummary = false; + } else if (!isChecked && !TextUtils.isEmpty(summaryOff)) { + summaryText.setText(summaryOff); + useDefaultSummary = false; + } + if (useDefaultSummary) { + CharSequence summary = getSummary(); + if (!TextUtils.isEmpty(summary)) { + summaryText.setText(summary); + useDefaultSummary = false; + } + } + int newVisibility = View.GONE; + if (!useDefaultSummary) { + // Someone has written to it + newVisibility = View.VISIBLE; + } + if (newVisibility != summaryText.getVisibility()) { + summaryText.setVisibility(newVisibility); + } + } + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + if (isPersistent()) { + // No need to save instance state since it's persistent + return superState; + } + SavedState myState = new SavedState(superState); + myState.checked = isChecked(); + return myState; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + if (state == null || !state.getClass().equals(SavedState.class)) { + // Didn't save state for us in onSaveInstanceState + super.onRestoreInstanceState(state); + return; + } + SavedState myState = (SavedState) state; + super.onRestoreInstanceState(myState.getSuperState()); + setChecked(myState.checked); + } + + static class SavedState extends BaseSavedState { + + boolean checked; + + public SavedState(Parcel source) { + super(source); + checked = source.readInt() == 1; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(checked ? 1 : 0); + } + + public SavedState(Parcelable superState) { + super(superState); + } + + public static final Creator CREATOR = new Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } +} + diff --git a/mdpreference/src/main/java/io/github/xhinliang/mdpreference/Typefaces.java b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/Typefaces.java new file mode 100644 index 0000000..1098703 --- /dev/null +++ b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/Typefaces.java @@ -0,0 +1,40 @@ +package io.github.xhinliang.mdpreference; + +import android.content.Context; +import android.graphics.Typeface; +import android.util.Log; + +import java.util.Hashtable; + +public class Typefaces { + + private static final String TAG = Typefaces.class.getSimpleName(); + private static final Hashtable cache = new Hashtable<>(); + + private Typefaces() { + // no instances + } + + static Typeface get(Context context, String assetPath) { + synchronized (cache) { + if (!cache.containsKey(assetPath)) { + try { + Typeface t = Typeface.createFromAsset(context.getAssets(), assetPath); + cache.put(assetPath, t); + } catch (Exception e) { + Log.e(TAG, "Could not get typeface '" + assetPath + "' Error: " + e.getMessage()); + return null; + } + } + return cache.get(assetPath); + } + } + + static Typeface getRobotoRegular(Context context) { + return get(context, "fonts/Roboto-Regular.ttf"); + } + + static Typeface getRobotoMedium(Context context) { + return get(context, "fonts/Roboto-Medium.ttf"); + } +} diff --git a/mdpreference/src/main/java/io/github/xhinliang/mdpreference/widget/PreferenceImageView.java b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/widget/PreferenceImageView.java new file mode 100644 index 0000000..c89285a --- /dev/null +++ b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/widget/PreferenceImageView.java @@ -0,0 +1,71 @@ +package io.github.xhinliang.mdpreference.widget; + +import android.annotation.TargetApi; +import android.content.Context; +import android.util.AttributeSet; +import android.widget.ImageView; + +import static android.os.Build.VERSION_CODES.LOLLIPOP; +import static android.view.View.MeasureSpec.AT_MOST; +import static android.view.View.MeasureSpec.UNSPECIFIED; +import static android.view.View.MeasureSpec.getMode; +import static android.view.View.MeasureSpec.getSize; +import static android.view.View.MeasureSpec.makeMeasureSpec; +import static java.lang.Integer.MAX_VALUE; + +/** + * Extension of ImageView that correctly applies maxWidth and maxHeight. + */ +public class PreferenceImageView extends ImageView { + + private int maxWidth = MAX_VALUE >> 1; + private int maxHeight = MAX_VALUE >> 1; + + public PreferenceImageView(Context context) { + super(context); + } + + public PreferenceImageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public PreferenceImageView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @TargetApi(LOLLIPOP) + public PreferenceImageView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public void setMaxWidth(int maxWidth) { + super.setMaxWidth(maxWidth); + this.maxWidth = maxWidth; + } + + @Override + public void setMaxHeight(int maxHeight) { + super.setMaxHeight(maxHeight); + this.maxHeight = maxHeight; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthMode = getMode(widthMeasureSpec); + if (widthMode == AT_MOST || widthMode == UNSPECIFIED) { + int widthSize = getSize(widthMeasureSpec); + if (maxWidth != MAX_VALUE && (maxWidth < widthSize || widthMode == UNSPECIFIED)) { + widthMeasureSpec = makeMeasureSpec(maxWidth, AT_MOST); + } + } + int heightMode = getMode(heightMeasureSpec); + if (heightMode == AT_MOST || heightMode == UNSPECIFIED) { + int heightSize = getSize(heightMeasureSpec); + if (maxHeight != MAX_VALUE && (maxHeight < heightSize || heightMode == UNSPECIFIED)) { + heightMeasureSpec = makeMeasureSpec(maxHeight, AT_MOST); + } + } + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } +} \ No newline at end of file diff --git a/mdpreference/src/main/res/drawable/color_divider_light.xml b/mdpreference/src/main/res/drawable/color_divider_light.xml new file mode 100644 index 0000000..a705c6b --- /dev/null +++ b/mdpreference/src/main/res/drawable/color_divider_light.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/mdpreference/src/main/res/drawable/color_label_light.xml b/mdpreference/src/main/res/drawable/color_label_light.xml new file mode 100644 index 0000000..ee5327c --- /dev/null +++ b/mdpreference/src/main/res/drawable/color_label_light.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/mdpreference/src/main/res/drawable/selector.xml b/mdpreference/src/main/res/drawable/selector.xml new file mode 100644 index 0000000..7d2917a --- /dev/null +++ b/mdpreference/src/main/res/drawable/selector.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/mdpreference/src/main/res/layout/mp_checkbox_preference.xml b/mdpreference/src/main/res/layout/mp_checkbox_preference.xml new file mode 100644 index 0000000..c729396 --- /dev/null +++ b/mdpreference/src/main/res/layout/mp_checkbox_preference.xml @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/mdpreference/src/main/res/layout/mp_edittext.xml b/mdpreference/src/main/res/layout/mp_edittext.xml new file mode 100644 index 0000000..db0a9bc --- /dev/null +++ b/mdpreference/src/main/res/layout/mp_edittext.xml @@ -0,0 +1,10 @@ + + \ No newline at end of file diff --git a/mdpreference/src/main/res/layout/mp_preference.xml b/mdpreference/src/main/res/layout/mp_preference.xml new file mode 100644 index 0000000..55a6a6b --- /dev/null +++ b/mdpreference/src/main/res/layout/mp_preference.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/mdpreference/src/main/res/layout/mp_preference_category.xml b/mdpreference/src/main/res/layout/mp_preference_category.xml new file mode 100644 index 0000000..c7b117b --- /dev/null +++ b/mdpreference/src/main/res/layout/mp_preference_category.xml @@ -0,0 +1,11 @@ + + \ No newline at end of file diff --git a/mdpreference/src/main/res/layout/mp_switch_preference.xml b/mdpreference/src/main/res/layout/mp_switch_preference.xml new file mode 100644 index 0000000..86cdf51 --- /dev/null +++ b/mdpreference/src/main/res/layout/mp_switch_preference.xml @@ -0,0 +1,14 @@ + + \ No newline at end of file diff --git a/mdpreference/src/main/res/values/colors.xml b/mdpreference/src/main/res/values/colors.xml new file mode 100644 index 0000000..5e7168a --- /dev/null +++ b/mdpreference/src/main/res/values/colors.xml @@ -0,0 +1,8 @@ + + + #afadad + #009688 + + #FF03A9F4 + #FF78909C + \ No newline at end of file diff --git a/mdpreference/src/main/res/values/mp_attrs.xml b/mdpreference/src/main/res/values/mp_attrs.xml new file mode 100644 index 0000000..19bc14c --- /dev/null +++ b/mdpreference/src/main/res/values/mp_attrs.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/mdpreference/src/main/res/values/strings.xml b/mdpreference/src/main/res/values/strings.xml new file mode 100644 index 0000000..f43db31 --- /dev/null +++ b/mdpreference/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + MaterialPreference + Cancel + Confirm + diff --git a/mdpreference/src/main/res/values/styles.xml b/mdpreference/src/main/res/values/styles.xml new file mode 100644 index 0000000..8c92f73 --- /dev/null +++ b/mdpreference/src/main/res/values/styles.xml @@ -0,0 +1,22 @@ + + + + + + + \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..03393d0 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':app', ':material', ':mdpreference'