Android SearchView源碼解析

zsf141 8年前發布 | 27K 次閱讀 Android Android開發 移動開發

SearchView是一個搜索框控件,樣式也挺好看的。這次解析主要圍繞android.support.v7.widget包下的SearchView(API >= 7),android.widget.SearchView支持API >= 11, 另外有個android.support.v4.widget.SearchViewCompat

Android SearchView源碼解析

1. 源碼解析

v7版本:23.2.1

1.1 繼承關系

java.lang.Object
   ? android.view.View
     ? android.view.ViewGroup
       ? android.support.v7.widget.LinearLayoutCompat
         ? android.support.v7.widget.SearchView

1.2 主要組件

    private final SearchAutoComplete mSearchSrcTextView;
    private final View mSearchEditFrame;
    private final View mSearchPlate;
    private final View mSubmitArea;
    private final ImageView mSearchButton;
    private final ImageView mGoButton;
    private final ImageView mCloseButton;
    private final ImageView mVoiceButton;
    private final View mDropDownAnchor;
    private final ImageView mCollapsedIcon;

看命名也能大概知道控件各自充當了什么角色了。

1.3 構造方法和自定義

接下來看構造方法public SearchView(Context context, AttributeSet attrs, int defStyleAttr),v7SearchView并不是用TypedArray而是使用TintTypedArray,看了源碼發現TintTypedArray里有個:private final TypedArray mWrapped;所以主要還是TypedArray,不同點是getDrawable(int index)和新加的getDrawableIfKnown(int index)方法, 并在滿足條件下會調用AppCompatDrawableManager.get().getDrawable(mContext, resourceId)

為了能更好的自定義,SearchView的layout也是可以指定的,不過自定義的layout必須包括上面那些控件,同時id也是指定的, 不然后面會報錯,因為findViewById(id)無法找到各自控件,然后調用控件方法的時候就。。。

構造方法最后是更新控件狀態,mIconifiedByDefault默認是true的,setIconifiedByDefault(boolean iconified)改變值后也會執行如下方法:

    public void setIconifiedByDefault(boolean iconified) {
        if (mIconifiedByDefault == iconified) return;
        mIconifiedByDefault = iconified;
        //更新組件
        updateViewsVisibility(iconified);
        updateQueryHint();
    }

