├── .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 | [](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 | 
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 | 
16 |
17 | 2. **Floating Label**
18 |
19 | normal:
20 |
21 | 
22 |
23 | highlight:
24 |
25 | 
26 |
27 | custom floating label text:
28 |
29 | 
30 |
31 | 3. **Single Line Ellipsis**
32 |
33 | 
34 |
35 | 4. **Max/Min Characters**
36 |
37 | 
38 |
39 | 
40 |
41 | 
42 |
43 | 5. **Helper Text and Error Text**
44 |
45 | 
46 |
47 | 6. **Custom Base/Primary/Error/HelperText Colors**
48 |
49 | 
50 |
51 | 7. **Custom accent typeface**
52 |
53 | floating label, error/helper text, character counter, etc.
54 |
55 | 
56 |
57 | 8. **Hide Underline**
58 |
59 | 
60 |
61 | 8. **Material Design Icon**
62 |
63 | 
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 |
85 |
86 |
87 |
88 |
95 |
96 |
102 |
103 |
108 |
109 |
116 |
117 |
123 |
124 |
129 |
130 |
137 |
138 |
144 |
145 |
151 |
152 |
159 |
160 |
166 |
167 |
174 |
175 |
182 |
183 |
189 |
190 |
195 |
196 |
202 |
203 |
208 |
209 |
215 |
216 |
222 |
223 |
230 |
231 |
237 |
238 |
242 |
243 |
250 |
251 |
257 |
258 |
264 |
265 |
271 |
272 |
273 |
274 |
281 |
282 |
288 |
289 |
293 |
294 |
301 |
302 |
308 |
309 |
310 |
311 |
318 |
319 |
325 |
326 |
341 |
342 |
349 |
350 |
356 |
357 |
366 |
367 |
374 |
375 |
381 |
382 |
387 |
388 |
395 |
396 |
402 |
403 |
408 |
409 |
416 |
417 |
423 |
424 |
431 |
432 |
433 |
434 |
--------------------------------------------------------------------------------
/sample/src/main/res/menu/menu_main.xml:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/sample/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3f51b5
4 | #303f9f
5 | #00bcd4
6 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Material EditText
5 | Hello world!
6 | Settings
7 | New Account
8 | Description
9 | Title
10 |
11 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':sample', ':library'
2 |
--------------------------------------------------------------------------------