Android SearchView源碼解析
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
,……
有興趣的同學可以搜下相關資料,這里就不再深入了。
如果我哪里分析錯了,請大家及時糾正我,謝謝。:)