diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c117df --- /dev/null +++ b/.gitignore @@ -0,0 +1,65 @@ +# Built application files +*.apk +*.ap_ + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +.idea/caches + +# Keystore files +# Uncomment the following line if you do not want to check your keystore files in. +#*.jks + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild + +# Google Services (e.g. APIs or Firebase) +google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..166f588 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,35 @@ +apply plugin: 'com.android.application' + +apply plugin: 'kotlin-android' + +apply plugin: 'kotlin-android-extensions' + +android { + compileSdkVersion 27 + defaultConfig { + applicationId "br.com.opencraft.openinputlayout" + minSdkVersion 19 + targetSdkVersion 27 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" + implementation 'com.android.support:appcompat-v7:27.1.1' + implementation 'com.android.support:design:27.1.1' + implementation 'com.android.support.constraint:constraint-layout:1.1.0' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + implementation project(':library') +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/src/androidTest/java/br/com/opencraft/openinputlayout/ExampleInstrumentedTest.kt b/app/src/androidTest/java/br/com/opencraft/openinputlayout/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..01de21f --- /dev/null +++ b/app/src/androidTest/java/br/com/opencraft/openinputlayout/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package br.com.opencraft.openinputlayout + +import android.support.test.InstrumentationRegistry +import android.support.test.runner.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getTargetContext() + assertEquals("br.com.opencraft.openinputlayout", appContext.packageName) + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..dcf0ac3 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/br/com/opencraft/openinputlayout/MainActivity.kt b/app/src/main/java/br/com/opencraft/openinputlayout/MainActivity.kt new file mode 100644 index 0000000..96e56cc --- /dev/null +++ b/app/src/main/java/br/com/opencraft/openinputlayout/MainActivity.kt @@ -0,0 +1,12 @@ +package br.com.opencraft.openinputlayout + +import android.support.v7.app.AppCompatActivity +import android.os.Bundle + +class MainActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + } +} diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..c7bd21d --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..d5fccc5 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..7d4896f --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..a2f5908 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..1b52399 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..ff10afd Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..115a4c7 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..dcd3cd8 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..459ca60 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..8ca12fe Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..8e19b41 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..b824ebd Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..4c19a13 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..3ab3e9c --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #3F51B5 + #303F9F + #FF4081 + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..c46a1d1 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + OpenInputLayout + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..df0de9c --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/app/src/test/java/br/com/opencraft/openinputlayout/ExampleUnitTest.kt b/app/src/test/java/br/com/opencraft/openinputlayout/ExampleUnitTest.kt new file mode 100644 index 0000000..e9e0e62 --- /dev/null +++ b/app/src/test/java/br/com/opencraft/openinputlayout/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package br.com.opencraft.openinputlayout + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..444e2d8 --- /dev/null +++ b/build.gradle @@ -0,0 +1,27 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + ext.kotlin_version = '1.2.41' + repositories { + google() + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:3.1.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..743d692 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,13 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/library/.gitignore b/library/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/library/.gitignore @@ -0,0 +1 @@ +/build diff --git a/library/build.gradle b/library/build.gradle new file mode 100644 index 0000000..cc30164 --- /dev/null +++ b/library/build.gradle @@ -0,0 +1,39 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 27 + + + + defaultConfig { + minSdkVersion 19 + targetSdkVersion 27 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation 'com.android.support:appcompat-v7:27.1.1' + implementation 'com.android.support:design:27.1.1' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} +repositories { + mavenCentral() +} diff --git a/library/proguard-rules.pro b/library/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/library/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/library/src/androidTest/java/br/com/opencraft/library/ExampleInstrumentedTest.java b/library/src/androidTest/java/br/com/opencraft/library/ExampleInstrumentedTest.java new file mode 100644 index 0000000..ca76c07 --- /dev/null +++ b/library/src/androidTest/java/br/com/opencraft/library/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package br.com.opencraft.library; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("br.com.opencraft.library.test", appContext.getPackageName()); + } +} diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml new file mode 100644 index 0000000..366eac1 --- /dev/null +++ b/library/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/library/src/main/java/br/com/opencraft/library/OpenAnimationUtils.java b/library/src/main/java/br/com/opencraft/library/OpenAnimationUtils.java new file mode 100644 index 0000000..f64c97e --- /dev/null +++ b/library/src/main/java/br/com/opencraft/library/OpenAnimationUtils.java @@ -0,0 +1,27 @@ +package br.com.opencraft.library; + +import android.support.v4.view.animation.FastOutLinearInInterpolator; +import android.support.v4.view.animation.FastOutSlowInInterpolator; +import android.support.v4.view.animation.LinearOutSlowInInterpolator; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; +import android.view.animation.LinearInterpolator; + +public class OpenAnimationUtils { + static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); + static final Interpolator FAST_OUT_SLOW_IN_INTERPOLATOR = new FastOutSlowInInterpolator(); + static final Interpolator FAST_OUT_LINEAR_IN_INTERPOLATOR = new FastOutLinearInInterpolator(); + static final Interpolator LINEAR_OUT_SLOW_IN_INTERPOLATOR = new LinearOutSlowInInterpolator(); + static final Interpolator DECELERATE_INTERPOLATOR = new DecelerateInterpolator(); + + /** + * Linear interpolation between {@code startValue} and {@code endValue} by {@code fraction}. + */ + static float lerp(float startValue, float endValue, float fraction) { + return startValue + (fraction * (endValue - startValue)); + } + + static int lerp(int startValue, int endValue, float fraction) { + return startValue + Math.round(fraction * (endValue - startValue)); + } +} diff --git a/library/src/main/java/br/com/opencraft/library/OpenCheckableImageButton.java b/library/src/main/java/br/com/opencraft/library/OpenCheckableImageButton.java new file mode 100644 index 0000000..97a1631 --- /dev/null +++ b/library/src/main/java/br/com/opencraft/library/OpenCheckableImageButton.java @@ -0,0 +1,78 @@ +package br.com.opencraft.library; + +import android.content.Context; +import android.support.v4.view.AccessibilityDelegateCompat; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.accessibility.AccessibilityEventCompat; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.support.v7.widget.AppCompatImageButton; +import android.util.AttributeSet; +import android.view.View; +import android.view.accessibility.AccessibilityEvent; +import android.widget.Checkable; + +public class OpenCheckableImageButton extends AppCompatImageButton implements Checkable { + + private static final int[] DRAWABLE_STATE_CHECKED = new int[]{android.R.attr.state_checked}; + + private boolean mChecked; + + public OpenCheckableImageButton(Context context) { + this(context, null); + } + + public OpenCheckableImageButton(Context context, AttributeSet attrs) { + this(context, attrs, android.support.v7.appcompat.R.attr.imageButtonStyle); + } + + public OpenCheckableImageButton(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + ViewCompat.setAccessibilityDelegate(this, new AccessibilityDelegateCompat() { + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(host, event); + event.setChecked(isChecked()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, + AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(host, info); + info.setCheckable(true); + info.setChecked(isChecked()); + } + }); + } + + @Override + public void setChecked(boolean checked) { + if (mChecked != checked) { + mChecked = checked; + refreshDrawableState(); + sendAccessibilityEvent( + AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED); + } + } + + @Override + public boolean isChecked() { + return mChecked; + } + + @Override + public void toggle() { + setChecked(!mChecked); + } + + @Override + public int[] onCreateDrawableState(int extraSpace) { + if (mChecked) { + return mergeDrawableStates( + super.onCreateDrawableState(extraSpace + DRAWABLE_STATE_CHECKED.length), + DRAWABLE_STATE_CHECKED); + } else { + return super.onCreateDrawableState(extraSpace); + } + } +} diff --git a/library/src/main/java/br/com/opencraft/library/OpenCollapsingTextHelper.java b/library/src/main/java/br/com/opencraft/library/OpenCollapsingTextHelper.java new file mode 100644 index 0000000..fea8d84 --- /dev/null +++ b/library/src/main/java/br/com/opencraft/library/OpenCollapsingTextHelper.java @@ -0,0 +1,714 @@ +package br.com.opencraft.library; + +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.os.Build; +import android.support.annotation.ColorInt; +import android.support.annotation.StyleRes; +import android.support.v4.math.MathUtils; +import android.support.v4.text.TextDirectionHeuristicsCompat; +import android.support.v4.view.GravityCompat; +import android.support.v4.view.ViewCompat; +import android.text.TextPaint; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.view.animation.Interpolator; + +public class OpenCollapsingTextHelper { + // Pre-JB-MR2 doesn't support HW accelerated canvas scaled text so we will workaround it + // by using our own texture + private static final boolean USE_SCALING_TEXTURE = Build.VERSION.SDK_INT < 18; + + private static final boolean DEBUG_DRAW = false; + private static final Paint DEBUG_DRAW_PAINT; + + static { + DEBUG_DRAW_PAINT = DEBUG_DRAW ? new Paint() : null; + if (DEBUG_DRAW_PAINT != null) { + DEBUG_DRAW_PAINT.setAntiAlias(true); + DEBUG_DRAW_PAINT.setColor(Color.MAGENTA); + } + } + + private final View mView; + + private boolean mDrawTitle; + private float mExpandedFraction; + + private final Rect mExpandedBounds; + private final Rect mCollapsedBounds; + private final RectF mCurrentBounds; + private int mExpandedTextGravity = Gravity.CENTER_VERTICAL; + private int mCollapsedTextGravity = Gravity.CENTER_VERTICAL; + private float mExpandedTextSize = 15; + private float mCollapsedTextSize = 15; + private ColorStateList mExpandedTextColor; + private ColorStateList mCollapsedTextColor; + + private float mExpandedDrawY; + private float mCollapsedDrawY; + private float mExpandedDrawX; + private float mCollapsedDrawX; + private float mCurrentDrawX; + private float mCurrentDrawY; + private Typeface mCollapsedTypeface; + private Typeface mExpandedTypeface; + private Typeface mCurrentTypeface; + + private CharSequence mText; + private CharSequence mTextToDraw; + private boolean mIsRtl; + + private boolean mUseTexture; + private Bitmap mExpandedTitleTexture; + private Paint mTexturePaint; + private float mTextureAscent; + private float mTextureDescent; + + private float mScale; + private float mCurrentTextSize; + + private int[] mState; + + private boolean mBoundsChanged; + + private final TextPaint mTextPaint; + + private Interpolator mPositionInterpolator; + private Interpolator mTextSizeInterpolator; + + private float mCollapsedShadowRadius, mCollapsedShadowDx, mCollapsedShadowDy; + private int mCollapsedShadowColor; + + private float mExpandedShadowRadius, mExpandedShadowDx, mExpandedShadowDy; + private int mExpandedShadowColor; + private AttributeSet mAttrs; + + public OpenCollapsingTextHelper(View view, AttributeSet attrs) { + mView = view; + mAttrs = attrs; + mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.SUBPIXEL_TEXT_FLAG); + + mCollapsedBounds = new Rect(); + mExpandedBounds = new Rect(); + mCurrentBounds = new RectF(); + } + + void setTextSizeInterpolator(Interpolator interpolator) { + mTextSizeInterpolator = interpolator; + recalculate(); + } + + void setPositionInterpolator(Interpolator interpolator) { + mPositionInterpolator = interpolator; + recalculate(); + } + + void setExpandedTextSize(float textSize) { + if (mExpandedTextSize != textSize) { + mExpandedTextSize = textSize; + recalculate(); + } + } + + void setCollapsedTextSize(float textSize) { + if (mCollapsedTextSize != textSize) { + mCollapsedTextSize = textSize; + recalculate(); + } + } + + void setCollapsedTextColor(ColorStateList textColor) { + if (mCollapsedTextColor != textColor) { + mCollapsedTextColor = textColor; + recalculate(); + } + } + + void setExpandedTextColor(ColorStateList textColor) { + if (mExpandedTextColor != textColor) { + mExpandedTextColor = textColor; + recalculate(); + } + } + + void setExpandedBounds(int left, int top, int right, int bottom) { + if (!rectEquals(mExpandedBounds, left, top, right, bottom)) { + mExpandedBounds.set(left, top, right, bottom); + mBoundsChanged = true; + onBoundsChanged(); + } + } + + void setCollapsedBounds(int left, int top, int right, int bottom) { + if (!rectEquals(mCollapsedBounds, left, top, right, bottom)) { + mCollapsedBounds.set(left, top, right, bottom); + mBoundsChanged = true; + onBoundsChanged(); + } + } + + void onBoundsChanged() { + mDrawTitle = mCollapsedBounds.width() > 0 && mCollapsedBounds.height() > 0 + && mExpandedBounds.width() > 0 && mExpandedBounds.height() > 0; + } + + void setExpandedTextGravity(int gravity) { + if (mExpandedTextGravity != gravity) { + mExpandedTextGravity = gravity; + recalculate(); + } + } + + int getExpandedTextGravity() { + return mExpandedTextGravity; + } + + void setCollapsedTextGravity(int gravity) { + if (mCollapsedTextGravity != gravity) { + mCollapsedTextGravity = gravity; + recalculate(); + } + } + + int getCollapsedTextGravity() { + return mCollapsedTextGravity; + } + + void setCollapsedTextAppearance(@StyleRes int resId) { + + TypedArray a = mView.getContext().getTheme().obtainStyledAttributes(resId, + android.support.v7.appcompat.R.styleable.TextAppearance); + + + if (a.hasValue(android.support.v7.appcompat.R.styleable.TextAppearance_android_textColor)) { + mCollapsedTextColor = a.getColorStateList( + android.support.v7.appcompat.R.styleable.TextAppearance_android_textColor); + } + if (a.hasValue(android.support.v7.appcompat.R.styleable.TextAppearance_android_textSize)) { + mCollapsedTextSize = a.getDimensionPixelSize( + android.support.v7.appcompat.R.styleable.TextAppearance_android_textSize, + (int) mCollapsedTextSize); + } + mCollapsedShadowColor = a.getInt( + android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowColor, 0); + mCollapsedShadowDx = a.getFloat( + android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowDx, 0); + mCollapsedShadowDy = a.getFloat( + android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowDy, 0); + mCollapsedShadowRadius = a.getFloat( + android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowRadius, 0); + a.recycle(); + + if (Build.VERSION.SDK_INT >= 16) { + mCollapsedTypeface = readFontFamilyTypeface(resId); + } + + recalculate(); + } + + void setExpandedTextAppearance(int resId) { + TypedArray a = mView.getContext().getTheme().obtainStyledAttributes( + mAttrs, + android.support.v7.appcompat.R.styleable.TextAppearance, + 0, 0); + if (a.hasValue(android.support.v7.appcompat.R.styleable.TextAppearance_android_textColor)) { + mExpandedTextColor = a.getColorStateList( + android.support.v7.appcompat.R.styleable.TextAppearance_android_textColor); + } + if (a.hasValue(android.support.v7.appcompat.R.styleable.TextAppearance_android_textSize)) { + mExpandedTextSize = a.getDimensionPixelSize( + android.support.v7.appcompat.R.styleable.TextAppearance_android_textSize, + (int) mExpandedTextSize); + } + mExpandedShadowColor = a.getInt( + android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowColor, 0); + mExpandedShadowDx = a.getFloat( + android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowDx, 0); + mExpandedShadowDy = a.getFloat( + android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowDy, 0); + mExpandedShadowRadius = a.getFloat( + android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowRadius, 0); + a.recycle(); + + if (Build.VERSION.SDK_INT >= 16) { + mExpandedTypeface = readFontFamilyTypeface(resId); + } + + recalculate(); + } + + private Typeface readFontFamilyTypeface(int resId) { + final TypedArray a = mView.getContext().obtainStyledAttributes(resId, + new int[]{android.R.attr.fontFamily}); + try { + final String family = a.getString(0); + if (family != null) { + return Typeface.create(family, Typeface.NORMAL); + } + } finally { + a.recycle(); + } + return null; + } + + void setCollapsedTypeface(Typeface typeface) { + if (areTypefacesDifferent(mCollapsedTypeface, typeface)) { + mCollapsedTypeface = typeface; + recalculate(); + } + } + + void setExpandedTypeface(Typeface typeface) { + if (areTypefacesDifferent(mExpandedTypeface, typeface)) { + mExpandedTypeface = typeface; + recalculate(); + } + } + + void setTypefaces(Typeface typeface) { + mCollapsedTypeface = mExpandedTypeface = typeface; + recalculate(); + } + + Typeface getCollapsedTypeface() { + return mCollapsedTypeface != null ? mCollapsedTypeface : Typeface.DEFAULT; + } + + Typeface getExpandedTypeface() { + return mExpandedTypeface != null ? mExpandedTypeface : Typeface.DEFAULT; + } + + /** + * Set the value indicating the current scroll value. This decides how much of the + * background will be displayed, as well as the title metrics/positioning. + *

