SearchView是一个搜索框控件,样式也挺好看的。这次解析主要围绕android.support.v7.widget
包下的SearchView(API >= 7),android.widget.SearchView
支持API >= 11, 另外有个android.support.v4.widget.SearchViewCompat
。
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)
,v7
的SearchView
并不是用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);
然后在mTextWatcher
的onTextChanged()
方法里调用了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
(矢量)有关。
VectorDrawable 能创建一个基于xml描述的矢量图;
AnimatedVectorDrawable 使用
ObjectAnimator
和AnimatorSet
为VectorDrawable创建动画。
然后我粗略的看了方法名,有几个关键词: Tint
着色,Cache
,……
有兴趣的同学可以搜下相关资料,这里就不再深入了。