InputFilterとTextWatcher

17460 ワード

Androidの成長は蓄積と共有にある
原文:https://www.jianshu.com/p/223dcbac3e4d
本文は参考博編集長が書いたオリジナルの文章で、原文を参考にします.https://blog.csdn.net/u014606081/article/details/53101629
文書ディレクトリ
  • UCSモバイル端末技術共有
  • ガイド
  • 前言
  • InputFilter
  • TextWatcher
  • ケース一:setText()
  • ケース2:キーボード入力
  • まとめ
  • 感謝
  • UCSモバイル端末技術の共有
    技術形式の1種の分かち合いで、書く過程で他の人のブログを参考にすることを発見すると同時に、確かに彼が書いたもっと完璧な点を発見して、だから彼のブログの基礎の上で一部の自分の理解するものを追加しました.悪いところがあれば、一緒に修正して勉強してほしいです.
    リード
    InputFilterソースコード解析、TextWatcherソースコード解析
    前言
    AndroidではInputFilterとTextWatcherの機能と役割が非常に似ており、EditText入力コンテンツの傍受と制御が可能です.その両者は具体的にどのような違いがあり、入力内容の傍受をどのように実現しているのか.次に、ソースコードの観点から分析します.
    ソースコードを分析する前に、EditTextはTextViewから継承され、90%の機能はTextViewと一致し、4つのプライベートメソッドしかなく、残りの8つはTextViewを書き換える方法です.だからEditTextの大部分の機能はTextViewで完成して、具体的な論理もTextViewの中にあります.
    InputFilter
    InputFilterにはメソッドfilter()が1つしかありません.戻り値はCharSequenceで、フィルタまたは入力/挿入のための文字列です.戻り値がnullでない場合、戻り結果を使用して元の入力/挿入の文字列を置き換えます.
    package android.text;
    
    /**
     * InputFilters can be attached to {@link Editable}s to constrain the
     * changes that can be made to them.
     */
    public interface InputFilter {
    
            /**
             * @param source          ,      、  
             * @param start  source     , 0(            )  -0,  -0
             * @param end  source   :   -     ,  -0
             * @param dest  EditText         ,       
             * @param dstart        :  -     ,  -        
             * @param dend    -     ,  -        
            */
           public CharSequence filter(CharSequence source, int start, int end,
                                   Spanned dest, int dstart, int dend);
           ... ...
    }
    
    

    TextViewクラスのsetText()メソッドでは、filter()メソッドが呼び出され、フィルタされた文字列が得られます.一部のソースコードは次のとおりです.
    注意:*setText()*メソッドにはリロードメソッドがたくさんありますが、最終的には次のように呼び出されます.この方法は重要で、肝心な論理の注釈を見るだけです.
    /**
     * @param text         
     * @param type        (static text, styleable/spannable text, or editable text)
     * @param notifyBefore       TextWacther before
     * @param oldlen         
    */
    private void setText(CharSequence text, BufferType type,
                             boolean notifyBefore, int oldlen) {
            mTextFromResource = false;
            if (text == null) {
                text = "";
            }
    
            ... ...
    
            //   InputFilter  text
            int n = mFilters.length;
            for (int i = 0; i < n; i++) {
                CharSequence out = mFilters[i].filter(text, 0, text.length(), EMPTY_SPANNED, 0, 0);
                if (out != null) {
                    text = out;
                }
            }
    
            ... ...
    
            //    EditText, new  Editable
            if (type == BufferType.EDITABLE || getKeyListener() != null ||
                    needEditableForNotification) {
                createEditorIfNeeded();
                Editable t = mEditableFactory.newEditable(text);
                text = t;
                setFilters(t, mFilters); // filter          
                InputMethodManager imm = InputMethodManager.peekInstance();
                if (imm != null) imm.restartInput(this);
            } else if (type == BufferType.SPANNABLE || mMovement != null) {
                text = mSpannableFactory.newSpannable(text);
            } else if (!(text instanceof CharWrapper)) {
                text = TextUtils.stringOrSpannedString(text);
            }
    
            ...   ...
        }
    

    mFiltersは、1つ以上のフィルタがあるため、InputFilter配列です.forサイクルによりtextをすべてのフィルタ条件ですべてフィルタリングし、最終的に**の「合格」**のtextを得る.
    setTextメソッドに加えて、キーボード入力中もフィルタリングされ、TextViewのsetFiltersメソッドはEditableにfiltersを設定し、Editableのreplaceメソッドでフィルタリングされます.
    private void setFilters(Editable e, InputFilter[] filters) {
            if (mEditor != null) {
                final boolean undoFilter = mEditor.mUndoInputFilter != null;
                final boolean keyFilter = mEditor.mKeyListener instanceof InputFilter;
                int num = 0;
                if (undoFilter) num++;
                if (keyFilter) num++;
                if (num > 0) {
                    InputFilter[] nf = new InputFilter[filters.length + num];
    
                    System.arraycopy(filters, 0, nf, 0, filters.length);
                    num = 0;
                    if (undoFilter) {
                        nf[filters.length] = mEditor.mUndoInputFilter;
                        num++;
                    }
                    if (keyFilter) {
                        nf[filters.length + num] = (InputFilter) mEditor.mKeyListener;
                    }
    
                    e.setFilters(nf);
                    return;
                }
            }
            e.setFilters(filters);
    

    例えばSpannablesStringBuilderにおけるreplace法の実装.
     // Documentation from interface
        public SpannableStringBuilder replace(final int start, final int end,
                CharSequence tb, int tbstart, int tbend) {
            checkRange("replace", start, end);
    
            int filtercount = mFilters.length;
            for (int i = 0; i < filtercount; i++) {
                CharSequence repl = mFilters[i].filter(tb, tbstart, tbend, this, start, end);
    
                if (repl != null) {
                    tb = repl;
                    tbstart = 0;
                    tbend = repl.length();
                }
            }
    
         ...  ...
    }
    

    InputFilterがどのように機能するかを知ったら、あとは**filter()メソッドの各パラメータの意味を明らかにし、自分に必要なInputFilterを書くことです.
    SDKは、AllCapsとLengthFilterの2つの実装を提供しています.次に、InputFilterの使用方法をLengthFilterで解読します.ソースコードのセグメントは次のとおりです.
    public static class LengthFilter implements InputFilter {
    
            private final int mMax;
    
            public LengthFilter(int max) {
                mMax = max;
            }
    
            //  source:        ,      、  
            //  start:source     , 0(            )  -0,  -0
            //  end:source   :   -     ,  -0
            //  dest:EditText         ,       
            //  dstart:      :  -     ,  -        
            //  dend:  -     ,  -        
            public CharSequence filter(CharSequence source, int start, int end, Spanned dest,
                                       int dstart, int dend) {
                int keep = mMax - (dest.length() - (dend - dstart));
                if (keep <= 0) {
                    //         ,   “”
                    return "";
                } else if (keep >= end - start) {
                    //         ,   null(      null,TextView       source)
                    return null; // keep original
                } else {
                    keep += start;
                    if (Character.isHighSurrogate(source.charAt(keep - 1))) {
                        //          HighSurrogate(   , 2    ),  kepp 1,         
                        --keep;
                        if (keep == start) {
                            return "";
                        }
                    }
                    return source.subSequence(start, keep);
                }
            }
    
            /**
             * @return the maximum length enforced by this input filter
             */
            public int getMax() {
                return mMax;
            }
        }
    

    TextWatcher
    クラスはその名の通りTextの入力削除などの変化を観察するために用いられる.
    package android.text;
    
    /**
     * When an object of a type is attached to an Editable, its methods will
     * be called when the text is changed.
     */
    public interface TextWatcher extends NoCopySpan {
        /**
         * @param s   
         * @param start           
         * @param count         
         * @param after         
         */
        public void beforeTextChanged(CharSequence s, int start,
                                      int count, int after);
        /**
         * @param s         
         * @param start            
         * @param before         
         * @param count          
         */
        public void onTextChanged(CharSequence s, int start, int before, int count);
    
        /**
         * @param s         ( s       TextWatcher)
         */
        public void afterTextChanged(Editable s);
    }
    
    

    TextViewの内容を変更してリスナーに通知する場合は2つあります.1つ目はsetText()メソッド、2つ目はキーボードから入力します.どちらもsendBeforeTextChangedメソッドを呼び出して通知を送信します(例beforeTextChanged).
    private void sendBeforeTextChanged(CharSequence text, int start, int before, int after) {
            if (mListeners != null) {
                final ArrayList list = mListeners;
                final int count = list.size();
                for (int i = 0; i < count; i++) {
                    list.get(i).beforeTextChanged(text, start, before, after);
                }
            }
    
            // The spans that are inside or intersect the modified region no longer make sense
            removeIntersectingNonAdjacentSpans(start, start + before, SpellCheckSpan.class);
            removeIntersectingNonAdjacentSpans(start, start + before, SuggestionSpan.class);
        }
    

    まず、Googleのデザインコンセプトを紹介します.正直に言うと、原文がどこから出たのか分かりません.これはある大物のブログから取ったものです.
    Googleの**に対する「文字列を変える」というデザインコンセプトは「置換」**です.削除内容であれば、削除する文字列を空の文字列で置き換えます.コンテンツを追加する場合は、空の文字列を新しい文字列で置き換えます.まず、次の概念を明らかにします.
  • 元内容:変更前のTextViewの内容;
  • 置換コンテンツ起点座標:一部のコンテンツを編集する場合、直接新しいコンテンツを追加するか、既存のコンテンツを選択して、新しいコンテンツで置換する可能性があります.
  • 置換されたコンテンツの長さ:直接新しいコンテンツを追加する場合、置換されたコンテンツの長さは0である.
  • 新たに追加する内容:setText()にとってはメソッド中のパラメータであり、キーボード入力にとってはキーボード入力の内容
  • である.
    この2つの状況を分析します.
    ケース1:setText()
    同様に,まずTextViewにおける**setText()メソッドによる関連イベントの処理を見る.
    /**
     * @param text         
     * @param type        (static text, styleable/spannable text, or editable text)
     * @param notifyBefore       TextWacther before
     * @param oldlen         
    */
    private void setText(CharSequence text, BufferType type,
                             boolean notifyBefore, int oldlen) {
            mTextFromResource = false;
            if (text == null) {
                text = "";
            }
    
            ... ...
    
            //    ,       TextChange
            int n = mFilters.length;
            for (int i = 0; i < n; i++) {
                CharSequence out = mFilters[i].filter(text, 0, text.length(), EMPTY_SPANNED, 0, 0);
                if (out != null) {
                    text = out;
                }
            }
    
            // char[]        sendBeforeTextChanged,   notifyBefore  false
            if (notifyBefore) {
                //     TextWatcher beforeTextChanged()  
                if (mText != null) {
                    oldlen = mText.length();
                    sendBeforeTextChanged(mText, 0, oldlen, text.length());
                } else {
                    sendBeforeTextChanged("", 0, 0, text.length());
                }
            }
    
            boolean needEditableForNotification = false;
    
            if (mListeners != null && mListeners.size() != 0) {
                needEditableForNotification = true;
            }
    
            if (type == BufferType.EDITABLE || getKeyListener() != null
                    || needEditableForNotification) {
                createEditorIfNeeded();
                mEditor.forgetUndoRedo();
                Editable t = mEditableFactory.newEditable(text); //   Editable,             
                text = t;
                setFilters(t, mFilters);
                InputMethodManager imm = InputMethodManager.peekInstance();
                if (imm != null) imm.restartInput(this);
            } else if (type == BufferType.SPANNABLE || mMovement != null) {
                text = mSpannableFactory.newSpannable(text);
            } else if (!(text instanceof CharWrapper)) {
                text = TextUtils.stringOrSpannedString(text);
            }
    
            ... ...
    
            if (text instanceof Spannable && !mAllowTransformationLengthChange) {
                Spannable sp = (Spannable) text;
    
                // Remove any ChangeWatchers that might have come from other TextViews.
                final ChangeWatcher[] watchers = sp.getSpans(0, sp.length(), ChangeWatcher.class);
                final int count = watchers.length;
                for (int i = 0; i < count; i++) {
                    sp.removeSpan(watchers[i]);
                }
    
                if (mChangeWatcher == null) mChangeWatcher = new ChangeWatcher();
    
                //       TextWatcher  (     ChangeWatcher  )
                sp.setSpan(mChangeWatcher, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE
                        | (CHANGE_WATCHER_PRIORITY << Spanned.SPAN_PRIORITY_SHIFT));
    
                if (mEditor != null) mEditor.addSpanWatchers(sp);
    
                if (mTransformation != null) {
                    sp.setSpan(mTransformation, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
                }
    
            ... ...
    
            }
    
            ... ...
    
            //     TextWatcher onTextChanged()  
            sendOnTextChanged(text, 0, oldlen, textLength);
            onTextChanged(text, 0, oldlen, textLength);
    
            //   view  
            notifyViewAccessibilityStateChangedIfNeeded(AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT);
    
            //     TextWatcher afterTextChanged()  
            if (needEditableForNotification) {
                sendAfterTextChanged((Editable) text);
            } else {
                // Always notify AutoFillManager - it will return right away if autofill is disabled.
                notifyAutoFillManagerAfterTextChangedIfNeeded();
            }
    
            // SelectionModifierCursorController depends on textCanBeSelected, which depends on text
            if (mEditor != null) mEditor.prepareCursorControllers();
    
    }
    
    

    ここで、setTextメソッドを呼び出すと必ずTextWatcher関連イベントがトリガーされるので、TextWatcherの**onTextChanged()メソッドで文字をフィルタリングしてからsetText()メソッドを呼び出して文字列をリセットし、TextWatcherのonTextChanged()メソッドでsetText()メソッド(いくつかのつらいニーズ)を呼び出す必要がある場合は、デッドサイクルを防ぐことに注意してください.setText()メソッドはまたonTextChanged()メソッドをコールバックするので、デッドサイクルを形成します.
    ケース2:キーボード入力
    TextViewの構築方法では、android:text属性の値を取得し、setText()メソッドを呼び出して初期内容を設定します.ここで、BufferTypeのタイプを判断し、EditTextであればEditableを作成します(この論理は上のsetText()ソースコードを参照).
    最終的にnewはSpannablesStringBuilderオブジェクトを出し、SpannablesStringBuilderはEditable、Appendableインタフェースを実現した.Appendableは、新しいコンテンツ(キーボード入力から)を元のコンテンツに追加するためのインタフェース(3つのリロードがある):append()を提供します.そこでSpannablesStringBuilderに行ってappend()メソッドの具体的な実装を見てみましょう.
    3つのリロードインタフェースには、3つの具体的な実装がありますが、原理は同じで、最終的にはreplace()メソッドが呼び出されます.次に、append()実装の1つで分析します.
    //        :       ;           ,      ,            ;
    //             
    public SpannableStringBuilder append(CharSequence text, int start, int end) {
        // length        
        int length = length();
        //       replace()   “  ”  。       ,Google              “  ”,      ,           ,       ,            
        return replace(length, length, text, start, end);
    }
    
    public SpannableStringBuilder replace(final int start, final int end,
                CharSequence tb, int tbstart, int tbend) {
            checkRange("replace", start, end);
    
        //  setText()  ,           
        int filtercount = mFilters.length;
        for (int i = 0; i < filtercount; i++) {
            CharSequence repl = mFilters[i].filter(tb, tbstart, tbend, this, start, end);
    
            if (repl != null) {
                tb = repl;
                tbstart = 0;
                tbend = repl.length();
            }
        }
    
        //          ,end  start,  origLen  0
        final int origLen = end - start;
        //        
        final int newLen = tbend - tbstart;
    
        if (origLen == 0 && newLen == 0 && !hasNonExclusiveExclusiveSpanAt(tb, tbstart)) {
            return this;
        }
    
        TextWatcher[] textWatchers = getSpans(start, start + origLen, TextWatcher.class);
        //   TextWatcher  beforeTextChanged()  ,   TextView    ,       
        sendBeforeTextChanged(textWatchers, start, origLen, newLen);
    
        boolean adjustSelection = origLen != 0 && newLen != 0;
        int selectionStart = 0;
        int selectionEnd = 0;
        if (adjustSelection) {
            selectionStart = Selection.getSelectionStart(this);
            selectionEnd = Selection.getSelectionEnd(this);
        }
    
        change(start, end, tb, tbstart, tbend);
    
        if (adjustSelection) {
            if (selectionStart > start && selectionStart < end) {
                final int offset = (selectionStart - start) * newLen / origLen;
                selectionStart = start + offset;
    
                setSpan(false, Selection.SELECTION_START, selectionStart, selectionStart,
                            Spanned.SPAN_POINT_POINT);
                }
            if (selectionEnd > start && selectionEnd < end) {
                final int offset = (selectionEnd - start) * newLen / origLen;
                selectionEnd = start + offset;
    
                setSpan(false, Selection.SELECTION_END, selectionEnd, selectionEnd,
                            Spanned.SPAN_POINT_POINT);
            }
        }
    
        //   TextWatcher  onTextChanged()、afterTextChanged()  。    ,           ,   setText()      ,        
        sendTextChanged(textWatchers, start, origLen, newLen);
        sendAfterTextChanged(textWatchers);
    
        sendToSpanWatchers(start, end, newLen - origLen);
    
        return this;
    }
    

    解析により,以下のような結論が得られるだろう:(キーボード入力のソース分析でこの結論を確認できる)
    // s:   
    // start:         ,  setText()          ,     0
    // count:        ,  setText()          ,    mText.length()
    // after:        
    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) {
    }
    
    // s:        
    public void afterTextChanged(Editable s) {
    }
    

    まとめ
  • は、InputFilterを使用して文字列を制御、フィルタリングすることが望ましい.
  • できるだけTextWatcherの**onTextChanged()メソッドで文字をフィルタリングしないで、setText()メソッドを呼び出して文字列をリセットします.効率はInputFilterより明らかに低いです.
  • TextWatcherの**onTextChanged()メソッドでsetText()メソッドを呼び出す必要がある場合は、デッドサイクル防止に注意してください.setText()メソッドはまたonTextChanged()メソッドをコールバックするので、デッドサイクルになります.
  • TextWatcherの主な機能は傍受であり、Googleのこのクラスの命名から見ることができる.

  • に感謝
    thinkreduce原文リンク:https://blog.csdn.net/u014606081/article/details/53101629