如何在操作栏中构建类似搜索框的Gmail?

时间:2013-09-18 09:16:28

标签: android autocomplete android-actionbar searchview

我目前正在SearchView内使用ActionBarcompat小部件来搜索时过滤列表。

当用户开始输入文本时,主布局中的ListView会更新并使用适配器来过滤结果。我这样做是通过实现OnQueryTextListener并过滤每个键击的结果。

相反,我想创建一个类似Gmail的搜索框,其中包含生成的自动推荐列表,并且没有对基础视图进行任何更改

enter image description here

我已使用this tutorial使用SearchView组件,但需要可搜索的活动。我希望下拉菜单位于MainActivity上,我有ListView(就像在Gmail应用程序中一样),而不是专用的活动。
此外,以与教程相同的方式实现它似乎对我想要的东西(只是一个下拉列表)来说太过分了

7 个答案:

答案 0 :(得分:26)

如果您只想要一个能够解决问题所述内容的组件,我建议library。您也可以实现out-of-the-bx searchable interface,但请注意它确实有UI限制:

要实现类似于Gmail应用的界面,您必须了解以下内容:

  • 内容提供商;
  • 在SQLite中保留数据
  • Listview或RecyclerView及其适配器;
  • 在活动之间传递数据;

最终结果应如下所示:

final result

有很多(很多)方法可以得到相同的结果(或更好),我会描述一种可能的方式。

第01部分:布局

我决定在新的Activity中管理整个界面,因为我已经创建了三个XML布局:

  • custom_searchable.xml:汇集一个RelativeLayout中的所有UI元素,用作SearchActivity的内容;

    <include
        android:id="@+id/cs_header"
        layout="@layout/custom_searchable_header_layout" />
    
    <android.support.v7.widget.RecyclerView
        android:id="@+id/cs_result_list"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:stackFromBottom="true"
        android:transcriptMode="normal"/>
    

  • custom_searchable_header_layout.xml:保存用户将在其中键入查询的搜索栏。它还包含麦克风,擦除和返回btn;

    <RelativeLayout
        android:id="@+id/custombar_return_wrapper"
        android:layout_width="55dp"
        android:layout_height="fill_parent"
        android:gravity="center_vertical"
        android:background="@drawable/right_oval_ripple"
        android:focusable="true"
        android:clickable="true" >
    
        <ImageView
            android:id="@+id/custombar_return"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_centerHorizontal="true"
            android:background="#00000000"
            android:src="@drawable/arrow_left_icon"/>
    </RelativeLayout>
    
    <android.support.design.widget.TextInputLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_toRightOf="@+id/custombar_return_wrapper"
        android:layout_marginRight="60dp"
        android:layout_marginLeft="10dp"
        android:layout_marginTop="10dp"
        android:layout_marginBottom="10dp">
    
        <EditText
            android:id="@+id/custombar_text"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:hint="Search..."
            android:textColor="@color/textPrimaryColor"
            android:singleLine="true"
            android:imeOptions="actionSearch"
            android:background="#00000000">
            <requestFocus/>
        </EditText>
    
    </android.support.design.widget.TextInputLayout>
    
    <RelativeLayout
        android:id="@+id/custombar_mic_wrapper"
        android:layout_width="55dp"
        android:layout_height="fill_parent"
        android:layout_alignParentRight="true"
        android:gravity="center_vertical"
        android:background="@drawable/left_oval_ripple"
        android:focusable="true"
        android:clickable="true" >
    
        <ImageView
            android:id="@+id/custombar_mic"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_centerHorizontal="true"
            android:background="#00000000"
            android:src="@drawable/mic_icon"/>
    </RelativeLayout>
    

  • custom_searchable_row_details.xml:保存要显示在结果列表中的UI元素,以响应用户查询而显示;

    <ImageView
        android:id="@+id/rd_left_icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="3dp"
        android:layout_centerVertical="true"
        android:layout_marginLeft="5dp"
        android:src="@drawable/clock_icon" />
    
    <LinearLayout
        android:id="@+id/rd_wrapper"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center_vertical"
        android:layout_toRightOf="@+id/rd_left_icon"
        android:layout_marginLeft="20dp"
        android:layout_marginRight="50dp">
    
        <TextView
            android:id="@+id/rd_header_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="@color/textPrimaryColor"
            android:text="Header"
            android:textSize="16dp"
            android:textStyle="bold"
            android:maxLines="1"/>
    
        <TextView
            android:id="@+id/rd_sub_header_text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textColor="@color/textPrimaryColor"
            android:text="Sub Header"
            android:textSize="14dp"
            android:maxLines="1" />
    </LinearLayout>
    
    <ImageView
        android:id="@+id/rd_right_icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="3dp"
        android:layout_alignParentRight="true"
        android:layout_centerVertical="true"
        android:src="@drawable/arrow_left_up_icon"/>
    

