TextWatcher的使用及原始碼解析

bt發表於2019-03-26

TextWatcher 定義

官方: 當文字發生變化時會觸發介面回撥. 一般應用場景為自定義輸入框用於對使用者的輸入進行限制,比如只能輸入英文,數字等等

TextWatcher使用

TextWatcher 為介面,通常配合 TextViewEditText進行使用,需要我們自行實現介面方法並且通過textview.addTextChangedListener(textWatcher) 為對應的view新增監聽

方法解讀

public interface TextWatcher extends NoCopySpan {
    /** 
     * 文字改變前回撥方法
     * 改變完成前的字串 s 的 start 位置開始的 count 個字元將會被 after 個字元替換
     * index 為 start 的 after 個字元被刪除後,新的字串插入到 start 位置
     */
    public void beforeTextChanged(CharSequence s, int start, int count, int after);

    /** 
     * 文字改變完成回撥方法
     * 改變完成後的字串 s 的 start 位置開始的 before 個字元已經被 count 個字元替換
     */   

    public void onTextChanged(CharSequence s, int start, int before, int count);
    /** 
     * 文字改變後的回撥方法,在 onTextChanged 方法後呼叫
     * s為改變完成的字串
     */
    public void afterTextChanged(Editable s);
}
複製程式碼

觸發條件

  1. 鍵盤文字輸入 鍵盤輸入直接對文字進行修改,包括且不限於文字貼上,文字修改,新增文字等等.

  2. setText(newStr)TextView 進行文字設定,不侷限於 setText() 方法,包括 replace() , apeend() 等等直接對文字進行修改操作的方法,都會觸發 TextWatcher 的回撥.

  3. 修改回撥方法中的 Editable 物件 Editable 屬於可變字串,對其進行字串操作不會產生新的物件,等同於對 TextView 持有的字串進行操作.

原始碼分析

新增監聽

textview.addTextChangedListener(textWatcher)

    // TextView.java

    public void addTextChangedListener(TextWatcher watcher) {
        if (mListeners == null) {
            mListeners = new ArrayList<TextWatcher>();
        }

        mListeners.add(watcher);
    }
複製程式碼

通過原始碼可知新增監聽是將所有的已新增監聽通過 List 進行儲存,由此可以得知可以對同一 TextView 設定多個 TextWatcher

擴充套件: removeTextChangedListener(TextWatcher watcher) 可以移除指定 TextWatcher, 沒有類似 removeAllXXX 這種移除全部 TextWatcher 的方法.

setText(newStr)

setText(newStr) 是一種特殊的改變文字的方式,無論 newStr 是什麼,都會完全覆蓋原值

// TextView.java

private void setText(CharSequence text, BufferType type, boolean notifyBefore, int oldlen) {
    ...
    if (mText != null) {
        oldlen = mText.length();
        sendBeforeTextChanged(mText, 0, oldlen, text.length());
    } else {
        sendBeforeTextChanged("", 0, 0, text.length());
    }
    ...
}

private void sendBeforeTextChanged(CharSequence text, int start, int before, int after) {
    if (mListeners != null) {
        final ArrayList<TextWatcher> list = mListeners;
        final int count = list.size();
        for (int i = 0; i < count; i++) {
            list.get(i).beforeTextChanged(text, start, before, after);
        }
    }
    ...
}
複製程式碼

通過原始碼可知如果 newStr 不為null,那麼 beforeTextChanged的第一個引數 text 始終為原字串.第二個引數 start 始終為0, setText() 的本質是完全替換現有字串,從起始位置0進行替換.第三個引數 before 為現有的字串長度,因為要完全替換所有的字元.第四個引數 after 為新字串的長度.

// TextView.java

private void setText(CharSequence text, BufferType type, boolean notifyBefore, int oldlen) {
    ...
    sendOnTextChanged(text, 0, oldlen, textLength);
    ...
    if (needEditableForNotification) {
        sendAfterTextChanged((Editable) text);
    }
    ...
}
複製程式碼

在對持有字串進行新的賦值後會呼叫剩餘2個回撥方法,如果設定了 TextWatcher ,那麼needEditableForNotification 的值就為true,意味著只要對 TextView 設定了輸入監聽,那麼 afterTextChanged() 一定能夠被回撥. 呼叫回撥方法的詳細操作就不列出,與 beforeTextChanged 一樣,遍歷所有已設定的 TextWatcher 之後迴圈呼叫

Editable

前面已經說過觸發回撥的方式的其中一個方式: 直接對 afterTextChanged(Editable s) 回撥方法返回的Editable 物件進行操作

Editable是一種特殊的字串,對其做任何操作,其記憶體地址不會發生變化,始終指向原記憶體地址,不同於常規 String 型別每次賦值都會在記憶體中開闢新的記憶體進行儲存.

