Android SearchView源码解析

zsf141 8年前
   <p>SearchView是一个搜索框控件,样式也挺好看的。这次解析主要围绕<code>android.support.v7.widget</code>包下的SearchView(API >= 7),<code>android.widget.SearchView</code>支持API >= 11, 另外有个<code>android.support.v4.widget.SearchViewCompat</code>。</p>    <p><a href="https://simg.open-open.com/show/774ac32c4927e488c6932de0400191f7.gif"><img src="https://simg.open-open.com/show/774ac32c4927e488c6932de0400191f7.gif" alt="Android SearchView源码解析" width="300" height="46"></a></p>    <h2>1. 源码解析</h2>    <p>v7版本:23.2.1</p>    <h3>1.1 继承关系</h3>    <table>     <tbody>      <tr>       <td colspan="5">java.lang.Object</td>      </tr>      <tr>       <td>   ↳</td>       <td colspan="4">android.view.View</td>      </tr>      <tr>       <td> </td>       <td>   ↳</td>       <td colspan="3">android.view.ViewGroup</td>      </tr>      <tr>       <td> </td>       <td> </td>       <td>   ↳</td>       <td colspan="2">android.support.v7.widget.LinearLayoutCompat</td>      </tr>      <tr>       <td> </td>       <td> </td>       <td> </td>       <td>   ↳</td>       <td colspan="1">android.support.v7.widget.SearchView</td>      </tr>     </tbody>    </table>    <h3>1.2 主要组件</h3>    <pre>  <code class="language-java">    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;  </code></pre>    <p>看命名也能大概知道控件各自充当了什么角色了。</p>    <h3>1.3 构造方法和自定义</h3>    <p>接下来看构造方法<code>public SearchView(Context context, AttributeSet attrs, int defStyleAttr)</code>,<code>v7</code>的<code>SearchView</code>并不是用<code>TypedArray</code>而是使用<code>TintTypedArray</code>,看了源码发现<code>TintTypedArray</code>里有个:<code>private final TypedArray mWrapped;</code>所以主要还是<code>TypedArray</code>,不同点是<code>getDrawable(int index)</code>和新加的<code>getDrawableIfKnown(int index)</code>方法, 并在满足条件下会调用<code>AppCompatDrawableManager.get().getDrawable(mContext, resourceId)</code>。</p>    <p>为了能更好的自定义,<code>SearchView</code>的layout也是可以指定的,不过自定义的layout必须包括上面那些控件,同时id也是指定的, 不然后面会报错,因为<code>findViewById(id)</code>无法找到各自控件,然后调用控件方法的时候就。。。</p>    <p>构造方法最后是更新控件状态,<code>mIconifiedByDefault</code>默认是<code>true</code>的,<code>setIconifiedByDefault(boolean iconified)</code>改变值后也会执行如下方法:</p>    <pre>  <code class="language-java">    public void setIconifiedByDefault(boolean iconified) {          if (mIconifiedByDefault == iconified) return;          mIconifiedByDefault = iconified;          //更新组件          updateViewsVisibility(iconified);          updateQueryHint();      }</code></pre>    <p>所以<code>setIconifiedByDefault(false)</code>会让SearchView一直呈现展开状态,并且输入框内icon也会不显示。具体方法如下,该方法在<code>updateQueryHint()</code>中被调用:</p>    <pre>  <code class="language-java">    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;      }  </code></pre>    <h3>1.4 Listener</h3>    <p>然后,我们来看看<code>SearchView</code>里面有哪些Listener:</p>    <pre>  <code class="language-java">    //里面有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等</code></pre>    <p>我们看看OnQueryTextListener是怎样进行监听的:</p>    <ul>     <li>onQueryTextChange(String newText)</li>    </ul>    <pre>  <code class="language-java">    //在构造方法里添加了监听      mSearchSrcTextView.addTextChangedListener(mTextWatcher);</code></pre>    <p>然后在<code>mTextWatcher</code>的<code>onTextChanged()</code>方法里调用了SearchView的<code>onTextChanged(CharSequence newText)</code>方法, 也就是在这里进行了判断触发:</p>    <pre>  <code class="language-java">    private void onTextChanged(CharSequence newText) {          /**           * 省略代码,主要是更新组件           */            //当listener!=null和当文本不一样的时候会触发。          if (mOnQueryChangeListener != null && !TextUtils.equals(newText, mOldQueryText)) {              mOnQueryChangeListener.onQueryTextChange(newText.toString());          }            //省略代码      }  </code></pre>    <ul>     <li>onQueryTextSubmit(String query)</li>    </ul>    <pre>  <code class="language-java">    //同在构造方法里添加了监听      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();              }          }      }</code></pre>    <p>在if里加入<code>!mOnQueryChangeListener.onQueryTextSubmit(query.toString())</code>,这样做就可以让使用者自己决定是否完全自己处理, 灵活性也更高。</p>    <p>其他Listener差不多也是这样,那接下来看看其他的。</p>    <h3>1.5 CollapsibleActionView接口</h3>    <p>SearchView实现了CollapsibleActionView接口:onActionViewExpanded()和onActionViewCollapsed(),具体操作就是 设置键盘及控件,并使用全局变量<code>mExpandedInActionView</code>记录ActionView是否伸展。只有当SearchView作为MenuItem的时候 才会触发,如果是使用v7包的话,想要通过menu获取SearchView就需要使用MenuItemCompat类,具体可以看demo。</p>    <pre>  <code class="language-java">    MenuItemCompat.getActionView(android.view.MenuItem item);</code></pre>    <h3>1.6 状态的保存和恢复</h3>    <p>SearchView覆写了onSaveInstanceState()和onRestoreInstanceState(Parcelable state)用来保存和恢复状态,为什么要覆写呢? 因为需要额外保存<code>boolean mIconified</code>,为此还建了个内部静态类SavedState用来保存mIconified。</p>    <pre>  <code class="language-java">    //实现了Parcelable序列化      static class SavedState extends BaseSavedState {          boolean isIconified;            /**           * 省略其他代码           */      }  </code></pre>    <h3>1.7 关于Suggestions和Searchable</h3>    <p>如果你使用了Suggestions,而且没有setSearchableInfo,那么当你点击建议可选项的时候会log:</p>    <pre>  <code class="language-java">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)                    ......</code></pre>    <p>定位到第1620行:</p>    <pre>  <code class="language-java">    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;          }      }</code></pre>    <p>发现调用mSearchable的方法之前并没有检查mSearchable是否为null,其他地方是有判断的,由于做了catch所以不会crash, 也不影响使用,另外,如果setOnSuggestionListener:</p>    <pre>  <code class="language-java">    mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {          @Override          public boolean onQueryTextSubmit(String query) {              return false;          }            @Override          public boolean onQueryTextChange(String newText) {              return true; //返回true          }      });</code></pre>    <p>onSuggestionClick(int position) 返回 true 就不会执行<code>createIntentFromSuggestion(~)</code>, 也就不会log了,但这样,键盘的隐藏和可选项pop的dismiss也不会执行,需要自己处理,使用SearchView的<code>clearFocus()</code>方法就能达到同样的效果。</p>    <p>那既然是报null,那就设置Searchable吧,设置后是会startActivity的(执行完createIntentFromSuggestion(~)后就会执行)。 然后效果就是当你点击了可选项就会startActivity,看需求做选择吧。。</p>    <h3>1.8 语音搜索功能</h3>    <p>SearchView还有语音搜索功能(API >= 8),需要通过配置Searchable来开启,在xml配置文件中加入:</p>    <pre>  <code class="language-java">android:voiceSearchMode="showVoiceSearchButton|launchRecognizer"</code></pre>    <p><code>showVoiceSearchButton</code>显示语音搜索按钮,<code>launchRecognizer</code>表示要启动一个语音识别器来转换成文字传给指定的searchable activity。 有个全局变量<code>boolean mVoiceButtonEnabled</code>表示是否启用,在<code>setSearchableInfo(~)</code>方法里进行了设置:</p>    <pre>  <code class="language-java">mVoiceButtonEnabled = IS_AT_LEAST_FROYO && hasVoiceSearch();</code></pre>    <p>IS_AT_LEAST_FROYO是Build.VERSION.SDK_INT >= 8,为了确保正确性,我试了下,结果并没有显示语言搜索按钮, debug后发现在hasVoiceSearch()里:</p>    <pre>  <code class="language-java">    ResolveInfo ri = getContext().getPackageManager().resolveActivity(testIntent,              PackageManager.MATCH_DEFAULT_ONLY);      return ri != null;</code></pre>    <p>在这里并没有resolve到Activity,结果return false,mVoiceButtonEnabled也就变成false了。(┙>∧<)┙へ┻┻</p>    <p>终于知道为什么了,原来阉割版的系统都不会出现语音搜索按钮,华为/魅族/Genymotion试过都不行(没有试过全版本系统), AS自带模拟器可以(有Google服务),具体应该就是没有resolve到Google语音识别Activity。对语音识别有兴趣的同学可以搜索RecognizerIntent。</p>    <h3>1.9 AutoCompleteTextViewReflector</h3>    <p>v7包的SearchView使用了反射机制,通过反射拿到AutoCompleteTextView和InputMethodManager隐藏的方法。</p>    <pre>  <code class="language-java">    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);          }              }  </code></pre>    <h3>1.10 onMeasure 测量</h3>    <p>查看了下<code>onMeasure</code>,发现有个地方还是比较在意的。 当<code>isIconified()</code>返回<code>false</code>的时候,width的mode在最后都会被设置成<code>MeasureSpec.EXACTLY</code>。 在SearchView伸展收缩的时候,<code>onMeasure</code>会被执行多次,width根据其mode改变, 之后mode设置为EXACTLY再调用父类super方法进行测量。</p>    <p>设置为EXACTLY,这样父控件就能确切的决定view的大小,那为什么只对width而不对height进行设置呢?</p>    <p>通过查看默认的 <a href="/misc/goto?guid=4959672688581850404">layout</a>, 可以看到主要组件的layout_height的大多都是match_parent(对应EXACTLY模式),而layout_width基本都是wrap_content(对应AT_MOST模式)。 另外,不是只有伸展收缩的时候,<code>onMeasure</code>才会被执行, 点击语音搜索按钮/输入框获取焦点的时候/...也会执行。</p>    <pre>  <code class="language-java">    @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);      }  </code></pre>    <h2> </h2>    <h2>2. 展望未来</h2>    <p>在v7包的SearchView里,有一个声明并初始化了的变量,但并没有用到过:</p>    <pre>  <code class="language-java">    private final AppCompatDrawableManager mDrawableManager;        //在构造方法里初始化      mDrawableManager = AppCompatDrawableManager.get();</code></pre>    <p>或许后续版本会用到吧! 抱着好奇的心去看了<code>AppCompatDrawableManager</code>源码,但并没有注释说明这个类是干什么用的,看名字只知道是管理Drawable的。 既然这样,那就来看下<code>AppCompatDrawableManager</code>能干些什么吧。</p>    <p>一步一步来,先看看它初始化的时候干了些什么,查看<code>get()</code>方法:</p>    <pre>  <code class="language-java">    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());              }          }      }  </code></pre>    <p>从这里, 我们可以看出跟<code>Vector</code>(矢量)有关。</p>    <ul>     <li><a href="/misc/goto?guid=4959672688688285069">VectorDrawable</a> 能创建一个基于xml描述的矢量图;</li>     <li><a href="/misc/goto?guid=4959672688768031824">AnimatedVectorDrawable</a> 使用<code>ObjectAnimator</code>和<code>AnimatorSet</code>为VectorDrawable创建动画。</li>    </ul>    <p>然后我粗略的看了方法名,有几个关键词: <code>Tint</code>着色,<code>Cache</code>,……</p>    <p>有兴趣的同学可以搜下相关资料,这里就不再深入了。</p>    <p>如果我哪里分析错了,请大家及时纠正我,谢谢。:)</p>    <p> </p>