所以setIconifiedByDefault(false)會讓SearchView一直呈現展開狀態,并且輸入框內icon也會不顯示。具體方法如下,該方法在updateQueryHint()中被調用:

    private CharSequence getDecoratedHint(CharSequence hintText) {
        //如果mIconifiedByDefault為false或者mSearchHintIcon為null
        //將不會添加搜索icon到提示hint中
        if (!mIconifiedByDefault || mSearchHintIcon == null) {
            return hintText;
        }

        final int textSize = (int) (mSearchSrcTextView.getTextSize() * 1.25);
        mSearchHintIcon.setBounds(0, 0, textSize, textSize);

        final SpannableStringBuilder ssb = new SpannableStringBuilder("   ");
        ssb.setSpan(new ImageSpan(mSearchHintIcon), 1, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        ssb.append(hintText);
        return ssb;
    }

1.4 Listener

然后,我們來看看SearchView里面有哪些Listener:

    //里面有2個方法:
        //onQueryTextSubmit(String query):當用戶提交查詢的時候會調用
        //onQueryTextChange(String newText):當查詢文字改變的時候會調用
    private OnQueryTextListener mOnQueryChangeListener;

    //里面有1個方法:boolean onClose();
        //onClose():當mCloseButton被點擊和setIconified(true)會判斷是否調用
        //是否調用是在onCloseClicked()里判斷,后面會進行分析 
    private OnCloseListener mOnCloseListener;

    //View類里定義的接口
    private OnFocusChangeListener mOnQueryTextFocusChangeListener;

    //里面有2個方法:
        //onSuggestionSelect(int position):選擇建議可選項(搜索框下方出現的)后觸發
        //onSuggestionClick(int position):點擊建議可選項后觸發
    private OnSuggestionListener mOnSuggestionListener;

    //View類里定義的接口
    private OnClickListener mOnSearchClickListener;

    //還有其他mOnClickListener,mTextKeyListener等

我們看看OnQueryTextListener是怎樣進行監聽的:

  • onQueryTextChange(String newText)
    //在構造方法里添加了監聽
    mSearchSrcTextView.addTextChangedListener(mTextWatcher);

然后在mTextWatcheronTextChanged()方法里調用了SearchView的onTextChanged(CharSequence newText)方法, 也就是在這里進行了判斷觸發:

    private void onTextChanged(CharSequence newText) {
        /**
         * 省略代碼,主要是更新組件
         */

        //當listener!=null和當文本不一樣的時候會觸發。
        if (mOnQueryChangeListener != null && !TextUtils.equals(newText, mOldQueryText)) {
            mOnQueryChangeListener.onQueryTextChange(newText.toString());
        }

        //省略代碼
    }
  • onQueryTextSubmit(String query)
    //同在構造方法里添加了監聽
    mSearchSrcTextView.setOnEditorActionListener(mOnEditorActionListener);

    private final OnEditorActionListener mOnEditorActionListener = new OnEditorActionListener() {

        /**
         * Called when the input method default action key is pressed.
         */
        public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
            onSubmitQuery();
            return true;
        }
    };

    private void onSubmitQuery() {
        CharSequence query = mSearchSrcTextView.getText();
        if (query != null && TextUtils.getTrimmedLength(query) > 0) {

            //當監聽OnQueryChangeListener了之后,
            //當onQueryTextSubmit() return true的話,是不會執行下面操作的
            if (mOnQueryChangeListener == null
                    || !mOnQueryChangeListener.onQueryTextSubmit(query.toString())) {

                //設置了Searchable后,會startActivity到配置指定的Activity    
                if (mSearchable != null) {
                    launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, query.toString());
                }
                //設置鍵盤是否顯示
                setImeVisibility(false); 

                //下拉可選項是用ListPopupWindow顯示的,具體可看 AutoCompleteTextView 源碼
                //搜索提交后,dismiss后就不會繼續顯示而擋住內容什么的
                dismissSuggestions();
            }
        }
    }

在if里加入!mOnQueryChangeListener.onQueryTextSubmit(query.toString()),這樣做就可以讓使用者自己決定是否完全自己處理, 靈活性也更高。

其他Listener差不多也是這樣,那接下來看看其他的。

1.5 CollapsibleActionView接口

SearchView實現了CollapsibleActionView接口:onActionViewExpanded()和onActionViewCollapsed(),具體操作就是 設置鍵盤及控件,并使用全局變量mExpandedInActionView記錄ActionView是否伸展。只有當SearchView作為MenuItem的時候 才會觸發,如果是使用v7包的話,想要通過menu獲取SearchView就需要使用MenuItemCompat類,具體可以看demo。

    MenuItemCompat.getActionView(android.view.MenuItem item);

1.6 狀態的保存和恢復

SearchView覆寫了onSaveInstanceState()和onRestoreInstanceState(Parcelable state)用來保存和恢復狀態,為什么要覆寫呢? 因為需要額外保存boolean mIconified,為此還建了個內部靜態類SavedState用來保存mIconified。

    //實現了Parcelable序列化
    static class SavedState extends BaseSavedState {
        boolean isIconified;

        /**
         * 省略其他代碼
         */
    }

1.7 關于Suggestions和Searchable

如果你使用了Suggestions,而且沒有setSearchableInfo,那么當你點擊建議可選項的時候會log:

W/SearchView: Search suggestions cursor at row 0 returned exception.
              java.lang.NullPointerException
                  at android.support.v7.widget.SearchView.createIntentFromSuggestion(SearchView.java:1620)
                  at android.support.v7.widget.SearchView.launchSuggestion(SearchView.java:1436)
                  at android.support.v7.widget.SearchView.onItemClicked(SearchView.java:1349)
                  at android.support.v7.widget.SearchView.access$1800(SearchView.java:103)
                  at android.support.v7.widget.SearchView$10.onItemClick(SearchView.java:1373)
                  ......

