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

Posted

技术标签:

【中文标题】如何在操作栏中构建类似 Gmail 的搜索框?【英文标题】:How to build Gmail like search box in the action bar? 【发布时间】:2013-09-22 23:29:35 【问题描述】:

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

当用户开始输入文本时,主布局中的 ListView 会使用 Adapter 更新以过滤结果。我通过实现OnQueryTextListener 并过滤每个击键的结果来做到这一点。

相反,我想创建一个类似 Gmail 的搜索框,并生成自动建议列表并且不更改底层视图

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

【问题讨论】:

【参考方案1】:

如果您只想要一个执行问题中描述的组件,我建议您使用library。您也可以实现 out-of-the-bx searchable interface,但是请注意它确实有 UI 限制:

要实现类似于 Gmail 应用程序的界面,您必须了解以下概念:

内容提供者; 在 SQLite 中持久化数据 Listview 或 RecyclerView 及其适配器; 在活动之间传递数据;

最终结果应该类似于:

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

第 1 部分:布局

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

custom_searchable.xml:将所有 UI 元素组合到一个 RelativeLayout 中,作为 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_
    android:layout_
    android:stackFromBottom="true"
    android:transcriptMode="normal"/>

custom_searchable_header_layout.xml:保存用户输入查询的搜索栏。它还将包含麦克风、擦除和返回 btn;

<RelativeLayout
    android:id="@+id/custombar_return_wrapper"
    android:layout_
    android:layout_
    android:gravity="center_vertical"
    android:background="@drawable/right_oval_ripple"
    android:focusable="true"
    android:clickable="true" >

    <ImageView
        android:id="@+id/custombar_return"
        android:layout_
        android:layout_
        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_
    android:layout_
    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_
        android:layout_
        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_
    android:layout_
    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_
        android:layout_
        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_
    android:layout_
    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_
    android:layout_
    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_
        android:layout_
        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_
        android:layout_
        android:textColor="@color/textPrimaryColor"
        android:text="Sub Header"
        android:textSize="14dp"
        android:maxLines="1" />
</LinearLayout>

<ImageView
    android:id="@+id/rd_right_icon"
    android:layout_
    android:layout_
    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);
                
            
        );
    

内容提供者

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

    向用户建议最近的查询:这意味着每次用户进行搜索时,键入的查询都将保存在数据库中,以便以后检索作为未来搜索的建议; 向用户建议自定义选项:开发人员将尝试通过处理已经输入的字母来预测用户想要什么;

在这两种情况下,答案都应作为 Cursor 对象返回,该对象的内容将在结果列表中显示为 itens。这整个过程可以使用 Content Provider API 来实现。有关如何使用 Content Providers 的详细信息,请访问此link。

在开发者想要实现 1. 中描述的行为的情况下,使用扩展 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 配置可能尚不可用。

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

【讨论】:

这里的新问题:在 custom_searchable_header_layout.xml 中,你为什么将东西包装在包装器中。为什么在 LinearLayout 或 RelativeLayout 中没有 ImageView、EditText 和 ImageView? 如果我没记错的话,我这样做是为了应用到 RelativeLayout 的涟漪效应。 tnx 为您解答。一件事我不明白。你为什么使用内容提供者而不是“@Doa”和存储库模式?因为在我们的应用程序的数据库中保存一些东西不需要使用内容提供者。我猜你回答问题的时间是“@Doa”和存储库模式没有在 android 中使用或呈现。【参考方案2】:

我为此设置了一个小教程

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_
    android:layout_
    android:layout_gravity="fill_horizontal"
    android:focusable="true" >

    <AutoCompleteTextView
        android:id="@+id/search_box"
        android:layout_
        android:layout_
        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_
    android:layout_
    android:textAlignment="gravity" >

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

    <TextView
        android:id="@+id/search_item_venue_address"
        android:layout_
        android:layout_
        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 小部件 - 一个放大镜,当您单击它时会打开一个搜索框(并且有一点 X按钮将其关闭并恢复正常) 还没想好怎么自定义下拉框,比如Gmail的好像有阴影,我的是平的,换行分隔符的颜色等等...

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

【讨论】:

能否提供项目的github链接?【参考方案3】:

如果您只想实现下拉效果,请选择AutoCompleteTextView

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

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

【讨论】:

谢谢,忘了说我已经在用ActionBarCompat【参考方案4】:

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

【讨论】:

【参考方案5】:

我已成功使用 Michaels 回复 (https://***.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

【讨论】:

【参考方案6】:

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

【讨论】:

【参考方案7】:

你需要使用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>

【讨论】:

以上是关于如何在操作栏中构建类似 Gmail 的搜索框?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 bootstrap 3 导航栏中使 select2 v4 搜索框响应?

Swift 如何从我在搜索栏中定义的地图项构建路线

如何在 twitter bootstrap3 的导航栏中增加搜索栏的宽度

如何创建仅在主题行开头搜索文本的 Gmail 过滤器?

如何更改 wordpress 搜索栏中的搜索操作?

如何在搜索栏中获取项目的详细信息?