保持 EditText 的簡潔
原文 http://mrfu.me/android/2015/11/15/Keeping_it_clean/
原文地址:保持 EditText 的簡潔
翻譯原文: Keeping it clean
項目地址(歡迎 Star): ClearEditText
在 Android design support 包中提供了一種在輸入不合適字符時一直顯示的提示方式來顯示,現在已經開始在更多的應用上被使用了;這些 Android app 在顯示他們的錯誤提示時采用的不同的方式常常讓人感覺非常的不和諧。
即這個一直顯示的錯誤消息是在 TextInputLayout 中的 EditText 周圍的。這也是,作為一個獎勵,提供了材料設計風格中,活潑的浮動標簽在一個 APP 的用戶體驗中常常是最無聊的部分。
每次一個新版本【指Android support library】發布的時候我就像一個小孩在過圣誕節:我沖下樓去看圣誕老人送來的新玩具是什么,但是發現他帶來新玩具的時候,我的新玩具火車缺少一些零件,他還弄壞了一些我最喜歡的玩具,還把煙囪里的煙灰踩到了地攤上。
在這篇文章中,我將討論如何在你的輸入表單上去創建一個通用的、可重用的組件來實現所有的字段驗證。因為你想要在用戶改正了錯誤的輸入時就去隱藏錯誤提示。我們可以通過使用 TextWatchers 來實現驗證。
不幸的是,在最新的support library (23.1)中,一旦你隱藏了錯誤提示,讓它們再顯示的時候,會有一個 bug。所以這個例子是建立在這個 23.0.1 support library 上的。此時我對這個 support library 是又愛又恨的關系——每次一個新版本發布的時候我就像一個小孩在過圣誕節:我沖下樓去看圣誕老人送來的新玩具是什么,但是發現他帶來新玩具的時候,我的新玩具火車缺少一些零件,他還弄壞了一些我最喜歡的玩具,還把煙囪里的煙灰踩到了地攤上。
創建我們通用的類
把我的小埋怨放到一邊,讓我們創建一個實現了 TextWatcher 的接口的抽象的 ErrorTextWatcher 類。對于這個簡單的例子,我想說我們的 TextWatcher 總是帶有 TextInputLayout,而且它可以顯示一個簡單的錯誤消息。你的用戶體驗設計團隊可能想要顯示不同的錯誤——如:“密碼不能為空”,“密碼必須包含至少一個數字”,“請輸入至少 4 個字符”等。—— 但為了簡單起見,每個 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; } protected String getEditTextValue() { return mTextInputLayout.getEditText().getText().toString(); }
我也想要我所有的 ErrorTextWatchers 都實現 validate() 方法,如果如果輸入是正確的就返回 true,這樣能簡單的去顯示或隱藏錯誤:
public abstract boolean validate(); 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(); } }
在我的代碼上,這個庫在這里有另外一個功能:在我看來通過設置錯誤提示的 enabled 為 false,你就應該能隱藏錯誤提示,但是這會讓 EditText 的下劃線仍然顯示不正確的顏色,所以你既需要設置錯誤提示為空,也需要讓下劃線的顏色恢復。同樣,如果你不斷地設置相同的錯誤字符串,這個錯誤提示會隨著動畫不斷的閃爍,所以只有當錯誤提示有新的值時才要去重寫。
最后,當焦點在 TextWatcher 內的 EditText 上時,我有一點點調皮的要求 ——當你看到我是如何驗證輸入表單的,希望你能明白我為什么這么做,但是對于你的需求,你可能想要把這段邏輯移到其他地方。
作為一個額外的優化,我發現我可以在 onTextChanged 方法的 TextWatcher 接口內實現我所有的邏輯,所以我給 beforeTextChanged 和 afterTextChanged 的父類增加了兩個空方法。
最小長度驗證
現在,讓我們這個類的一個具體的例子。一個常見的用例是輸入字段需要至少為 x 個的字符。因此,讓我們創建一個 MinimumLengthTextWatcher。它帶有一個最小長度值,當然,在父類中,我還需要 TextInputLayout 和 message。此外,我不想在他們輸入完成之前一直告訴用戶他們需要輸入 x 個字符——這會是一個壞的用戶體驗——所以我們應該在用戶已經超出了最小限制字符的時候來開始顯示錯誤。(譯者注:可以理解為當用戶輸入的長度超過最小限制字符之后,用戶再刪除一部分字符,如果此時少于最小限制字符,就會顯示錯誤了,這樣就能理解了)
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; }
這里有兩個構造方法:一個是具有默認的消息,還有一個是對于特殊的文本字段你可以創建一個更具體的值。因為我們想要支持當地化,我們采用 Android string 資源文件,而不是硬編碼 String 的值。
我們文本的改變和驗證方法現在已經像下面這樣簡單的實現了:
@Override public void onTextChanged(final CharSequence text…) { 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(); }
你會注意到,一旦驗證方法在 TextWatcher 中被調起的話,它將會顯示錯誤。我想這適用于大多數情況,但是你可能想要引入一個 setter 方法去重置某些情況下的這種行為。
你現在需要去給你的 TextInputLayout 增加 TextWatcher,接著在你的 Activity 或 Fragment 中去創建 views。就像這樣:
mPasswordView = (TextInputLayout) findViewById(R.id.password_text_input_layout); mValidPasswordTextWatcher = new MinimumLengthTextWatcher(mPasswordView, getResources().getInteger(R.integer.min_length_password)); mPasswordView.getEditText().addTextChangedListener(mValidPasswordTextWatcher);
然后,在你代碼的合適位置,你可以檢查一個字段是否有效:
boolean isValid = mValidPasswordTextWatcher.validate();
如果密碼是無效的,這個 View 會自動的獲得焦點并將屏幕滾動到這里。
驗證電子郵件地址
另一種常見的驗證用例是檢查電子郵件地址是否是有效的。我可以很容易的寫一整篇都關于用正則表達式來驗證郵件地址的文章,但是因為這常常是有爭議的,我已經從 TextWatcher 本身分開了郵件驗證的邏輯。示例項目包含了可測試的 EmailAddressValidator,你可以用它,或者你也可以用你自己想要的邏輯來實現。
既然我已經把郵件驗證邏輯分離出來了,ValidEmailTextWatcher 是和 MinimumLengthTextWatcher 非常相似的。
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(…) { if (mValidated) { validate(); } } @Override public boolean validate() { showError(!mValidator.isValid(getEditTextValue())); mValidated = true; return !hasError(); }
這個 TextWatcher 在我們的 Activity 或 Fragment 內的實現方式是和之前的是非常像的:
mEmailView = (TextInputLayout) findViewById(R.id.email_text_input_layout); mValidEmailTextWatcher = new ValidEmailTextWatcher(mEmailView); mEmailView.getEditText().addTextChangedListener(mValidEmailTextWatcher);
把它放在一起
對于表單注冊或登錄,在提交給你的 API 之前,你通常會驗證所有的字段。因為我們要求關注在 TextWatcher 的任何 views 的失敗驗證。我一般在從下往上驗證所有的 view。這樣,應用程序顯示所有需要糾正字段的錯誤,然后跳轉到表單上第一個錯誤輸入的文本。例如:
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; }
你可以找到上述所有代碼的例子在 GitHub 1 上。這是一個在 ClearableEditText 上的分支,我是基于 讓你的 EditText 全部清除 2 這篇博客上的代碼來進行闡述的,但是把它用在標準的 EditText 上也是一樣的。它還包括了一些更多的技巧和 bug 處理,我沒有時間在這里提了。
盡管我只顯示了兩個 TextWatcher 的例子,但我希望你能看到這是多么簡單,你現在能添加其他的 TextWatcher 去給任何文本輸入添加不同的驗證方法,并在你的 APP 中去請求驗證和重用。