定位到第1620行:

    private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) {
        try {

            // use specific action if supplied, or default action if supplied, or fixed default
            String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION);

            //在這里并沒有檢查mSearchable是否為null
            if (action == null && Build.VERSION.SDK_INT >= 8) {
                action = mSearchable.getSuggestIntentAction();  //第1620行
            }

            /**
             *省略部分代碼
             */

            return createIntent(action, dataUri, extraData, query, actionKey, actionMsg);
        } catch (RuntimeException e ) {

            /**
             *省略部分代碼
             */

            Log.w(LOG_TAG, "Search suggestions cursor at row " + rowNum +
                                    " returned exception.", e);
            return null;
        }
    }

發現調用mSearchable的方法之前并沒有檢查mSearchable是否為null,其他地方是有判斷的,由于做了catch所以不會crash, 也不影響使用,另外,如果setOnSuggestionListener:

    mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
        @Override
        public boolean onQueryTextSubmit(String query) {
            return false;
        }

        @Override
        public boolean onQueryTextChange(String newText) {
            return true; //返回true
        }
    });

onSuggestionClick(int position) 返回 true 就不會執行createIntentFromSuggestion(~), 也就不會log了,但這樣,鍵盤的隱藏和可選項pop的dismiss也不會執行,需要自己處理,使用SearchView的clearFocus()方法就能達到同樣的效果。

那既然是報null,那就設置Searchable吧,設置后是會startActivity的(執行完createIntentFromSuggestion(~)后就會執行)。 然后效果就是當你點擊了可選項就會startActivity,看需求做選擇吧。。

1.8 語音搜索功能

SearchView還有語音搜索功能(API >= 8),需要通過配置Searchable來開啟,在xml配置文件中加入:

android:voiceSearchMode="showVoiceSearchButton|launchRecognizer"

showVoiceSearchButton顯示語音搜索按鈕,launchRecognizer表示要啟動一個語音識別器來轉換成文字傳給指定的searchable activity。 有個全局變量boolean mVoiceButtonEnabled表示是否啟用,在setSearchableInfo(~)方法里進行了設置:

mVoiceButtonEnabled = IS_AT_LEAST_FROYO && hasVoiceSearch();

IS_AT_LEAST_FROYO是Build.VERSION.SDK_INT >= 8,為了確保正確性,我試了下,結果并沒有顯示語言搜索按鈕, debug后發現在hasVoiceSearch()里:

    ResolveInfo ri = getContext().getPackageManager().resolveActivity(testIntent,
            PackageManager.MATCH_DEFAULT_ONLY);
    return ri != null;

在這里并沒有resolve到Activity,結果return false,mVoiceButtonEnabled也就變成false了。(┙>∧<)┙へ┻┻

終于知道為什么了,原來閹割版的系統都不會出現語音搜索按鈕,華為/魅族/Genymotion試過都不行(沒有試過全版本系統), AS自帶模擬器可以(有Google服務),具體應該就是沒有resolve到Google語音識別Activity。對語音識別有興趣的同學可以搜索RecognizerIntent。

1.9 AutoCompleteTextViewReflector

v7包的SearchView使用了反射機制,通過反射拿到AutoCompleteTextView和InputMethodManager隱藏的方法。

    static final AutoCompleteTextViewReflector HIDDEN_METHOD_INVOKER = new AutoCompleteTextViewReflector();

    private static class AutoCompleteTextViewReflector {
        private Method doBeforeTextChanged, doAfterTextChanged;
        private Method ensureImeVisible;
        private Method showSoftInputUnchecked;

        AutoCompleteTextViewReflector() {

            /**
             * 省略部分代碼
             */

            try {
                showSoftInputUnchecked = InputMethodManager.class.getMethod(
                        "showSoftInputUnchecked", int.class, ResultReceiver.class);
                showSoftInputUnchecked.setAccessible(true);
            } catch (NoSuchMethodException e) {
                // Ah well.
            }
        }    

        /**
         * 省略部分代碼
         */        

        void showSoftInputUnchecked(InputMethodManager imm, View view, int flags) {
            if (showSoftInputUnchecked != null) {
                try {
                    showSoftInputUnchecked.invoke(imm, flags, null);
                    return;
                } catch (Exception e) {
                }
            }

            //只有這個方法才有在if后面做處理
            // Hidden method failed, call public version instead
            imm.showSoftInput(view, flags);
        }        
    }