第02部分:实施SearchActivity

这个想法是,当用户键入搜索按钮(您可以将其放置在任何您想要的位置)时,将调用此SearchActivity。它有一些主要的责任:

  • 绑定到custom_searchable_header_layout.xml中的UI元素:通过这样做,可以:

  • 为EditText提供侦听器(用户将在其中键入其查询):

    TextView.OnEditorActionListener searchListener = new TextView.OnEditorActionListener() {
    public boolean onEditorAction(TextView exampleView, int actionId, KeyEvent event) {
        // do processing
       }
    }
    
    searchInput.setOnEditorActionListener(searchListener);
    
    searchInput.addTextChangedListener(new TextWatcher() {        
    public void onTextChanged(final CharSequence s, int start, int before, int count) {
         // Do processing
       }
    }
    
  • 为返回按钮添加侦听器(依次调用finish()并返回调用者活动):

    this.dismissDialog.setOnClickListener(new View.OnClickListener() {
        public void onClick(View v) {
        finish();
    }    
    
  • 调用谷歌语音转文本API的意图:

        private void implementVoiceInputListener () {
            this.voiceInput.setOnClickListener(new View.OnClickListener() {
    
                public void onClick(View v) {
                    if (micIcon.isSelected()) {
                        searchInput.setText("");
                        query = "";
                        micIcon.setSelected(Boolean.FALSE);
                        micIcon.setImageResource(R.drawable.mic_icon);
                    } else {
                        Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
    
                        intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
                        intent.putExtra(RecognizerIntent.EXTRA_PROMPT, "Speak now");
    
                        SearchActivity.this.startActivityForResult(intent, VOICE_RECOGNITION_CODE);
                    }
                }
            });
        }
    

内容提供商

在构建搜索界面时,开发人员有两个选择:

  1. 建议用户最近的查询:这意味着每次用户进行搜索时,键入的查询将保留在数据库中,以便在以后检索,作为将来搜索的建议;
  2. 为用户建议自定义选项:开发人员将尝试通过处理已输入的字母来预测用户想要的内容;
  3. 在这两种情况下,答案都应作为Cursor对象传回,该对象的内容在结果列表中显示为itens。可以使用Content Provider API实现整个过程。有关如何使用内容提供商的详细信息,请参阅此link

    如果开发人员想要实现1.中描述的行为,那么使用exventing SearchRecentSuggestionsProvider类的策略会很有用。有关如何操作的详细信息,请参阅此link

    实施搜索界面

    此接口应提供以下行为:

    • 当用户键入一个字母时,对检索到的内容提供者类的查询方法的调用应该返回一个填充的光标,并在列表中显示建议 - 你应该采取不冻结UI线程的方法,所以我建议在AsyncTask中执行此搜索:

          public void onTextChanged(final CharSequence s, int start, int before, int count) {
              if (!"".equals(searchInput.getText().toString())) {
                  query = searchInput.getText().toString();
      
                  setClearTextIcon();
      
                  if (isRecentSuggestionsProvider) {
                      // Provider is descendant of SearchRecentSuggestionsProvider
                      mapResultsFromRecentProviderToList(); // query is performed in this method
                  } else {
                      // Provider is custom and shall follow the contract
                      mapResultsFromCustomProviderToList(); // query is performed in this method
                  }
              } else {
                  setMicIcon();
              }
          }
      
    • 在AsyncTask的onPostExecute()方法中,您应该检索一个列表(应该来自doInBackground()方法),其中包含要在ResultList中显示的结果(您可以将它映射到POJO类中)并将其传递给您的自定义适配器,或者您可以使用CursorAdapter,这将是此任务的最佳实践):

      protected void onPostExecute(List resultList) {
           SearchAdapter adapter = new SearchAdapter(resultList);
           searchResultList.setAdapter(adapter);
      }
      
      protected List doInBackground(Void[] params) {
          Cursor results = results = queryCustomSuggestionProvider();
          List<ResultItem> resultList = new ArrayList<>();
      
          Integer headerIdx = results.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1);
          Integer subHeaderIdx = results.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2);
          Integer leftIconIdx = results.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1);
          Integer rightIconIdx = results.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_2);
      
          while (results.moveToNext()) {
              String header = results.getString(headerIdx);
              String subHeader = (subHeaderIdx == -1) ? null : results.getString(subHeaderIdx);
              Integer leftIcon = (leftIconIdx == -1) ? 0 : results.getInt(leftIconIdx);
              Integer rightIcon = (rightIconIdx == -1) ? 0 : results.getInt(rightIconIdx);
      
              ResultItem aux = new ResultItem(header, subHeader, leftIcon, rightIcon);
              resultList.add(aux);
          }
      
          results.close();
          return resultList;
      
    • 确定用户何时触摸软键盘上的搜索按钮。当他这样做时,向可搜索的活动(负责处理搜索结果的活动)发送意图,并将该查询添加为意图中的额外信息

      protected void onActivityResult(int requestCode, int resultCode, Intent data) {
          super.onActivityResult(requestCode, resultCode, data);
          switch (requestCode) {
              case VOICE_RECOGNITION_CODE: {
                  if (resultCode == RESULT_OK && null != data) {
                      ArrayList<String> text = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
                      searchInput.setText(text.get(0));
                  }
                  break;
              }
          }
      }
      
    • 确定用户点击其中一个显示的建议并发送和包含项目信息的意图(此意图应与上一步骤不同)

      private void sendSuggestionIntent(ResultItem item) {
          try {
              Intent sendIntent = new Intent(this, Class.forName(searchableActivity));
              sendIntent.setAction(Intent.ACTION_VIEW);
              sendIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
      
              Bundle b = new Bundle();
              b.putParcelable(CustomSearchableConstants.CLICKED_RESULT_ITEM, item);
      
              sendIntent.putExtras(b);
              startActivity(sendIntent);
              finish();
          } catch (ClassNotFoundException e) {
              e.printStackTrace();
          }
      }
      

    所描述的步骤应足以实现您自己的界面。所有代码示例均来自here。我已经创建了这个库,它完成了上面描述的内容。它还没有经过良好测试,有些UI配置可能还没有。

    我希望这个答案可以帮助有需要的人。

答案 1 :(得分:5)

我已经设置了一个小教程

http://drzon.net/how-to-create-a-clearable-autocomplete-dropdown-with-autocompletetextview/

<强>概述

我必须按照建议将SearchView替换为AutoCompleteTextView

首先,创建一个适配器。就我而言,它是一个JSONObject ArrayAdapter。我想在下拉列表中显示的数据是场地名称和场地地址。请注意,适配器必须为Filtarable并覆盖getFilter()

// adapter for the search dropdown auto suggest
ArrayAdapter<JSONObject> searchAdapter = new ArrayAdapter<JSONObject>(this, android.R.id.text1) {
private Filter filter;

public View getView(final int position, View convertView, ViewGroup parent) {
    if (convertView == null) {
        convertView = this.getLayoutInflater().inflate(R.layout.search_item, parent, false);
    }

    TextView venueName = (TextView) convertView.findViewById(R.id.search_item_venue_name);
    TextView venueAddress = (TextView) convertView.findViewById(R.id.search_item_venue_address);

    final JSONObject venue = this.getItem(position);
    convertView.setTag(venue);
    try {

        CharSequence name = highlightText(venue.getString("name"));
        CharSequence address = highlightText(venue.getString("address"));

        venueName.setText(name);
        venueAddress.setText(address);
    }
    catch (JSONException e) {
        Log.i(Consts.TAG, e.getMessage());
    }

    return convertView;

}

@Override
public Filter getFilter() {
    if (filter == null) {
        filter = new VenueFilter();
    }
    return filter;
}
};

以下是自定义VenueFilter

private class VenueFilter extends Filter {

    @Override
    protected FilterResults performFiltering(CharSequence constraint) {
        List<JSONObject> list = new ArrayList<JSONObject>(venues);
        FilterResults result = new FilterResults();
        String substr = constraint.toString().toLowerCase();

        if (substr == null || substr.length() == 0) {
            result.values = list;
            result.count = list.size();
        } else {

            final ArrayList<JSONObject> retList = new ArrayList<JSONObject>();
            for (JSONObject venue : list) {
                try {
                    if (venue.getString("name").toLowerCase().contains(constraint) ||  venue.getString("address").toLowerCase().contains(constraint) || 
                         {
                        retList.add(venue);
                    }
                } catch (JSONException e) {
                    Log.i(Consts.TAG, e.getMessage());
                }
            }
            result.values = retList;
            result.count = retList.size();
        }
        return result;
    }

    @SuppressWarnings("unchecked")
    @Override
    protected void publishResults(CharSequence constraint, FilterResults results) {
        searchAdapter.clear();
        if (results.count > 0) {
            for (JSONObject o : (ArrayList<JSONObject>) results.values) {
                searchAdapter.add(o);
            }
        }
    }

}

现在设置搜索框(actionbar_search.xml)的布局:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="match_parent"
    android:layout_gravity="fill_horizontal"
    android:focusable="true" >

