├── .gitignore ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── basic.png ├── custom_accent_typeface.png ├── custom_colors.png ├── custom_floating_label_text.png ├── ellipsis.png ├── floating_label.png ├── helper_error_text.png ├── hide_underline.png ├── highlight.png ├── material_design_icon.png ├── material_edittext.png ├── max_characters.png ├── min_and_max.png └── min_characters.png ├── library ├── .gitignore ├── build.gradle ├── gradle.properties ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── rengwuxian │ │ └── materialedittext │ │ ├── ApplicationTest.java │ │ └── MaterialEditTextTest.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── rengwuxian │ │ └── materialedittext │ │ ├── Colors.java │ │ ├── Density.java │ │ ├── MaterialAutoCompleteTextView.java │ │ ├── MaterialEditText.java │ │ ├── MaterialMultiAutoCompleteTextView.java │ │ └── validation │ │ ├── METLengthChecker.java │ │ ├── METValidator.java │ │ └── RegexpValidator.java │ └── res │ ├── drawable-hdpi │ └── met_ic_clear.png │ ├── drawable-mdpi │ └── met_ic_clear.png │ ├── drawable-xhdpi │ └── met_ic_clear.png │ ├── drawable-xxhdpi │ └── met_ic_clear.png │ ├── drawable-xxxhdpi │ └── met_ic_clear.png │ └── values │ ├── attrs.xml │ └── dimens.xml ├── maven_push.gradle ├── sample ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── rengwuxian │ │ └── materialedittext │ │ └── sample │ │ └── ApplicationTest.java │ └── main │ ├── AndroidManifest.xml │ ├── assets │ └── fonts │ │ └── Roboto-LightItalic.ttf │ ├── java │ └── com │ │ └── rengwuxian │ │ └── materialedittext │ │ └── sample │ │ └── MainActivity.java │ └── res │ ├── color │ └── text_color.xml │ ├── drawable-hdpi │ ├── ic_launcher.png │ └── ic_phone.png │ ├── drawable-mdpi │ ├── ic_launcher.png │ └── ic_phone.png │ ├── drawable-xhdpi │ ├── ic_launcher.png │ └── ic_phone.png │ ├── drawable-xxhdpi │ ├── ic_launcher.png │ └── ic_phone.png │ ├── drawable │ └── text_cursor_cyan.xml │ ├── layout │ └── activity_main.xml │ ├── menu │ └── menu_main.xml │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/ 4 | .DS_Store 5 | /build 6 | *.iml 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MaterialEditText 2 | ================ 3 | [![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-MaterialEditText-brightgreen.svg?style=flat)](https://android-arsenal.com/details/1/1085) 4 | 5 | > ## NOTE: 2.0 is NOT BACKWARDS COMPATIBLE! See more on [wiki](https://github.com/rengwuxian/MaterialEditText/wiki) or [中文看这里](http://www.rengwuxian.com/post/materialedittext) 6 | 7 | 8 | ![MaterialEditText](./images/material_edittext.png) 9 | 10 | AppCompat v21 makes it easy to use Material Design EditText in our apps, but it's so limited. If you've tried that, you know what I mean. So I wrote MaterialEditText, the EditText in Material Design, with more features that [Google Material Design Spec](http://www.google.com/design/spec/components/text-fields.html) has introduced. 11 | 12 | ## Features 13 | 1. **Basic** 14 | 15 | ![Basic](./images/basic.png) 16 | 17 | 2. **Floating Label** 18 | 19 | normal: 20 | 21 | ![FloatingLabel](./images/floating_label.png) 22 | 23 | highlight: 24 | 25 | ![HighlightFloatingLabel](./images/highlight.png) 26 | 27 | custom floating label text: 28 | 29 | ![CustomFloatingLabelText](./images/custom_floating_label_text.png) 30 | 31 | 3. **Single Line Ellipsis** 32 | 33 | ![SingLineEllipsis](./images/ellipsis.png) 34 | 35 | 4. **Max/Min Characters** 36 | 37 | ![MaxCharacters](./images/max_characters.png) 38 | 39 | ![MinCharacters](./images/min_characters.png) 40 | 41 | ![MinAndMaxCharacters](./images/min_and_max.png) 42 | 43 | 5. **Helper Text and Error Text** 44 | 45 | ![HelperTextAndErrorText](./images/helper_error_text.png) 46 | 47 | 6. **Custom Base/Primary/Error/HelperText Colors** 48 | 49 | ![CustomColors](./images/custom_colors.png) 50 | 51 | 7. **Custom accent typeface** 52 | 53 | floating label, error/helper text, character counter, etc. 54 | 55 | ![CustomAccentTypeface](./images/custom_accent_typeface.png) 56 | 57 | 8. **Hide Underline** 58 | 59 | ![HideUnderLine](./images/hide_underline.png) 60 | 61 | 8. **Material Design Icon** 62 | 63 | ![MaterialDesignIcon](./images/material_design_icon.png) 64 | 65 | ## Sample 66 | 67 | [MaterialEditText-2.1.4-sample.apk](https://github.com/rengwuxian/MaterialEditText/releases/download/2.1.4/MaterialEditText-2.1.4-sample.apk) 68 | 69 | ## Download 70 | 71 | Eclipse: 72 | [MaterialEditText-2.1.4.aar](https://github.com/rengwuxian/MaterialEditText/releases/download/2.1.4/MaterialEditText-2.1.4.aar) 73 | 74 | gradle: 75 | 76 | ```groovy 77 | compile 'com.rengwuxian.materialedittext:library:2.1.4' 78 | ``` 79 | 80 | Maven: 81 | ```xml 82 | 83 | com.rengwuxian.materialedittext 84 | library 85 | 2.1.4 86 | aar 87 | 88 | ``` 89 | 90 | ## Usage 91 | 92 | See on [Wiki Page](https://github.com/rengwuxian/MaterialEditText/wiki) or [中文看这里](http://www.rengwuxian.com/post/materialedittext) 93 | 94 | ## Thanks to 95 | 96 | [NineOldAndroids](https://github.com/JakeWharton/NineOldAndroids/) 97 | 98 | ## License 99 | 100 | Copyright 2014 rengwuxian 101 | 102 | Licensed under the Apache License, Version 2.0 (the "License"); 103 | you may not use this file except in compliance with the License. 104 | You may obtain a copy of the License at 105 | 106 | http://www.apache.org/licenses/LICENSE-2.0 107 | 108 | Unless required by applicable law or agreed to in writing, software 109 | distributed under the License is distributed on an "AS IS" BASIS, 110 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 111 | See the License for the specific language governing permissions and 112 | limitations under the License. 113 | 114 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | repositories { 4 | jcenter() 5 | } 6 | dependencies { 7 | classpath 'com.android.tools.build:gradle:1.2.0' 8 | } 9 | } 10 | 11 | def isReleaseBuild() { 12 | return version.contains("SNAPSHOT") == false 13 | } 14 | 15 | allprojects { 16 | version = VERSION_NAME 17 | group = GROUP 18 | 19 | repositories { 20 | jcenter() 21 | maven { url 'https://oss.sonatype.org/content/groups/public' } 22 | } 23 | } 24 | 25 | apply plugin: 'android-reporting' -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | VERSION_NAME=2.1.4 2 | VERSION_CODE=43 3 | GROUP=com.rengwuxian.materialedittext 4 | 5 | POM_DESCRIPTION=Android EditText in Material Design 6 | POM_URL=https://github.com/rengwuxian/MaterialEditText 7 | POM_SCM_URL=https://github.com/rengwuxian/MaterialEditText 8 | POM_SCM_CONNECTION=scm:git@github.com:rengwuxian/MaterialEditText.git 9 | POM_SCM_DEV_CONNECTION=scm:git@github.com:rengwuxian/MaterialEditText.git 10 | POM_LICENCE_NAME=The Apache Software License, Version 2.0 11 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt 12 | POM_LICENCE_DIST=repo 13 | POM_DEVELOPER_ID=rengwuxian 14 | POM_DEVELOPER_NAME=Kai Zhu 15 | 16 | ANDROID_BUILD_TARGET_SDK_VERSION=22 17 | ANDROID_BUILD_TOOLS_VERSION=22.0.1 18 | ANDROID_BUILD_SDK_VERSION=22 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rengwuxian/MaterialEditText/f6e9fa42213cda552764c2a63f93a1464fa91d04/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Dec 07 22:23:47 CST 2014 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /images/basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rengwuxian/MaterialEditText/f6e9fa42213cda552764c2a63f93a1464fa91d04/images/basic.png -------------------------------------------------------------------------------- /images/custom_accent_typeface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rengwuxian/MaterialEditText/f6e9fa42213cda552764c2a63f93a1464fa91d04/images/custom_accent_typeface.png -------------------------------------------------------------------------------- /images/custom_colors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rengwuxian/MaterialEditText/f6e9fa42213cda552764c2a63f93a1464fa91d04/images/custom_colors.png -------------------------------------------------------------------------------- /images/custom_floating_label_text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rengwuxian/MaterialEditText/f6e9fa42213cda552764c2a63f93a1464fa91d04/images/custom_floating_label_text.png -------------------------------------------------------------------------------- /images/ellipsis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rengwuxian/MaterialEditText/f6e9fa42213cda552764c2a63f93a1464fa91d04/images/ellipsis.png -------------------------------------------------------------------------------- /images/floating_label.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rengwuxian/MaterialEditText/f6e9fa42213cda552764c2a63f93a1464fa91d04/images/floating_label.png -------------------------------------------------------------------------------- /images/helper_error_text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rengwuxian/MaterialEditText/f6e9fa42213cda552764c2a63f93a1464fa91d04/images/helper_error_text.png -------------------------------------------------------------------------------- /images/hide_underline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rengwuxian/MaterialEditText/f6e9fa42213cda552764c2a63f93a1464fa91d04/images/hide_underline.png -------------------------------------------------------------------------------- /images/highlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rengwuxian/MaterialEditText/f6e9fa42213cda552764c2a63f93a1464fa91d04/images/highlight.png -------------------------------------------------------------------------------- /images/material_design_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rengwuxian/MaterialEditText/f6e9fa42213cda552764c2a63f93a1464fa91d04/images/material_design_icon.png -------------------------------------------------------------------------------- /images/material_edittext.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rengwuxian/MaterialEditText/f6e9fa42213cda552764c2a63f93a1464fa91d04/images/material_edittext.png -------------------------------------------------------------------------------- /images/max_characters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rengwuxian/MaterialEditText/f6e9fa42213cda552764c2a63f93a1464fa91d04/images/max_characters.png -------------------------------------------------------------------------------- /images/min_and_max.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rengwuxian/MaterialEditText/f6e9fa42213cda552764c2a63f93a1464fa91d04/images/min_and_max.png -------------------------------------------------------------------------------- /images/min_characters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rengwuxian/MaterialEditText/f6e9fa42213cda552764c2a63f93a1464fa91d04/images/min_characters.png -------------------------------------------------------------------------------- /library/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion Integer.parseInt(project.ANDROID_BUILD_SDK_VERSION) 5 | buildToolsVersion project.ANDROID_BUILD_TOOLS_VERSION 6 | 7 | defaultConfig { 8 | minSdkVersion 7 9 | targetSdkVersion Integer.parseInt(project.ANDROID_BUILD_SDK_VERSION) 10 | } 11 | buildTypes { 12 | release { 13 | minifyEnabled false 14 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 15 | } 16 | } 17 | } 18 | 19 | dependencies { 20 | compile 'com.android.support:support-annotations:22.2.0' 21 | compile 'com.nineoldandroids:library:2.4.0' 22 | compile 'com.android.support:appcompat-v7:22.2.0' 23 | } 24 | 25 | // Used to push in maven 26 | apply from: '../maven_push.gradle' 27 | -------------------------------------------------------------------------------- /library/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=Material EditText Library 2 | POM_ARTIFACT_ID=library 3 | POM_PACKAGING=aar -------------------------------------------------------------------------------- /library/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /library/src/androidTest/java/com/rengwuxian/materialedittext/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.rengwuxian.materialedittext; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /library/src/androidTest/java/com/rengwuxian/materialedittext/MaterialEditTextTest.java: -------------------------------------------------------------------------------- 1 | package com.rengwuxian.materialedittext; 2 | 3 | import android.test.AndroidTestCase; 4 | 5 | /** 6 | * Misc tests for {@link com.rengwuxian.materialedittext.MaterialEditText}. 7 | *

8 | * Created by egor on 25/11/14. 9 | */ 10 | public class MaterialEditTextTest extends AndroidTestCase { 11 | 12 | private MaterialEditText editTextUnderTest; 13 | 14 | @Override 15 | protected void setUp() throws Exception { 16 | super.setUp(); 17 | editTextUnderTest = new MaterialEditText(getContext()); 18 | } 19 | 20 | public void testGetErrorReturnsNullIfNoErrorMessageWasSet() { 21 | assertNull(editTextUnderTest.getError()); 22 | } 23 | 24 | public void testGetErrorReturnsMessageSetEarlierViaSetError() { 25 | editTextUnderTest.layout(0, 0, 1000, 1000); 26 | editTextUnderTest.setError("Error!"); 27 | assertEquals("Error!", editTextUnderTest.getError().toString()); 28 | } 29 | 30 | public void testSetErrorWithZeroSizeDoesNotThrow() { 31 | editTextUnderTest.setError("Error!"); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /library/src/main/java/com/rengwuxian/materialedittext/Colors.java: -------------------------------------------------------------------------------- 1 | package com.rengwuxian.materialedittext; 2 | 3 | import android.graphics.Color; 4 | 5 | /** 6 | * Created by Administrator on 2014/12/12. 7 | */ 8 | public class Colors { 9 | public static boolean isLight(int color) { 10 | return Math.sqrt( 11 | Color.red(color) * Color.red(color) * .241 + 12 | Color.green(color) * Color.green(color) * .691 + 13 | Color.blue(color) * Color.blue(color) * .068) > 130; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /library/src/main/java/com/rengwuxian/materialedittext/Density.java: -------------------------------------------------------------------------------- 1 | package com.rengwuxian.materialedittext; 2 | 3 | import android.content.Context; 4 | import android.content.res.Resources; 5 | import android.util.TypedValue; 6 | 7 | /** 8 | * Created by Zhukai on 2014/5/29 0029. 9 | */ 10 | class Density { 11 | public static int dp2px(Context context, float dp) { 12 | Resources r = context.getResources(); 13 | float px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, r.getDisplayMetrics()); 14 | return Math.round(px); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /library/src/main/java/com/rengwuxian/materialedittext/MaterialAutoCompleteTextView.java: -------------------------------------------------------------------------------- 1 | package com.rengwuxian.materialedittext; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.content.res.Configuration; 6 | import android.content.res.TypedArray; 7 | import android.graphics.Bitmap; 8 | import android.graphics.BitmapFactory; 9 | import android.graphics.Canvas; 10 | import android.graphics.Color; 11 | import android.graphics.Paint; 12 | import android.graphics.PorterDuff; 13 | import android.graphics.Typeface; 14 | import android.graphics.drawable.Drawable; 15 | import android.os.Build; 16 | import android.support.annotation.DrawableRes; 17 | import android.support.annotation.IntDef; 18 | import android.support.annotation.NonNull; 19 | import android.support.annotation.Nullable; 20 | import android.support.v7.widget.AppCompatAutoCompleteTextView; 21 | import android.text.Editable; 22 | import android.text.Layout; 23 | import android.text.StaticLayout; 24 | import android.text.TextPaint; 25 | import android.text.TextUtils; 26 | import android.text.TextWatcher; 27 | import android.text.method.TransformationMethod; 28 | import android.util.AttributeSet; 29 | import android.util.TypedValue; 30 | import android.view.Gravity; 31 | import android.view.MotionEvent; 32 | import android.view.View; 33 | import android.content.res.ColorStateList; 34 | 35 | import com.nineoldandroids.animation.ArgbEvaluator; 36 | import com.nineoldandroids.animation.ObjectAnimator; 37 | import com.rengwuxian.materialedittext.validation.METLengthChecker; 38 | import com.rengwuxian.materialedittext.validation.METValidator; 39 | 40 | import java.util.ArrayList; 41 | import java.util.List; 42 | import java.util.regex.Matcher; 43 | import java.util.regex.Pattern; 44 | 45 | /** 46 | * AutoCompleteTextView in Material Design 47 | *

48 | * author:rengwuxian 49 | *

50 | */ 51 | public class MaterialAutoCompleteTextView extends AppCompatAutoCompleteTextView { 52 | 53 | @IntDef({FLOATING_LABEL_NONE, FLOATING_LABEL_NORMAL, FLOATING_LABEL_HIGHLIGHT}) 54 | public @interface FloatingLabelType { 55 | } 56 | 57 | public static final int FLOATING_LABEL_NONE = 0; 58 | public static final int FLOATING_LABEL_NORMAL = 1; 59 | public static final int FLOATING_LABEL_HIGHLIGHT = 2; 60 | 61 | /** 62 | * the spacing between the main text and the inner top padding. 63 | */ 64 | private int extraPaddingTop; 65 | 66 | /** 67 | * the spacing between the main text and the inner bottom padding. 68 | */ 69 | private int extraPaddingBottom; 70 | 71 | /** 72 | * the extra spacing between the main text and the left, actually for the left icon. 73 | */ 74 | private int extraPaddingLeft; 75 | 76 | /** 77 | * the extra spacing between the main text and the right, actually for the right icon. 78 | */ 79 | private int extraPaddingRight; 80 | 81 | /** 82 | * the floating label's text size. 83 | */ 84 | private int floatingLabelTextSize; 85 | 86 | /** 87 | * the floating label's text color. 88 | */ 89 | private int floatingLabelTextColor; 90 | 91 | /** 92 | * the bottom texts' size. 93 | */ 94 | private int bottomTextSize; 95 | 96 | /** 97 | * the spacing between the main text and the floating label. 98 | */ 99 | private int floatingLabelPadding; 100 | 101 | /** 102 | * the spacing between the main text and the bottom components (bottom ellipsis, helper/error text, characters counter). 103 | */ 104 | private int bottomSpacing; 105 | 106 | /** 107 | * whether the floating label should be shown. default is false. 108 | */ 109 | private boolean floatingLabelEnabled; 110 | 111 | /** 112 | * whether to highlight the floating label's text color when focused (with the main color). default is true. 113 | */ 114 | private boolean highlightFloatingLabel; 115 | 116 | /** 117 | * the base color of the line and the texts. default is black. 118 | */ 119 | private int baseColor; 120 | 121 | /** 122 | * inner top padding 123 | */ 124 | private int innerPaddingTop; 125 | 126 | /** 127 | * inner bottom padding 128 | */ 129 | private int innerPaddingBottom; 130 | 131 | /** 132 | * inner left padding 133 | */ 134 | private int innerPaddingLeft; 135 | 136 | /** 137 | * inner right padding 138 | */ 139 | private int innerPaddingRight; 140 | 141 | /** 142 | * the underline's highlight color, and the highlight color of the floating label if app:highlightFloatingLabel is set true in the xml. default is black(when app:darkTheme is false) or white(when app:darkTheme is true) 143 | */ 144 | private int primaryColor; 145 | 146 | /** 147 | * the color for when something is wrong.(e.g. exceeding max characters) 148 | */ 149 | private int errorColor; 150 | 151 | /** 152 | * min characters count limit. 0 means no limit. default is 0. NOTE: the character counter will increase the View's height. 153 | */ 154 | private int minCharacters; 155 | 156 | /** 157 | * max characters count limit. 0 means no limit. default is 0. NOTE: the character counter will increase the View's height. 158 | */ 159 | private int maxCharacters; 160 | 161 | /** 162 | * whether to show the bottom ellipsis in singleLine mode. default is false. NOTE: the bottom ellipsis will increase the View's height. 163 | */ 164 | private boolean singleLineEllipsis; 165 | 166 | /** 167 | * Always show the floating label, instead of animating it in/out. False by default. 168 | */ 169 | private boolean floatingLabelAlwaysShown; 170 | 171 | /** 172 | * Always show the helper text, no matter if the edit text is focused. False by default. 173 | */ 174 | private boolean helperTextAlwaysShown; 175 | 176 | /** 177 | * bottom ellipsis's height 178 | */ 179 | private int bottomEllipsisSize; 180 | 181 | /** 182 | * min bottom lines count. 183 | */ 184 | private int minBottomLines; 185 | 186 | /** 187 | * reserved bottom text lines count, no matter if there is some helper/error text. 188 | */ 189 | private int minBottomTextLines; 190 | 191 | /** 192 | * real-time bottom lines count. used for bottom extending/collapsing animation. 193 | */ 194 | private float currentBottomLines; 195 | 196 | /** 197 | * bottom lines count. 198 | */ 199 | private float bottomLines; 200 | 201 | /** 202 | * Helper text at the bottom 203 | */ 204 | private String helperText; 205 | 206 | /** 207 | * Helper text color 208 | */ 209 | private int helperTextColor = -1; 210 | 211 | /** 212 | * error text for manually invoked {@link #setError(CharSequence)} 213 | */ 214 | private String tempErrorText; 215 | 216 | /** 217 | * animation fraction of the floating label (0 as totally hidden). 218 | */ 219 | private float floatingLabelFraction; 220 | 221 | /** 222 | * whether the floating label is being shown. 223 | */ 224 | private boolean floatingLabelShown; 225 | 226 | /** 227 | * the floating label's focusFraction 228 | */ 229 | private float focusFraction; 230 | 231 | /** 232 | * The font used for the accent texts (floating label, error/helper text, character counter, etc.) 233 | */ 234 | private Typeface accentTypeface; 235 | 236 | /** 237 | * The font used on the view (EditText content) 238 | */ 239 | private Typeface typeface; 240 | 241 | /** 242 | * Text for the floatLabel if different from the hint 243 | */ 244 | private CharSequence floatingLabelText; 245 | 246 | /** 247 | * Whether or not to show the underline. Shown by default 248 | */ 249 | private boolean hideUnderline; 250 | 251 | /** 252 | * Underline's color 253 | */ 254 | private int underlineColor; 255 | 256 | /** 257 | * Whether to validate as soon as the text has changed. False by default 258 | */ 259 | private boolean autoValidate; 260 | 261 | /** 262 | * Whether the characters count is valid 263 | */ 264 | private boolean charactersCountValid; 265 | 266 | /** 267 | * Whether use animation to show/hide the floating label. 268 | */ 269 | private boolean floatingLabelAnimating; 270 | 271 | /** 272 | * Whether check the characters count at the beginning it's shown. 273 | */ 274 | private boolean checkCharactersCountAtBeginning; 275 | 276 | /** 277 | * Left Icon 278 | */ 279 | private Bitmap[] iconLeftBitmaps; 280 | 281 | /** 282 | * Right Icon 283 | */ 284 | private Bitmap[] iconRightBitmaps; 285 | 286 | /** 287 | * Clear Button 288 | */ 289 | private Bitmap[] clearButtonBitmaps; 290 | 291 | /** 292 | * Auto validate when focus lost. 293 | */ 294 | private boolean validateOnFocusLost; 295 | 296 | private boolean showClearButton; 297 | private boolean firstShown; 298 | private int iconSize; 299 | private int iconOuterWidth; 300 | private int iconOuterHeight; 301 | private int iconPadding; 302 | private boolean clearButtonTouched; 303 | private boolean clearButtonClicking; 304 | private ColorStateList textColorStateList; 305 | private ColorStateList textColorHintStateList; 306 | private ArgbEvaluator focusEvaluator = new ArgbEvaluator(); 307 | Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); 308 | TextPaint textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); 309 | StaticLayout textLayout; 310 | ObjectAnimator labelAnimator; 311 | ObjectAnimator labelFocusAnimator; 312 | ObjectAnimator bottomLinesAnimator; 313 | OnFocusChangeListener innerFocusChangeListener; 314 | OnFocusChangeListener outerFocusChangeListener; 315 | private List validators; 316 | private METLengthChecker lengthChecker; 317 | 318 | public MaterialAutoCompleteTextView(Context context) { 319 | super(context); 320 | init(context, null); 321 | } 322 | 323 | public MaterialAutoCompleteTextView(Context context, AttributeSet attrs) { 324 | super(context, attrs); 325 | init(context, attrs); 326 | } 327 | 328 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 329 | public MaterialAutoCompleteTextView(Context context, AttributeSet attrs, int style) { 330 | super(context, attrs, style); 331 | init(context, attrs); 332 | } 333 | 334 | private void init(Context context, AttributeSet attrs) { 335 | iconSize = getPixel(32); 336 | iconOuterWidth = getPixel(48); 337 | iconOuterHeight = getPixel(32); 338 | 339 | bottomSpacing = getResources().getDimensionPixelSize(R.dimen.inner_components_spacing); 340 | bottomEllipsisSize = getResources().getDimensionPixelSize(R.dimen.bottom_ellipsis_height); 341 | 342 | // default baseColor is black 343 | int defaultBaseColor = Color.BLACK; 344 | 345 | TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MaterialEditText); 346 | textColorStateList = typedArray.getColorStateList(R.styleable.MaterialEditText_met_textColor); 347 | textColorHintStateList = typedArray.getColorStateList(R.styleable.MaterialEditText_met_textColorHint); 348 | baseColor = typedArray.getColor(R.styleable.MaterialEditText_met_baseColor, defaultBaseColor); 349 | 350 | // retrieve the default primaryColor 351 | int defaultPrimaryColor; 352 | TypedValue primaryColorTypedValue = new TypedValue(); 353 | try { 354 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 355 | context.getTheme().resolveAttribute(android.R.attr.colorPrimary, primaryColorTypedValue, true); 356 | defaultPrimaryColor = primaryColorTypedValue.data; 357 | } else { 358 | throw new RuntimeException("SDK_INT less than LOLLIPOP"); 359 | } 360 | } catch (Exception e) { 361 | try { 362 | int colorPrimaryId = getResources().getIdentifier("colorPrimary", "attr", getContext().getPackageName()); 363 | if (colorPrimaryId != 0) { 364 | context.getTheme().resolveAttribute(colorPrimaryId, primaryColorTypedValue, true); 365 | defaultPrimaryColor = primaryColorTypedValue.data; 366 | } else { 367 | throw new RuntimeException("colorPrimary not found"); 368 | } 369 | } catch (Exception e1) { 370 | defaultPrimaryColor = baseColor; 371 | } 372 | } 373 | 374 | primaryColor = typedArray.getColor(R.styleable.MaterialEditText_met_primaryColor, defaultPrimaryColor); 375 | setFloatingLabelInternal(typedArray.getInt(R.styleable.MaterialEditText_met_floatingLabel, 0)); 376 | errorColor = typedArray.getColor(R.styleable.MaterialEditText_met_errorColor, Color.parseColor("#e7492E")); 377 | minCharacters = typedArray.getInt(R.styleable.MaterialEditText_met_minCharacters, 0); 378 | maxCharacters = typedArray.getInt(R.styleable.MaterialEditText_met_maxCharacters, 0); 379 | singleLineEllipsis = typedArray.getBoolean(R.styleable.MaterialEditText_met_singleLineEllipsis, false); 380 | helperText = typedArray.getString(R.styleable.MaterialEditText_met_helperText); 381 | helperTextColor = typedArray.getColor(R.styleable.MaterialEditText_met_helperTextColor, -1); 382 | minBottomTextLines = typedArray.getInt(R.styleable.MaterialEditText_met_minBottomTextLines, 0); 383 | String fontPathForAccent = typedArray.getString(R.styleable.MaterialEditText_met_accentTypeface); 384 | if (fontPathForAccent != null && !isInEditMode()) { 385 | accentTypeface = getCustomTypeface(fontPathForAccent); 386 | textPaint.setTypeface(accentTypeface); 387 | } 388 | String fontPathForView = typedArray.getString(R.styleable.MaterialEditText_met_typeface); 389 | if (fontPathForView != null && !isInEditMode()) { 390 | typeface = getCustomTypeface(fontPathForView); 391 | setTypeface(typeface); 392 | } 393 | floatingLabelText = typedArray.getString(R.styleable.MaterialEditText_met_floatingLabelText); 394 | if (floatingLabelText == null) { 395 | floatingLabelText = getHint(); 396 | } 397 | floatingLabelPadding = typedArray.getDimensionPixelSize(R.styleable.MaterialEditText_met_floatingLabelPadding, bottomSpacing); 398 | floatingLabelTextSize = typedArray.getDimensionPixelSize(R.styleable.MaterialEditText_met_floatingLabelTextSize, getResources().getDimensionPixelSize(R.dimen.floating_label_text_size)); 399 | floatingLabelTextColor = typedArray.getColor(R.styleable.MaterialEditText_met_floatingLabelTextColor, -1); 400 | floatingLabelAnimating = typedArray.getBoolean(R.styleable.MaterialEditText_met_floatingLabelAnimating, true); 401 | bottomTextSize = typedArray.getDimensionPixelSize(R.styleable.MaterialEditText_met_bottomTextSize, getResources().getDimensionPixelSize(R.dimen.bottom_text_size)); 402 | hideUnderline = typedArray.getBoolean(R.styleable.MaterialEditText_met_hideUnderline, false); 403 | underlineColor = typedArray.getColor(R.styleable.MaterialEditText_met_underlineColor, -1); 404 | autoValidate = typedArray.getBoolean(R.styleable.MaterialEditText_met_autoValidate, false); 405 | iconLeftBitmaps = generateIconBitmaps(typedArray.getResourceId(R.styleable.MaterialEditText_met_iconLeft, -1)); 406 | iconRightBitmaps = generateIconBitmaps(typedArray.getResourceId(R.styleable.MaterialEditText_met_iconRight, -1)); 407 | showClearButton = typedArray.getBoolean(R.styleable.MaterialEditText_met_clearButton, false); 408 | clearButtonBitmaps = generateIconBitmaps(R.drawable.met_ic_clear); 409 | iconPadding = typedArray.getDimensionPixelSize(R.styleable.MaterialEditText_met_iconPadding, getPixel(16)); 410 | floatingLabelAlwaysShown = typedArray.getBoolean(R.styleable.MaterialEditText_met_floatingLabelAlwaysShown, false); 411 | helperTextAlwaysShown = typedArray.getBoolean(R.styleable.MaterialEditText_met_helperTextAlwaysShown, false); 412 | validateOnFocusLost = typedArray.getBoolean(R.styleable.MaterialEditText_met_validateOnFocusLost, false); 413 | checkCharactersCountAtBeginning = typedArray.getBoolean(R.styleable.MaterialEditText_met_checkCharactersCountAtBeginning, true); 414 | typedArray.recycle(); 415 | 416 | int[] paddings = new int[]{ 417 | android.R.attr.padding, // 0 418 | android.R.attr.paddingLeft, // 1 419 | android.R.attr.paddingTop, // 2 420 | android.R.attr.paddingRight, // 3 421 | android.R.attr.paddingBottom // 4 422 | }; 423 | TypedArray paddingsTypedArray = context.obtainStyledAttributes(attrs, paddings); 424 | int padding = paddingsTypedArray.getDimensionPixelSize(0, 0); 425 | innerPaddingLeft = paddingsTypedArray.getDimensionPixelSize(1, padding); 426 | innerPaddingTop = paddingsTypedArray.getDimensionPixelSize(2, padding); 427 | innerPaddingRight = paddingsTypedArray.getDimensionPixelSize(3, padding); 428 | innerPaddingBottom = paddingsTypedArray.getDimensionPixelSize(4, padding); 429 | paddingsTypedArray.recycle(); 430 | 431 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { 432 | setBackground(null); 433 | } else { 434 | setBackgroundDrawable(null); 435 | } 436 | if (singleLineEllipsis) { 437 | TransformationMethod transformationMethod = getTransformationMethod(); 438 | setSingleLine(); 439 | setTransformationMethod(transformationMethod); 440 | } 441 | initMinBottomLines(); 442 | initPadding(); 443 | initText(); 444 | initFloatingLabel(); 445 | initTextWatcher(); 446 | checkCharactersCount(); 447 | } 448 | 449 | private void initText() { 450 | if (!TextUtils.isEmpty(getText())) { 451 | CharSequence text = getText(); 452 | setText(null); 453 | resetHintTextColor(); 454 | setText(text); 455 | setSelection(text.length()); 456 | floatingLabelFraction = 1; 457 | floatingLabelShown = true; 458 | } else { 459 | resetHintTextColor(); 460 | } 461 | resetTextColor(); 462 | } 463 | 464 | private void initTextWatcher() { 465 | addTextChangedListener(new TextWatcher() { 466 | @Override 467 | public void beforeTextChanged(CharSequence s, int start, int count, int after) { 468 | } 469 | 470 | @Override 471 | public void onTextChanged(CharSequence s, int start, int before, int count) { 472 | } 473 | 474 | @Override 475 | public void afterTextChanged(Editable s) { 476 | checkCharactersCount(); 477 | if (autoValidate) { 478 | validate(); 479 | } else { 480 | setError(null); 481 | } 482 | postInvalidate(); 483 | } 484 | }); 485 | } 486 | 487 | private Typeface getCustomTypeface(@NonNull String fontPath) { 488 | return Typeface.createFromAsset(getContext().getAssets(), fontPath); 489 | } 490 | 491 | public void setIconLeft(@DrawableRes int res) { 492 | iconLeftBitmaps = generateIconBitmaps(res); 493 | initPadding(); 494 | } 495 | 496 | public void setIconLeft(Drawable drawable) { 497 | iconLeftBitmaps = generateIconBitmaps(drawable); 498 | initPadding(); 499 | } 500 | 501 | public void setIconLeft(Bitmap bitmap) { 502 | iconLeftBitmaps = generateIconBitmaps(bitmap); 503 | initPadding(); 504 | } 505 | 506 | public void setIconRight(@DrawableRes int res) { 507 | iconRightBitmaps = generateIconBitmaps(res); 508 | initPadding(); 509 | } 510 | 511 | public void setIconRight(Drawable drawable) { 512 | iconRightBitmaps = generateIconBitmaps(drawable); 513 | initPadding(); 514 | } 515 | 516 | public void setIconRight(Bitmap bitmap) { 517 | iconRightBitmaps = generateIconBitmaps(bitmap); 518 | initPadding(); 519 | } 520 | 521 | public boolean isShowClearButton() { 522 | return showClearButton; 523 | } 524 | 525 | public void setShowClearButton(boolean show) { 526 | showClearButton = show; 527 | correctPaddings(); 528 | } 529 | 530 | private Bitmap[] generateIconBitmaps(@DrawableRes int origin) { 531 | if (origin == -1) { 532 | return null; 533 | } 534 | BitmapFactory.Options options = new BitmapFactory.Options(); 535 | options.inJustDecodeBounds = true; 536 | BitmapFactory.decodeResource(getResources(), origin, options); 537 | int size = Math.max(options.outWidth, options.outHeight); 538 | options.inSampleSize = size > iconSize ? size / iconSize : 1; 539 | options.inJustDecodeBounds = false; 540 | return generateIconBitmaps(BitmapFactory.decodeResource(getResources(), origin, options)); 541 | } 542 | 543 | private Bitmap[] generateIconBitmaps(Drawable drawable) { 544 | if (drawable == null) 545 | return null; 546 | Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); 547 | Canvas canvas = new Canvas(bitmap); 548 | drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); 549 | drawable.draw(canvas); 550 | return generateIconBitmaps(Bitmap.createScaledBitmap(bitmap, iconSize, iconSize, false)); 551 | } 552 | 553 | private Bitmap[] generateIconBitmaps(Bitmap origin) { 554 | if (origin == null) { 555 | return null; 556 | } 557 | Bitmap[] iconBitmaps = new Bitmap[4]; 558 | origin = scaleIcon(origin); 559 | iconBitmaps[0] = origin.copy(Bitmap.Config.ARGB_8888, true); 560 | Canvas canvas = new Canvas(iconBitmaps[0]); 561 | canvas.drawColor(baseColor & 0x00ffffff | (Colors.isLight(baseColor) ? 0xff000000 : 0x8a000000), PorterDuff.Mode.SRC_IN); 562 | iconBitmaps[1] = origin.copy(Bitmap.Config.ARGB_8888, true); 563 | canvas = new Canvas(iconBitmaps[1]); 564 | canvas.drawColor(primaryColor, PorterDuff.Mode.SRC_IN); 565 | iconBitmaps[2] = origin.copy(Bitmap.Config.ARGB_8888, true); 566 | canvas = new Canvas(iconBitmaps[2]); 567 | canvas.drawColor(baseColor & 0x00ffffff | (Colors.isLight(baseColor) ? 0x4c000000 : 0x42000000), PorterDuff.Mode.SRC_IN); 568 | iconBitmaps[3] = origin.copy(Bitmap.Config.ARGB_8888, true); 569 | canvas = new Canvas(iconBitmaps[3]); 570 | canvas.drawColor(errorColor, PorterDuff.Mode.SRC_IN); 571 | return iconBitmaps; 572 | } 573 | 574 | private Bitmap scaleIcon(Bitmap origin) { 575 | int width = origin.getWidth(); 576 | int height = origin.getHeight(); 577 | int size = Math.max(width, height); 578 | if (size == iconSize) { 579 | return origin; 580 | } else if (size > iconSize) { 581 | int scaledWidth; 582 | int scaledHeight; 583 | if (width > iconSize) { 584 | scaledWidth = iconSize; 585 | scaledHeight = (int) (iconSize * ((float) height / width)); 586 | } else { 587 | scaledHeight = iconSize; 588 | scaledWidth = (int) (iconSize * ((float) width / height)); 589 | } 590 | return Bitmap.createScaledBitmap(origin, scaledWidth, scaledHeight, false); 591 | } else { 592 | return origin; 593 | } 594 | } 595 | 596 | public float getFloatingLabelFraction() { 597 | return floatingLabelFraction; 598 | } 599 | 600 | public void setFloatingLabelFraction(float floatingLabelFraction) { 601 | this.floatingLabelFraction = floatingLabelFraction; 602 | invalidate(); 603 | } 604 | 605 | public float getFocusFraction() { 606 | return focusFraction; 607 | } 608 | 609 | public void setFocusFraction(float focusFraction) { 610 | this.focusFraction = focusFraction; 611 | invalidate(); 612 | } 613 | 614 | public float getCurrentBottomLines() { 615 | return currentBottomLines; 616 | } 617 | 618 | public void setCurrentBottomLines(float currentBottomLines) { 619 | this.currentBottomLines = currentBottomLines; 620 | initPadding(); 621 | } 622 | 623 | public boolean isFloatingLabelAlwaysShown() { 624 | return floatingLabelAlwaysShown; 625 | } 626 | 627 | public void setFloatingLabelAlwaysShown(boolean floatingLabelAlwaysShown) { 628 | this.floatingLabelAlwaysShown = floatingLabelAlwaysShown; 629 | invalidate(); 630 | } 631 | 632 | public boolean isHelperTextAlwaysShown() { 633 | return helperTextAlwaysShown; 634 | } 635 | 636 | public void setHelperTextAlwaysShown(boolean helperTextAlwaysShown) { 637 | this.helperTextAlwaysShown = helperTextAlwaysShown; 638 | invalidate(); 639 | } 640 | 641 | @Nullable 642 | public Typeface getAccentTypeface() { 643 | return accentTypeface; 644 | } 645 | 646 | /** 647 | * Set typeface used for the accent texts (floating label, error/helper text, character counter, etc.) 648 | */ 649 | public void setAccentTypeface(Typeface accentTypeface) { 650 | this.accentTypeface = accentTypeface; 651 | this.textPaint.setTypeface(accentTypeface); 652 | postInvalidate(); 653 | } 654 | 655 | public boolean isHideUnderline() { 656 | return hideUnderline; 657 | } 658 | 659 | /** 660 | * Set whether or not to hide the underline (shown by default). 661 | *

662 | * The positions of text below will be adjusted accordingly (error/helper text, character counter, ellipses, etc.) 663 | *

664 | * NOTE: You probably don't want to hide this if you have any subtext features of this enabled, as it can look weird to not have a dividing line between them. 665 | */ 666 | public void setHideUnderline(boolean hideUnderline) { 667 | this.hideUnderline = hideUnderline; 668 | initPadding(); 669 | postInvalidate(); 670 | } 671 | 672 | /** 673 | * get the color of the underline for normal state 674 | */ 675 | public int getUnderlineColor() { 676 | return underlineColor; 677 | } 678 | 679 | /** 680 | * Set the color of the underline for normal state 681 | * @param color 682 | */ 683 | public void setUnderlineColor(int color) { 684 | this.underlineColor = color; 685 | postInvalidate(); 686 | } 687 | 688 | public CharSequence getFloatingLabelText() { 689 | return floatingLabelText; 690 | } 691 | 692 | /** 693 | * Set the floating label text. 694 | *

695 | * Pass null to force fallback to use hint's value. 696 | * 697 | * @param floatingLabelText 698 | */ 699 | public void setFloatingLabelText(@Nullable CharSequence floatingLabelText) { 700 | this.floatingLabelText = floatingLabelText == null ? getHint() : floatingLabelText; 701 | postInvalidate(); 702 | } 703 | 704 | public int getFloatingLabelTextSize() { 705 | return floatingLabelTextSize; 706 | } 707 | 708 | public void setFloatingLabelTextSize(int size) { 709 | floatingLabelTextSize = size; 710 | initPadding(); 711 | } 712 | 713 | public int getFloatingLabelTextColor() { 714 | return floatingLabelTextColor; 715 | } 716 | 717 | public void setFloatingLabelTextColor(int color) { 718 | this.floatingLabelTextColor = color; 719 | postInvalidate(); 720 | } 721 | 722 | public int getBottomTextSize() { 723 | return bottomTextSize; 724 | } 725 | 726 | public void setBottomTextSize(int size) { 727 | bottomTextSize = size; 728 | initPadding(); 729 | } 730 | 731 | private int getPixel(int dp) { 732 | return Density.dp2px(getContext(), dp); 733 | } 734 | 735 | private void initPadding() { 736 | extraPaddingTop = floatingLabelEnabled ? floatingLabelTextSize + floatingLabelPadding : floatingLabelPadding; 737 | textPaint.setTextSize(bottomTextSize); 738 | Paint.FontMetrics textMetrics = textPaint.getFontMetrics(); 739 | extraPaddingBottom = (int) ((textMetrics.descent - textMetrics.ascent) * currentBottomLines) + (hideUnderline ? bottomSpacing : bottomSpacing * 2); 740 | extraPaddingLeft = iconLeftBitmaps == null ? 0 : (iconOuterWidth + iconPadding); 741 | extraPaddingRight = iconRightBitmaps == null ? 0 : (iconOuterWidth + iconPadding); 742 | correctPaddings(); 743 | } 744 | 745 | /** 746 | * calculate {@link #minBottomLines} 747 | */ 748 | private void initMinBottomLines() { 749 | boolean extendBottom = minCharacters > 0 || maxCharacters > 0 || singleLineEllipsis || tempErrorText != null || helperText != null; 750 | currentBottomLines = minBottomLines = minBottomTextLines > 0 ? minBottomTextLines : extendBottom ? 1 : 0; 751 | } 752 | 753 | /** 754 | * use {@link #setPaddings(int, int, int, int)} instead, or the paddingTop and the paddingBottom may be set incorrectly. 755 | */ 756 | @Deprecated 757 | @Override 758 | public final void setPadding(int left, int top, int right, int bottom) { 759 | super.setPadding(left, top, right, bottom); 760 | } 761 | 762 | /** 763 | * Use this method instead of {@link #setPadding(int, int, int, int)} to automatically set the paddingTop and the paddingBottom correctly. 764 | */ 765 | public void setPaddings(int left, int top, int right, int bottom) { 766 | innerPaddingTop = top; 767 | innerPaddingBottom = bottom; 768 | innerPaddingLeft = left; 769 | innerPaddingRight = right; 770 | correctPaddings(); 771 | } 772 | 773 | /** 774 | * Set paddings to the correct values 775 | */ 776 | private void correctPaddings() { 777 | int buttonsWidthLeft = 0, buttonsWidthRight = 0; 778 | int buttonsWidth = iconOuterWidth * getButtonsCount(); 779 | if (isRTL()) { 780 | buttonsWidthLeft = buttonsWidth; 781 | } else { 782 | buttonsWidthRight = buttonsWidth; 783 | } 784 | super.setPadding(innerPaddingLeft + extraPaddingLeft + buttonsWidthLeft, innerPaddingTop + extraPaddingTop, innerPaddingRight + extraPaddingRight + buttonsWidthRight, innerPaddingBottom + extraPaddingBottom); 785 | } 786 | 787 | private int getButtonsCount() { 788 | return isShowClearButton() ? 1 : 0; 789 | } 790 | 791 | @Override 792 | protected void onAttachedToWindow() { 793 | super.onAttachedToWindow(); 794 | if (!firstShown) { 795 | firstShown = true; 796 | } 797 | } 798 | 799 | @Override 800 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 801 | super.onLayout(changed, left, top, right, bottom); 802 | if (changed) { 803 | adjustBottomLines(); 804 | } 805 | } 806 | 807 | /** 808 | * @return True, if adjustments were made that require the view to be invalidated. 809 | */ 810 | private boolean adjustBottomLines() { 811 | // Bail out if we have a zero width; lines will be adjusted during next layout. 812 | if (getWidth() == 0) { 813 | return false; 814 | } 815 | int destBottomLines; 816 | textPaint.setTextSize(bottomTextSize); 817 | if (tempErrorText != null || helperText != null) { 818 | Layout.Alignment alignment = (getGravity() & Gravity.RIGHT) == Gravity.RIGHT || isRTL() ? 819 | Layout.Alignment.ALIGN_OPPOSITE : (getGravity() & Gravity.LEFT) == Gravity.LEFT ? 820 | Layout.Alignment.ALIGN_NORMAL : Layout.Alignment.ALIGN_CENTER; 821 | textLayout = new StaticLayout(tempErrorText != null ? tempErrorText : helperText, textPaint, getWidth() - getBottomTextLeftOffset() - getBottomTextRightOffset() - getPaddingLeft() - getPaddingRight(), alignment, 1.0f, 0.0f, true); 822 | destBottomLines = Math.max(textLayout.getLineCount(), minBottomTextLines); 823 | } else { 824 | destBottomLines = minBottomLines; 825 | } 826 | if (bottomLines != destBottomLines) { 827 | getBottomLinesAnimator(destBottomLines).start(); 828 | } 829 | bottomLines = destBottomLines; 830 | return true; 831 | } 832 | 833 | /** 834 | * get inner top padding, not the real paddingTop 835 | */ 836 | public int getInnerPaddingTop() { 837 | return innerPaddingTop; 838 | } 839 | 840 | /** 841 | * get inner bottom padding, not the real paddingBottom 842 | */ 843 | public int getInnerPaddingBottom() { 844 | return innerPaddingBottom; 845 | } 846 | 847 | /** 848 | * get inner left padding, not the real paddingLeft 849 | */ 850 | public int getInnerPaddingLeft() { 851 | return innerPaddingLeft; 852 | } 853 | 854 | /** 855 | * get inner right padding, not the real paddingRight 856 | */ 857 | public int getInnerPaddingRight() { 858 | return innerPaddingRight; 859 | } 860 | 861 | private void initFloatingLabel() { 862 | // observe the text changing 863 | addTextChangedListener(new TextWatcher() { 864 | @Override 865 | public void beforeTextChanged(CharSequence s, int start, int count, int after) { 866 | } 867 | 868 | @Override 869 | public void onTextChanged(CharSequence s, int start, int before, int count) { 870 | } 871 | 872 | @Override 873 | public void afterTextChanged(Editable s) { 874 | if (floatingLabelEnabled) { 875 | if (s.length() == 0) { 876 | if (floatingLabelShown) { 877 | floatingLabelShown = false; 878 | getLabelAnimator().reverse(); 879 | } 880 | } else if (!floatingLabelShown) { 881 | floatingLabelShown = true; 882 | getLabelAnimator().start(); 883 | } 884 | } 885 | } 886 | }); 887 | // observe the focus state to animate the floating label's text color appropriately 888 | innerFocusChangeListener = new OnFocusChangeListener() { 889 | @Override 890 | public void onFocusChange(View v, boolean hasFocus) { 891 | if (floatingLabelEnabled && highlightFloatingLabel) { 892 | if (hasFocus) { 893 | getLabelFocusAnimator().start(); 894 | } else { 895 | getLabelFocusAnimator().reverse(); 896 | } 897 | } 898 | if (validateOnFocusLost && !hasFocus) { 899 | validate(); 900 | } 901 | if (outerFocusChangeListener != null) { 902 | outerFocusChangeListener.onFocusChange(v, hasFocus); 903 | } 904 | } 905 | }; 906 | super.setOnFocusChangeListener(innerFocusChangeListener); 907 | } 908 | 909 | public boolean isValidateOnFocusLost() { 910 | return validateOnFocusLost; 911 | } 912 | 913 | public void setValidateOnFocusLost(boolean validate) { 914 | this.validateOnFocusLost = validate; 915 | } 916 | 917 | public void setBaseColor(int color) { 918 | if (baseColor != color) { 919 | baseColor = color; 920 | } 921 | 922 | initText(); 923 | 924 | postInvalidate(); 925 | } 926 | 927 | public void setPrimaryColor(int color) { 928 | primaryColor = color; 929 | postInvalidate(); 930 | } 931 | 932 | /** 933 | * Same function as {@link #setTextColor(int)}. (Directly overriding the built-in one could cause some error, so use this method instead.) 934 | */ 935 | public void setMetTextColor(int color) { 936 | textColorStateList = ColorStateList.valueOf(color); 937 | resetTextColor(); 938 | } 939 | 940 | /** 941 | * Same function as {@link #setTextColor(ColorStateList)}. (Directly overriding the built-in one could cause some error, so use this method instead.) 942 | */ 943 | public void setMetTextColor(ColorStateList colors) { 944 | textColorStateList = colors; 945 | resetTextColor(); 946 | } 947 | 948 | private void resetTextColor() { 949 | if (textColorStateList == null) { 950 | textColorStateList = new ColorStateList(new int[][]{new int[]{android.R.attr.state_enabled}, EMPTY_STATE_SET}, new int[]{baseColor & 0x00ffffff | 0xdf000000, baseColor & 0x00ffffff | 0x44000000}); 951 | setTextColor(textColorStateList); 952 | } else { 953 | setTextColor(textColorStateList); 954 | } 955 | } 956 | 957 | /** 958 | * Same function as {@link #setHintTextColor(int)}. (The built-in one is a final method that can't be overridden, so use this method instead.) 959 | */ 960 | public void setMetHintTextColor(int color) { 961 | textColorHintStateList = ColorStateList.valueOf(color); 962 | resetHintTextColor(); 963 | } 964 | 965 | /** 966 | * Same function as {@link #setHintTextColor(ColorStateList)}. (The built-in one is a final method that can't be overridden, so use this method instead.) 967 | */ 968 | public void setMetHintTextColor(ColorStateList colors) { 969 | textColorHintStateList = colors; 970 | resetHintTextColor(); 971 | } 972 | 973 | private void resetHintTextColor() { 974 | if (textColorHintStateList == null) { 975 | setHintTextColor(baseColor & 0x00ffffff | 0x44000000); 976 | } else { 977 | setHintTextColor(textColorHintStateList); 978 | } 979 | } 980 | 981 | private void setFloatingLabelInternal(int mode) { 982 | switch (mode) { 983 | case FLOATING_LABEL_NORMAL: 984 | floatingLabelEnabled = true; 985 | highlightFloatingLabel = false; 986 | break; 987 | case FLOATING_LABEL_HIGHLIGHT: 988 | floatingLabelEnabled = true; 989 | highlightFloatingLabel = true; 990 | break; 991 | default: 992 | floatingLabelEnabled = false; 993 | highlightFloatingLabel = false; 994 | break; 995 | } 996 | } 997 | 998 | public void setFloatingLabel(@FloatingLabelType int mode) { 999 | setFloatingLabelInternal(mode); 1000 | initPadding(); 1001 | } 1002 | 1003 | public int getFloatingLabelPadding() { 1004 | return floatingLabelPadding; 1005 | } 1006 | 1007 | public void setFloatingLabelPadding(int padding) { 1008 | floatingLabelPadding = padding; 1009 | postInvalidate(); 1010 | } 1011 | 1012 | public boolean isFloatingLabelAnimating() { 1013 | return floatingLabelAnimating; 1014 | } 1015 | 1016 | public void setFloatingLabelAnimating(boolean animating) { 1017 | floatingLabelAnimating = animating; 1018 | } 1019 | 1020 | public void setSingleLineEllipsis() { 1021 | setSingleLineEllipsis(true); 1022 | } 1023 | 1024 | public void setSingleLineEllipsis(boolean enabled) { 1025 | singleLineEllipsis = enabled; 1026 | initMinBottomLines(); 1027 | initPadding(); 1028 | postInvalidate(); 1029 | } 1030 | 1031 | public int getMaxCharacters() { 1032 | return maxCharacters; 1033 | } 1034 | 1035 | public void setMaxCharacters(int max) { 1036 | maxCharacters = max; 1037 | initMinBottomLines(); 1038 | initPadding(); 1039 | postInvalidate(); 1040 | } 1041 | 1042 | public int getMinCharacters() { 1043 | return minCharacters; 1044 | } 1045 | 1046 | public void setMinCharacters(int min) { 1047 | minCharacters = min; 1048 | initMinBottomLines(); 1049 | initPadding(); 1050 | postInvalidate(); 1051 | } 1052 | 1053 | public int getMinBottomTextLines() { 1054 | return minBottomTextLines; 1055 | } 1056 | 1057 | public void setMinBottomTextLines(int lines) { 1058 | minBottomTextLines = lines; 1059 | initMinBottomLines(); 1060 | initPadding(); 1061 | postInvalidate(); 1062 | } 1063 | 1064 | public boolean isAutoValidate() { 1065 | return autoValidate; 1066 | } 1067 | 1068 | public void setAutoValidate(boolean autoValidate) { 1069 | this.autoValidate = autoValidate; 1070 | if (autoValidate) { 1071 | validate(); 1072 | } 1073 | } 1074 | 1075 | public int getErrorColor() { 1076 | return errorColor; 1077 | } 1078 | 1079 | public void setErrorColor(int color) { 1080 | errorColor = color; 1081 | postInvalidate(); 1082 | } 1083 | 1084 | public void setHelperText(CharSequence helperText) { 1085 | this.helperText = helperText == null ? null : helperText.toString(); 1086 | if (adjustBottomLines()) { 1087 | postInvalidate(); 1088 | } 1089 | } 1090 | 1091 | public String getHelperText() { 1092 | return helperText; 1093 | } 1094 | 1095 | public int getHelperTextColor() { 1096 | return helperTextColor; 1097 | } 1098 | 1099 | public void setHelperTextColor(int color) { 1100 | helperTextColor = color; 1101 | postInvalidate(); 1102 | } 1103 | 1104 | @Override 1105 | public void setError(CharSequence errorText) { 1106 | tempErrorText = errorText == null ? null : errorText.toString(); 1107 | if (adjustBottomLines()) { 1108 | postInvalidate(); 1109 | } 1110 | } 1111 | 1112 | @Override 1113 | public CharSequence getError() { 1114 | return tempErrorText; 1115 | } 1116 | 1117 | /** 1118 | * only used to draw the bottom line 1119 | */ 1120 | private boolean isInternalValid() { 1121 | return tempErrorText == null && isCharactersCountValid(); 1122 | } 1123 | 1124 | /** 1125 | * if the main text matches the regex 1126 | * 1127 | * @deprecated use the new validator interface to add your own custom validator 1128 | */ 1129 | @Deprecated 1130 | public boolean isValid(String regex) { 1131 | if (regex == null) { 1132 | return false; 1133 | } 1134 | Pattern pattern = Pattern.compile(regex); 1135 | Matcher matcher = pattern.matcher(getText()); 1136 | return matcher.matches(); 1137 | } 1138 | 1139 | /** 1140 | * check if the main text matches the regex, and set the error text if not. 1141 | * 1142 | * @return true if it matches the regex, false if not. 1143 | * @deprecated use the new validator interface to add your own custom validator 1144 | */ 1145 | @Deprecated 1146 | public boolean validate(String regex, CharSequence errorText) { 1147 | boolean isValid = isValid(regex); 1148 | if (!isValid) { 1149 | setError(errorText); 1150 | } 1151 | postInvalidate(); 1152 | return isValid; 1153 | } 1154 | 1155 | /** 1156 | * Run validation on a single validator instance 1157 | * 1158 | * @param validator Validator to check 1159 | * @return True if valid, false if not 1160 | */ 1161 | public boolean validateWith(@NonNull METValidator validator) { 1162 | CharSequence text = getText(); 1163 | boolean isValid = validator.isValid(text, text.length() == 0); 1164 | if (!isValid) { 1165 | setError(validator.getErrorMessage()); 1166 | } 1167 | postInvalidate(); 1168 | return isValid; 1169 | } 1170 | 1171 | /** 1172 | * Check all validators, sets the error text if not 1173 | *

1174 | * NOTE: this stops at the first validator to report invalid. 1175 | * 1176 | * @return True if all validators pass, false if not 1177 | */ 1178 | public boolean validate() { 1179 | if (validators == null || validators.isEmpty()) { 1180 | return true; 1181 | } 1182 | 1183 | CharSequence text = getText(); 1184 | boolean isEmpty = text.length() == 0; 1185 | 1186 | boolean isValid = true; 1187 | for (METValidator validator : validators) { 1188 | //noinspection ConstantConditions 1189 | isValid = isValid && validator.isValid(text, isEmpty); 1190 | if (!isValid) { 1191 | setError(validator.getErrorMessage()); 1192 | break; 1193 | } 1194 | } 1195 | if (isValid) { 1196 | setError(null); 1197 | } 1198 | 1199 | postInvalidate(); 1200 | return isValid; 1201 | } 1202 | 1203 | public boolean hasValidators() { 1204 | return this.validators != null && !this.validators.isEmpty(); 1205 | } 1206 | 1207 | /** 1208 | * Adds a new validator to the View's list of validators 1209 | *

1210 | * This will be checked with the others in {@link #validate()} 1211 | * 1212 | * @param validator Validator to add 1213 | * @return This instance, for easy chaining 1214 | */ 1215 | public MaterialAutoCompleteTextView addValidator(METValidator validator) { 1216 | if (validators == null) { 1217 | this.validators = new ArrayList<>(); 1218 | } 1219 | this.validators.add(validator); 1220 | return this; 1221 | } 1222 | 1223 | public void clearValidators() { 1224 | if (this.validators != null) { 1225 | this.validators.clear(); 1226 | } 1227 | } 1228 | 1229 | @Nullable 1230 | public List getValidators() { 1231 | return this.validators; 1232 | } 1233 | 1234 | public void setLengthChecker(METLengthChecker lengthChecker) { 1235 | this.lengthChecker = lengthChecker; 1236 | } 1237 | 1238 | @Override 1239 | public void setOnFocusChangeListener(OnFocusChangeListener listener) { 1240 | if (innerFocusChangeListener == null) { 1241 | super.setOnFocusChangeListener(listener); 1242 | } else { 1243 | outerFocusChangeListener = listener; 1244 | } 1245 | } 1246 | 1247 | private ObjectAnimator getLabelAnimator() { 1248 | if (labelAnimator == null) { 1249 | labelAnimator = ObjectAnimator.ofFloat(this, "floatingLabelFraction", 0f, 1f); 1250 | } 1251 | labelAnimator.setDuration(floatingLabelAnimating ? 300 : 0); 1252 | return labelAnimator; 1253 | } 1254 | 1255 | private ObjectAnimator getLabelFocusAnimator() { 1256 | if (labelFocusAnimator == null) { 1257 | labelFocusAnimator = ObjectAnimator.ofFloat(this, "focusFraction", 0f, 1f); 1258 | } 1259 | return labelFocusAnimator; 1260 | } 1261 | 1262 | private ObjectAnimator getBottomLinesAnimator(float destBottomLines) { 1263 | if (bottomLinesAnimator == null) { 1264 | bottomLinesAnimator = ObjectAnimator.ofFloat(this, "currentBottomLines", destBottomLines); 1265 | } else { 1266 | bottomLinesAnimator.cancel(); 1267 | bottomLinesAnimator.setFloatValues(destBottomLines); 1268 | } 1269 | return bottomLinesAnimator; 1270 | } 1271 | 1272 | @Override 1273 | protected void onDraw(@NonNull Canvas canvas) { 1274 | int startX = getScrollX() + (iconLeftBitmaps == null ? 0 : (iconOuterWidth + iconPadding)); 1275 | int endX = getScrollX() + (iconRightBitmaps == null ? getWidth() : getWidth() - iconOuterWidth - iconPadding); 1276 | int lineStartY = getScrollY() + getHeight() - getPaddingBottom(); 1277 | 1278 | // draw the icon(s) 1279 | paint.setAlpha(255); 1280 | if (iconLeftBitmaps != null) { 1281 | Bitmap icon = iconLeftBitmaps[!isInternalValid() ? 3 : !isEnabled() ? 2 : hasFocus() ? 1 : 0]; 1282 | int iconLeft = startX - iconPadding - iconOuterWidth + (iconOuterWidth - icon.getWidth()) / 2; 1283 | int iconTop = lineStartY + bottomSpacing - iconOuterHeight + (iconOuterHeight - icon.getHeight()) / 2; 1284 | canvas.drawBitmap(icon, iconLeft, iconTop, paint); 1285 | } 1286 | if (iconRightBitmaps != null) { 1287 | Bitmap icon = iconRightBitmaps[!isInternalValid() ? 3 : !isEnabled() ? 2 : hasFocus() ? 1 : 0]; 1288 | int iconRight = endX + iconPadding + (iconOuterWidth - icon.getWidth()) / 2; 1289 | int iconTop = lineStartY + bottomSpacing - iconOuterHeight + (iconOuterHeight - icon.getHeight()) / 2; 1290 | canvas.drawBitmap(icon, iconRight, iconTop, paint); 1291 | } 1292 | 1293 | // draw the clear button 1294 | if (hasFocus() && showClearButton && !TextUtils.isEmpty(getText()) && isEnabled()) { 1295 | paint.setAlpha(255); 1296 | int buttonLeft; 1297 | if (isRTL()) { 1298 | buttonLeft = startX; 1299 | } else { 1300 | buttonLeft = endX - iconOuterWidth; 1301 | } 1302 | Bitmap clearButtonBitmap = clearButtonBitmaps[0]; 1303 | buttonLeft += (iconOuterWidth - clearButtonBitmap.getWidth()) / 2; 1304 | int iconTop = lineStartY + bottomSpacing - iconOuterHeight + (iconOuterHeight - clearButtonBitmap.getHeight()) / 2; 1305 | canvas.drawBitmap(clearButtonBitmap, buttonLeft, iconTop, paint); 1306 | } 1307 | 1308 | // draw the underline 1309 | if (!hideUnderline) { 1310 | lineStartY += bottomSpacing; 1311 | if (!isInternalValid()) { // not valid 1312 | paint.setColor(errorColor); 1313 | canvas.drawRect(startX, lineStartY, endX, lineStartY + getPixel(2), paint); 1314 | } else if (!isEnabled()) { // disabled 1315 | paint.setColor(underlineColor != -1 ? underlineColor : baseColor & 0x00ffffff | 0x44000000); 1316 | float interval = getPixel(1); 1317 | for (float xOffset = 0; xOffset < getWidth(); xOffset += interval * 3) { 1318 | canvas.drawRect(startX + xOffset, lineStartY, startX + xOffset + interval, lineStartY + getPixel(1), paint); 1319 | } 1320 | } else if (hasFocus()) { // focused 1321 | paint.setColor(primaryColor); 1322 | canvas.drawRect(startX, lineStartY, endX, lineStartY + getPixel(2), paint); 1323 | } else { // normal 1324 | paint.setColor(underlineColor != -1 ? underlineColor : baseColor & 0x00ffffff | 0x1E000000); 1325 | canvas.drawRect(startX, lineStartY, endX, lineStartY + getPixel(1), paint); 1326 | } 1327 | } 1328 | 1329 | textPaint.setTextSize(bottomTextSize); 1330 | Paint.FontMetrics textMetrics = textPaint.getFontMetrics(); 1331 | float relativeHeight = -textMetrics.ascent - textMetrics.descent; 1332 | float bottomTextPadding = bottomTextSize + textMetrics.ascent + textMetrics.descent; 1333 | 1334 | // draw the characters counter 1335 | if ((hasFocus() && hasCharactersCounter()) || !isCharactersCountValid()) { 1336 | textPaint.setColor(isCharactersCountValid() ? (baseColor & 0x00ffffff | 0x44000000) : errorColor); 1337 | String charactersCounterText = getCharactersCounterText(); 1338 | canvas.drawText(charactersCounterText, isRTL() ? startX : endX - textPaint.measureText(charactersCounterText), lineStartY + bottomSpacing + relativeHeight, textPaint); 1339 | } 1340 | 1341 | // draw the bottom text 1342 | if (textLayout != null) { 1343 | if (tempErrorText != null || ((helperTextAlwaysShown || hasFocus()) && !TextUtils.isEmpty(helperText))) { // error text or helper text 1344 | textPaint.setColor(tempErrorText != null ? errorColor : helperTextColor != -1 ? helperTextColor : (baseColor & 0x00ffffff | 0x44000000)); 1345 | canvas.save(); 1346 | if (isRTL()) { 1347 | canvas.translate(endX - textLayout.getWidth(), lineStartY + bottomSpacing - bottomTextPadding); 1348 | } else { 1349 | canvas.translate(startX + getBottomTextLeftOffset(), lineStartY + bottomSpacing - bottomTextPadding); 1350 | } 1351 | textLayout.draw(canvas); 1352 | canvas.restore(); 1353 | } 1354 | } 1355 | 1356 | // draw the floating label 1357 | if (floatingLabelEnabled && !TextUtils.isEmpty(floatingLabelText)) { 1358 | textPaint.setTextSize(floatingLabelTextSize); 1359 | // calculate the text color 1360 | textPaint.setColor((Integer) focusEvaluator.evaluate(focusFraction * (isEnabled() ? 1 : 0), floatingLabelTextColor != -1 ? floatingLabelTextColor : (baseColor & 0x00ffffff | 0x44000000), primaryColor)); 1361 | 1362 | // calculate the horizontal position 1363 | float floatingLabelWidth = textPaint.measureText(floatingLabelText.toString()); 1364 | int floatingLabelStartX; 1365 | if ((getGravity() & Gravity.RIGHT) == Gravity.RIGHT || isRTL()) { 1366 | floatingLabelStartX = (int) (endX - floatingLabelWidth); 1367 | } else if ((getGravity() & Gravity.LEFT) == Gravity.LEFT) { 1368 | floatingLabelStartX = startX; 1369 | } else { 1370 | floatingLabelStartX = startX + (int) (getInnerPaddingLeft() + (getWidth() - getInnerPaddingLeft() - getInnerPaddingRight() - floatingLabelWidth) / 2); 1371 | } 1372 | 1373 | // calculate the vertical position 1374 | int distance = floatingLabelPadding; 1375 | int floatingLabelStartY = (int) (innerPaddingTop + floatingLabelTextSize + floatingLabelPadding - distance * (floatingLabelAlwaysShown ? 1 : floatingLabelFraction) + getScrollY()); 1376 | 1377 | // calculate the alpha 1378 | int alpha = ((int) ((floatingLabelAlwaysShown ? 1 : floatingLabelFraction) * 0xff * (0.74f * focusFraction * (isEnabled() ? 1 : 0) + 0.26f) * (floatingLabelTextColor != -1 ? 1 : Color.alpha(floatingLabelTextColor) / 256f))); 1379 | textPaint.setAlpha(alpha); 1380 | 1381 | // draw the floating label 1382 | canvas.drawText(floatingLabelText.toString(), floatingLabelStartX, floatingLabelStartY, textPaint); 1383 | } 1384 | 1385 | // draw the bottom ellipsis 1386 | if (hasFocus() && singleLineEllipsis && getScrollX() != 0) { 1387 | paint.setColor(isInternalValid() ? primaryColor : errorColor); 1388 | float startY = lineStartY + bottomSpacing; 1389 | int ellipsisStartX; 1390 | if (isRTL()) { 1391 | ellipsisStartX = endX; 1392 | } else { 1393 | ellipsisStartX = startX; 1394 | } 1395 | int signum = isRTL() ? -1 : 1; 1396 | canvas.drawCircle(ellipsisStartX + signum * bottomEllipsisSize / 2, startY + bottomEllipsisSize / 2, bottomEllipsisSize / 2, paint); 1397 | canvas.drawCircle(ellipsisStartX + signum * bottomEllipsisSize * 5 / 2, startY + bottomEllipsisSize / 2, bottomEllipsisSize / 2, paint); 1398 | canvas.drawCircle(ellipsisStartX + signum * bottomEllipsisSize * 9 / 2, startY + bottomEllipsisSize / 2, bottomEllipsisSize / 2, paint); 1399 | } 1400 | 1401 | // draw the original things 1402 | super.onDraw(canvas); 1403 | } 1404 | 1405 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) 1406 | private boolean isRTL() { 1407 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) { 1408 | return false; 1409 | } 1410 | Configuration config = getResources().getConfiguration(); 1411 | return config.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; 1412 | } 1413 | 1414 | private int getBottomTextLeftOffset() { 1415 | return isRTL() ? getCharactersCounterWidth() : getBottomEllipsisWidth(); 1416 | } 1417 | 1418 | private int getBottomTextRightOffset() { 1419 | return isRTL() ? getBottomEllipsisWidth() : getCharactersCounterWidth(); 1420 | } 1421 | 1422 | private int getCharactersCounterWidth() { 1423 | return hasCharactersCounter() ? (int) textPaint.measureText(getCharactersCounterText()) : 0; 1424 | } 1425 | 1426 | private int getBottomEllipsisWidth() { 1427 | return singleLineEllipsis ? (bottomEllipsisSize * 5 + getPixel(4)) : 0; 1428 | } 1429 | 1430 | private void checkCharactersCount() { 1431 | if ((!firstShown && !checkCharactersCountAtBeginning) || !hasCharactersCounter()) { 1432 | charactersCountValid = true; 1433 | } else { 1434 | CharSequence text = getText(); 1435 | int count = text == null ? 0 : checkLength(text); 1436 | charactersCountValid = (count >= minCharacters && (maxCharacters <= 0 || count <= maxCharacters)); 1437 | } 1438 | } 1439 | 1440 | public boolean isCharactersCountValid() { 1441 | return charactersCountValid; 1442 | } 1443 | 1444 | private boolean hasCharactersCounter() { 1445 | return minCharacters > 0 || maxCharacters > 0; 1446 | } 1447 | 1448 | private String getCharactersCounterText() { 1449 | String text; 1450 | if (minCharacters <= 0) { 1451 | text = isRTL() ? maxCharacters + " / " + checkLength(getText()) : checkLength(getText()) + " / " + maxCharacters; 1452 | } else if (maxCharacters <= 0) { 1453 | text = isRTL() ? "+" + minCharacters + " / " + checkLength(getText()) : checkLength(getText()) + " / " + minCharacters + "+"; 1454 | } else { 1455 | text = isRTL() ? maxCharacters + "-" + minCharacters + " / " + checkLength(getText()) : checkLength(getText()) + " / " + minCharacters + "-" + maxCharacters; 1456 | } 1457 | return text; 1458 | } 1459 | 1460 | @Override 1461 | public boolean onTouchEvent(MotionEvent event) { 1462 | if (singleLineEllipsis && getScrollX() > 0 && event.getAction() == MotionEvent.ACTION_DOWN && event.getX() < getPixel(4 * 5) && event.getY() > getHeight() - extraPaddingBottom - innerPaddingBottom && event.getY() < getHeight() - innerPaddingBottom) { 1463 | setSelection(0); 1464 | return false; 1465 | } 1466 | if (hasFocus() && showClearButton && isEnabled()) { 1467 | switch (event.getAction()) { 1468 | case MotionEvent.ACTION_DOWN: 1469 | if (insideClearButton(event)) { 1470 | clearButtonTouched = true; 1471 | clearButtonClicking = true; 1472 | return true; 1473 | } 1474 | case MotionEvent.ACTION_MOVE: 1475 | if (clearButtonClicking && !insideClearButton(event)) { 1476 | clearButtonClicking = false; 1477 | } 1478 | if (clearButtonTouched) { 1479 | return true; 1480 | } 1481 | break; 1482 | case MotionEvent.ACTION_UP: 1483 | if (clearButtonClicking) { 1484 | if (!TextUtils.isEmpty(getText())) { 1485 | setText(null); 1486 | } 1487 | clearButtonClicking = false; 1488 | } 1489 | if (clearButtonTouched) { 1490 | clearButtonTouched = false; 1491 | return true; 1492 | } 1493 | clearButtonTouched = false; 1494 | break; 1495 | case MotionEvent.ACTION_CANCEL: 1496 | clearButtonTouched = false; 1497 | clearButtonClicking = false; 1498 | break; 1499 | } 1500 | } 1501 | return super.onTouchEvent(event); 1502 | } 1503 | 1504 | private boolean insideClearButton(MotionEvent event) { 1505 | float x = event.getX(); 1506 | float y = event.getY(); 1507 | int startX = getScrollX() + (iconLeftBitmaps == null ? 0 : (iconOuterWidth + iconPadding)); 1508 | int endX = getScrollX() + (iconRightBitmaps == null ? getWidth() : getWidth() - iconOuterWidth - iconPadding); 1509 | int buttonLeft; 1510 | if (isRTL()) { 1511 | buttonLeft = startX; 1512 | } else { 1513 | buttonLeft = endX - iconOuterWidth; 1514 | } 1515 | int buttonTop = getScrollY() + getHeight() - getPaddingBottom() + bottomSpacing - iconOuterHeight; 1516 | return (x >= buttonLeft && x < buttonLeft + iconOuterWidth && y >= buttonTop && y < buttonTop + iconOuterHeight); 1517 | } 1518 | 1519 | private int checkLength(CharSequence text) { 1520 | if (lengthChecker==null) return text.length(); 1521 | return lengthChecker.getLength(text); 1522 | } 1523 | } -------------------------------------------------------------------------------- /library/src/main/java/com/rengwuxian/materialedittext/MaterialMultiAutoCompleteTextView.java: -------------------------------------------------------------------------------- 1 | package com.rengwuxian.materialedittext; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.content.res.ColorStateList; 6 | import android.content.res.Configuration; 7 | import android.content.res.TypedArray; 8 | import android.graphics.Bitmap; 9 | import android.graphics.BitmapFactory; 10 | import android.graphics.Canvas; 11 | import android.graphics.Color; 12 | import android.graphics.Paint; 13 | import android.graphics.PorterDuff; 14 | import android.graphics.Typeface; 15 | import android.graphics.drawable.Drawable; 16 | import android.os.Build; 17 | import android.support.annotation.DrawableRes; 18 | import android.support.annotation.IntDef; 19 | import android.support.annotation.NonNull; 20 | import android.support.annotation.Nullable; 21 | import android.support.v7.widget.AppCompatMultiAutoCompleteTextView; 22 | import android.text.Editable; 23 | import android.text.Layout; 24 | import android.text.StaticLayout; 25 | import android.text.TextPaint; 26 | import android.text.TextUtils; 27 | import android.text.TextWatcher; 28 | import android.text.method.TransformationMethod; 29 | import android.util.AttributeSet; 30 | import android.util.TypedValue; 31 | import android.view.Gravity; 32 | import android.view.MotionEvent; 33 | import android.view.View; 34 | 35 | import com.nineoldandroids.animation.ArgbEvaluator; 36 | import com.nineoldandroids.animation.ObjectAnimator; 37 | import com.rengwuxian.materialedittext.validation.METLengthChecker; 38 | import com.rengwuxian.materialedittext.validation.METValidator; 39 | 40 | import java.util.ArrayList; 41 | import java.util.List; 42 | import java.util.regex.Matcher; 43 | import java.util.regex.Pattern; 44 | 45 | /** 46 | * Created by rengwuxian on 2015/1/8. 47 | */ 48 | public class MaterialMultiAutoCompleteTextView extends AppCompatMultiAutoCompleteTextView { 49 | 50 | @IntDef({FLOATING_LABEL_NONE, FLOATING_LABEL_NORMAL, FLOATING_LABEL_HIGHLIGHT}) 51 | public @interface FloatingLabelType { 52 | } 53 | 54 | public static final int FLOATING_LABEL_NONE = 0; 55 | public static final int FLOATING_LABEL_NORMAL = 1; 56 | public static final int FLOATING_LABEL_HIGHLIGHT = 2; 57 | 58 | /** 59 | * the spacing between the main text and the inner top padding. 60 | */ 61 | private int extraPaddingTop; 62 | 63 | /** 64 | * the spacing between the main text and the inner bottom padding. 65 | */ 66 | private int extraPaddingBottom; 67 | 68 | /** 69 | * the extra spacing between the main text and the left, actually for the left icon. 70 | */ 71 | private int extraPaddingLeft; 72 | 73 | /** 74 | * the extra spacing between the main text and the right, actually for the right icon. 75 | */ 76 | private int extraPaddingRight; 77 | 78 | /** 79 | * the floating label's text size. 80 | */ 81 | private int floatingLabelTextSize; 82 | 83 | /** 84 | * the floating label's text color. 85 | */ 86 | private int floatingLabelTextColor; 87 | 88 | /** 89 | * the bottom texts' size. 90 | */ 91 | private int bottomTextSize; 92 | 93 | /** 94 | * the spacing between the main text and the floating label. 95 | */ 96 | private int floatingLabelPadding; 97 | 98 | /** 99 | * the spacing between the main text and the bottom components (bottom ellipsis, helper/error text, characters counter). 100 | */ 101 | private int bottomSpacing; 102 | 103 | /** 104 | * whether the floating label should be shown. default is false. 105 | */ 106 | private boolean floatingLabelEnabled; 107 | 108 | /** 109 | * whether to highlight the floating label's text color when focused (with the main color). default is true. 110 | */ 111 | private boolean highlightFloatingLabel; 112 | 113 | /** 114 | * the base color of the line and the texts. default is black. 115 | */ 116 | private int baseColor; 117 | 118 | /** 119 | * inner top padding 120 | */ 121 | private int innerPaddingTop; 122 | 123 | /** 124 | * inner bottom padding 125 | */ 126 | private int innerPaddingBottom; 127 | 128 | /** 129 | * inner left padding 130 | */ 131 | private int innerPaddingLeft; 132 | 133 | /** 134 | * inner right padding 135 | */ 136 | private int innerPaddingRight; 137 | 138 | /** 139 | * the underline's highlight color, and the highlight color of the floating label if app:highlightFloatingLabel is set true in the xml. default is black(when app:darkTheme is false) or white(when app:darkTheme is true) 140 | */ 141 | private int primaryColor; 142 | 143 | /** 144 | * the color for when something is wrong.(e.g. exceeding max characters) 145 | */ 146 | private int errorColor; 147 | 148 | /** 149 | * min characters count limit. 0 means no limit. default is 0. NOTE: the character counter will increase the View's height. 150 | */ 151 | private int minCharacters; 152 | 153 | /** 154 | * max characters count limit. 0 means no limit. default is 0. NOTE: the character counter will increase the View's height. 155 | */ 156 | private int maxCharacters; 157 | 158 | /** 159 | * whether to show the bottom ellipsis in singleLine mode. default is false. NOTE: the bottom ellipsis will increase the View's height. 160 | */ 161 | private boolean singleLineEllipsis; 162 | 163 | /** 164 | * Always show the floating label, instead of animating it in/out. False by default. 165 | */ 166 | private boolean floatingLabelAlwaysShown; 167 | 168 | /** 169 | * Always show the helper text, no matter if the edit text is focused. False by default. 170 | */ 171 | private boolean helperTextAlwaysShown; 172 | 173 | /** 174 | * bottom ellipsis's height 175 | */ 176 | private int bottomEllipsisSize; 177 | 178 | /** 179 | * min bottom lines count. 180 | */ 181 | private int minBottomLines; 182 | 183 | /** 184 | * reserved bottom text lines count, no matter if there is some helper/error text. 185 | */ 186 | private int minBottomTextLines; 187 | 188 | /** 189 | * real-time bottom lines count. used for bottom extending/collapsing animation. 190 | */ 191 | private float currentBottomLines; 192 | 193 | /** 194 | * bottom lines count. 195 | */ 196 | private float bottomLines; 197 | 198 | /** 199 | * Helper text at the bottom 200 | */ 201 | private String helperText; 202 | 203 | /** 204 | * Helper text color 205 | */ 206 | private int helperTextColor = -1; 207 | 208 | /** 209 | * error text for manually invoked {@link #setError(CharSequence)} 210 | */ 211 | private String tempErrorText; 212 | 213 | /** 214 | * animation fraction of the floating label (0 as totally hidden). 215 | */ 216 | private float floatingLabelFraction; 217 | 218 | /** 219 | * whether the floating label is being shown. 220 | */ 221 | private boolean floatingLabelShown; 222 | 223 | /** 224 | * the floating label's focusFraction 225 | */ 226 | private float focusFraction; 227 | 228 | /** 229 | * The font used for the accent texts (floating label, error/helper text, character counter, etc.) 230 | */ 231 | private Typeface accentTypeface; 232 | 233 | /** 234 | * The font used on the view (EditText content) 235 | */ 236 | private Typeface typeface; 237 | 238 | /** 239 | * Text for the floatLabel if different from the hint 240 | */ 241 | private CharSequence floatingLabelText; 242 | 243 | /** 244 | * Whether or not to show the underline. Shown by default 245 | */ 246 | private boolean hideUnderline; 247 | 248 | /** 249 | * Underline's color 250 | */ 251 | private int underlineColor; 252 | 253 | /** 254 | * Whether to validate as soon as the text has changed. False by default 255 | */ 256 | private boolean autoValidate; 257 | 258 | /** 259 | * Whether the characters count is valid 260 | */ 261 | private boolean charactersCountValid; 262 | 263 | /** 264 | * Whether use animation to show/hide the floating label. 265 | */ 266 | private boolean floatingLabelAnimating; 267 | 268 | /** 269 | * Whether check the characters count at the beginning it's shown. 270 | */ 271 | private boolean checkCharactersCountAtBeginning; 272 | 273 | /** 274 | * Left Icon 275 | */ 276 | private Bitmap[] iconLeftBitmaps; 277 | 278 | /** 279 | * Right Icon 280 | */ 281 | private Bitmap[] iconRightBitmaps; 282 | 283 | /** 284 | * Clear Button 285 | */ 286 | private Bitmap[] clearButtonBitmaps; 287 | 288 | /** 289 | * Auto validate when focus lost. 290 | */ 291 | private boolean validateOnFocusLost; 292 | 293 | private boolean showClearButton; 294 | private boolean firstShown; 295 | private int iconSize; 296 | private int iconOuterWidth; 297 | private int iconOuterHeight; 298 | private int iconPadding; 299 | private boolean clearButtonTouched; 300 | private boolean clearButtonClicking; 301 | private ColorStateList textColorStateList; 302 | private ColorStateList textColorHintStateList; 303 | private ArgbEvaluator focusEvaluator = new ArgbEvaluator(); 304 | Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); 305 | TextPaint textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); 306 | StaticLayout textLayout; 307 | ObjectAnimator labelAnimator; 308 | ObjectAnimator labelFocusAnimator; 309 | ObjectAnimator bottomLinesAnimator; 310 | OnFocusChangeListener innerFocusChangeListener; 311 | OnFocusChangeListener outerFocusChangeListener; 312 | private List validators; 313 | private METLengthChecker lengthChecker; 314 | 315 | public MaterialMultiAutoCompleteTextView(Context context) { 316 | super(context); 317 | init(context, null); 318 | } 319 | 320 | public MaterialMultiAutoCompleteTextView(Context context, AttributeSet attrs) { 321 | super(context, attrs); 322 | init(context, attrs); 323 | } 324 | 325 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 326 | public MaterialMultiAutoCompleteTextView(Context context, AttributeSet attrs, int style) { 327 | super(context, attrs, style); 328 | init(context, attrs); 329 | } 330 | 331 | private void init(Context context, AttributeSet attrs) { 332 | iconSize = getPixel(32); 333 | iconOuterWidth = getPixel(48); 334 | iconOuterHeight = getPixel(32); 335 | 336 | bottomSpacing = getResources().getDimensionPixelSize(R.dimen.inner_components_spacing); 337 | bottomEllipsisSize = getResources().getDimensionPixelSize(R.dimen.bottom_ellipsis_height); 338 | 339 | // default baseColor is black 340 | int defaultBaseColor = Color.BLACK; 341 | 342 | TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MaterialEditText); 343 | textColorStateList = typedArray.getColorStateList(R.styleable.MaterialEditText_met_textColor); 344 | textColorHintStateList = typedArray.getColorStateList(R.styleable.MaterialEditText_met_textColorHint); 345 | baseColor = typedArray.getColor(R.styleable.MaterialEditText_met_baseColor, defaultBaseColor); 346 | 347 | // retrieve the default primaryColor 348 | int defaultPrimaryColor; 349 | TypedValue primaryColorTypedValue = new TypedValue(); 350 | try { 351 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 352 | context.getTheme().resolveAttribute(android.R.attr.colorPrimary, primaryColorTypedValue, true); 353 | defaultPrimaryColor = primaryColorTypedValue.data; 354 | } else { 355 | throw new RuntimeException("SDK_INT less than LOLLIPOP"); 356 | } 357 | } catch (Exception e) { 358 | try { 359 | int colorPrimaryId = getResources().getIdentifier("colorPrimary", "attr", getContext().getPackageName()); 360 | if (colorPrimaryId != 0) { 361 | context.getTheme().resolveAttribute(colorPrimaryId, primaryColorTypedValue, true); 362 | defaultPrimaryColor = primaryColorTypedValue.data; 363 | } else { 364 | throw new RuntimeException("colorPrimary not found"); 365 | } 366 | } catch (Exception e1) { 367 | defaultPrimaryColor = baseColor; 368 | } 369 | } 370 | 371 | primaryColor = typedArray.getColor(R.styleable.MaterialEditText_met_primaryColor, defaultPrimaryColor); 372 | setFloatingLabelInternal(typedArray.getInt(R.styleable.MaterialEditText_met_floatingLabel, 0)); 373 | errorColor = typedArray.getColor(R.styleable.MaterialEditText_met_errorColor, Color.parseColor("#e7492E")); 374 | minCharacters = typedArray.getInt(R.styleable.MaterialEditText_met_minCharacters, 0); 375 | maxCharacters = typedArray.getInt(R.styleable.MaterialEditText_met_maxCharacters, 0); 376 | singleLineEllipsis = typedArray.getBoolean(R.styleable.MaterialEditText_met_singleLineEllipsis, false); 377 | helperText = typedArray.getString(R.styleable.MaterialEditText_met_helperText); 378 | helperTextColor = typedArray.getColor(R.styleable.MaterialEditText_met_helperTextColor, -1); 379 | minBottomTextLines = typedArray.getInt(R.styleable.MaterialEditText_met_minBottomTextLines, 0); 380 | String fontPathForAccent = typedArray.getString(R.styleable.MaterialEditText_met_accentTypeface); 381 | if (fontPathForAccent != null && !isInEditMode()) { 382 | accentTypeface = getCustomTypeface(fontPathForAccent); 383 | textPaint.setTypeface(accentTypeface); 384 | } 385 | String fontPathForView = typedArray.getString(R.styleable.MaterialEditText_met_typeface); 386 | if (fontPathForView != null && !isInEditMode()) { 387 | typeface = getCustomTypeface(fontPathForView); 388 | setTypeface(typeface); 389 | } 390 | floatingLabelText = typedArray.getString(R.styleable.MaterialEditText_met_floatingLabelText); 391 | if (floatingLabelText == null) { 392 | floatingLabelText = getHint(); 393 | } 394 | floatingLabelPadding = typedArray.getDimensionPixelSize(R.styleable.MaterialEditText_met_floatingLabelPadding, bottomSpacing); 395 | floatingLabelTextSize = typedArray.getDimensionPixelSize(R.styleable.MaterialEditText_met_floatingLabelTextSize, getResources().getDimensionPixelSize(R.dimen.floating_label_text_size)); 396 | floatingLabelTextColor = typedArray.getColor(R.styleable.MaterialEditText_met_floatingLabelTextColor, -1); 397 | floatingLabelAnimating = typedArray.getBoolean(R.styleable.MaterialEditText_met_floatingLabelAnimating, true); 398 | bottomTextSize = typedArray.getDimensionPixelSize(R.styleable.MaterialEditText_met_bottomTextSize, getResources().getDimensionPixelSize(R.dimen.bottom_text_size)); 399 | hideUnderline = typedArray.getBoolean(R.styleable.MaterialEditText_met_hideUnderline, false); 400 | underlineColor = typedArray.getColor(R.styleable.MaterialEditText_met_underlineColor, -1); 401 | autoValidate = typedArray.getBoolean(R.styleable.MaterialEditText_met_autoValidate, false); 402 | iconLeftBitmaps = generateIconBitmaps(typedArray.getResourceId(R.styleable.MaterialEditText_met_iconLeft, -1)); 403 | iconRightBitmaps = generateIconBitmaps(typedArray.getResourceId(R.styleable.MaterialEditText_met_iconRight, -1)); 404 | showClearButton = typedArray.getBoolean(R.styleable.MaterialEditText_met_clearButton, false); 405 | clearButtonBitmaps = generateIconBitmaps(R.drawable.met_ic_clear); 406 | iconPadding = typedArray.getDimensionPixelSize(R.styleable.MaterialEditText_met_iconPadding, getPixel(16)); 407 | floatingLabelAlwaysShown = typedArray.getBoolean(R.styleable.MaterialEditText_met_floatingLabelAlwaysShown, false); 408 | helperTextAlwaysShown = typedArray.getBoolean(R.styleable.MaterialEditText_met_helperTextAlwaysShown, false); 409 | validateOnFocusLost = typedArray.getBoolean(R.styleable.MaterialEditText_met_validateOnFocusLost, false); 410 | checkCharactersCountAtBeginning = typedArray.getBoolean(R.styleable.MaterialEditText_met_checkCharactersCountAtBeginning, true); 411 | typedArray.recycle(); 412 | 413 | int[] paddings = new int[]{ 414 | android.R.attr.padding, // 0 415 | android.R.attr.paddingLeft, // 1 416 | android.R.attr.paddingTop, // 2 417 | android.R.attr.paddingRight, // 3 418 | android.R.attr.paddingBottom // 4 419 | }; 420 | TypedArray paddingsTypedArray = context.obtainStyledAttributes(attrs, paddings); 421 | int padding = paddingsTypedArray.getDimensionPixelSize(0, 0); 422 | innerPaddingLeft = paddingsTypedArray.getDimensionPixelSize(1, padding); 423 | innerPaddingTop = paddingsTypedArray.getDimensionPixelSize(2, padding); 424 | innerPaddingRight = paddingsTypedArray.getDimensionPixelSize(3, padding); 425 | innerPaddingBottom = paddingsTypedArray.getDimensionPixelSize(4, padding); 426 | paddingsTypedArray.recycle(); 427 | 428 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { 429 | setBackground(null); 430 | } else { 431 | setBackgroundDrawable(null); 432 | } 433 | if (singleLineEllipsis) { 434 | TransformationMethod transformationMethod = getTransformationMethod(); 435 | setSingleLine(); 436 | setTransformationMethod(transformationMethod); 437 | } 438 | initMinBottomLines(); 439 | initPadding(); 440 | initText(); 441 | initFloatingLabel(); 442 | initTextWatcher(); 443 | checkCharactersCount(); 444 | } 445 | 446 | private void initText() { 447 | if (!TextUtils.isEmpty(getText())) { 448 | CharSequence text = getText(); 449 | setText(null); 450 | resetHintTextColor(); 451 | setText(text); 452 | setSelection(text.length()); 453 | floatingLabelFraction = 1; 454 | floatingLabelShown = true; 455 | } else { 456 | resetHintTextColor(); 457 | } 458 | resetTextColor(); 459 | } 460 | 461 | private void initTextWatcher() { 462 | addTextChangedListener(new TextWatcher() { 463 | @Override 464 | public void beforeTextChanged(CharSequence s, int start, int count, int after) { 465 | } 466 | 467 | @Override 468 | public void onTextChanged(CharSequence s, int start, int before, int count) { 469 | } 470 | 471 | @Override 472 | public void afterTextChanged(Editable s) { 473 | checkCharactersCount(); 474 | if (autoValidate) { 475 | validate(); 476 | } else { 477 | setError(null); 478 | } 479 | postInvalidate(); 480 | } 481 | }); 482 | } 483 | 484 | private Typeface getCustomTypeface(@NonNull String fontPath) { 485 | return Typeface.createFromAsset(getContext().getAssets(), fontPath); 486 | } 487 | 488 | public void setIconLeft(@DrawableRes int res) { 489 | iconLeftBitmaps = generateIconBitmaps(res); 490 | initPadding(); 491 | } 492 | 493 | public void setIconLeft(Drawable drawable) { 494 | iconLeftBitmaps = generateIconBitmaps(drawable); 495 | initPadding(); 496 | } 497 | 498 | public void setIconLeft(Bitmap bitmap) { 499 | iconLeftBitmaps = generateIconBitmaps(bitmap); 500 | initPadding(); 501 | } 502 | 503 | public void setIconRight(@DrawableRes int res) { 504 | iconRightBitmaps = generateIconBitmaps(res); 505 | initPadding(); 506 | } 507 | 508 | public void setIconRight(Drawable drawable) { 509 | iconRightBitmaps = generateIconBitmaps(drawable); 510 | initPadding(); 511 | } 512 | 513 | public void setIconRight(Bitmap bitmap) { 514 | iconRightBitmaps = generateIconBitmaps(bitmap); 515 | initPadding(); 516 | } 517 | 518 | public boolean isShowClearButton() { 519 | return showClearButton; 520 | } 521 | 522 | public void setShowClearButton(boolean show) { 523 | showClearButton = show; 524 | correctPaddings(); 525 | } 526 | 527 | private Bitmap[] generateIconBitmaps(@DrawableRes int origin) { 528 | if (origin == -1) { 529 | return null; 530 | } 531 | BitmapFactory.Options options = new BitmapFactory.Options(); 532 | options.inJustDecodeBounds = true; 533 | BitmapFactory.decodeResource(getResources(), origin, options); 534 | int size = Math.max(options.outWidth, options.outHeight); 535 | options.inSampleSize = size > iconSize ? size / iconSize : 1; 536 | options.inJustDecodeBounds = false; 537 | return generateIconBitmaps(BitmapFactory.decodeResource(getResources(), origin, options)); 538 | } 539 | 540 | private Bitmap[] generateIconBitmaps(Drawable drawable) { 541 | if (drawable == null) 542 | return null; 543 | Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); 544 | Canvas canvas = new Canvas(bitmap); 545 | drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); 546 | drawable.draw(canvas); 547 | return generateIconBitmaps(Bitmap.createScaledBitmap(bitmap, iconSize, iconSize, false)); 548 | } 549 | 550 | private Bitmap[] generateIconBitmaps(Bitmap origin) { 551 | if (origin == null) { 552 | return null; 553 | } 554 | Bitmap[] iconBitmaps = new Bitmap[4]; 555 | origin = scaleIcon(origin); 556 | iconBitmaps[0] = origin.copy(Bitmap.Config.ARGB_8888, true); 557 | Canvas canvas = new Canvas(iconBitmaps[0]); 558 | canvas.drawColor(baseColor & 0x00ffffff | (Colors.isLight(baseColor) ? 0xff000000 : 0x8a000000), PorterDuff.Mode.SRC_IN); 559 | iconBitmaps[1] = origin.copy(Bitmap.Config.ARGB_8888, true); 560 | canvas = new Canvas(iconBitmaps[1]); 561 | canvas.drawColor(primaryColor, PorterDuff.Mode.SRC_IN); 562 | iconBitmaps[2] = origin.copy(Bitmap.Config.ARGB_8888, true); 563 | canvas = new Canvas(iconBitmaps[2]); 564 | canvas.drawColor(baseColor & 0x00ffffff | (Colors.isLight(baseColor) ? 0x4c000000 : 0x42000000), PorterDuff.Mode.SRC_IN); 565 | iconBitmaps[3] = origin.copy(Bitmap.Config.ARGB_8888, true); 566 | canvas = new Canvas(iconBitmaps[3]); 567 | canvas.drawColor(errorColor, PorterDuff.Mode.SRC_IN); 568 | return iconBitmaps; 569 | } 570 | 571 | private Bitmap scaleIcon(Bitmap origin) { 572 | int width = origin.getWidth(); 573 | int height = origin.getHeight(); 574 | int size = Math.max(width, height); 575 | if (size == iconSize) { 576 | return origin; 577 | } else if (size > iconSize) { 578 | int scaledWidth; 579 | int scaledHeight; 580 | if (width > iconSize) { 581 | scaledWidth = iconSize; 582 | scaledHeight = (int) (iconSize * ((float) height / width)); 583 | } else { 584 | scaledHeight = iconSize; 585 | scaledWidth = (int) (iconSize * ((float) width / height)); 586 | } 587 | return Bitmap.createScaledBitmap(origin, scaledWidth, scaledHeight, false); 588 | } else { 589 | return origin; 590 | } 591 | } 592 | 593 | public float getFloatingLabelFraction() { 594 | return floatingLabelFraction; 595 | } 596 | 597 | public void setFloatingLabelFraction(float floatingLabelFraction) { 598 | this.floatingLabelFraction = floatingLabelFraction; 599 | invalidate(); 600 | } 601 | 602 | public float getFocusFraction() { 603 | return focusFraction; 604 | } 605 | 606 | public void setFocusFraction(float focusFraction) { 607 | this.focusFraction = focusFraction; 608 | invalidate(); 609 | } 610 | 611 | public float getCurrentBottomLines() { 612 | return currentBottomLines; 613 | } 614 | 615 | public void setCurrentBottomLines(float currentBottomLines) { 616 | this.currentBottomLines = currentBottomLines; 617 | initPadding(); 618 | } 619 | 620 | public boolean isFloatingLabelAlwaysShown() { 621 | return floatingLabelAlwaysShown; 622 | } 623 | 624 | public void setFloatingLabelAlwaysShown(boolean floatingLabelAlwaysShown) { 625 | this.floatingLabelAlwaysShown = floatingLabelAlwaysShown; 626 | invalidate(); 627 | } 628 | 629 | public boolean isHelperTextAlwaysShown() { 630 | return helperTextAlwaysShown; 631 | } 632 | 633 | public void setHelperTextAlwaysShown(boolean helperTextAlwaysShown) { 634 | this.helperTextAlwaysShown = helperTextAlwaysShown; 635 | invalidate(); 636 | } 637 | 638 | @Nullable 639 | public Typeface getAccentTypeface() { 640 | return accentTypeface; 641 | } 642 | 643 | /** 644 | * Set typeface used for the accent texts (floating label, error/helper text, character counter, etc.) 645 | */ 646 | public void setAccentTypeface(Typeface accentTypeface) { 647 | this.accentTypeface = accentTypeface; 648 | this.textPaint.setTypeface(accentTypeface); 649 | postInvalidate(); 650 | } 651 | 652 | public boolean isHideUnderline() { 653 | return hideUnderline; 654 | } 655 | 656 | /** 657 | * Set whether or not to hide the underline (shown by default). 658 | *

659 | * The positions of text below will be adjusted accordingly (error/helper text, character counter, ellipses, etc.) 660 | *

661 | * NOTE: You probably don't want to hide this if you have any subtext features of this enabled, as it can look weird to not have a dividing line between them. 662 | */ 663 | public void setHideUnderline(boolean hideUnderline) { 664 | this.hideUnderline = hideUnderline; 665 | initPadding(); 666 | postInvalidate(); 667 | } 668 | 669 | /** 670 | * get the color of the underline for normal state 671 | */ 672 | public int getUnderlineColor() { 673 | return underlineColor; 674 | } 675 | 676 | /** 677 | * Set the color of the underline for normal state 678 | * @param color 679 | */ 680 | public void setUnderlineColor(int color) { 681 | this.underlineColor = color; 682 | postInvalidate(); 683 | } 684 | 685 | public CharSequence getFloatingLabelText() { 686 | return floatingLabelText; 687 | } 688 | 689 | /** 690 | * Set the floating label text. 691 | *

692 | * Pass null to force fallback to use hint's value. 693 | * 694 | * @param floatingLabelText 695 | */ 696 | public void setFloatingLabelText(@Nullable CharSequence floatingLabelText) { 697 | this.floatingLabelText = floatingLabelText == null ? getHint() : floatingLabelText; 698 | postInvalidate(); 699 | } 700 | 701 | public int getFloatingLabelTextSize() { 702 | return floatingLabelTextSize; 703 | } 704 | 705 | public void setFloatingLabelTextSize(int size) { 706 | floatingLabelTextSize = size; 707 | initPadding(); 708 | } 709 | 710 | public int getFloatingLabelTextColor() { 711 | return floatingLabelTextColor; 712 | } 713 | 714 | public void setFloatingLabelTextColor(int color) { 715 | this.floatingLabelTextColor = color; 716 | postInvalidate(); 717 | } 718 | 719 | public int getBottomTextSize() { 720 | return bottomTextSize; 721 | } 722 | 723 | public void setBottomTextSize(int size) { 724 | bottomTextSize = size; 725 | initPadding(); 726 | } 727 | 728 | private int getPixel(int dp) { 729 | return Density.dp2px(getContext(), dp); 730 | } 731 | 732 | private void initPadding() { 733 | extraPaddingTop = floatingLabelEnabled ? floatingLabelTextSize + floatingLabelPadding : floatingLabelPadding; 734 | textPaint.setTextSize(bottomTextSize); 735 | Paint.FontMetrics textMetrics = textPaint.getFontMetrics(); 736 | extraPaddingBottom = (int) ((textMetrics.descent - textMetrics.ascent) * currentBottomLines) + (hideUnderline ? bottomSpacing : bottomSpacing * 2); 737 | extraPaddingLeft = iconLeftBitmaps == null ? 0 : (iconOuterWidth + iconPadding); 738 | extraPaddingRight = iconRightBitmaps == null ? 0 : (iconOuterWidth + iconPadding); 739 | correctPaddings(); 740 | } 741 | 742 | /** 743 | * calculate {@link #minBottomLines} 744 | */ 745 | private void initMinBottomLines() { 746 | boolean extendBottom = minCharacters > 0 || maxCharacters > 0 || singleLineEllipsis || tempErrorText != null || helperText != null; 747 | currentBottomLines = minBottomLines = minBottomTextLines > 0 ? minBottomTextLines : extendBottom ? 1 : 0; 748 | } 749 | 750 | /** 751 | * use {@link #setPaddings(int, int, int, int)} instead, or the paddingTop and the paddingBottom may be set incorrectly. 752 | */ 753 | @Deprecated 754 | @Override 755 | public final void setPadding(int left, int top, int right, int bottom) { 756 | super.setPadding(left, top, right, bottom); 757 | } 758 | 759 | /** 760 | * Use this method instead of {@link #setPadding(int, int, int, int)} to automatically set the paddingTop and the paddingBottom correctly. 761 | */ 762 | public void setPaddings(int left, int top, int right, int bottom) { 763 | innerPaddingTop = top; 764 | innerPaddingBottom = bottom; 765 | innerPaddingLeft = left; 766 | innerPaddingRight = right; 767 | correctPaddings(); 768 | } 769 | 770 | /** 771 | * Set paddings to the correct values 772 | */ 773 | private void correctPaddings() { 774 | int buttonsWidthLeft = 0, buttonsWidthRight = 0; 775 | int buttonsWidth = iconOuterWidth * getButtonsCount(); 776 | if (isRTL()) { 777 | buttonsWidthLeft = buttonsWidth; 778 | } else { 779 | buttonsWidthRight = buttonsWidth; 780 | } 781 | super.setPadding(innerPaddingLeft + extraPaddingLeft + buttonsWidthLeft, innerPaddingTop + extraPaddingTop, innerPaddingRight + extraPaddingRight + buttonsWidthRight, innerPaddingBottom + extraPaddingBottom); 782 | } 783 | 784 | private int getButtonsCount() { 785 | return isShowClearButton() ? 1 : 0; 786 | } 787 | 788 | @Override 789 | protected void onAttachedToWindow() { 790 | super.onAttachedToWindow(); 791 | if (!firstShown) { 792 | firstShown = true; 793 | } 794 | } 795 | 796 | @Override 797 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 798 | super.onLayout(changed, left, top, right, bottom); 799 | if (changed) { 800 | adjustBottomLines(); 801 | } 802 | } 803 | 804 | /** 805 | * @return True, if adjustments were made that require the view to be invalidated. 806 | */ 807 | private boolean adjustBottomLines() { 808 | // Bail out if we have a zero width; lines will be adjusted during next layout. 809 | if (getWidth() == 0) { 810 | return false; 811 | } 812 | int destBottomLines; 813 | textPaint.setTextSize(bottomTextSize); 814 | if (tempErrorText != null || helperText != null) { 815 | Layout.Alignment alignment = (getGravity() & Gravity.RIGHT) == Gravity.RIGHT || isRTL() ? 816 | Layout.Alignment.ALIGN_OPPOSITE : (getGravity() & Gravity.LEFT) == Gravity.LEFT ? 817 | Layout.Alignment.ALIGN_NORMAL : Layout.Alignment.ALIGN_CENTER; 818 | textLayout = new StaticLayout(tempErrorText != null ? tempErrorText : helperText, textPaint, getWidth() - getBottomTextLeftOffset() - getBottomTextRightOffset() - getPaddingLeft() - getPaddingRight(), alignment, 1.0f, 0.0f, true); 819 | destBottomLines = Math.max(textLayout.getLineCount(), minBottomTextLines); 820 | } else { 821 | destBottomLines = minBottomLines; 822 | } 823 | if (bottomLines != destBottomLines) { 824 | getBottomLinesAnimator(destBottomLines).start(); 825 | } 826 | bottomLines = destBottomLines; 827 | return true; 828 | } 829 | 830 | /** 831 | * get inner top padding, not the real paddingTop 832 | */ 833 | public int getInnerPaddingTop() { 834 | return innerPaddingTop; 835 | } 836 | 837 | /** 838 | * get inner bottom padding, not the real paddingBottom 839 | */ 840 | public int getInnerPaddingBottom() { 841 | return innerPaddingBottom; 842 | } 843 | 844 | /** 845 | * get inner left padding, not the real paddingLeft 846 | */ 847 | public int getInnerPaddingLeft() { 848 | return innerPaddingLeft; 849 | } 850 | 851 | /** 852 | * get inner right padding, not the real paddingRight 853 | */ 854 | public int getInnerPaddingRight() { 855 | return innerPaddingRight; 856 | } 857 | 858 | private void initFloatingLabel() { 859 | // observe the text changing 860 | addTextChangedListener(new TextWatcher() { 861 | @Override 862 | public void beforeTextChanged(CharSequence s, int start, int count, int after) { 863 | } 864 | 865 | @Override 866 | public void onTextChanged(CharSequence s, int start, int before, int count) { 867 | } 868 | 869 | @Override 870 | public void afterTextChanged(Editable s) { 871 | if (floatingLabelEnabled) { 872 | if (s.length() == 0) { 873 | if (floatingLabelShown) { 874 | floatingLabelShown = false; 875 | getLabelAnimator().reverse(); 876 | } 877 | } else if (!floatingLabelShown) { 878 | floatingLabelShown = true; 879 | getLabelAnimator().start(); 880 | } 881 | } 882 | } 883 | }); 884 | // observe the focus state to animate the floating label's text color appropriately 885 | innerFocusChangeListener = new OnFocusChangeListener() { 886 | @Override 887 | public void onFocusChange(View v, boolean hasFocus) { 888 | if (floatingLabelEnabled && highlightFloatingLabel) { 889 | if (hasFocus) { 890 | getLabelFocusAnimator().start(); 891 | } else { 892 | getLabelFocusAnimator().reverse(); 893 | } 894 | } 895 | if (validateOnFocusLost && !hasFocus) { 896 | validate(); 897 | } 898 | if (outerFocusChangeListener != null) { 899 | outerFocusChangeListener.onFocusChange(v, hasFocus); 900 | } 901 | } 902 | }; 903 | super.setOnFocusChangeListener(innerFocusChangeListener); 904 | } 905 | 906 | public boolean isValidateOnFocusLost() { 907 | return validateOnFocusLost; 908 | } 909 | 910 | public void setValidateOnFocusLost(boolean validate) { 911 | this.validateOnFocusLost = validate; 912 | } 913 | 914 | public void setBaseColor(int color) { 915 | if (baseColor != color) { 916 | baseColor = color; 917 | } 918 | 919 | initText(); 920 | 921 | postInvalidate(); 922 | } 923 | 924 | public void setPrimaryColor(int color) { 925 | primaryColor = color; 926 | postInvalidate(); 927 | } 928 | 929 | /** 930 | * Same function as {@link #setTextColor(int)}. (Directly overriding the built-in one could cause some error, so use this method instead.) 931 | */ 932 | public void setMetTextColor(int color) { 933 | textColorStateList = ColorStateList.valueOf(color); 934 | resetTextColor(); 935 | } 936 | 937 | /** 938 | * Same function as {@link #setTextColor(ColorStateList)}. (Directly overriding the built-in one could cause some error, so use this method instead.) 939 | */ 940 | public void setMetTextColor(ColorStateList colors) { 941 | textColorStateList = colors; 942 | resetTextColor(); 943 | } 944 | 945 | private void resetTextColor() { 946 | if (textColorStateList == null) { 947 | textColorStateList = new ColorStateList(new int[][]{new int[]{android.R.attr.state_enabled}, EMPTY_STATE_SET}, new int[]{baseColor & 0x00ffffff | 0xdf000000, baseColor & 0x00ffffff | 0x44000000}); 948 | setTextColor(textColorStateList); 949 | } else { 950 | setTextColor(textColorStateList); 951 | } 952 | } 953 | 954 | /** 955 | * Same function as {@link #setHintTextColor(int)}. (The built-in one is a final method that can't be overridden, so use this method instead.) 956 | */ 957 | public void setMetHintTextColor(int color) { 958 | textColorHintStateList = ColorStateList.valueOf(color); 959 | resetHintTextColor(); 960 | } 961 | 962 | /** 963 | * Same function as {@link #setHintTextColor(ColorStateList)}. (The built-in one is a final method that can't be overridden, so use this method instead.) 964 | */ 965 | public void setMetHintTextColor(ColorStateList colors) { 966 | textColorHintStateList = colors; 967 | resetHintTextColor(); 968 | } 969 | 970 | private void resetHintTextColor() { 971 | if (textColorHintStateList == null) { 972 | setHintTextColor(baseColor & 0x00ffffff | 0x44000000); 973 | } else { 974 | setHintTextColor(textColorHintStateList); 975 | } 976 | } 977 | 978 | private void setFloatingLabelInternal(int mode) { 979 | switch (mode) { 980 | case FLOATING_LABEL_NORMAL: 981 | floatingLabelEnabled = true; 982 | highlightFloatingLabel = false; 983 | break; 984 | case FLOATING_LABEL_HIGHLIGHT: 985 | floatingLabelEnabled = true; 986 | highlightFloatingLabel = true; 987 | break; 988 | default: 989 | floatingLabelEnabled = false; 990 | highlightFloatingLabel = false; 991 | break; 992 | } 993 | } 994 | 995 | public void setFloatingLabel(@FloatingLabelType int mode) { 996 | setFloatingLabelInternal(mode); 997 | initPadding(); 998 | } 999 | 1000 | public int getFloatingLabelPadding() { 1001 | return floatingLabelPadding; 1002 | } 1003 | 1004 | public void setFloatingLabelPadding(int padding) { 1005 | floatingLabelPadding = padding; 1006 | postInvalidate(); 1007 | } 1008 | 1009 | public boolean isFloatingLabelAnimating() { 1010 | return floatingLabelAnimating; 1011 | } 1012 | 1013 | public void setFloatingLabelAnimating(boolean animating) { 1014 | floatingLabelAnimating = animating; 1015 | } 1016 | 1017 | public void setSingleLineEllipsis() { 1018 | setSingleLineEllipsis(true); 1019 | } 1020 | 1021 | public void setSingleLineEllipsis(boolean enabled) { 1022 | singleLineEllipsis = enabled; 1023 | initMinBottomLines(); 1024 | initPadding(); 1025 | postInvalidate(); 1026 | } 1027 | 1028 | public int getMaxCharacters() { 1029 | return maxCharacters; 1030 | } 1031 | 1032 | public void setMaxCharacters(int max) { 1033 | maxCharacters = max; 1034 | initMinBottomLines(); 1035 | initPadding(); 1036 | postInvalidate(); 1037 | } 1038 | 1039 | public int getMinCharacters() { 1040 | return minCharacters; 1041 | } 1042 | 1043 | public void setMinCharacters(int min) { 1044 | minCharacters = min; 1045 | initMinBottomLines(); 1046 | initPadding(); 1047 | postInvalidate(); 1048 | } 1049 | 1050 | public int getMinBottomTextLines() { 1051 | return minBottomTextLines; 1052 | } 1053 | 1054 | public void setMinBottomTextLines(int lines) { 1055 | minBottomTextLines = lines; 1056 | initMinBottomLines(); 1057 | initPadding(); 1058 | postInvalidate(); 1059 | } 1060 | 1061 | public boolean isAutoValidate() { 1062 | return autoValidate; 1063 | } 1064 | 1065 | public void setAutoValidate(boolean autoValidate) { 1066 | this.autoValidate = autoValidate; 1067 | if (autoValidate) { 1068 | validate(); 1069 | } 1070 | } 1071 | 1072 | public int getErrorColor() { 1073 | return errorColor; 1074 | } 1075 | 1076 | public void setErrorColor(int color) { 1077 | errorColor = color; 1078 | postInvalidate(); 1079 | } 1080 | 1081 | public void setHelperText(CharSequence helperText) { 1082 | this.helperText = helperText == null ? null : helperText.toString(); 1083 | if (adjustBottomLines()) { 1084 | postInvalidate(); 1085 | } 1086 | } 1087 | 1088 | public String getHelperText() { 1089 | return helperText; 1090 | } 1091 | 1092 | public int getHelperTextColor() { 1093 | return helperTextColor; 1094 | } 1095 | 1096 | public void setHelperTextColor(int color) { 1097 | helperTextColor = color; 1098 | postInvalidate(); 1099 | } 1100 | 1101 | @Override 1102 | public void setError(CharSequence errorText) { 1103 | tempErrorText = errorText == null ? null : errorText.toString(); 1104 | if (adjustBottomLines()) { 1105 | postInvalidate(); 1106 | } 1107 | } 1108 | 1109 | @Override 1110 | public CharSequence getError() { 1111 | return tempErrorText; 1112 | } 1113 | 1114 | /** 1115 | * only used to draw the bottom line 1116 | */ 1117 | private boolean isInternalValid() { 1118 | return tempErrorText == null && isCharactersCountValid(); 1119 | } 1120 | 1121 | /** 1122 | * if the main text matches the regex 1123 | * 1124 | * @deprecated use the new validator interface to add your own custom validator 1125 | */ 1126 | @Deprecated 1127 | public boolean isValid(String regex) { 1128 | if (regex == null) { 1129 | return false; 1130 | } 1131 | Pattern pattern = Pattern.compile(regex); 1132 | Matcher matcher = pattern.matcher(getText()); 1133 | return matcher.matches(); 1134 | } 1135 | 1136 | /** 1137 | * check if the main text matches the regex, and set the error text if not. 1138 | * 1139 | * @return true if it matches the regex, false if not. 1140 | * @deprecated use the new validator interface to add your own custom validator 1141 | */ 1142 | @Deprecated 1143 | public boolean validate(String regex, CharSequence errorText) { 1144 | boolean isValid = isValid(regex); 1145 | if (!isValid) { 1146 | setError(errorText); 1147 | } 1148 | postInvalidate(); 1149 | return isValid; 1150 | } 1151 | 1152 | /** 1153 | * Run validation on a single validator instance 1154 | * 1155 | * @param validator Validator to check 1156 | * @return True if valid, false if not 1157 | */ 1158 | public boolean validateWith(@NonNull METValidator validator) { 1159 | CharSequence text = getText(); 1160 | boolean isValid = validator.isValid(text, text.length() == 0); 1161 | if (!isValid) { 1162 | setError(validator.getErrorMessage()); 1163 | } 1164 | postInvalidate(); 1165 | return isValid; 1166 | } 1167 | 1168 | /** 1169 | * Check all validators, sets the error text if not 1170 | *

1171 | * NOTE: this stops at the first validator to report invalid. 1172 | * 1173 | * @return True if all validators pass, false if not 1174 | */ 1175 | public boolean validate() { 1176 | if (validators == null || validators.isEmpty()) { 1177 | return true; 1178 | } 1179 | 1180 | CharSequence text = getText(); 1181 | boolean isEmpty = text.length() == 0; 1182 | 1183 | boolean isValid = true; 1184 | for (METValidator validator : validators) { 1185 | //noinspection ConstantConditions 1186 | isValid = isValid && validator.isValid(text, isEmpty); 1187 | if (!isValid) { 1188 | setError(validator.getErrorMessage()); 1189 | break; 1190 | } 1191 | } 1192 | if (isValid) { 1193 | setError(null); 1194 | } 1195 | 1196 | postInvalidate(); 1197 | return isValid; 1198 | } 1199 | 1200 | public boolean hasValidators() { 1201 | return this.validators != null && !this.validators.isEmpty(); 1202 | } 1203 | 1204 | /** 1205 | * Adds a new validator to the View's list of validators 1206 | *

1207 | * This will be checked with the others in {@link #validate()} 1208 | * 1209 | * @param validator Validator to add 1210 | * @return This instance, for easy chaining 1211 | */ 1212 | public MaterialMultiAutoCompleteTextView addValidator(METValidator validator) { 1213 | if (validators == null) { 1214 | this.validators = new ArrayList<>(); 1215 | } 1216 | this.validators.add(validator); 1217 | return this; 1218 | } 1219 | 1220 | public void clearValidators() { 1221 | if (this.validators != null) { 1222 | this.validators.clear(); 1223 | } 1224 | } 1225 | 1226 | @Nullable 1227 | public List getValidators() { 1228 | return this.validators; 1229 | } 1230 | 1231 | public void setLengthChecker(METLengthChecker lengthChecker) { 1232 | this.lengthChecker = lengthChecker; 1233 | } 1234 | 1235 | @Override 1236 | public void setOnFocusChangeListener(OnFocusChangeListener listener) { 1237 | if (innerFocusChangeListener == null) { 1238 | super.setOnFocusChangeListener(listener); 1239 | } else { 1240 | outerFocusChangeListener = listener; 1241 | } 1242 | } 1243 | 1244 | private ObjectAnimator getLabelAnimator() { 1245 | if (labelAnimator == null) { 1246 | labelAnimator = ObjectAnimator.ofFloat(this, "floatingLabelFraction", 0f, 1f); 1247 | } 1248 | labelAnimator.setDuration(floatingLabelAnimating ? 300 : 0); 1249 | return labelAnimator; 1250 | } 1251 | 1252 | private ObjectAnimator getLabelFocusAnimator() { 1253 | if (labelFocusAnimator == null) { 1254 | labelFocusAnimator = ObjectAnimator.ofFloat(this, "focusFraction", 0f, 1f); 1255 | } 1256 | return labelFocusAnimator; 1257 | } 1258 | 1259 | private ObjectAnimator getBottomLinesAnimator(float destBottomLines) { 1260 | if (bottomLinesAnimator == null) { 1261 | bottomLinesAnimator = ObjectAnimator.ofFloat(this, "currentBottomLines", destBottomLines); 1262 | } else { 1263 | bottomLinesAnimator.cancel(); 1264 | bottomLinesAnimator.setFloatValues(destBottomLines); 1265 | } 1266 | return bottomLinesAnimator; 1267 | } 1268 | 1269 | @Override 1270 | protected void onDraw(@NonNull Canvas canvas) { 1271 | int startX = getScrollX() + (iconLeftBitmaps == null ? 0 : (iconOuterWidth + iconPadding)); 1272 | int endX = getScrollX() + (iconRightBitmaps == null ? getWidth() : getWidth() - iconOuterWidth - iconPadding); 1273 | int lineStartY = getScrollY() + getHeight() - getPaddingBottom(); 1274 | 1275 | // draw the icon(s) 1276 | paint.setAlpha(255); 1277 | if (iconLeftBitmaps != null) { 1278 | Bitmap icon = iconLeftBitmaps[!isInternalValid() ? 3 : !isEnabled() ? 2 : hasFocus() ? 1 : 0]; 1279 | int iconLeft = startX - iconPadding - iconOuterWidth + (iconOuterWidth - icon.getWidth()) / 2; 1280 | int iconTop = lineStartY + bottomSpacing - iconOuterHeight + (iconOuterHeight - icon.getHeight()) / 2; 1281 | canvas.drawBitmap(icon, iconLeft, iconTop, paint); 1282 | } 1283 | if (iconRightBitmaps != null) { 1284 | Bitmap icon = iconRightBitmaps[!isInternalValid() ? 3 : !isEnabled() ? 2 : hasFocus() ? 1 : 0]; 1285 | int iconRight = endX + iconPadding + (iconOuterWidth - icon.getWidth()) / 2; 1286 | int iconTop = lineStartY + bottomSpacing - iconOuterHeight + (iconOuterHeight - icon.getHeight()) / 2; 1287 | canvas.drawBitmap(icon, iconRight, iconTop, paint); 1288 | } 1289 | 1290 | // draw the clear button 1291 | if (hasFocus() && showClearButton && !TextUtils.isEmpty(getText()) && isEnabled()) { 1292 | paint.setAlpha(255); 1293 | int buttonLeft; 1294 | if (isRTL()) { 1295 | buttonLeft = startX; 1296 | } else { 1297 | buttonLeft = endX - iconOuterWidth; 1298 | } 1299 | Bitmap clearButtonBitmap = clearButtonBitmaps[0]; 1300 | buttonLeft += (iconOuterWidth - clearButtonBitmap.getWidth()) / 2; 1301 | int iconTop = lineStartY + bottomSpacing - iconOuterHeight + (iconOuterHeight - clearButtonBitmap.getHeight()) / 2; 1302 | canvas.drawBitmap(clearButtonBitmap, buttonLeft, iconTop, paint); 1303 | } 1304 | 1305 | // draw the underline 1306 | if (!hideUnderline) { 1307 | lineStartY += bottomSpacing; 1308 | if (!isInternalValid()) { // not valid 1309 | paint.setColor(errorColor); 1310 | canvas.drawRect(startX, lineStartY, endX, lineStartY + getPixel(2), paint); 1311 | } else if (!isEnabled()) { // disabled 1312 | paint.setColor(underlineColor != -1 ? underlineColor : baseColor & 0x00ffffff | 0x44000000); 1313 | float interval = getPixel(1); 1314 | for (float xOffset = 0; xOffset < getWidth(); xOffset += interval * 3) { 1315 | canvas.drawRect(startX + xOffset, lineStartY, startX + xOffset + interval, lineStartY + getPixel(1), paint); 1316 | } 1317 | } else if (hasFocus()) { // focused 1318 | paint.setColor(primaryColor); 1319 | canvas.drawRect(startX, lineStartY, endX, lineStartY + getPixel(2), paint); 1320 | } else { // normal 1321 | paint.setColor(underlineColor != -1 ? underlineColor : baseColor & 0x00ffffff | 0x1E000000); 1322 | canvas.drawRect(startX, lineStartY, endX, lineStartY + getPixel(1), paint); 1323 | } 1324 | } 1325 | 1326 | textPaint.setTextSize(bottomTextSize); 1327 | Paint.FontMetrics textMetrics = textPaint.getFontMetrics(); 1328 | float relativeHeight = -textMetrics.ascent - textMetrics.descent; 1329 | float bottomTextPadding = bottomTextSize + textMetrics.ascent + textMetrics.descent; 1330 | 1331 | // draw the characters counter 1332 | if ((hasFocus() && hasCharactersCounter()) || !isCharactersCountValid()) { 1333 | textPaint.setColor(isCharactersCountValid() ? (baseColor & 0x00ffffff | 0x44000000) : errorColor); 1334 | String charactersCounterText = getCharactersCounterText(); 1335 | canvas.drawText(charactersCounterText, isRTL() ? startX : endX - textPaint.measureText(charactersCounterText), lineStartY + bottomSpacing + relativeHeight, textPaint); 1336 | } 1337 | 1338 | // draw the bottom text 1339 | if (textLayout != null) { 1340 | if (tempErrorText != null || ((helperTextAlwaysShown || hasFocus()) && !TextUtils.isEmpty(helperText))) { // error text or helper text 1341 | textPaint.setColor(tempErrorText != null ? errorColor : helperTextColor != -1 ? helperTextColor : (baseColor & 0x00ffffff | 0x44000000)); 1342 | canvas.save(); 1343 | if (isRTL()) { 1344 | canvas.translate(endX - textLayout.getWidth(), lineStartY + bottomSpacing - bottomTextPadding); 1345 | } else { 1346 | canvas.translate(startX + getBottomTextLeftOffset(), lineStartY + bottomSpacing - bottomTextPadding); 1347 | } 1348 | textLayout.draw(canvas); 1349 | canvas.restore(); 1350 | } 1351 | } 1352 | 1353 | // draw the floating label 1354 | if (floatingLabelEnabled && !TextUtils.isEmpty(floatingLabelText)) { 1355 | textPaint.setTextSize(floatingLabelTextSize); 1356 | // calculate the text color 1357 | textPaint.setColor((Integer) focusEvaluator.evaluate(focusFraction * (isEnabled() ? 1 : 0), floatingLabelTextColor != -1 ? floatingLabelTextColor : (baseColor & 0x00ffffff | 0x44000000), primaryColor)); 1358 | 1359 | // calculate the horizontal position 1360 | float floatingLabelWidth = textPaint.measureText(floatingLabelText.toString()); 1361 | int floatingLabelStartX; 1362 | if ((getGravity() & Gravity.RIGHT) == Gravity.RIGHT || isRTL()) { 1363 | floatingLabelStartX = (int) (endX - floatingLabelWidth); 1364 | } else if ((getGravity() & Gravity.LEFT) == Gravity.LEFT) { 1365 | floatingLabelStartX = startX; 1366 | } else { 1367 | floatingLabelStartX = startX + (int) (getInnerPaddingLeft() + (getWidth() - getInnerPaddingLeft() - getInnerPaddingRight() - floatingLabelWidth) / 2); 1368 | } 1369 | 1370 | // calculate the vertical position 1371 | int distance = floatingLabelPadding; 1372 | int floatingLabelStartY = (int) (innerPaddingTop + floatingLabelTextSize + floatingLabelPadding - distance * (floatingLabelAlwaysShown ? 1 : floatingLabelFraction) + getScrollY()); 1373 | 1374 | // calculate the alpha 1375 | int alpha = ((int) ((floatingLabelAlwaysShown ? 1 : floatingLabelFraction) * 0xff * (0.74f * focusFraction * (isEnabled() ? 1 : 0) + 0.26f) * (floatingLabelTextColor != -1 ? 1 : Color.alpha(floatingLabelTextColor) / 256f))); 1376 | textPaint.setAlpha(alpha); 1377 | 1378 | // draw the floating label 1379 | canvas.drawText(floatingLabelText.toString(), floatingLabelStartX, floatingLabelStartY, textPaint); 1380 | } 1381 | 1382 | // draw the bottom ellipsis 1383 | if (hasFocus() && singleLineEllipsis && getScrollX() != 0) { 1384 | paint.setColor(isInternalValid() ? primaryColor : errorColor); 1385 | float startY = lineStartY + bottomSpacing; 1386 | int ellipsisStartX; 1387 | if (isRTL()) { 1388 | ellipsisStartX = endX; 1389 | } else { 1390 | ellipsisStartX = startX; 1391 | } 1392 | int signum = isRTL() ? -1 : 1; 1393 | canvas.drawCircle(ellipsisStartX + signum * bottomEllipsisSize / 2, startY + bottomEllipsisSize / 2, bottomEllipsisSize / 2, paint); 1394 | canvas.drawCircle(ellipsisStartX + signum * bottomEllipsisSize * 5 / 2, startY + bottomEllipsisSize / 2, bottomEllipsisSize / 2, paint); 1395 | canvas.drawCircle(ellipsisStartX + signum * bottomEllipsisSize * 9 / 2, startY + bottomEllipsisSize / 2, bottomEllipsisSize / 2, paint); 1396 | } 1397 | 1398 | // draw the original things 1399 | super.onDraw(canvas); 1400 | } 1401 | 1402 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) 1403 | private boolean isRTL() { 1404 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) { 1405 | return false; 1406 | } 1407 | Configuration config = getResources().getConfiguration(); 1408 | return config.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; 1409 | } 1410 | 1411 | private int getBottomTextLeftOffset() { 1412 | return isRTL() ? getCharactersCounterWidth() : getBottomEllipsisWidth(); 1413 | } 1414 | 1415 | private int getBottomTextRightOffset() { 1416 | return isRTL() ? getBottomEllipsisWidth() : getCharactersCounterWidth(); 1417 | } 1418 | 1419 | private int getCharactersCounterWidth() { 1420 | return hasCharactersCounter() ? (int) textPaint.measureText(getCharactersCounterText()) : 0; 1421 | } 1422 | 1423 | private int getBottomEllipsisWidth() { 1424 | return singleLineEllipsis ? (bottomEllipsisSize * 5 + getPixel(4)) : 0; 1425 | } 1426 | 1427 | private void checkCharactersCount() { 1428 | if ((!firstShown && !checkCharactersCountAtBeginning) || !hasCharactersCounter()) { 1429 | charactersCountValid = true; 1430 | } else { 1431 | CharSequence text = getText(); 1432 | int count = text == null ? 0 : checkLength(text); 1433 | charactersCountValid = (count >= minCharacters && (maxCharacters <= 0 || count <= maxCharacters)); 1434 | } 1435 | } 1436 | 1437 | public boolean isCharactersCountValid() { 1438 | return charactersCountValid; 1439 | } 1440 | 1441 | private boolean hasCharactersCounter() { 1442 | return minCharacters > 0 || maxCharacters > 0; 1443 | } 1444 | 1445 | private String getCharactersCounterText() { 1446 | String text; 1447 | if (minCharacters <= 0) { 1448 | text = isRTL() ? maxCharacters + " / " + checkLength(getText()) : checkLength(getText()) + " / " + maxCharacters; 1449 | } else if (maxCharacters <= 0) { 1450 | text = isRTL() ? "+" + minCharacters + " / " + checkLength(getText()) : checkLength(getText()) + " / " + minCharacters + "+"; 1451 | } else { 1452 | text = isRTL() ? maxCharacters + "-" + minCharacters + " / " + checkLength(getText()) : checkLength(getText()) + " / " + minCharacters + "-" + maxCharacters; 1453 | } 1454 | return text; 1455 | } 1456 | 1457 | @Override 1458 | public boolean onTouchEvent(MotionEvent event) { 1459 | if (singleLineEllipsis && getScrollX() > 0 && event.getAction() == MotionEvent.ACTION_DOWN && event.getX() < getPixel(4 * 5) && event.getY() > getHeight() - extraPaddingBottom - innerPaddingBottom && event.getY() < getHeight() - innerPaddingBottom) { 1460 | setSelection(0); 1461 | return false; 1462 | } 1463 | if (hasFocus() && showClearButton && isEnabled()) { 1464 | switch (event.getAction()) { 1465 | case MotionEvent.ACTION_DOWN: 1466 | if (insideClearButton(event)) { 1467 | clearButtonTouched = true; 1468 | clearButtonClicking = true; 1469 | return true; 1470 | } 1471 | case MotionEvent.ACTION_MOVE: 1472 | if (clearButtonClicking && !insideClearButton(event)) { 1473 | clearButtonClicking = false; 1474 | } 1475 | if (clearButtonTouched) { 1476 | return true; 1477 | } 1478 | break; 1479 | case MotionEvent.ACTION_UP: 1480 | if (clearButtonClicking) { 1481 | if (!TextUtils.isEmpty(getText())) { 1482 | setText(null); 1483 | } 1484 | clearButtonClicking = false; 1485 | } 1486 | if (clearButtonTouched) { 1487 | clearButtonTouched = false; 1488 | return true; 1489 | } 1490 | clearButtonTouched = false; 1491 | break; 1492 | case MotionEvent.ACTION_CANCEL: 1493 | clearButtonTouched = false; 1494 | clearButtonClicking = false; 1495 | break; 1496 | } 1497 | } 1498 | return super.onTouchEvent(event); 1499 | } 1500 | 1501 | private boolean insideClearButton(MotionEvent event) { 1502 | float x = event.getX(); 1503 | float y = event.getY(); 1504 | int startX = getScrollX() + (iconLeftBitmaps == null ? 0 : (iconOuterWidth + iconPadding)); 1505 | int endX = getScrollX() + (iconRightBitmaps == null ? getWidth() : getWidth() - iconOuterWidth - iconPadding); 1506 | int buttonLeft; 1507 | if (isRTL()) { 1508 | buttonLeft = startX; 1509 | } else { 1510 | buttonLeft = endX - iconOuterWidth; 1511 | } 1512 | int buttonTop = getScrollY() + getHeight() - getPaddingBottom() + bottomSpacing - iconOuterHeight; 1513 | return (x >= buttonLeft && x < buttonLeft + iconOuterWidth && y >= buttonTop && y < buttonTop + iconOuterHeight); 1514 | } 1515 | 1516 | private int checkLength(CharSequence text) { 1517 | if (lengthChecker==null) return text.length(); 1518 | return lengthChecker.getLength(text); 1519 | } 1520 | } -------------------------------------------------------------------------------- /library/src/main/java/com/rengwuxian/materialedittext/validation/METLengthChecker.java: -------------------------------------------------------------------------------- 1 | package com.rengwuxian.materialedittext.validation; 2 | 3 | /** 4 | * Created by mariotaku on 15/4/12. 5 | */ 6 | public abstract class METLengthChecker { 7 | 8 | public abstract int getLength(CharSequence text); 9 | 10 | } 11 | -------------------------------------------------------------------------------- /library/src/main/java/com/rengwuxian/materialedittext/validation/METValidator.java: -------------------------------------------------------------------------------- 1 | package com.rengwuxian.materialedittext.validation; 2 | 3 | import android.support.annotation.NonNull; 4 | 5 | /** 6 | * Base Validator class to either implement or inherit from for custom validation 7 | */ 8 | public abstract class METValidator { 9 | 10 | /** 11 | * Error message that the view will display if validation fails. 12 | *

13 | * This is protected, so you can change this dynamically in your {@link #isValid(CharSequence, boolean)} 14 | * implementation. If necessary, you can also interact with this via its getter and setter. 15 | */ 16 | protected String errorMessage; 17 | 18 | public METValidator(@NonNull String errorMessage) { 19 | this.errorMessage = errorMessage; 20 | } 21 | 22 | public void setErrorMessage(@NonNull String errorMessage) { 23 | this.errorMessage = errorMessage; 24 | } 25 | 26 | @NonNull 27 | public String getErrorMessage() { 28 | return this.errorMessage; 29 | } 30 | 31 | /** 32 | * Abstract method to implement your own validation checking. 33 | * 34 | * @param text The CharSequence representation of the text in the EditText field. Cannot be null, but may be empty. 35 | * @param isEmpty Boolean indicating whether or not the text param is empty 36 | * @return True if valid, false if not 37 | */ 38 | public abstract boolean isValid(@NonNull CharSequence text, boolean isEmpty); 39 | 40 | } 41 | -------------------------------------------------------------------------------- /library/src/main/java/com/rengwuxian/materialedittext/validation/RegexpValidator.java: -------------------------------------------------------------------------------- 1 | package com.rengwuxian.materialedittext.validation; 2 | 3 | import android.support.annotation.NonNull; 4 | 5 | import java.util.regex.Pattern; 6 | 7 | /** 8 | * Custom validator for Regexes 9 | */ 10 | public class RegexpValidator extends METValidator { 11 | 12 | private Pattern pattern; 13 | 14 | public RegexpValidator(@NonNull String errorMessage, @NonNull String regex) { 15 | super(errorMessage); 16 | pattern = Pattern.compile(regex); 17 | } 18 | 19 | public RegexpValidator(@NonNull String errorMessage, @NonNull Pattern pattern) { 20 | super(errorMessage); 21 | this.pattern = pattern; 22 | } 23 | 24 | @Override 25 | public boolean isValid(@NonNull CharSequence text, boolean isEmpty) { 26 | return pattern.matcher(text).matches(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /library/src/main/res/drawable-hdpi/met_ic_clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rengwuxian/MaterialEditText/f6e9fa42213cda552764c2a63f93a1464fa91d04/library/src/main/res/drawable-hdpi/met_ic_clear.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-mdpi/met_ic_clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rengwuxian/MaterialEditText/f6e9fa42213cda552764c2a63f93a1464fa91d04/library/src/main/res/drawable-mdpi/met_ic_clear.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-xhdpi/met_ic_clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rengwuxian/MaterialEditText/f6e9fa42213cda552764c2a63f93a1464fa91d04/library/src/main/res/drawable-xhdpi/met_ic_clear.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-xxhdpi/met_ic_clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rengwuxian/MaterialEditText/f6e9fa42213cda552764c2a63f93a1464fa91d04/library/src/main/res/drawable-xxhdpi/met_ic_clear.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-xxxhdpi/met_ic_clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rengwuxian/MaterialEditText/f6e9fa42213cda552764c2a63f93a1464fa91d04/library/src/main/res/drawable-xxxhdpi/met_ic_clear.png -------------------------------------------------------------------------------- /library/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /library/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8dp 4 | 8dp 5 | 0dp 6 | 0dp 7 | 12sp 8 | 12sp 9 | 8dp 10 | 4dp 11 | -------------------------------------------------------------------------------- /maven_push.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'maven' 2 | apply plugin: 'signing' 3 | 4 | def sonatypeRepositoryUrl 5 | if (isReleaseBuild()) { 6 | println 'RELEASE BUILD' 7 | sonatypeRepositoryUrl = hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL 8 | : "https://oss.sonatype.org/service/local/staging/deploy/maven2/" 9 | } else { 10 | println 'DEBUG BUILD' 11 | sonatypeRepositoryUrl = hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL 12 | : "https://oss.sonatype.org/content/repositories/snapshots/" 13 | } 14 | 15 | def getRepositoryUsername() { 16 | return hasProperty('nexusUsername') ? nexusUsername : "" 17 | } 18 | 19 | def getRepositoryPassword() { 20 | return hasProperty('nexusPassword') ? nexusPassword : "" 21 | } 22 | 23 | afterEvaluate { project -> 24 | uploadArchives { 25 | repositories { 26 | mavenDeployer { 27 | beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } 28 | 29 | pom.artifactId = POM_ARTIFACT_ID 30 | 31 | repository(url: sonatypeRepositoryUrl) { 32 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) 33 | } 34 | 35 | pom.project { 36 | name POM_NAME 37 | packaging POM_PACKAGING 38 | description POM_DESCRIPTION 39 | url POM_URL 40 | 41 | scm { 42 | url POM_SCM_URL 43 | connection POM_SCM_CONNECTION 44 | developerConnection POM_SCM_DEV_CONNECTION 45 | } 46 | 47 | licenses { 48 | license { 49 | name POM_LICENCE_NAME 50 | url POM_LICENCE_URL 51 | distribution POM_LICENCE_DIST 52 | } 53 | } 54 | 55 | developers { 56 | developer { 57 | id POM_DEVELOPER_ID 58 | name POM_DEVELOPER_NAME 59 | } 60 | } 61 | } 62 | } 63 | } 64 | } 65 | 66 | signing { 67 | required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") } 68 | sign configurations.archives 69 | } 70 | 71 | task androidJavadocs(type: Javadoc) { 72 | source = android.sourceSets.main.java.sourceFiles 73 | } 74 | 75 | task androidJavadocsJar(type: Jar) { 76 | classifier = 'javadoc' 77 | //basename = artifact_id 78 | from androidJavadocs.destinationDir 79 | } 80 | 81 | task androidSourcesJar(type: Jar) { 82 | classifier = 'sources' 83 | //basename = artifact_id 84 | from android.sourceSets.main.java.sourceFiles 85 | } 86 | 87 | artifacts { 88 | //archives packageReleaseJar 89 | archives androidSourcesJar 90 | archives androidJavadocsJar 91 | } 92 | } -------------------------------------------------------------------------------- /sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion Integer.parseInt(project.ANDROID_BUILD_SDK_VERSION) 5 | buildToolsVersion project.ANDROID_BUILD_TOOLS_VERSION 6 | 7 | defaultConfig { 8 | applicationId "com.rengwuxian.materialedittext.sample" 9 | minSdkVersion 12 10 | targetSdkVersion Integer.parseInt(project.ANDROID_BUILD_SDK_VERSION) 11 | } 12 | buildTypes { 13 | release { 14 | minifyEnabled false 15 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 16 | } 17 | } 18 | } 19 | 20 | dependencies { 21 | compile fileTree(dir: 'libs', include: ['*.jar']) 22 | compile 'com.android.support:appcompat-v7:22.2.0' 23 | // compile 'com.rengwuxian.materialedittext:library:2.1.4' 24 | compile project(':library') 25 | } -------------------------------------------------------------------------------- /sample/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\Android\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /sample/src/androidTest/java/com/rengwuxian/materialedittext/sample/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.rengwuxian.materialedittext.sample; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /sample/src/main/assets/fonts/Roboto-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rengwuxian/MaterialEditText/f6e9fa42213cda552764c2a63f93a1464fa91d04/sample/src/main/assets/fonts/Roboto-LightItalic.ttf -------------------------------------------------------------------------------- /sample/src/main/java/com/rengwuxian/materialedittext/sample/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.rengwuxian.materialedittext.sample; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.view.Menu; 6 | import android.view.MenuItem; 7 | import android.view.View; 8 | import android.widget.Button; 9 | import android.widget.EditText; 10 | 11 | import com.rengwuxian.materialedittext.MaterialEditText; 12 | import com.rengwuxian.materialedittext.validation.RegexpValidator; 13 | 14 | 15 | public class MainActivity extends AppCompatActivity { 16 | 17 | @Override 18 | protected void onCreate(Bundle savedInstanceState) { 19 | super.onCreate(savedInstanceState); 20 | setContentView(R.layout.activity_main); 21 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 22 | getSupportActionBar().setDisplayShowTitleEnabled(false); 23 | initEnableBt(); 24 | initSingleLineEllipsisEt(); 25 | initSetErrorEt(); 26 | initValidationEt(); 27 | } 28 | 29 | private void initEnableBt() { 30 | final EditText basicEt = (EditText) findViewById(R.id.basicEt); 31 | final Button enableBt = (Button) findViewById(R.id.enableBt); 32 | enableBt.setOnClickListener(new View.OnClickListener() { 33 | @Override 34 | public void onClick(View v) { 35 | basicEt.setEnabled(!basicEt.isEnabled()); 36 | enableBt.setText(basicEt.isEnabled() ? "DISABLE" : "ENABLE"); 37 | } 38 | }); 39 | } 40 | 41 | private void initSingleLineEllipsisEt() { 42 | EditText singleLineEllipsisEt = (EditText) findViewById(R.id.singleLineEllipsisEt); 43 | singleLineEllipsisEt.setSelection(singleLineEllipsisEt.getText().length()); 44 | } 45 | 46 | private void initSetErrorEt() { 47 | final EditText bottomTextEt = (EditText) findViewById(R.id.bottomTextEt); 48 | final Button setErrorBt = (Button) findViewById(R.id.setErrorBt); 49 | setErrorBt.setOnClickListener(new View.OnClickListener() { 50 | @Override 51 | public void onClick(View v) { 52 | bottomTextEt.setError("1-line Error!"); 53 | } 54 | }); 55 | final Button setError2Bt = (Button) findViewById(R.id.setError2Bt); 56 | setError2Bt.setOnClickListener(new View.OnClickListener() { 57 | @Override 58 | public void onClick(View v) { 59 | bottomTextEt.setError("2-line\nError!"); 60 | } 61 | }); 62 | final Button setError3Bt = (Button) findViewById(R.id.setError3Bt); 63 | setError3Bt.setOnClickListener(new View.OnClickListener() { 64 | @Override 65 | public void onClick(View v) { 66 | bottomTextEt.setError("So Many Errors! So Many Errors! So Many Errors! So Many Errors! So Many Errors! So Many Errors! So Many Errors! So Many Errors!"); 67 | } 68 | }); 69 | } 70 | 71 | private void initValidationEt() { 72 | final MaterialEditText validationEt = (MaterialEditText) findViewById(R.id.validationEt); 73 | validationEt.addValidator(new RegexpValidator("Only Integer Valid!", "\\d+")); 74 | final Button validateBt = (Button) findViewById(R.id.validateBt); 75 | validateBt.setOnClickListener(new View.OnClickListener() { 76 | @Override 77 | public void onClick(View v) { 78 | // validate 79 | validationEt.validate(); 80 | } 81 | }); 82 | } 83 | 84 | @Override 85 | public boolean onCreateOptionsMenu(Menu menu) { 86 | // Inflate the menu; this adds items to the action bar if it is present. 87 | getMenuInflater().inflate(R.menu.menu_main, menu); 88 | return true; 89 | } 90 | 91 | @Override 92 | public boolean onOptionsItemSelected(MenuItem item) { 93 | // Handle action bar item clicks here. The action bar will 94 | // automatically handle clicks on the Home/Up button, so long 95 | // as you specify a parent activity in AndroidManifest.xml. 96 | int id = item.getItemId(); 97 | 98 | //noinspection SimplifiableIfStatement 99 | if (id == R.id.action_settings) { 100 | return true; 101 | } 102 | 103 | return super.onOptionsItemSelected(item); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /sample/src/main/res/color/text_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rengwuxian/MaterialEditText/f6e9fa42213cda552764c2a63f93a1464fa91d04/sample/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/ic_phone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rengwuxian/MaterialEditText/f6e9fa42213cda552764c2a63f93a1464fa91d04/sample/src/main/res/drawable-hdpi/ic_phone.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rengwuxian/MaterialEditText/f6e9fa42213cda552764c2a63f93a1464fa91d04/sample/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/ic_phone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rengwuxian/MaterialEditText/f6e9fa42213cda552764c2a63f93a1464fa91d04/sample/src/main/res/drawable-mdpi/ic_phone.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rengwuxian/MaterialEditText/f6e9fa42213cda552764c2a63f93a1464fa91d04/sample/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_phone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rengwuxian/MaterialEditText/f6e9fa42213cda552764c2a63f93a1464fa91d04/sample/src/main/res/drawable-xhdpi/ic_phone.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rengwuxian/MaterialEditText/f6e9fa42213cda552764c2a63f93a1464fa91d04/sample/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_phone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rengwuxian/MaterialEditText/f6e9fa42213cda552764c2a63f93a1464fa91d04/sample/src/main/res/drawable-xxhdpi/ic_phone.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable/text_cursor_cyan.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | 14 | 23 | 24 | 36 | 37 | 47 | 48 | 49 | 50 | 58 | 59 | 65 | 66 | 70 | 71 | 78 | 79 |