1.10 onMeasure 測量

查看了下onMeasure,發現有個地方還是比較在意的。 當isIconified()返回false的時候,width的mode在最后都會被設置成MeasureSpec.EXACTLY。 在SearchView伸展收縮的時候,onMeasure會被執行多次,width根據其mode改變, 之后mode設置為EXACTLY再調用父類super方法進行測量。

設置為EXACTLY,這樣父控件就能確切的決定view的大小,那為什么只對width而不對height進行設置呢?

通過查看默認的 layout, 可以看到主要組件的layout_height的大多都是match_parent(對應EXACTLY模式),而layout_width基本都是wrap_content(對應AT_MOST模式)。 另外,不是只有伸展收縮的時候,onMeasure才會被執行, 點擊語音搜索按鈕/輸入框獲取焦點的時候/...也會執行。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // Let the standard measurements take effect in iconified state.
        if (isIconified()) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            return;
        }

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);

        switch (widthMode) {
            case MeasureSpec.AT_MOST:
                // If there is an upper limit, don't exceed maximum width (explicit or implicit)
                if (mMaxWidth > 0) {
                    width = Math.min(mMaxWidth, width);
                } else {
                    width = Math.min(getPreferredWidth(), width);
                }
                break;
            case MeasureSpec.EXACTLY:
                // If an exact width is specified, still don't exceed any specified maximum width
                if (mMaxWidth > 0) {
                    width = Math.min(mMaxWidth, width);
                }
                break;
            case MeasureSpec.UNSPECIFIED:
                // Use maximum width, if specified, else preferred width
                width = mMaxWidth > 0 ? mMaxWidth : getPreferredWidth();
                break;
        }
        widthMode = MeasureSpec.EXACTLY;
        super.onMeasure(MeasureSpec.makeMeasureSpec(width, widthMode), heightMeasureSpec);
    }

 

2. 展望未來

在v7包的SearchView里,有一個聲明并初始化了的變量,但并沒有用到過:

    private final AppCompatDrawableManager mDrawableManager;

    //在構造方法里初始化
    mDrawableManager = AppCompatDrawableManager.get();

或許后續版本會用到吧! 抱著好奇的心去看了AppCompatDrawableManager源碼,但并沒有注釋說明這個類是干什么用的,看名字只知道是管理Drawable的。 既然這樣,那就來看下AppCompatDrawableManager能干些什么吧。

一步一步來,先看看它初始化的時候干了些什么,查看get()方法:

    public static AppCompatDrawableManager get() {
        //使用了懶漢式
        if (INSTANCE == null) {
            INSTANCE = new AppCompatDrawableManager();
            installDefaultInflateDelegates(INSTANCE);
        }
        return INSTANCE;
    }


    private static void installDefaultInflateDelegates(@NonNull AppCompatDrawableManager manager) {
        final int sdk = Build.VERSION.SDK_INT;
        // 只在Android 5.0以下的系統
        if (sdk < 21) {
            // 在需要的時候使用 VectorDrawableCompat 進行自動處理
            manager.addDelegate("vector", new VdcInflateDelegate());

            if (sdk >= 11) {
                // AnimatedVectorDrawableCompat 只能在 API v11+ 使用
                manager.addDelegate("animated-vector", new AvdcInflateDelegate());
            }
        }
    }

從這里, 我們可以看出跟Vector(矢量)有關。

然后我粗略的看了方法名,有幾個關鍵詞: Tint著色,Cache,……

有興趣的同學可以搜下相關資料,這里就不再深入了。

如果我哪里分析錯了,請大家及時糾正我,謝謝。:)

 

 本文由用戶 zsf141 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!