+ * A value of {@code 0.0} indicates that the layout is fully expanded. + * A value of {@code 1.0} indicates that the layout is fully collapsed. + */ + void setExpansionFraction(float fraction) { + fraction = MathUtils.clamp(fraction, 0f, 1f); + + if (fraction != mExpandedFraction) { + mExpandedFraction = fraction; + calculateCurrentOffsets(); + } + } + + final boolean setState(final int[] state) { + mState = state; + + if (isStateful()) { + recalculate(); + return true; + } + + return false; + } + + final boolean isStateful() { + return (mCollapsedTextColor != null && mCollapsedTextColor.isStateful()) + || (mExpandedTextColor != null && mExpandedTextColor.isStateful()); + } + + float getExpansionFraction() { + return mExpandedFraction; + } + + float getCollapsedTextSize() { + return mCollapsedTextSize; + } + + float getExpandedTextSize() { + return mExpandedTextSize; + } + + private void calculateCurrentOffsets() { + calculateOffsets(mExpandedFraction); + } + + private void calculateOffsets(final float fraction) { + interpolateBounds(fraction); + mCurrentDrawX = lerp(mExpandedDrawX, mCollapsedDrawX, fraction, + mPositionInterpolator); + mCurrentDrawY = lerp(mExpandedDrawY, mCollapsedDrawY, fraction, + mPositionInterpolator); + + setInterpolatedTextSize(lerp(mExpandedTextSize, mCollapsedTextSize, + fraction, mTextSizeInterpolator)); + + if (mCollapsedTextColor != mExpandedTextColor) { + // If the collapsed and expanded text colors are different, blend them based on the + // fraction + mTextPaint.setColor(blendColors( + getCurrentExpandedTextColor(), getCurrentCollapsedTextColor(), fraction)); + } else { + mTextPaint.setColor(getCurrentCollapsedTextColor()); + } + + mTextPaint.setShadowLayer( + lerp(mExpandedShadowRadius, mCollapsedShadowRadius, fraction, null), + lerp(mExpandedShadowDx, mCollapsedShadowDx, fraction, null), + lerp(mExpandedShadowDy, mCollapsedShadowDy, fraction, null), + blendColors(mExpandedShadowColor, mCollapsedShadowColor, fraction)); + + ViewCompat.postInvalidateOnAnimation(mView); + } + + @ColorInt + private int getCurrentExpandedTextColor() { + if (mState != null) { + return mExpandedTextColor.getColorForState(mState, 0); + } else { + return mExpandedTextColor.getDefaultColor(); + } + } + + @ColorInt + private int getCurrentCollapsedTextColor() { + if (mState != null) { + return mCollapsedTextColor.getColorForState(mState, 0); + } else { + return mCollapsedTextColor.getDefaultColor(); + } + } + + private void calculateBaseOffsets() { + final float currentTextSize = mCurrentTextSize; + + // We then calculate the collapsed text size, using the same logic + calculateUsingTextSize(mCollapsedTextSize); + float width = mTextToDraw != null ? + mTextPaint.measureText(mTextToDraw, 0, mTextToDraw.length()) : 0; + final int collapsedAbsGravity = GravityCompat.getAbsoluteGravity(mCollapsedTextGravity, + mIsRtl ? ViewCompat.LAYOUT_DIRECTION_RTL : ViewCompat.LAYOUT_DIRECTION_LTR); + switch (collapsedAbsGravity & Gravity.VERTICAL_GRAVITY_MASK) { + case Gravity.BOTTOM: + mCollapsedDrawY = mCollapsedBounds.bottom; + break; + case Gravity.TOP: + mCollapsedDrawY = mCollapsedBounds.top - mTextPaint.ascent(); + break; + case Gravity.CENTER_VERTICAL: + default: + float textHeight = mTextPaint.descent() - mTextPaint.ascent(); + float textOffset = (textHeight / 2) - mTextPaint.descent(); + mCollapsedDrawY = mCollapsedBounds.centerY() + textOffset; + break; + } + switch (collapsedAbsGravity & GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK) { + case Gravity.CENTER_HORIZONTAL: + mCollapsedDrawX = mCollapsedBounds.centerX() - (width / 2); + break; + case Gravity.RIGHT: + mCollapsedDrawX = mCollapsedBounds.right - width; + break; + case Gravity.LEFT: + default: + mCollapsedDrawX = mCollapsedBounds.left; + break; + } + + calculateUsingTextSize(mExpandedTextSize); + width = mTextToDraw != null + ? mTextPaint.measureText(mTextToDraw, 0, mTextToDraw.length()) : 0; + final int expandedAbsGravity = GravityCompat.getAbsoluteGravity(mExpandedTextGravity, + mIsRtl ? ViewCompat.LAYOUT_DIRECTION_RTL : ViewCompat.LAYOUT_DIRECTION_LTR); + switch (expandedAbsGravity & Gravity.VERTICAL_GRAVITY_MASK) { + case Gravity.BOTTOM: + mExpandedDrawY = mExpandedBounds.bottom; + break; + case Gravity.TOP: + mExpandedDrawY = mExpandedBounds.top - mTextPaint.ascent(); + break; + case Gravity.CENTER_VERTICAL: + default: + float textHeight = mTextPaint.descent() - mTextPaint.ascent(); + float textOffset = (textHeight / 2) - mTextPaint.descent(); + mExpandedDrawY = mExpandedBounds.centerY() + textOffset; + break; + } + switch (expandedAbsGravity & GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK) { + case Gravity.CENTER_HORIZONTAL: + mExpandedDrawX = mExpandedBounds.centerX() - (width / 2); + break; + case Gravity.RIGHT: + mExpandedDrawX = mExpandedBounds.right - width; + break; + case Gravity.LEFT: + default: + mExpandedDrawX = mExpandedBounds.left; + break; + } + + // The bounds have changed so we need to clear the texture + clearTexture(); + // Now reset the text size back to the original + setInterpolatedTextSize(currentTextSize); + } + + private void interpolateBounds(float fraction) { + mCurrentBounds.left = lerp(mExpandedBounds.left, mCollapsedBounds.left, + fraction, mPositionInterpolator); + mCurrentBounds.top = lerp(mExpandedDrawY, mCollapsedDrawY, + fraction, mPositionInterpolator); + mCurrentBounds.right = lerp(mExpandedBounds.right, mCollapsedBounds.right, + fraction, mPositionInterpolator); + mCurrentBounds.bottom = lerp(mExpandedBounds.bottom, mCollapsedBounds.bottom, + fraction, mPositionInterpolator); + } + + public void draw(Canvas canvas) { + final int saveCount = canvas.save(); + + if (mTextToDraw != null && mDrawTitle) { + float x = mCurrentDrawX; + float y = mCurrentDrawY; + + final boolean drawTexture = mUseTexture && mExpandedTitleTexture != null; + + final float ascent; + final float descent; + if (drawTexture) { + ascent = mTextureAscent * mScale; + descent = mTextureDescent * mScale; + } else { + ascent = mTextPaint.ascent() * mScale; + descent = mTextPaint.descent() * mScale; + } + + if (DEBUG_DRAW) { + // Just a debug tool, which drawn a magenta rect in the text bounds + canvas.drawRect(mCurrentBounds.left, y + ascent, mCurrentBounds.right, y + descent, + DEBUG_DRAW_PAINT); + } + + if (drawTexture) { + y += ascent; + } + + if (mScale != 1f) { + canvas.scale(mScale, mScale, x, y); + } + + if (drawTexture) { + // If we should use a texture, draw it instead of text + canvas.drawBitmap(mExpandedTitleTexture, x, y, mTexturePaint); + } else { + canvas.drawText(mTextToDraw, 0, mTextToDraw.length(), x, y, mTextPaint); + } + } + + canvas.restoreToCount(saveCount); + } + + private boolean calculateIsRtl(CharSequence text) { + final boolean defaultIsRtl = ViewCompat.getLayoutDirection(mView) + == ViewCompat.LAYOUT_DIRECTION_RTL; + return (defaultIsRtl + ? TextDirectionHeuristicsCompat.FIRSTSTRONG_RTL + : TextDirectionHeuristicsCompat.FIRSTSTRONG_LTR).isRtl(text, 0, text.length()); + } + + private void setInterpolatedTextSize(float textSize) { + calculateUsingTextSize(textSize); + + // Use our texture if the scale isn't 1.0 + mUseTexture = USE_SCALING_TEXTURE && mScale != 1f; + + if (mUseTexture) { + // Make sure we have an expanded texture if needed + ensureExpandedTexture(); + } + + ViewCompat.postInvalidateOnAnimation(mView); + } + + private boolean areTypefacesDifferent(Typeface first, Typeface second) { + return (first != null && !first.equals(second)) || (first == null && second != null); + } + + private void calculateUsingTextSize(final float textSize) { + if (mText == null) return; + + final float collapsedWidth = mCollapsedBounds.width(); + final float expandedWidth = mExpandedBounds.width(); + + final float availableWidth; + final float newTextSize; + boolean updateDrawText = false; + + if (isClose(textSize, mCollapsedTextSize)) { + newTextSize = mCollapsedTextSize; + mScale = 1f; + if (areTypefacesDifferent(mCurrentTypeface, mCollapsedTypeface)) { + mCurrentTypeface = mCollapsedTypeface; + updateDrawText = true; + } + availableWidth = collapsedWidth; + } else { + newTextSize = mExpandedTextSize; + if (areTypefacesDifferent(mCurrentTypeface, mExpandedTypeface)) { + mCurrentTypeface = mExpandedTypeface; + updateDrawText = true; + } + if (isClose(textSize, mExpandedTextSize)) { + // If we're close to the expanded text size, snap to it and use a scale of 1 + mScale = 1f; + } else { + // Else, we'll scale down from the expanded text size + mScale = textSize / mExpandedTextSize; + } + + final float textSizeRatio = mCollapsedTextSize / mExpandedTextSize; + // This is the size of the expanded bounds when it is scaled to match the + // collapsed text size + final float scaledDownWidth = expandedWidth * textSizeRatio; + + if (scaledDownWidth > collapsedWidth) { + // If the scaled down size is larger than the actual collapsed width, we need to + // cap the available width so that when the expanded text scales down, it matches + // the collapsed width + availableWidth = Math.min(collapsedWidth / textSizeRatio, expandedWidth); + } else { + // Otherwise we'll just use the expanded width + availableWidth = expandedWidth; + } + } + + if (availableWidth > 0) { + updateDrawText = (mCurrentTextSize != newTextSize) || mBoundsChanged || updateDrawText; + mCurrentTextSize = newTextSize; + mBoundsChanged = false; + } + + if (mTextToDraw == null || updateDrawText) { + mTextPaint.setTextSize(mCurrentTextSize); + mTextPaint.setTypeface(mCurrentTypeface); + // Use linear text scaling if we're scaling the canvas + mTextPaint.setLinearText(mScale != 1f); + + // If we don't currently have text to draw, or the text size has changed, ellipsize... + final CharSequence title = TextUtils.ellipsize(mText, mTextPaint, + availableWidth, TextUtils.TruncateAt.END); + if (!TextUtils.equals(title, mTextToDraw)) { + mTextToDraw = title; + mIsRtl = calculateIsRtl(mTextToDraw); + } + } + } + + private void ensureExpandedTexture() { + if (mExpandedTitleTexture != null || mExpandedBounds.isEmpty() + || TextUtils.isEmpty(mTextToDraw)) { + return; + } + + calculateOffsets(0f); + mTextureAscent = mTextPaint.ascent(); + mTextureDescent = mTextPaint.descent(); + + final int w = Math.round(mTextPaint.measureText(mTextToDraw, 0, mTextToDraw.length())); + final int h = Math.round(mTextureDescent - mTextureAscent); + + if (w <= 0 || h <= 0) { + return; // If the width or height are 0, return + } + + mExpandedTitleTexture = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); + + Canvas c = new Canvas(mExpandedTitleTexture); + c.drawText(mTextToDraw, 0, mTextToDraw.length(), 0, h - mTextPaint.descent(), mTextPaint); + + if (mTexturePaint == null) { + // Make sure we have a paint + mTexturePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); + } + } + + public void recalculate() { + if (mView.getHeight() > 0 && mView.getWidth() > 0) { + // If we've already been laid out, calculate everything now otherwise we'll wait + // until a layout + calculateBaseOffsets(); + calculateCurrentOffsets(); + } + } + + /** + * Set the title to display + * + * @param text + */ + void setText(CharSequence text) { + if (text == null || !text.equals(mText)) { + mText = text; + mTextToDraw = null; + clearTexture(); + recalculate(); + } + } + + CharSequence getText() { + return mText; + } + + private void clearTexture() { + if (mExpandedTitleTexture != null) { + mExpandedTitleTexture.recycle(); + mExpandedTitleTexture = null; + } + } + + /** + * Returns true if {@code value} is 'close' to it's closest decimal value. Close is currently + * defined as it's difference being < 0.001. + */ + private static boolean isClose(float value, float targetValue) { + return Math.abs(value - targetValue) < 0.001f; + } + + ColorStateList getExpandedTextColor() { + return mExpandedTextColor; + } + + ColorStateList getCollapsedTextColor() { + return mCollapsedTextColor; + } + + /** + * Blend {@code color1} and {@code color2} using the given ratio. + * + * @param ratio of which to blend. 0.0 will return {@code color1}, 0.5 will give an even blend, + * 1.0 will return {@code color2}. + */ + private static int blendColors(int color1, int color2, float ratio) { + final float inverseRatio = 1f - ratio; + float a = (Color.alpha(color1) * inverseRatio) + (Color.alpha(color2) * ratio); + float r = (Color.red(color1) * inverseRatio) + (Color.red(color2) * ratio); + float g = (Color.green(color1) * inverseRatio) + (Color.green(color2) * ratio); + float b = (Color.blue(color1) * inverseRatio) + (Color.blue(color2) * ratio); + return Color.argb((int) a, (int) r, (int) g, (int) b); + } + + private static float lerp(float startValue, float endValue, float fraction, + Interpolator interpolator) { + if (interpolator != null) { + fraction = interpolator.getInterpolation(fraction); + } + return OpenAnimationUtils.lerp(startValue, endValue, fraction); + } + + private static boolean rectEquals(Rect r, int left, int top, int right, int bottom) { + return !(r.left != left || r.top != top || r.right != right || r.bottom != bottom); + } +} diff --git a/library/src/main/java/br/com/opencraft/library/OpenDrawableUtils.java b/library/src/main/java/br/com/opencraft/library/OpenDrawableUtils.java new file mode 100644 index 0000000..a7665d3 --- /dev/null +++ b/library/src/main/java/br/com/opencraft/library/OpenDrawableUtils.java @@ -0,0 +1,46 @@ +package br.com.opencraft.library; + +import android.graphics.drawable.Drawable; +import android.graphics.drawable.DrawableContainer; +import android.util.Log; + +import java.lang.reflect.Method; + +public class OpenDrawableUtils { + + private static final String LOG_TAG = "DrawableUtils"; + + private static Method sSetConstantStateMethod; + private static boolean sSetConstantStateMethodFetched; + + private OpenDrawableUtils() {} + + static boolean setContainerConstantState(DrawableContainer drawable, + Drawable.ConstantState constantState) { + // We can use getDeclaredMethod() on v9+ + return setContainerConstantStateV9(drawable, constantState); + } + + private static boolean setContainerConstantStateV9(DrawableContainer drawable, + Drawable.ConstantState constantState) { + if (!sSetConstantStateMethodFetched) { + try { + sSetConstantStateMethod = DrawableContainer.class.getDeclaredMethod( + "setConstantState", DrawableContainer.DrawableContainerState.class); + sSetConstantStateMethod.setAccessible(true); + } catch (NoSuchMethodException e) { + Log.e(LOG_TAG, "Could not fetch setConstantState(). Oh well."); + } + sSetConstantStateMethodFetched = true; + } + if (sSetConstantStateMethod != null) { + try { + sSetConstantStateMethod.invoke(drawable, constantState); + return true; + } catch (Exception e) { + Log.e(LOG_TAG, "Could not invoke setConstantState(). Oh well."); + } + } + return false; + } +} diff --git a/library/src/main/java/br/com/opencraft/library/OpenInputLayout.java b/library/src/main/java/br/com/opencraft/library/OpenInputLayout.java new file mode 100644 index 0000000..f02a952 --- /dev/null +++ b/library/src/main/java/br/com/opencraft/library/OpenInputLayout.java @@ -0,0 +1,1524 @@ +package br.com.opencraft.library; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.DrawableContainer; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.DrawableRes; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.StringRes; +import android.support.annotation.StyleRes; +import android.support.annotation.VisibleForTesting; +import android.support.design.widget.TextInputEditText; +import android.support.design.widget.TextInputLayout; +import android.support.v4.content.ContextCompat; +import android.support.v4.graphics.drawable.DrawableCompat; +import android.support.v4.view.AbsSavedState; +import android.support.v4.view.AccessibilityDelegateCompat; +import android.support.v4.view.GravityCompat; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.support.v4.widget.Space; +import android.support.v4.widget.TextViewCompat; +import android.support.v4.widget.ViewGroupUtils; +import android.support.v7.content.res.AppCompatResources; +import android.support.v7.widget.AppCompatDrawableManager; +import android.support.v7.widget.AppCompatTextView; +import android.support.v7.widget.WithHint; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.text.method.PasswordTransformationMethod; +import android.util.AttributeSet; +import android.util.Log; +import android.util.SparseArray; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewStructure; +import android.view.accessibility.AccessibilityEvent; +import android.view.animation.AccelerateInterpolator; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TextView; + + +/** + * >>> Customized version of TextInputLayout intended to allow the hint to move below + * >>> the edittext with some margin - João Rutkoski + *

+ *

+ * >>>TextInputLayout + * Layout which wraps an {@link android.widget.EditText} (or descendant) to show a floating label + * when the hint is hidden due to the user inputting text. + *

+ *

Also supports showing an error via {@link #setErrorEnabled(boolean)} and + * {@link #setError(CharSequence)}, and a character counter via + * {@link #setCounterEnabled(boolean)}.

+ *

+ *

Password visibility toggling is also supported via the + * {@link #setPasswordVisibilityToggleEnabled(boolean)} API and related attribute. + * If enabled, a button is displayed to toggle between the password being displayed as plain-text + * or disguised, when your EditText is set to display a password.

+ *

+ *

Note: When using the password toggle functionality, the 'end' compound + * drawable of the EditText will be overridden while the toggle is enabled. To ensure that any + * existing drawables are restored correctly, you should set those compound drawables relatively + * (start/end), opposed to absolutely (left/right).

+ *

+ * The {@link TextInputEditText} class is provided to be used as a child of this layout. Using + * TextInputEditText allows TextInputLayout greater control over the visual aspects of any + * text input. An example usage is as so: + *

+ *

+ * <android.support.design.widget.TextInputLayout
+ *         android:layout_width="match_parent"
+ *         android:layout_height="wrap_content">
+ *
+ *     <android.support.design.widget.TextInputEditText
+ *             android:layout_width="match_parent"
+ *             android:layout_height="wrap_content"
+ *             android:hint="@string/form_username"/>
+ *
+ * </android.support.design.widget.TextInputLayout>
+ * 
+ *

+ *

Note: The actual view hierarchy present under TextInputLayout is + * NOT guaranteed to match the view hierarchy as written in XML. As a result, + * calls to getParent() on children of the TextInputLayout -- such as an TextInputEditText -- + * may not return the TextInputLayout itself, but rather an intermediate View. If you need + * to access a View directly, set an {@code android:id} and use {@link View#findViewById(int)}. + */ + +public class OpenInputLayout extends LinearLayout implements WithHint { + + private static final int ANIMATION_DURATION = 200; + private static final int INVALID_MAX_LENGTH = -1; + + private static final String LOG_TAG = "TextInputLayout"; + + private final FrameLayout mInputFrame; + EditText mEditText; + private CharSequence mOriginalHint; + + private boolean mHintEnabled; + private CharSequence mHint; + + private Paint mTmpPaint; + private final Rect mTmpRect = new Rect(); + + private LinearLayout mIndicatorArea; + private int mIndicatorsAdded; + + private Typeface mTypeface; + + private boolean mErrorEnabled; + TextView mErrorView; + private int mErrorTextAppearance; + private boolean mErrorShown; + private CharSequence mError; + + boolean mCounterEnabled; + private TextView mCounterView; + private int mCounterMaxLength; + private int mCounterTextAppearance; + private int mCounterOverflowTextAppearance; + private boolean mCounterOverflowed; + + private boolean mPasswordToggleEnabled; + private Drawable mPasswordToggleDrawable; + private CharSequence mPasswordToggleContentDesc; + private OpenCheckableImageButton mPasswordToggleView; + private boolean mPasswordToggledVisible; + private Drawable mPasswordToggleDummyDrawable; + private Drawable mOriginalEditTextEndDrawable; + + private ColorStateList mPasswordToggleTintList; + private boolean mHasPasswordToggleTintList; + private PorterDuff.Mode mPasswordToggleTintMode; + private boolean mHasPasswordToggleTintMode; + + private ColorStateList mDefaultTextColor; + private ColorStateList mFocusedTextColor; + + // Only used for testing + private boolean mHintExpanded; + + private OpenCollapsingTextHelper mCollapsingTextHelper; + + private boolean mHintAnimationEnabled; + private ValueAnimator mAnimator; + + private boolean mHasReconstructedEditTextBackground; + private boolean mInDrawableStateChanged; + + private boolean mRestoringSavedState; + + //Defines whether the hint will move up or down + private boolean mMoveLabelUp = true; + private float mLabelSpacingExtra = 0f; + + public OpenInputLayout(Context context) { + this(context, null); + } + + public OpenInputLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public OpenInputLayout(Context context, AttributeSet attrs, int defStyleAttr) { + // Can't call through to super(Context, AttributeSet, int) since it doesn't exist on API 10 + super(context, attrs); + + mCollapsingTextHelper = new OpenCollapsingTextHelper(this, attrs); + +// ThemeUtils.checkAppCompatTheme(context); + + setOrientation(VERTICAL); + setWillNotDraw(false); + setAddStatesFromChildren(true); + + mInputFrame = new FrameLayout(context); + mInputFrame.setAddStatesFromChildren(true); + addView(mInputFrame); + + mCollapsingTextHelper.setTextSizeInterpolator(OpenAnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR); + mCollapsingTextHelper.setPositionInterpolator(new AccelerateInterpolator()); + mCollapsingTextHelper.setCollapsedTextGravity(Gravity.TOP | GravityCompat.START); + + final TypedArray a = context.getTheme().obtainStyledAttributes( + attrs, + R.styleable.OpenInputLayout, + defStyleAttr, android.support.design.R.style.Widget_Design_TextInputLayout); + + mHintEnabled = a.getBoolean(R.styleable.OpenInputLayout_hintEnabled, true); + setHint(a.getText(R.styleable.OpenInputLayout_android_hint)); + mHintAnimationEnabled = a.getBoolean(R.styleable.OpenInputLayout_hintAnimationEnabled, true); + + if (a.hasValue(R.styleable.OpenInputLayout_android_textColorHint)) { + mDefaultTextColor = mFocusedTextColor = a.getColorStateList(R.styleable.OpenInputLayout_android_textColorHint); + } + + final int hintAppearance = a.getResourceId(R.styleable.OpenInputLayout_hintTextAppearance, -1); + if (hintAppearance != -1) { + setHintTextAppearance(a.getResourceId(R.styleable.OpenInputLayout_hintTextAppearance, 0)); + } + + mErrorTextAppearance = a.getResourceId(R.styleable.OpenInputLayout_errorTextAppearance, 0); + final boolean errorEnabled = a.getBoolean(R.styleable.OpenInputLayout_errorEnabled, false); + + final boolean counterEnabled = a.getBoolean(R.styleable.OpenInputLayout_counterEnabled, false); + setCounterMaxLength(a.getInt(R.styleable.OpenInputLayout_counterMaxLength, INVALID_MAX_LENGTH)); + mCounterTextAppearance = a.getResourceId(R.styleable.OpenInputLayout_counterTextAppearance, 0); + mCounterOverflowTextAppearance = a.getResourceId(R.styleable.OpenInputLayout_counterOverflowTextAppearance, 0); + mPasswordToggleEnabled = a.getBoolean(R.styleable.OpenInputLayout_passwordToggleEnabled, false); + mPasswordToggleDrawable = a.getDrawable(R.styleable.OpenInputLayout_passwordToggleDrawable); +// mPasswordToggleContentDesc = a.getText(R.styleable.OpenInputLayout_android_passwordToggleContentDescription); + if (a.hasValue(R.styleable.OpenInputLayout_moveLabelUp)) { + mMoveLabelUp = a.getBoolean(R.styleable.OpenInputLayout_moveLabelUp, true); + } + if (a.hasValue(R.styleable.OpenInputLayout_passwordToggleTint)) { + mHasPasswordToggleTintList = true; + mPasswordToggleTintList = a.getColorStateList(R.styleable.OpenInputLayout_passwordToggleTint); + } + + mLabelSpacingExtra = a.getDimension(R.styleable.OpenInputLayout_labelSpacingExtra, 0f); +// if (a.hasValue(R.styleable.OpenInputLayout_passwordToggleTintMode)) { +// mHasPasswordToggleTintMode = true; +// mPasswordToggleTintMode = ViewUtils.parseTintMode( +// a.getInt(android.support.design.R.styleable.OpenInputLayout_passwordToggleTintMode, -1), null); +// } + + a.recycle(); + + setErrorEnabled(errorEnabled); + setCounterEnabled(counterEnabled); + applyPasswordToggleTint(); + + if (ViewCompat.getImportantForAccessibility(this) + == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + // Make sure we're important for accessibility if we haven't been explicitly not + ViewCompat.setImportantForAccessibility(this, + ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); + } + + //TODO: TEST ACCESSIBILITY +// ViewCompat.setAccessibilityDelegate(this, new TextInputLayout.TextInputAccessibilityDelegate()); + } + + @Override + public void addView(View child, int index, final ViewGroup.LayoutParams params) { + if (child instanceof EditText) { + // Make sure that the EditText is vertically at the bottom, so that it sits on the + // EditText's underline + FrameLayout.LayoutParams flp = new FrameLayout.LayoutParams(params); + flp.gravity = Gravity.CENTER_VERTICAL | (flp.gravity & ~Gravity.VERTICAL_GRAVITY_MASK); + mInputFrame.addView(child, flp); + + // Now use the EditText's LayoutParams as our own and update them to make enough space + // for the label + mInputFrame.setLayoutParams(params); + updateInputLayoutMargins(); + + setEditText((EditText) child); + } else { + // Carry on adding the View... + super.addView(child, index, params); + } + } + + /** + * Set the typeface to use for the hint and any label views (such as counter and error views). + * + * @param typeface typeface to use, or {@code null} to use the default. + */ + public void setTypeface(@Nullable Typeface typeface) { + if ((mTypeface != null && !mTypeface.equals(typeface)) + || (mTypeface == null && typeface != null)) { + mTypeface = typeface; + + mCollapsingTextHelper.setTypefaces(typeface); + if (mCounterView != null) { + mCounterView.setTypeface(typeface); + } + if (mErrorView != null) { + mErrorView.setTypeface(typeface); + } + } + } + + /** + * Returns the typeface used for the hint and any label views (such as counter and error views). + */ + @NonNull + public Typeface getTypeface() { + return mTypeface; + } + + @Override + public void dispatchProvideAutofillStructure(ViewStructure structure, int flags) { + if (mOriginalHint == null || mEditText == null) { + super.dispatchProvideAutofillStructure(structure, flags); + return; + } + + // Temporarily sets child's hint to its original value so it is properly set in the + // child's ViewStructure. + final CharSequence hint = mEditText.getHint(); + mEditText.setHint(mOriginalHint); + try { + super.dispatchProvideAutofillStructure(structure, flags); + } finally { + mEditText.setHint(hint); + } + } + + private void setEditText(EditText editText) { + // If we already have an EditText, throw an exception + if (mEditText != null) { + throw new IllegalArgumentException("We already have an EditText, can only have one"); + } + + if (!(editText instanceof TextInputEditText)) { + Log.i(LOG_TAG, "EditText added is not a TextInputEditText. Please switch to using that" + + " class instead."); + } + + mEditText = editText; + + final boolean hasPasswordTransformation = hasPasswordTransformation(); + + // Use the EditText's typeface, and it's text size for our expanded text + if (!hasPasswordTransformation) { + // We don't want a monospace font just because we have a password field + mCollapsingTextHelper.setTypefaces(mEditText.getTypeface()); + } + mCollapsingTextHelper.setExpandedTextSize(mEditText.getTextSize()); + + final int editTextGravity = mEditText.getGravity(); + mCollapsingTextHelper.setCollapsedTextGravity( + Gravity.TOP | (editTextGravity & ~Gravity.VERTICAL_GRAVITY_MASK)); + mCollapsingTextHelper.setExpandedTextGravity(editTextGravity); + + // Add a TextWatcher so that we know when the text input has changed + mEditText.addTextChangedListener(new TextWatcher() { + @Override + public void afterTextChanged(Editable s) { + updateLabelState(!mRestoringSavedState); + if (mCounterEnabled) { + updateCounter(s.length()); + } + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + }); + + // Use the EditText's hint colors if we don't have one set + if (mDefaultTextColor == null) { + mDefaultTextColor = mEditText.getHintTextColors(); + } + + // If we do not have a valid hint, try and retrieve it from the EditText, if enabled + if (mHintEnabled && TextUtils.isEmpty(mHint)) { + // Save the hint so it can be restored on dispatchProvideAutofillStructure(); + mOriginalHint = mEditText.getHint(); + setHint(mOriginalHint); + // Clear the EditText's hint as we will display it ourselves + mEditText.setHint(null); + } + + if (mCounterView != null) { + updateCounter(mEditText.getText().length()); + } + + if (mIndicatorArea != null) { + adjustIndicatorPadding(); + } + + updatePasswordToggleView(); + + // Update the label visibility with no animation, but force a state change + updateLabelState(false, true); + } + + private void updateInputLayoutMargins() { + // Create/update the LayoutParams so that we can add enough top margin + // to the EditText so make room for the label + final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mInputFrame.getLayoutParams(); + final int newMargin; + + if (mHintEnabled) { + if (mTmpPaint == null) { + mTmpPaint = new Paint(); + } + mTmpPaint.setTypeface(mCollapsingTextHelper.getCollapsedTypeface()); + mTmpPaint.setTextSize(mCollapsingTextHelper.getCollapsedTextSize()); + newMargin = mMoveLabelUp ? (int) -mTmpPaint.ascent() : (int) (mTmpPaint.descent() + (-mTmpPaint.ascent())); + } else { + newMargin = 0; + } + + if (mMoveLabelUp) { + if (newMargin != lp.topMargin) { + lp.topMargin = newMargin + (int) mLabelSpacingExtra; + mInputFrame.requestLayout(); + } + } else { + if (newMargin != lp.bottomMargin) { + lp.bottomMargin = newMargin + (int) mLabelSpacingExtra; + mInputFrame.requestLayout(); + } + } + } + + void updateLabelState(boolean animate) { + updateLabelState(animate, false); + } + + void updateLabelState(final boolean animate, final boolean force) { + final boolean isEnabled = isEnabled(); + final boolean hasText = mEditText != null && !TextUtils.isEmpty(mEditText.getText()); + final boolean isFocused = arrayContains(getDrawableState(), android.R.attr.state_focused); + final boolean isErrorShowing = !TextUtils.isEmpty(getError()); + + if (mDefaultTextColor != null) { + mCollapsingTextHelper.setExpandedTextColor(mDefaultTextColor); + } + + if (isEnabled && mCounterOverflowed && mCounterView != null) { + mCollapsingTextHelper.setCollapsedTextColor(mCounterView.getTextColors()); + } else if (isEnabled && isFocused && mFocusedTextColor != null) { + mCollapsingTextHelper.setCollapsedTextColor(mFocusedTextColor); + } else if (mDefaultTextColor != null) { + mCollapsingTextHelper.setCollapsedTextColor(mDefaultTextColor); + } + + if (hasText || (isEnabled() && (isFocused || isErrorShowing))) { + // We should be showing the label so do so if it isn't already + if (force || mHintExpanded) { + collapseHint(animate); + } + } else { + // We should not be showing the label so hide it + if (force || !mHintExpanded) { + expandHint(animate); + } + } + } + + /** + * Returns the {@link android.widget.EditText} used for text input. + */ + @Nullable + public EditText getEditText() { + return mEditText; + } + + /** + * Set the hint to be displayed in the floating label, if enabled. + * + * @attr ref android.support.design.R.styleable#OpenInputLayout_android_hint + * @see #setHintEnabled(boolean) + */ + public void setHint(@Nullable CharSequence hint) { + if (mHintEnabled) { + setHintInternal(hint); + sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + } + } + + private void setHintInternal(CharSequence hint) { + mHint = hint; + mCollapsingTextHelper.setText(hint); + } + + /** + * Returns the hint which is displayed in the floating label, if enabled. + * + * @return the hint, or null if there isn't one set, or the hint is not enabled. + * @attr ref android.support.design.R.styleable#OpenInputLayout_android_hint + */ + @Override + @Nullable + public CharSequence getHint() { + return mHintEnabled ? mHint : null; + } + + /** + * Sets whether the floating label functionality is enabled or not in this layout. + *

+ *

If enabled, any non-empty hint in the child EditText will be moved into the floating + * hint, and its existing hint will be cleared. If disabled, then any non-empty floating hint + * in this layout will be moved into the EditText, and this layout's hint will be cleared.

+ * + * @attr ref android.support.design.R.styleable#OpenInputLayout_hintEnabled + * @see #setHint(CharSequence) + * @see #isHintEnabled() + */ + public void setHintEnabled(boolean enabled) { + if (enabled != mHintEnabled) { + mHintEnabled = enabled; + + final CharSequence editTextHint = mEditText.getHint(); + if (!mHintEnabled) { + if (!TextUtils.isEmpty(mHint) && TextUtils.isEmpty(editTextHint)) { + // If the hint is disabled, but we have a hint set, and the EditText doesn't, + // pass it through... + mEditText.setHint(mHint); + } + // Now clear out any set hint + setHintInternal(null); + } else { + if (!TextUtils.isEmpty(editTextHint)) { + // If the hint is now enabled and the EditText has one set, we'll use it if + // we don't already have one, and clear the EditText's + if (TextUtils.isEmpty(mHint)) { + setHint(editTextHint); + } + mEditText.setHint(null); + } + } + + // Now update the EditText top margin + if (mEditText != null) { + updateInputLayoutMargins(); + } + } + } + + /** + * Returns whether the floating label functionality is enabled or not in this layout. + * + * @attr ref android.support.design.R.styleable#OpenInputLayout_hintEnabled + * @see #setHintEnabled(boolean) + */ + public boolean isHintEnabled() { + return mHintEnabled; + } + + /** + * Sets the hint text color, size, style from the specified TextAppearance resource. + * + * @attr ref android.support.design.R.styleable#OpenInputLayout_hintTextAppearance + */ + public void setHintTextAppearance(@StyleRes int resId) { + mCollapsingTextHelper.setCollapsedTextAppearance(resId); + mFocusedTextColor = mCollapsingTextHelper.getCollapsedTextColor(); + + if (mEditText != null) { + updateLabelState(false); + // Text size might have changed so update the top margin + updateInputLayoutMargins(); + } + } + + private void addIndicator(TextView indicator, int index) { + if (mIndicatorArea == null) { + mIndicatorArea = new LinearLayout(getContext()); + mIndicatorArea.setOrientation(LinearLayout.HORIZONTAL); + addView(mIndicatorArea, LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + + // Add a flexible spacer in the middle so that the left/right views stay pinned + final Space spacer = new Space(getContext()); + final LinearLayout.LayoutParams spacerLp = new LinearLayout.LayoutParams(0, 0, 1f); + mIndicatorArea.addView(spacer, spacerLp); + + if (mEditText != null) { + adjustIndicatorPadding(); + } + } + mIndicatorArea.setVisibility(View.VISIBLE); + mIndicatorArea.addView(indicator, index); + mIndicatorsAdded++; + } + + private void adjustIndicatorPadding() { + // Add padding to the error and character counter so that they match the EditText + ViewCompat.setPaddingRelative(mIndicatorArea, ViewCompat.getPaddingStart(mEditText), + 0, ViewCompat.getPaddingEnd(mEditText), mEditText.getPaddingBottom()); + } + + private void removeIndicator(TextView indicator) { + if (mIndicatorArea != null) { + mIndicatorArea.removeView(indicator); + if (--mIndicatorsAdded == 0) { + mIndicatorArea.setVisibility(View.GONE); + } + } + } + + /** + * Whether the error functionality is enabled or not in this layout. Enabling this + * functionality before setting an error message via {@link #setError(CharSequence)}, will mean + * that this layout will not change size when an error is displayed. + * + * @attr ref android.support.design.R.styleable#OpenInputLayout_errorEnabled + */ + public void setErrorEnabled(boolean enabled) { + if (mErrorEnabled != enabled) { + if (mErrorView != null) { + mErrorView.animate().cancel(); + } + + if (enabled) { + mErrorView = new AppCompatTextView(getContext()); + mErrorView.setId(android.support.design.R.id.textinput_error); + if (mTypeface != null) { + mErrorView.setTypeface(mTypeface); + } + boolean useDefaultColor = false; + try { + TextViewCompat.setTextAppearance(mErrorView, mErrorTextAppearance); + + if (Build.VERSION.SDK_INT >= 23 + && mErrorView.getTextColors().getDefaultColor() == Color.MAGENTA) { + // Caused by our theme not extending from Theme.Design*. On API 23 and + // above, unresolved theme attrs result in MAGENTA rather than an exception. + // Flag so that we use a decent default + useDefaultColor = true; + } + } catch (Exception e) { + // Caused by our theme not extending from Theme.Design*. Flag so that we use + // a decent default + useDefaultColor = true; + } + if (useDefaultColor) { + // Probably caused by our theme not extending from Theme.Design*. Instead + // we manually set something appropriate + TextViewCompat.setTextAppearance(mErrorView, + android.support.v7.appcompat.R.style.TextAppearance_AppCompat_Caption); + mErrorView.setTextColor(ContextCompat.getColor(getContext(), + android.support.v7.appcompat.R.color.error_color_material)); + } + mErrorView.setVisibility(INVISIBLE); + ViewCompat.setAccessibilityLiveRegion(mErrorView, + ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE); + addIndicator(mErrorView, 0); + } else { + mErrorShown = false; + updateEditTextBackground(); + removeIndicator(mErrorView); + mErrorView = null; + } + mErrorEnabled = enabled; + } + } + + /** + * Sets the text color and size for the error message from the specified + * TextAppearance resource. + * + * @attr ref android.support.design.R.styleable#OpenInputLayout_errorTextAppearance + */ + public void setErrorTextAppearance(@StyleRes int resId) { + mErrorTextAppearance = resId; + if (mErrorView != null) { + TextViewCompat.setTextAppearance(mErrorView, resId); + } + } + + /** + * Returns whether the error functionality is enabled or not in this layout. + * + * @attr ref android.support.design.R.styleable#OpenInputLayout_errorEnabled + * @see #setErrorEnabled(boolean) + */ + public boolean isErrorEnabled() { + return mErrorEnabled; + } + + /** + * Sets an error message that will be displayed below our {@link EditText}. If the + * {@code error} is {@code null}, the error message will be cleared. + *

+ * If the error functionality has not been enabled via {@link #setErrorEnabled(boolean)}, then + * it will be automatically enabled if {@code error} is not empty. + * + * @param error Error message to display, or null to clear + * @see #getError() + */ + public void setError(@Nullable final CharSequence error) { + // Only animate if we're enabled, laid out, and we have a different error message + setError(error, ViewCompat.isLaidOut(this) && isEnabled() + && (mErrorView == null || !TextUtils.equals(mErrorView.getText(), error))); + } + + private void setError(@Nullable final CharSequence error, final boolean animate) { + mError = error; + + if (!mErrorEnabled) { + if (TextUtils.isEmpty(error)) { + // If error isn't enabled, and the error is empty, just return + return; + } + // Else, we'll assume that they want to enable the error functionality + setErrorEnabled(true); + } + + mErrorShown = !TextUtils.isEmpty(error); + + // Cancel any on-going animation + mErrorView.animate().cancel(); + + if (mErrorShown) { + mErrorView.setText(error); + mErrorView.setVisibility(VISIBLE); + + if (animate) { + if (mErrorView.getAlpha() == 1f) { + // If it's currently 100% show, we'll animate it from 0 + mErrorView.setAlpha(0f); + } + mErrorView.animate() + .alpha(1f) + .setDuration(ANIMATION_DURATION) + .setInterpolator(OpenAnimationUtils.LINEAR_OUT_SLOW_IN_INTERPOLATOR) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animator) { + mErrorView.setVisibility(VISIBLE); + } + }).start(); + } else { + // Set alpha to 1f, just in case + mErrorView.setAlpha(1f); + } + } else { + if (mErrorView.getVisibility() == VISIBLE) { + if (animate) { + mErrorView.animate() + .alpha(0f) + .setDuration(ANIMATION_DURATION) + .setInterpolator(OpenAnimationUtils.FAST_OUT_LINEAR_IN_INTERPOLATOR) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animator) { + mErrorView.setText(error); + mErrorView.setVisibility(INVISIBLE); + } + }).start(); + } else { + mErrorView.setText(error); + mErrorView.setVisibility(INVISIBLE); + } + } + } + + updateEditTextBackground(); + updateLabelState(animate); + } + + /** + * Whether the character counter functionality is enabled or not in this layout. + * + * @attr ref android.support.design.R.styleable#OpenInputLayout_counterEnabled + */ + public void setCounterEnabled(boolean enabled) { + if (mCounterEnabled != enabled) { + if (enabled) { + mCounterView = new AppCompatTextView(getContext()); + mCounterView.setId(android.support.design.R.id.textinput_counter); + if (mTypeface != null) { + mCounterView.setTypeface(mTypeface); + } + mCounterView.setMaxLines(1); + try { + TextViewCompat.setTextAppearance(mCounterView, mCounterTextAppearance); + } catch (Exception e) { + // Probably caused by our theme not extending from Theme.Design*. Instead + // we manually set something appropriate + TextViewCompat.setTextAppearance(mCounterView, + android.support.v7.appcompat.R.style.TextAppearance_AppCompat_Caption); + mCounterView.setTextColor(ContextCompat.getColor(getContext(), + android.support.v7.appcompat.R.color.error_color_material)); + } + addIndicator(mCounterView, -1); + if (mEditText == null) { + updateCounter(0); + } else { + updateCounter(mEditText.getText().length()); + } + } else { + removeIndicator(mCounterView); + mCounterView = null; + } + mCounterEnabled = enabled; + } + } + + /** + * Returns whether the character counter functionality is enabled or not in this layout. + * + * @attr ref android.support.design.R.styleable#OpenInputLayout_counterEnabled + * @see #setCounterEnabled(boolean) + */ + public boolean isCounterEnabled() { + return mCounterEnabled; + } + + /** + * Sets the max length to display at the character counter. + * + * @param maxLength maxLength to display. Any value less than or equal to 0 will not be shown. + * @attr ref android.support.design.R.styleable#OpenInputLayout_counterMaxLength + */ + public void setCounterMaxLength(int maxLength) { + if (mCounterMaxLength != maxLength) { + if (maxLength > 0) { + mCounterMaxLength = maxLength; + } else { + mCounterMaxLength = INVALID_MAX_LENGTH; + } + if (mCounterEnabled) { + updateCounter(mEditText == null ? 0 : mEditText.getText().length()); + } + } + } + + @Override + public void setEnabled(boolean enabled) { + // Since we're set to addStatesFromChildren, we need to make sure that we set all + // children to enabled/disabled otherwise any enabled children will wipe out our disabled + // drawable state + recursiveSetEnabled(this, enabled); + super.setEnabled(enabled); + } + + private static void recursiveSetEnabled(final ViewGroup vg, final boolean enabled) { + for (int i = 0, count = vg.getChildCount(); i < count; i++) { + final View child = vg.getChildAt(i); + child.setEnabled(enabled); + if (child instanceof ViewGroup) { + recursiveSetEnabled((ViewGroup) child, enabled); + } + } + } + + /** + * Returns the max length shown at the character counter. + * + * @attr ref android.support.design.R.styleable#OpenInputLayout_counterMaxLength + */ + public int getCounterMaxLength() { + return mCounterMaxLength; + } + + void updateCounter(int length) { + boolean wasCounterOverflowed = mCounterOverflowed; + if (mCounterMaxLength == INVALID_MAX_LENGTH) { + mCounterView.setText(String.valueOf(length)); + mCounterOverflowed = false; + } else { + mCounterOverflowed = length > mCounterMaxLength; + if (wasCounterOverflowed != mCounterOverflowed) { + TextViewCompat.setTextAppearance(mCounterView, mCounterOverflowed + ? mCounterOverflowTextAppearance : mCounterTextAppearance); + } + mCounterView.setText(getContext().getString(android.support.design.R.string.character_counter_pattern, + length, mCounterMaxLength)); + } + if (mEditText != null && wasCounterOverflowed != mCounterOverflowed) { + updateLabelState(false); + updateEditTextBackground(); + } + } + + @SuppressLint("RestrictedApi") + private void updateEditTextBackground() { + if (mEditText == null) { + return; + } + + Drawable editTextBackground = mEditText.getBackground(); + if (editTextBackground == null) { + return; + } + + ensureBackgroundDrawableStateWorkaround(); + +// if (android.support.v7.widget.DrawableUtils.canSafelyMutateDrawable(editTextBackground)) { + editTextBackground = editTextBackground.mutate(); +// } + + if (mErrorShown && mErrorView != null) { + // Set a color filter of the error color + editTextBackground.setColorFilter( + AppCompatDrawableManager.getPorterDuffColorFilter( + mErrorView.getCurrentTextColor(), PorterDuff.Mode.SRC_IN)); + } else if (mCounterOverflowed && mCounterView != null) { + // Set a color filter of the counter color + editTextBackground.setColorFilter( + AppCompatDrawableManager.getPorterDuffColorFilter( + mCounterView.getCurrentTextColor(), PorterDuff.Mode.SRC_IN)); + } else { + // Else reset the color filter and refresh the drawable state so that the + // normal tint is used + DrawableCompat.clearColorFilter(editTextBackground); + mEditText.refreshDrawableState(); + } + } + + private void ensureBackgroundDrawableStateWorkaround() { + final int sdk = Build.VERSION.SDK_INT; + if (sdk != 21 && sdk != 22) { + // The workaround is only required on API 21-22 + return; + } + final Drawable bg = mEditText.getBackground(); + if (bg == null) { + return; + } + + if (!mHasReconstructedEditTextBackground) { + // This is gross. There is an issue in the platform which affects container Drawables + // where the first drawable retrieved from resources will propagate any changes + // (like color filter) to all instances from the cache. We'll try to workaround it... + + final Drawable newBg = bg.getConstantState().newDrawable(); + + if (bg instanceof DrawableContainer) { + // If we have a Drawable container, we can try and set it's constant state via + // reflection from the new Drawable + mHasReconstructedEditTextBackground = + OpenDrawableUtils.setContainerConstantState( + (DrawableContainer) bg, newBg.getConstantState()); + } + + if (!mHasReconstructedEditTextBackground) { + // If we reach here then we just need to set a brand new instance of the Drawable + // as the background. This has the unfortunate side-effect of wiping out any + // user set padding, but I'd hope that use of custom padding on an EditText + // is limited. + ViewCompat.setBackground(mEditText, newBg); + mHasReconstructedEditTextBackground = true; + } + } + } + + static class SavedState extends AbsSavedState { + CharSequence error; + boolean isPasswordToggledVisible; + + SavedState(Parcelable superState) { + super(superState); + } + + SavedState(Parcel source, ClassLoader loader) { + super(source, loader); + error = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); + isPasswordToggledVisible = (source.readInt() == 1); + + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + TextUtils.writeToParcel(error, dest, flags); + dest.writeInt(isPasswordToggledVisible ? 1 : 0); + } + + @Override + public String toString() { + return "TextInputLayout.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " error=" + error + "}"; + } + + public static final Creator CREATOR = new ClassLoaderCreator() { + @Override + public OpenInputLayout.SavedState createFromParcel(Parcel in, ClassLoader loader) { + return new OpenInputLayout.SavedState(in, loader); + } + + @Override + public OpenInputLayout.SavedState createFromParcel(Parcel in) { + return new OpenInputLayout.SavedState(in, null); + } + + @Override + public OpenInputLayout.SavedState[] newArray(int size) { + return new OpenInputLayout.SavedState[size]; + } + }; + } + + @Override + public Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + OpenInputLayout.SavedState ss = new OpenInputLayout.SavedState(superState); + if (mErrorShown) { + ss.error = getError(); + } + ss.isPasswordToggledVisible = mPasswordToggledVisible; + return ss; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + if (!(state instanceof OpenInputLayout.SavedState)) { + super.onRestoreInstanceState(state); + return; + } + OpenInputLayout.SavedState ss = (OpenInputLayout.SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + setError(ss.error); + if (ss.isPasswordToggledVisible) { + passwordVisibilityToggleRequested(true); + } + requestLayout(); + } + + @Override + protected void dispatchRestoreInstanceState(SparseArray container) { + mRestoringSavedState = true; + super.dispatchRestoreInstanceState(container); + mRestoringSavedState = false; + } + + /** + * Returns the error message that was set to be displayed with + * {@link #setError(CharSequence)}, or null if no error was set + * or if error displaying is not enabled. + * + * @see #setError(CharSequence) + */ + @Nullable + public CharSequence getError() { + return mErrorEnabled ? mError : null; + } + + /** + * Returns whether any hint state changes, due to being focused or non-empty text, are + * animated. + * + * @attr ref android.support.design.R.styleable#OpenInputLayout_hintAnimationEnabled + * @see #setHintAnimationEnabled(boolean) + */ + public boolean isHintAnimationEnabled() { + return mHintAnimationEnabled; + } + + /** + * Set whether any hint state changes, due to being focused or non-empty text, are + * animated. + * + * @attr ref android.support.design.R.styleable#OpenInputLayout_hintAnimationEnabled + * @see #isHintAnimationEnabled() + */ + public void setHintAnimationEnabled(boolean enabled) { + mHintAnimationEnabled = enabled; + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + + if (mHintEnabled) { + mCollapsingTextHelper.draw(canvas); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + updatePasswordToggleView(); + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + private void updatePasswordToggleView() { + if (mEditText == null) { + // If there is no EditText, there is nothing to update + return; + } + + if (shouldShowPasswordIcon()) { + if (mPasswordToggleView == null) { + mPasswordToggleView = (OpenCheckableImageButton) LayoutInflater.from(getContext()) + .inflate(android.support.design.R.layout.design_text_input_password_icon, mInputFrame, false); + mPasswordToggleView.setImageDrawable(mPasswordToggleDrawable); + mPasswordToggleView.setContentDescription(mPasswordToggleContentDesc); + mInputFrame.addView(mPasswordToggleView); + + mPasswordToggleView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + passwordVisibilityToggleRequested(false); + } + }); + } + + if (mEditText != null && ViewCompat.getMinimumHeight(mEditText) <= 0) { + // We should make sure that the EditText has the same min-height as the password + // toggle view. This ensure focus works properly, and there is no visual jump + // if the password toggle is enabled/disabled. + mEditText.setMinimumHeight(ViewCompat.getMinimumHeight(mPasswordToggleView)); + } + + mPasswordToggleView.setVisibility(VISIBLE); + mPasswordToggleView.setChecked(mPasswordToggledVisible); + + // We need to add a dummy drawable as the end compound drawable so that the text is + // indented and doesn't display below the toggle view + if (mPasswordToggleDummyDrawable == null) { + mPasswordToggleDummyDrawable = new ColorDrawable(); + } + mPasswordToggleDummyDrawable.setBounds(0, 0, mPasswordToggleView.getMeasuredWidth(), 1); + + final Drawable[] compounds = TextViewCompat.getCompoundDrawablesRelative(mEditText); + // Store the user defined end compound drawable so that we can restore it later + if (compounds[2] != mPasswordToggleDummyDrawable) { + mOriginalEditTextEndDrawable = compounds[2]; + } + TextViewCompat.setCompoundDrawablesRelative(mEditText, compounds[0], compounds[1], + mPasswordToggleDummyDrawable, compounds[3]); + + // Copy over the EditText's padding so that we match + mPasswordToggleView.setPadding(mEditText.getPaddingLeft(), + mEditText.getPaddingTop(), mEditText.getPaddingRight(), + mEditText.getPaddingBottom()); + } else { + if (mPasswordToggleView != null && mPasswordToggleView.getVisibility() == VISIBLE) { + mPasswordToggleView.setVisibility(View.GONE); + } + + if (mPasswordToggleDummyDrawable != null) { + // Make sure that we remove the dummy end compound drawable if it exists, and then + // clear it + final Drawable[] compounds = TextViewCompat.getCompoundDrawablesRelative(mEditText); + if (compounds[2] == mPasswordToggleDummyDrawable) { + TextViewCompat.setCompoundDrawablesRelative(mEditText, compounds[0], + compounds[1], mOriginalEditTextEndDrawable, compounds[3]); + mPasswordToggleDummyDrawable = null; + } + } + } + } + + /** + * Set the icon to use for the password visibility toggle button. + *

+ *

If you use an icon you should also set a description for its action + * using {@link #setPasswordVisibilityToggleContentDescription(CharSequence)}. + * This is used for accessibility.

+ * + * @param resId resource id of the drawable to set, or 0 to clear the icon + * @attr ref android.support.design.R.styleable#OpenInputLayout_passwordToggleDrawable + */ + public void setPasswordVisibilityToggleDrawable(@DrawableRes int resId) { + setPasswordVisibilityToggleDrawable(resId != 0 + ? AppCompatResources.getDrawable(getContext(), resId) + : null); + } + + /** + * Set the icon to use for the password visibility toggle button. + *

+ *

If you use an icon you should also set a description for its action + * using {@link #setPasswordVisibilityToggleContentDescription(CharSequence)}. + * This is used for accessibility.

+ * + * @param icon Drawable to set, may be null to clear the icon + * @attr ref android.support.design.R.styleable#OpenInputLayout_passwordToggleDrawable + */ + public void setPasswordVisibilityToggleDrawable(@Nullable Drawable icon) { + mPasswordToggleDrawable = icon; + if (mPasswordToggleView != null) { + mPasswordToggleView.setImageDrawable(icon); + } + } + + /** + * Set a content description for the navigation button if one is present. + *

+ *

The content description will be read via screen readers or other accessibility + * systems to explain the action of the password visibility toggle.

+ * + * @param resId Resource ID of a content description string to set, + * or 0 to clear the description + * @attr ref android.support.design.R.styleable#OpenInputLayout_passwordToggleContentDescription + */ + public void setPasswordVisibilityToggleContentDescription(@StringRes int resId) { + setPasswordVisibilityToggleContentDescription( + resId != 0 ? getResources().getText(resId) : null); + } + + /** + * Set a content description for the navigation button if one is present. + *

+ *

The content description will be read via screen readers or other accessibility + * systems to explain the action of the password visibility toggle.

+ * + * @param description Content description to set, or null to clear the content description + * @attr ref android.support.design.R.styleable#OpenInputLayout_passwordToggleContentDescription + */ + public void setPasswordVisibilityToggleContentDescription(@Nullable CharSequence description) { + mPasswordToggleContentDesc = description; + if (mPasswordToggleView != null) { + mPasswordToggleView.setContentDescription(description); + } + } + + /** + * Returns the icon currently used for the password visibility toggle button. + * + * @attr ref android.support.design.R.styleable#OpenInputLayout_passwordToggleDrawable + * @see #setPasswordVisibilityToggleDrawable(Drawable) + */ + @Nullable + public Drawable getPasswordVisibilityToggleDrawable() { + return mPasswordToggleDrawable; + } + + /** + * Returns the currently configured content description for the password visibility + * toggle button. + *

+ *

This will be used to describe the navigation action to users through mechanisms + * such as screen readers.

+ */ + @Nullable + public CharSequence getPasswordVisibilityToggleContentDescription() { + return mPasswordToggleContentDesc; + } + + /** + * Returns whether the password visibility toggle functionality is currently enabled. + * + * @see #setPasswordVisibilityToggleEnabled(boolean) + */ + public boolean isPasswordVisibilityToggleEnabled() { + return mPasswordToggleEnabled; + } + + /** + * Returns whether the password visibility toggle functionality is enabled or not. + *

+ *

When enabled, a button is placed at the end of the EditText which enables the user + * to switch between the field's input being visibly disguised or not.

+ * + * @param enabled true to enable the functionality + * @attr ref android.support.design.R.styleable#OpenInputLayout_passwordToggleEnabled + */ + public void setPasswordVisibilityToggleEnabled(final boolean enabled) { + if (mPasswordToggleEnabled != enabled) { + mPasswordToggleEnabled = enabled; + + if (!enabled && mPasswordToggledVisible && mEditText != null) { + // If the toggle is no longer enabled, but we remove the PasswordTransformation + // to make the password visible, add it back + mEditText.setTransformationMethod(PasswordTransformationMethod.getInstance()); + } + + // Reset the visibility tracking flag + mPasswordToggledVisible = false; + + updatePasswordToggleView(); + } + } + + /** + * Applies a tint to the the password visibility toggle drawable. Does not modify the current + * tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default. + *

+ *

Subsequent calls to {@link #setPasswordVisibilityToggleDrawable(Drawable)} will + * automatically mutate the drawable and apply the specified tint and tint mode using + * {@link DrawableCompat#setTintList(Drawable, ColorStateList)}.

+ * + * @param tintList the tint to apply, may be null to clear tint + * @attr ref android.support.design.R.styleable#OpenInputLayout_passwordToggleTint + */ + public void setPasswordVisibilityToggleTintList(@Nullable ColorStateList tintList) { + mPasswordToggleTintList = tintList; + mHasPasswordToggleTintList = true; + applyPasswordToggleTint(); + } + + /** + * Specifies the blending mode used to apply the tint specified by + * {@link #setPasswordVisibilityToggleTintList(ColorStateList)} to the password + * visibility toggle drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}.

+ * + * @param mode the blending mode used to apply the tint, may be null to clear tint + * @attr ref android.support.design.R.styleable#OpenInputLayout_passwordToggleTintMode + */ + public void setPasswordVisibilityToggleTintMode(@Nullable PorterDuff.Mode mode) { + mPasswordToggleTintMode = mode; + mHasPasswordToggleTintMode = true; + applyPasswordToggleTint(); + } + + private void passwordVisibilityToggleRequested(boolean shouldSkipAnimations) { + if (mPasswordToggleEnabled) { + // Store the current cursor position + final int selection = mEditText.getSelectionEnd(); + + if (hasPasswordTransformation()) { + mEditText.setTransformationMethod(null); + mPasswordToggledVisible = true; + } else { + mEditText.setTransformationMethod(PasswordTransformationMethod.getInstance()); + mPasswordToggledVisible = false; + } + + mPasswordToggleView.setChecked(mPasswordToggledVisible); + if (shouldSkipAnimations) { + mPasswordToggleView.jumpDrawablesToCurrentState(); + } + + // And restore the cursor position + mEditText.setSelection(selection); + } + } + + private boolean hasPasswordTransformation() { + return mEditText != null + && mEditText.getTransformationMethod() instanceof PasswordTransformationMethod; + } + + private boolean shouldShowPasswordIcon() { + return mPasswordToggleEnabled && (hasPasswordTransformation() || mPasswordToggledVisible); + } + + private void applyPasswordToggleTint() { + if (mPasswordToggleDrawable != null + && (mHasPasswordToggleTintList || mHasPasswordToggleTintMode)) { + mPasswordToggleDrawable = DrawableCompat.wrap(mPasswordToggleDrawable).mutate(); + + if (mHasPasswordToggleTintList) { + DrawableCompat.setTintList(mPasswordToggleDrawable, mPasswordToggleTintList); + } + if (mHasPasswordToggleTintMode) { + DrawableCompat.setTintMode(mPasswordToggleDrawable, mPasswordToggleTintMode); + } + + if (mPasswordToggleView != null + && mPasswordToggleView.getDrawable() != mPasswordToggleDrawable) { + mPasswordToggleView.setImageDrawable(mPasswordToggleDrawable); + } + } + } + + @SuppressLint("RestrictedApi") + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + + if (mHintEnabled && mEditText != null) { + final Rect rect = mTmpRect; + ViewGroupUtils.getDescendantRect(this, mEditText, rect); + + final int l = rect.left + mEditText.getCompoundPaddingLeft(); + final int r = rect.right - mEditText.getCompoundPaddingRight(); + + mCollapsingTextHelper.setExpandedBounds( + l, rect.top + mEditText.getCompoundPaddingTop(), + r, rect.bottom - mEditText.getCompoundPaddingBottom()); + if (mMoveLabelUp) { + + mCollapsingTextHelper.setCollapsedBounds(l, getPaddingTop(), + r, bottom - top - getPaddingBottom()); + mCollapsingTextHelper.setCollapsedTextGravity(Gravity.TOP); + } else { + + mCollapsingTextHelper.setCollapsedBounds(l, mEditText.getBottom(), + r, bottom); + mCollapsingTextHelper.setCollapsedTextGravity(Gravity.BOTTOM); + } + + mCollapsingTextHelper.recalculate(); + } + } + + private void collapseHint(boolean animate) { + if (mAnimator != null && mAnimator.isRunning()) { + mAnimator.cancel(); + } + if (animate && mHintAnimationEnabled) { + animateToExpansionFraction(1f); + } else { + mCollapsingTextHelper.setExpansionFraction(1f); + } + mHintExpanded = false; + } + + @Override + protected void drawableStateChanged() { + if (mInDrawableStateChanged) { + // Some of the calls below will update the drawable state of child views. Since we're + // using addStatesFromChildren we can get into infinite recursion, hence we'll just + // exit in this instance + return; + } + + mInDrawableStateChanged = true; + + super.drawableStateChanged(); + + final int[] state = getDrawableState(); + boolean changed = false; + + // Drawable state has changed so see if we need to update the label + updateLabelState(ViewCompat.isLaidOut(this) && isEnabled()); + + updateEditTextBackground(); + + if (mCollapsingTextHelper != null) { + changed |= mCollapsingTextHelper.setState(state); + } + + if (changed) { + invalidate(); + } + + mInDrawableStateChanged = false; + } + + private void expandHint(boolean animate) { + if (mAnimator != null && mAnimator.isRunning()) { + mAnimator.cancel(); + } + if (animate && mHintAnimationEnabled) { + animateToExpansionFraction(0f); + } else { + mCollapsingTextHelper.setExpansionFraction(0f); + } + mHintExpanded = true; + } + + void animateToExpansionFraction(final float target) { + if (mCollapsingTextHelper.getExpansionFraction() == target) { + return; + } + if (mAnimator == null) { + mAnimator = new ValueAnimator(); + mAnimator.setInterpolator(OpenAnimationUtils.LINEAR_INTERPOLATOR); + mAnimator.setDuration(ANIMATION_DURATION); + mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animator) { + mCollapsingTextHelper.setExpansionFraction((float) animator.getAnimatedValue()); + } + }); + } + mAnimator.setFloatValues(mCollapsingTextHelper.getExpansionFraction(), target); + mAnimator.start(); + } + + @VisibleForTesting + final boolean isHintExpanded() { + return mHintExpanded; + } + + private class TextInputAccessibilityDelegate extends AccessibilityDelegateCompat { + TextInputAccessibilityDelegate() { + } + + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(host, event); + event.setClassName(TextInputLayout.class.getSimpleName()); + } + + @Override + public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) { + super.onPopulateAccessibilityEvent(host, event); + + final CharSequence text = mCollapsingTextHelper.getText(); + if (!TextUtils.isEmpty(text)) { + event.getText().add(text); + } + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(host, info); + info.setClassName(TextInputLayout.class.getSimpleName()); + + final CharSequence text = mCollapsingTextHelper.getText(); + if (!TextUtils.isEmpty(text)) { + info.setText(text); + } + if (mEditText != null) { + info.setLabelFor(mEditText); + } + final CharSequence error = mErrorView != null ? mErrorView.getText() : null; + if (!TextUtils.isEmpty(error)) { + info.setContentInvalid(true); + info.setError(error); + } + } + } + + private static boolean arrayContains(int[] array, int value) { + for (int v : array) { + if (v == value) { + return true; + } + } + return false; + } +} diff --git a/library/src/main/java/br/com/opencraft/library/ViewUtils.java b/library/src/main/java/br/com/opencraft/library/ViewUtils.java new file mode 100644 index 0000000..4fbf3bf --- /dev/null +++ b/library/src/main/java/br/com/opencraft/library/ViewUtils.java @@ -0,0 +1,22 @@ +package br.com.opencraft.library; + +import android.graphics.PorterDuff; + +public class ViewUtils { + static PorterDuff.Mode parseTintMode(int value, PorterDuff.Mode defaultMode) { + switch (value) { + case 3: + return PorterDuff.Mode.SRC_OVER; + case 5: + return PorterDuff.Mode.SRC_IN; + case 9: + return PorterDuff.Mode.SRC_ATOP; + case 14: + return PorterDuff.Mode.MULTIPLY; + case 15: + return PorterDuff.Mode.SCREEN; + default: + return defaultMode; + } + } +} diff --git a/library/src/main/res/values/attrs.xml b/library/src/main/res/values/attrs.xml new file mode 100644 index 0000000..c12607c --- /dev/null +++ b/library/src/main/res/values/attrs.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/library/src/main/res/values/strings.xml b/library/src/main/res/values/strings.xml new file mode 100644 index 0000000..49fc91e --- /dev/null +++ b/library/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + library + diff --git a/library/src/test/java/br/com/opencraft/library/ExampleUnitTest.java b/library/src/test/java/br/com/opencraft/library/ExampleUnitTest.java new file mode 100644 index 0000000..12254a6 --- /dev/null +++ b/library/src/test/java/br/com/opencraft/library/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package br.com.opencraft.library; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..3306997 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':app', ':library'