From 2fdedf7e8b6ab1b9428b944abb72eb12aaf67720 Mon Sep 17 00:00:00 2001 From: MrFuFuFu Date: Sun, 15 Nov 2015 22:58:56 +0800 Subject: [PATCH] init --- .gitignore | 5 + LICENSE | 191 ++++++++++++++ app/.gitignore | 1 + app/build.gradle | 26 ++ app/proguard-rules.pro | 17 ++ .../clearableedittext/ApplicationTest.java | 13 + app/src/main/AndroidManifest.xml | 28 ++ .../clearableedittext/LoginActivity.java | 239 ++++++++++++++++++ .../textwatchers/ErrorTextWatcher.java | 50 ++++ .../MinimumLengthTextWatcher.java | 40 +++ .../textwatchers/ValidEmailTextWatcher.java | 39 +++ .../validator/EmailAddressValidator.java | 32 +++ .../utils/validator/Validator.java | 7 + .../utils/validator/ValidatorError.java | 4 + .../views/ClearableEditText.java | 110 ++++++++ app/src/main/res/layout/activity_login.xml | 80 ++++++ app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3418 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2206 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4842 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7718 bytes app/src/main/res/values-sw600dp/bools.xml | 4 + app/src/main/res/values/bools.xml | 4 + app/src/main/res/values/colors.xml | 8 + app/src/main/res/values/dimens.xml | 5 + app/src/main/res/values/integers.xml | 4 + app/src/main/res/values/strings.xml | 3 + .../res/values/strings_activity_login.xml | 12 + app/src/main/res/values/styles.xml | 18 ++ .../validator/EmailAddressValidatorTest.java | 80 ++++++ build.gradle | 19 ++ gradle.properties | 18 ++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 49896 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 164 ++++++++++++ gradlew.bat | 90 +++++++ settings.gradle | 1 + 36 files changed, 1318 insertions(+) create mode 100755 .gitignore create mode 100755 LICENSE create mode 100755 app/.gitignore create mode 100755 app/build.gradle create mode 100755 app/proguard-rules.pro create mode 100755 app/src/androidTest/java/com/example/barryirvine/clearableedittext/ApplicationTest.java create mode 100755 app/src/main/AndroidManifest.xml create mode 100755 app/src/main/java/com/example/barryirvine/clearableedittext/LoginActivity.java create mode 100755 app/src/main/java/com/example/barryirvine/clearableedittext/textwatchers/ErrorTextWatcher.java create mode 100755 app/src/main/java/com/example/barryirvine/clearableedittext/textwatchers/MinimumLengthTextWatcher.java create mode 100755 app/src/main/java/com/example/barryirvine/clearableedittext/textwatchers/ValidEmailTextWatcher.java create mode 100755 app/src/main/java/com/example/barryirvine/clearableedittext/utils/validator/EmailAddressValidator.java create mode 100755 app/src/main/java/com/example/barryirvine/clearableedittext/utils/validator/Validator.java create mode 100755 app/src/main/java/com/example/barryirvine/clearableedittext/utils/validator/ValidatorError.java create mode 100755 app/src/main/java/com/example/barryirvine/clearableedittext/views/ClearableEditText.java create mode 100755 app/src/main/res/layout/activity_login.xml create mode 100755 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100755 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100755 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100755 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100755 app/src/main/res/values-sw600dp/bools.xml create mode 100755 app/src/main/res/values/bools.xml create mode 100755 app/src/main/res/values/colors.xml create mode 100755 app/src/main/res/values/dimens.xml create mode 100755 app/src/main/res/values/integers.xml create mode 100755 app/src/main/res/values/strings.xml create mode 100755 app/src/main/res/values/strings_activity_login.xml create mode 100755 app/src/main/res/values/styles.xml create mode 100755 app/src/test/java/com/example/barryirvine/clearableedittext/utils/validator/EmailAddressValidatorTest.java create mode 100755 build.gradle create mode 100755 gradle.properties create mode 100755 gradle/wrapper/gradle-wrapper.jar create mode 100755 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100755 gradlew.bat create mode 100755 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..ff9fe05 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.gradle +/local.properties +/.idea/* +/build +*.iml \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100755 index 0000000..37ec93a --- /dev/null +++ b/LICENSE @@ -0,0 +1,191 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/app/.gitignore b/app/.gitignore new file mode 100755 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 100755 index 0000000..25b8674 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,26 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.1" + + defaultConfig { + applicationId "com.example.barryirvine.clearableedittext" + minSdkVersion 15 + targetSdkVersion 23 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.android.support:design:23.0.1' // Includes support-v4 and appcompat + testCompile 'junit:junit:4.12' +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100755 index 0000000..aa362b4 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/barryirvine/Library/Android/sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/app/src/androidTest/java/com/example/barryirvine/clearableedittext/ApplicationTest.java b/app/src/androidTest/java/com/example/barryirvine/clearableedittext/ApplicationTest.java new file mode 100755 index 0000000..b828ed0 --- /dev/null +++ b/app/src/androidTest/java/com/example/barryirvine/clearableedittext/ApplicationTest.java @@ -0,0 +1,13 @@ +package com.example.barryirvine.clearableedittext; + +import android.app.Application; +import android.test.ApplicationTestCase; + +/** + * Testing Fundamentals + */ +public class ApplicationTest extends ApplicationTestCase { + public ApplicationTest() { + super(Application.class); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100755 index 0000000..159f0ff --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/example/barryirvine/clearableedittext/LoginActivity.java b/app/src/main/java/com/example/barryirvine/clearableedittext/LoginActivity.java new file mode 100755 index 0000000..d8c88f3 --- /dev/null +++ b/app/src/main/java/com/example/barryirvine/clearableedittext/LoginActivity.java @@ -0,0 +1,239 @@ +package com.example.barryirvine.clearableedittext; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.content.res.Configuration; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Parcelable; +import android.os.PersistableBundle; +import android.support.design.widget.TextInputLayout; +import android.support.v7.app.AppCompatActivity; +import android.util.Log; +import android.view.KeyEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.inputmethod.EditorInfo; +import android.widget.Button; +import android.widget.TextView; + +import com.example.barryirvine.clearableedittext.textwatchers.MinimumLengthTextWatcher; +import com.example.barryirvine.clearableedittext.textwatchers.ValidEmailTextWatcher; + + +/** + * A login screen that offers login via email/password. + */ +public class LoginActivity extends AppCompatActivity { + + /** + * A dummy authentication store containing known user names and passwords. + * TODO: remove after connecting to a real authentication system. + */ + private static final String[] DUMMY_CREDENTIALS = new String[]{ + "foo@example.com:hello", "bar@example.com:world" + }; + /** + * Keep track of the login task to ensure we can cancel it if requested. + */ + private UserLoginTask mAuthTask = null; + + // UI references. + private TextInputLayout mEmailView; + private ValidEmailTextWatcher mValidEmailTextWatcher; + private TextInputLayout mPasswordView; + private MinimumLengthTextWatcher mValidPasswordTextWatcher; + private View mProgressView; + private View mLoginFormView; + + private static final String STATE_EMAIL_ERROR_TEXT = "STATE_EMAIL_ERROR_TEXT"; + private static final String STATE_PASSWORD_ERROR_TEXT = "STATE_PASSWORD_ERROR_TEXT"; + + @Override + protected void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Log.d("BARRY", "onCreate"); + setContentView(R.layout.activity_login); + setTitle(R.string.action_sign_in_short); + + // Set up the login form. + mEmailView = (TextInputLayout) findViewById(R.id.email_text_input_layout); + // The animation is switched off by default in the XML. This ensures that it only starts animating once the view is laid out. + mEmailView.post(new Runnable() { + @Override + public void run() { + mEmailView.setHintAnimationEnabled(true); + } + }); + mValidEmailTextWatcher = new ValidEmailTextWatcher(mEmailView); + mEmailView.getEditText().addTextChangedListener(mValidEmailTextWatcher); + mPasswordView = (TextInputLayout) findViewById(R.id.password_text_input_layout); + mValidPasswordTextWatcher = new MinimumLengthTextWatcher(mPasswordView, getResources().getInteger(R.integer.min_length_password)); + mPasswordView.getEditText().addTextChangedListener(mValidPasswordTextWatcher); + mPasswordView.getEditText().setOnEditorActionListener(new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(final TextView textView, final int id, final KeyEvent keyEvent) { + if (id == R.id.login || id == EditorInfo.IME_NULL) { + attemptLogin(); + return true; + } + return false; + } + }); + + final Button emailSignInButton = (Button) findViewById(R.id.email_sign_in_button); + emailSignInButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(final View view) { + attemptLogin(); + } + }); + + mLoginFormView = findViewById(R.id.login_form); + mProgressView = findViewById(R.id.login_progress); + + + } + + @Override + protected void onResume() { + super.onResume(); + // Hide toolbar when in landscape on phones to + if (!getResources().getBoolean(R.bool.is_tablet) && getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { + getSupportActionBar().hide(); + } else { + getSupportActionBar().show(); + } + } + + @Override + protected void onSaveInstanceState(final Bundle outState) { + /** + * TextInputLayout isn't saving its state like it should. This works around that issue. + * https://code.google.com/p/android/issues/detail?id=181621 + */ + outState.putCharSequence(STATE_EMAIL_ERROR_TEXT, mEmailView.getError()); + outState.putCharSequence(STATE_PASSWORD_ERROR_TEXT, mPasswordView.getError()); + super.onSaveInstanceState(outState); + } + + @Override + protected void onRestoreInstanceState(final Bundle savedInstanceState) { + /** + * TextInputLayout isn't saving its state like it should. This works around that issue. + * https://code.google.com/p/android/issues/detail?id=181621 + */ + super.onRestoreInstanceState(savedInstanceState); + mEmailView.setError(savedInstanceState.getCharSequence(STATE_EMAIL_ERROR_TEXT)); + mPasswordView.setError(savedInstanceState.getCharSequence(STATE_PASSWORD_ERROR_TEXT)); + } + + private boolean allFieldsAreValid() { + /** + * Since the text watchers automatically focus on erroneous fields, do them in reverse order so that the first one in the form gets focus + * &= may not be the easiest construct to decipher but it's a lot more concise. It just means that once it's false it doesn't get set to true + */ + boolean isValid = mValidPasswordTextWatcher.validate(); + isValid &= mValidEmailTextWatcher.validate(); + return isValid; + } + + /** + * Attempts to sign in or register the account specified by the login form. + * If there are form errors (invalid email, missing fields, etc.), the + * errors are presented and no actual login attempt is made. + */ + public void attemptLogin() { + if (mAuthTask != null) { + return; + } + if (allFieldsAreValid()) { + final String email = mEmailView.getEditText().getText().toString(); + final String password = mPasswordView.getEditText().getText().toString(); + showProgress(true); + mAuthTask = new UserLoginTask(email, password); + mAuthTask.execute((Void) null); + } + } + + /** + * Shows the progress UI and hides the login form. + */ + private void showProgress(final boolean show) { + int shortAnimTime = getResources().getInteger(android.R.integer.config_shortAnimTime); + + mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE); + mLoginFormView.animate().setDuration(shortAnimTime).alpha(show ? 0 : 1).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE); + } + }); + + mProgressView.setVisibility(show ? View.VISIBLE : View.GONE); + mProgressView.animate().setDuration(shortAnimTime).alpha(show ? 1 : 0).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mProgressView.setVisibility(show ? View.VISIBLE : View.GONE); + } + }); + } + + /** + * Represents an asynchronous login/registration task used to authenticate + * the user. + */ + public class UserLoginTask extends AsyncTask { + + private final String mEmail; + private final String mPassword; + + UserLoginTask(String email, String password) { + mEmail = email; + mPassword = password; + } + + @Override + protected Boolean doInBackground(Void... params) { + // TODO: attempt authentication against a network service. + + try { + // Simulate network access. + Thread.sleep(2000); + } catch (InterruptedException e) { + return false; + } + + for (String credential : DUMMY_CREDENTIALS) { + String[] pieces = credential.split(":"); + if (pieces[0].equals(mEmail)) { + // Account exists, return true if the password matches. + return pieces[1].equals(mPassword); + } + } + + // TODO: register the new account here. + return true; + } + + @Override + protected void onPostExecute(final Boolean success) { + mAuthTask = null; + showProgress(false); + + if (success) { + finish(); + } else { + mPasswordView.setError(getString(R.string.error_incorrect_password)); + mPasswordView.requestFocus(); + } + } + + @Override + protected void onCancelled() { + mAuthTask = null; + showProgress(false); + } + } +} + diff --git a/app/src/main/java/com/example/barryirvine/clearableedittext/textwatchers/ErrorTextWatcher.java b/app/src/main/java/com/example/barryirvine/clearableedittext/textwatchers/ErrorTextWatcher.java new file mode 100755 index 0000000..69301c6 --- /dev/null +++ b/app/src/main/java/com/example/barryirvine/clearableedittext/textwatchers/ErrorTextWatcher.java @@ -0,0 +1,50 @@ +package com.example.barryirvine.clearableedittext.textwatchers; + +import android.support.annotation.NonNull; +import android.support.design.widget.TextInputLayout; +import android.text.Editable; +import android.text.TextWatcher; + +public abstract class ErrorTextWatcher implements TextWatcher { + + private TextInputLayout mTextInputLayout; + private String errorMessage; + + protected ErrorTextWatcher(@NonNull final TextInputLayout textInputLayout, @NonNull final String errorMessage) { + this.mTextInputLayout = textInputLayout; + this.errorMessage = errorMessage; + } + + public final boolean hasError() { + return mTextInputLayout.getError() != null; + } + + public abstract boolean validate(); + + protected String getEditTextValue() { + return mTextInputLayout.getEditText().getText().toString(); + } + + protected void showError(final boolean error) { + if (!error) { + mTextInputLayout.setError(null); + mTextInputLayout.setErrorEnabled(false); + } else { + if (!errorMessage.equals(mTextInputLayout.getError())) { + // Stop the flickering that happens when setting the same error message multiple times + mTextInputLayout.setError(errorMessage); + } + mTextInputLayout.requestFocus(); + } + } + + @Override + public void beforeTextChanged(final CharSequence text, final int start, final int count, final int after) { + + } + + @Override + public void afterTextChanged(final Editable s) { + + } +} diff --git a/app/src/main/java/com/example/barryirvine/clearableedittext/textwatchers/MinimumLengthTextWatcher.java b/app/src/main/java/com/example/barryirvine/clearableedittext/textwatchers/MinimumLengthTextWatcher.java new file mode 100755 index 0000000..0295710 --- /dev/null +++ b/app/src/main/java/com/example/barryirvine/clearableedittext/textwatchers/MinimumLengthTextWatcher.java @@ -0,0 +1,40 @@ +package com.example.barryirvine.clearableedittext.textwatchers; + +import android.support.annotation.StringRes; +import android.support.design.widget.TextInputLayout; +import android.text.Editable; + +import com.example.barryirvine.clearableedittext.R; + +public class MinimumLengthTextWatcher extends ErrorTextWatcher { + + private final int mMinLength; + private boolean mReachedMinLength = false; + + public MinimumLengthTextWatcher(final TextInputLayout textInputLayout, final int minLength) { + this(textInputLayout, minLength, R.string.error_too_few_characters); + } + + public MinimumLengthTextWatcher(final TextInputLayout textInputLayout, final int minLength, @StringRes final int errorMessage) { + super(textInputLayout, String.format(textInputLayout.getContext().getString(errorMessage), minLength)); + this.mMinLength = minLength; + } + + @Override + public void onTextChanged(final CharSequence text, final int start, final int before, final int count) { + if (mReachedMinLength) { + validate(); + } + if (text.length() >= mMinLength) { + mReachedMinLength = true; + } + } + + @Override + public boolean validate() { + mReachedMinLength = true; // This may not be true but now we want to force the error to be shown + showError(getEditTextValue().length() < mMinLength); + return !hasError(); + } + +} diff --git a/app/src/main/java/com/example/barryirvine/clearableedittext/textwatchers/ValidEmailTextWatcher.java b/app/src/main/java/com/example/barryirvine/clearableedittext/textwatchers/ValidEmailTextWatcher.java new file mode 100755 index 0000000..c16cacd --- /dev/null +++ b/app/src/main/java/com/example/barryirvine/clearableedittext/textwatchers/ValidEmailTextWatcher.java @@ -0,0 +1,39 @@ +package com.example.barryirvine.clearableedittext.textwatchers; + +import android.support.annotation.NonNull; +import android.support.annotation.StringRes; +import android.support.design.widget.TextInputLayout; +import android.text.Editable; + +import com.example.barryirvine.clearableedittext.R; +import com.example.barryirvine.clearableedittext.utils.validator.EmailAddressValidator; + +public class ValidEmailTextWatcher extends ErrorTextWatcher { + + private final EmailAddressValidator mValidator = new EmailAddressValidator(); + private boolean mValidated = false; + + + public ValidEmailTextWatcher(@NonNull final TextInputLayout textInputLayout) { + this(textInputLayout, R.string.error_invalid_email); + } + + public ValidEmailTextWatcher(@NonNull final TextInputLayout textInputLayout, @StringRes final int errorMessage) { + super(textInputLayout, textInputLayout.getContext().getString(errorMessage)); + } + + @Override + public void onTextChanged(final CharSequence text, final int start, final int before, final int count) { + // The hasError is needed here because I can restore the instance state of the input layout without updating this flag in the watcher + if (mValidated || hasError()) { + validate(); + } + } + + @Override + public boolean validate() { + showError(!mValidator.isValid(getEditTextValue())); + mValidated = true; + return !hasError(); + } +} diff --git a/app/src/main/java/com/example/barryirvine/clearableedittext/utils/validator/EmailAddressValidator.java b/app/src/main/java/com/example/barryirvine/clearableedittext/utils/validator/EmailAddressValidator.java new file mode 100755 index 0000000..3d7213e --- /dev/null +++ b/app/src/main/java/com/example/barryirvine/clearableedittext/utils/validator/EmailAddressValidator.java @@ -0,0 +1,32 @@ +package com.example.barryirvine.clearableedittext.utils.validator; + +import java.util.regex.Pattern; + +public class EmailAddressValidator implements Validator { + + private static final Pattern EMAIL_ADDRESS_PATTERN + = Pattern.compile("(?!\\.)(?!.*\\.{2})(?!.*\\.@)(?!.*\\.[0-9]+[a-z]+$)[a-z0-9\\+\\._%\\$\\-]{1,256}@" + + "[a-z0-9][a-z0-9\\-\\.]*" + + "\\." + + "([a-z]{2,25})+" + , Pattern.CASE_INSENSITIVE); + // First look ahead clause - prevent leading fullstop + // Second look ahead clause - prevent consecutive fullstops + // Third look ahead clause - prevent fullstop before @ + // Fourth look ahead clause - prevent TLD beginning with number + + @Override + public final boolean isValid(final String emailAddress) { + return !(emailAddress == null || emailAddress.length() == 0) && EMAIL_ADDRESS_PATTERN.matcher(emailAddress).matches(); + + } + + @Override + public final ValidatorError[] getErrors() { + return new ValidatorError[]{EmailAddressValidatorError.EMAIL_INVALID}; + } + + public enum EmailAddressValidatorError implements ValidatorError { + EMAIL_INVALID + } +} diff --git a/app/src/main/java/com/example/barryirvine/clearableedittext/utils/validator/Validator.java b/app/src/main/java/com/example/barryirvine/clearableedittext/utils/validator/Validator.java new file mode 100755 index 0000000..c71b8e7 --- /dev/null +++ b/app/src/main/java/com/example/barryirvine/clearableedittext/utils/validator/Validator.java @@ -0,0 +1,7 @@ +package com.example.barryirvine.clearableedittext.utils.validator; + +public interface Validator { + boolean isValid(T t); + + ValidatorError[] getErrors(); +} \ No newline at end of file diff --git a/app/src/main/java/com/example/barryirvine/clearableedittext/utils/validator/ValidatorError.java b/app/src/main/java/com/example/barryirvine/clearableedittext/utils/validator/ValidatorError.java new file mode 100755 index 0000000..de2a261 --- /dev/null +++ b/app/src/main/java/com/example/barryirvine/clearableedittext/utils/validator/ValidatorError.java @@ -0,0 +1,4 @@ +package com.example.barryirvine.clearableedittext.utils.validator; + +public interface ValidatorError { +} diff --git a/app/src/main/java/com/example/barryirvine/clearableedittext/views/ClearableEditText.java b/app/src/main/java/com/example/barryirvine/clearableedittext/views/ClearableEditText.java new file mode 100755 index 0000000..1280a08 --- /dev/null +++ b/app/src/main/java/com/example/barryirvine/clearableedittext/views/ClearableEditText.java @@ -0,0 +1,110 @@ +package com.example.barryirvine.clearableedittext.views; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.support.v4.content.ContextCompat; +import android.support.v4.graphics.drawable.DrawableCompat; +import android.support.v7.widget.AppCompatEditText; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + +import com.example.barryirvine.clearableedittext.R; + +public class ClearableEditText extends AppCompatEditText implements View.OnTouchListener, View.OnFocusChangeListener, TextWatcher { + + private Drawable mClearTextIcon; + private OnFocusChangeListener mOnFocusChangeListener; + private OnTouchListener mOnTouchListener; + + public ClearableEditText(final Context context) { + super(context); + init(context); + } + + public ClearableEditText(final Context context, final AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public ClearableEditText(final Context context, final AttributeSet attrs, final int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context); + } + + @Override + public void setOnFocusChangeListener(final OnFocusChangeListener onFocusChangeListener) { + mOnFocusChangeListener = onFocusChangeListener; + } + + @Override + public void setOnTouchListener(final OnTouchListener onTouchListener) { + mOnTouchListener = onTouchListener; + } + + private void init(final Context context) { + final Drawable drawable = ContextCompat.getDrawable(context, R.drawable.abc_ic_clear_mtrl_alpha); + final Drawable wrappedDrawable = DrawableCompat.wrap(drawable); //Wrap the drawable so that it can be tinted pre Lollipop + DrawableCompat.setTint(wrappedDrawable, getCurrentHintTextColor()); + mClearTextIcon = wrappedDrawable; + mClearTextIcon.setBounds(0, 0, mClearTextIcon.getIntrinsicHeight(), mClearTextIcon.getIntrinsicHeight()); + setClearIconVisible(false); + super.setOnTouchListener(this); + super.setOnFocusChangeListener(this); + addTextChangedListener(this); + } + + + @Override + public void onFocusChange(final View view, final boolean hasFocus) { + if (hasFocus) { + setClearIconVisible(getText().length() > 0); + } else { + setClearIconVisible(false); + } + if (mOnFocusChangeListener != null) { + mOnFocusChangeListener.onFocusChange(view, hasFocus); + } + } + + @Override + public boolean onTouch(final View view, final MotionEvent motionEvent) { + final int x = (int) motionEvent.getX(); + if (mClearTextIcon.isVisible() && x > getWidth() - getPaddingRight() - mClearTextIcon.getIntrinsicWidth()) { + if (motionEvent.getAction() == MotionEvent.ACTION_UP) { + setError(null); + setText(""); + } + return true; + } + return mOnTouchListener != null && mOnTouchListener.onTouch(view, motionEvent); + } + + @Override + public final void onTextChanged(final CharSequence s, final int start, final int before, final int count) { + if (isFocused()) { + setClearIconVisible(s.length() > 0); + } + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void afterTextChanged(Editable s) { + } + + + private void setClearIconVisible(final boolean visible) { + mClearTextIcon.setVisible(visible, false); + final Drawable[] compoundDrawables = getCompoundDrawables(); + setCompoundDrawables( + compoundDrawables[0], + compoundDrawables[1], + visible ? mClearTextIcon : null, + compoundDrawables[3]); + } +} diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml new file mode 100755 index 0000000..ab317a6 --- /dev/null +++ b/app/src/main/res/layout/activity_login.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + +