SearchView源码解析

原文地址:https://github.com/nukc/SearchViewAnalysis/blob/master/README.md

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

目录

  • 1. 源码解析

    • 1.1 继承关系
    • 1.2 主要组件
    • 1.3 构造方法和自定义
    • 1.4 Listener
    • 1.5 CollapsibleActionView接口
    • 1.6 状态的保存和恢复
    • 1.7 关于Suggestions和Searchable
    • 1.8 语音搜索功能
    • 1.9 AutoCompleteTextViewReflector
    • 1.10 onMeasure 测量
  • 2. 展望未来

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,……

有兴趣的同学可以搜下相关资料,这里就不再深入了。

时间: 2024-10-19 18:10:33

SearchView源码解析的相关文章

ChrisRenke/DrawerArrowDrawable源码解析

转载请注明出处http://blog.csdn.net/crazy__chen/article/details/46334843 源码下载地址http://download.csdn.net/detail/kangaroo835127729/8765757 这次解析的控件DrawerArrowDrawable是一款侧拉抽屉效果的控件,在很多应用上我们都可以看到(例如知乎),控件的github地址为https://github.com/ChrisRenke/DrawerArrowDrawable

五.jQuery源码解析之jQuery.extend(),jQuery.fn.extend()

给jQuery做过扩展或者制作过jQuery插件的人这两个方法东西可能不陌生.jQuery.extend([deep],target,object1,,object2...[objectN]) jQuery.fn.extend([deep],target,object1,,object2...[objectN])这两个属性都是用于合并两个或多个对象的属性到target对象.deep是布尔值,表示是否进行深度合并,默认是false,不执行深度合并.通过这种方式可以在jQuery或jQuery.fn

eclipse中导入jdk源码、SpringMVC注解@RequestParam、SpringMVC文件上传源码解析、ajax上传excel文件

eclipse中导入jdk源码:http://blog.csdn.net/evolly/article/details/18403321, http://www.codingwhy.com/view/799.html. ------------------------------- SpringMVC注解@RequestParam:http://825635381.iteye.com/blog/2196911. --------------------------- SpringMVC文件上传源

String源码解析(一)

本篇文章内的方法介绍,在方法的上面的注释讲解的很清楚,这里只阐述一些要点. Java中的String类的定义如下: 1 public final class String 2 implements java.io.Serializable, Comparable<String>, CharSequence { ...} 可以看到,String是final的,而且继承了Serializable.Comparable和CharSequence接口. 正是因为这个特性,字符串对象可以被共享,例如下面

Flume-ng源码解析之Channel组件

如果还没看过Flume-ng源码解析之启动流程,可以点击Flume-ng源码解析之启动流程 查看 1 接口介绍 组件的分析顺序是按照上一篇中启动顺序来分析的,首先是Channel,然后是Sink,最后是Source,在开始看组件源码之前我们先来看一下两个重要的接口,一个是LifecycleAware ,另一个是NamedComponent 1.1 LifecycleAware @[email protected] interface LifecycleAware {  public void s

Spring源码解析-applicationContext

Demo uml类图 ApplicationContext ApplicationListener 源码解析 主流程 obtainFreshBeanFactory prepareBeanFactory invokeBeanFactoryPostProcessors registerBeanPostProcessors registerListeners finishRefresh 总结 在已经有BeanFactory可以完成Ioc功能情况下,spring又提供了ApplicationContex

socketserver源码解析和协程版socketserver

来,贴上一段代码让你仰慕一下欧socketserver的魅力,看欧怎么完美实现多并发的魅力 client import socket ip_port = ('127.0.0.1',8009) sk = socket.socket() sk.connect(ip_port) sk.settimeout(5) while True: data = sk.recv(1024) print('receive:',data.decode()) inp = input('please input:') sk

Handler机制(四)---Handler源码解析

Handler的主要用途有两个:(1).在将来的某个时刻执行消息或一个runnable,(2)把消息发送到消息队列. 主要依靠post(Runnable).postAtTime(Runnable, long).postDelayed(Runnable, long).sendEmptyMessage(int).sendMessage(Message).sendMessageAtTime(Message).sendMessageDelayed(Message, long)这些方法来来完成消息调度.p

Android EventBus源码解析, 带你深入理解EventBus

上一篇带大家初步了解了EventBus的使用方式,详见:Android EventBus实战 没听过你就out了,本篇博客将解析EventBus的源码,相信能够让大家深入理解该框架的实现,也能解决很多在使用中的疑问:为什么可以这么做?为什么这么做不好呢? 1.概述 一般使用EventBus的组件类,类似下面这种方式: [java] view plain copy public class SampleComponent extends Fragment { @Override public vo