    <AutoCompleteTextView
        android:id="@+id/search_box"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:dropDownVerticalOffset="5dp"
        android:dropDownWidth="wrap_content"
        android:inputType="textAutoComplete|textAutoCorrect"
        android:popupBackground="@color/white"
        android:textColor="#FFFFFF" >
    </AutoCompleteTextView>

</RelativeLayout>

个人下拉项目的布局(场地名称和场地地址)。这个看起来很糟糕,你必须自定义它:

<?xml version="1.0" encoding="UTF-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:textAlignment="gravity" >

    <TextView
        android:id="@+id/search_item_venue_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@color/cyan"
        android:layout_gravity="right" />

    <TextView
        android:id="@+id/search_item_venue_address"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_toStartOf="@+id/search_item_venue_name"
        android:gravity="right"
        android:textColor="@color/white" />


</RelativeLayout>

接下来我们想把它放在动作栏中

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    ActionBar actionBar = getSupportActionBar();
    actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_USE_LOGO | ActionBar.DISPLAY_SHOW_HOME
            | ActionBar.DISPLAY_HOME_AS_UP);
    LayoutInflater inflater = (LayoutInflater)this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    View v = inflater.inflate(R.layout.actionbar_search, null);
    AutoCompleteTextView textView =  (AutoCompleteTextView) v.findViewById(R.id.search_box);

    textView.setAdapter(searchAdapter);

    textView.setOnClickListener(new View.OnClickListener() {

        @Override
        public void onClick(View v) {
            // do something when the user clicks
        }
    });
    actionBar.setCustomView(v);
}

就是这样,我还有一些东西需要弄明白:

  • 这会在操作栏中显示“始终在那里”,我希望它像SearchView小部件一样 - 放大镜玻璃,当您点击它时会打开一个搜索框(并且有一点{ {1}}按钮关闭它并恢复正常)
  • 还没弄清楚如何自定义下拉框,例如,Gmail似乎有阴影,我的只是扁平,更改行分隔符的颜色等...

总体而言,这可以节省创建可搜索活动的所有开销。如果您知道如何自定义等,请添加。

答案 2 :(得分:2)

您应该使用ListPopupWindow并将其锚定到搜索视图小部件。

答案 3 :(得分:2)

如果您只想实施下拉效果请转到AutoCompleteTextView

你可以找到一个很好的教程here

然后,如果要实现精确设计,则必须实施ActionBar 如果你想实现更低版本,你需要实现ActionBarCombat 而不是ActionBar

答案 4 :(得分:2)

我已经成功使用了Michaels的回复(https://stackoverflow.com/a/18894726/2408033),但我不喜欢它的手册,夸大视图并将其添加到操作栏,切换其状态等。

我将其修改为使用ActionBar ActionView,而不是手动将视图添加到操作栏/工具栏。

我觉得这样做效果好得多,因为我不需要管理打开/关闭状态和隐藏视图,就像他在添加链接中的toggleSearch方法中的示例中所做的那样。它也可以与后退按钮完美配合。

在我的menu.xml中

 <item
        android:id="@+id/global_search"
        android:icon="@android:drawable/ic_menu_search"
        android:title="Search"
        app:actionLayout="@layout/actionbar_search"
        app:showAsAction="ifRoom|collapseActionView" />

在我的onCreateOptionsMenu

 View actionView = menu.findItem(R.id.global_search).getActionView();
 searchTextView = (ClearableAutoCompleteTextView) actionView.findViewById(R.id.search_box);
 searchTextView.setAdapter(searchAdapter);

您可以在我的项目中找到完整的实施版本。请注意,有两个搜索视图,因为我使用实际的SearchView来过滤listView。

https://github.com/babramovitch/ShambaTimes/blob/master/app/src/main/java/com/shambatimes/schedule/MainActivity.java

答案 5 :(得分:1)

请参阅此示例,该示例完全符合您的要求: http://wptrafficanalyzer.in/blog/android-searchview-widget-with-actionbarcompat-library/

答案 6 :(得分:-1)

您需要使用collapseActionView属性。

  

collapseActionView属性允许您的SearchView扩展为   占据整个动作栏并折叠回正常状态   不使用时的操作栏项目。

例如:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@+id/search"
          android:title="@string/search_title"
          android:icon="@drawable/ic_search"
          android:showAsAction="collapseActionView|ifRoom"
          android:actionViewClass="android.widget.SearchView" />
</menu>