下面以append()方法為例簡要概括修改Editable觸發監聽的流程

  1. 呼叫 Editable 物件的 append() 方法進行字串拼接.
  2. 獲取自身繫結的 TextWatcher
  3. 類比 setText() 方法的回撥觸發流程,在對應的時機觸發回撥

當然關鍵在於第二步 >>>>>> 獲取自身繫結的 TextWatcher

  1. setText() 時獲取 TextView 的內部類 ChangeWatcher 物件,並繫結給自身持有的字串物件上
  2. ChangeWatcher 實現了 TextWatcher 的回撥方法,並在回撥方法中呼叫 TextViewsendBeforeTextChanged 等方法進行遍歷呼叫.

下面通過原始碼進行流程分析

Editableappend() 方法解析

//TextView.java
...
if (mListeners != null && mListeners.size() != 0) {
    needEditableForNotification = true;
}

if (type == BufferType.EDITABLE || getKeyListener() != null
        || needEditableForNotification) {
    createEditorIfNeeded();
    mEditor.forgetUndoRedo();
    Editable t = mEditableFactory.newEditable(text);
    text = t;
    setFilters(t, mFilters);
    InputMethodManager imm = InputMethodManager.peekInstance();
    if (imm != null) imm.restartInput(this);
}
...
複製程式碼

這部分程式碼在 TextViewsetText() 方法中, 當我們需要監聽的控制元件是 EditText 時,在其構造方法內直接把type設定成了BufferType.EDITABLE ,當我們為其設定了監聽時, needEditableForNotification 的值也會被設定成true.

函式體內,我們重點關注Editable t = mEditableFactory.newEditable(text)

// Editable.Factory
public Editable newEditable(CharSequence source) {
     return new SpannableStringBuilder(source);
}
複製程式碼

通過工廠方法返回一個實現了Editable介面的物件SpannableStringBuilder,並且當我們進行append()操作時,經歷以下流程

// SpannableStringBuilder.java
public SpannableStringBuilder append(CharSequence text) {
    int length = length();
    return replace(length, length, text, 0, text.length());
}
...
public SpannableStringBuilder replace(final int start, final int end,
            CharSequence tb, int tbstart, int tbend) {
    ...
    TextWatcher[] textWatchers = getSpans(start, start + origLen, TextWatcher.class);
    sendBeforeTextChanged(textWatchers, start, origLen, newLen);
    ...
    sendTextChanged(textWatchers, start, origLen, newLen);
    sendAfterTextChanged(textWatchers);
    ...
}
複製程式碼

是不是很眼熟? 在setText()時的流程也十分相似, 獲取繫結的TextWatcher -> sendBeforeTextChanged() -> 字串修改 -> sendTextChanged() -> sendAfterTextChanged()

在對Editable物件進行呼叫相關方法修改而不是直接賦值時,將獲取與其繫結的TextWatcher並遍歷呼叫相關方法,那麼TextWatcher是何時與其繫結的呢?

Editable繫結TextWatcher

// TextView.setText
if (mChangeWatcher == null) mChangeWatcher = new ChangeWatcher();

sp.setSpan(mChangeWatcher, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE
        | (CHANGE_WATCHER_PRIORITY << Spanned.SPAN_PRIORITY_SHIFT));
複製程式碼

ChangeWatcherTextView的內部類,在 setText方法中獲取內部類例項並且設定給Editable物件

此時我們發現 Editable裡獲取的TextWatcher並不是 TextView裡面我們設定的監聽,而是獲取到了TextView的內部類例項,下面我們再看ChangeWatcher

private class ChangeWatcher implements TextWatcher, SpanWatcher {

    private CharSequence mBeforeText;

    public void beforeTextChanged(CharSequence buffer, int start,
                                  int before, int after) {
        ...  
        TextView.this.sendBeforeTextChanged(buffer, start, before, after);
    }

    public void onTextChanged(CharSequence buffer, int start, int before, int after) {
        ...
        TextView.this.handleTextChanged(buffer, start, before, after);

    }

    public void afterTextChanged(Editable buffer) {
        ...
        TextView.this.sendAfterTextChanged(buffer);
    }

}
複製程式碼

我們可以看出,ChangeWatcher 是實現了TextWatcher的三個方法,並且在方法體內分別呼叫了TextView的相關方法,而TextView中的方法又會遍歷TextWatcher 的list去分別呼叫這3個回撥

那麼Editable呼叫append()方法進行修改時的流程可以簡單理解為

  1. 呼叫replace()
  2. 獲取繫結TextWatcher
  3. 呼叫TextWatcher例項方法
  4. 其中的ChangeWatcher物件的方法會去呼叫TextView的方法
  5. 遍歷TextView的監聽器,觸發回撥

相關文章