How to build Gmail like search box in the action bar?

21,593

Solution 1

If you just want a component that does what is discribed in the question, I suggest this library. You can also implement the out-of-the-bx searchable interface, however, be aware that it does have UI limitations:

To implement an interface similar to Gmail App, you will have to understand conceps of:

  • Content Providers;
  • Persisting data in SQLite
  • Listview or RecyclerView and its adapters;
  • Passing data between activities;

Final result should look something like:

final result

There are many (many) ways to get to the same result (or better), I'll discribed one possible way.

Part 01: Layout

I decided to manage the entire interface in a new Activity, for that I've created three XML layouts:

  • custom_searchable.xml: assemblys all UI elements in one RelativeLayout that will serve as content for the 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: holds the search bar where the user will type his query. It will also contain the mic, erase and return 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: holds the UI elements to be displayed in the result list to be displayed in response to the user query;

    <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"/>
    

Part 02: Implementing the SearchActivity

The idea is that, when the user types the search button (which you can place any where you want), this SearchActivity will be called. It has some main responsabilities:

  • Bind to the UI elements in the custom_searchable_header_layout.xml: by doing that, it is possible:

  • to provide listeners for the EditText (where the user will type his query):

    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
       }
    }
    
  • add listener for the return button (which by its turn will just call finish() and return to the caller activity):

    this.dismissDialog.setOnClickListener(new View.OnClickListener() {
        public void onClick(View v) {
        finish();
    }    
    
  • calls the intent for google speech-to-text 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);
                    }
                }
            });
        }
    

Content Provider

When building the a search interface the developer has tipically two options:

  1. Suggest recent queries to the user: this implies that everytime the user makes a search, the typed query will be persisted in a database to be retrieved latter on as a suggestion for future searches;
  2. Suggest custom options to the user: the developer will try to predict what the user wants by processing the already typed letters;

In both cases the answers shall be delivered back as a Cursor object that will have its content displayed as itens in the result list. This whole process can be implement using the Content Provider API. Details about how to use Content Providers can be reached in this link.

In the case where the developer wants to implement the behavior described in 1., it can be usefull to use the strategy of exteding the SearchRecentSuggestionsProvider class. Details about how to do it can be reached in this link.

Implementing the search interface

This interface shall provide the following behavior:

  • When the user types a letter a call to the query method of the retrieved content provider class should return a filled cursor with the suggestion to be displayed in the list - you should take to not freeze the UI thread, so it I recommend to perform this search in an 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();
            }
        }
    
  • Inside the onPostExecute() method of your AsyncTask, you should retrieve a list (that should come from the doInBackground() method) containing the results to be displayed in the ResultList (you can map it in a POJO class and pass it to your custom adapter or you can use a CursorAdapter which would be the best practive for this task):

    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;
    
  • Identify when the user touches the search button from the soft keyboard. When he does that, send an intent to the searchable activity (the one responsible for handling the search result) and add the query as extra information in the intent

    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;
            }
        }
    }
    
  • Identify when the user clicks in one of the displayed suggestions and send and intent containing the item information (this intent should be different from the one of the previous step)

    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();
        }
    }
    

The discribed steps should be enough for implementing an interface yourseld. All code examples were taken from here. I've made this library that does pretty much of what is described above. It is not well tested yet and some of the UI configuration might not be available yet.

I hope this answer might help someone in need.

Solution 2

I have set up a small tutorial to do that

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

Overview

I had to replace the SearchView with AutoCompleteTextView as suggested.

First, create an adapter. In my case it was a JSONObject ArrayAdapter. The data I wanted to display in the drop down was a venue name and venue address. Notice that the adapter must be Filtarable and override 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;
}
};

Here is the custom 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);
            }
        }
    }

}

Now set up the layout for the search box (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>

And the layout for individual drop down item (venue name and venue address). This one looks bad, you'll have to customize it:

<?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>

Next we want to put it in the action bar

@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);
}

That's about it, I still have some stuff to figure out :

  • This puts an "always there" search in the action bar, I want it to be like the SearchView widget - a magnifier glass that opens up to a search box when you click it (and has a little X button to dismiss it and go back to normal)
  • Haven't figured out how to customize the drop down box yet, for example, the Gmail one seems to have shadow, mine is just flat, change the color of the line delimiters etc...

Overall this saves all the overhead of creating a searchable activity. Please add if you know how to customize it etc.

Solution 3

If you want to implement only drop down effect go for AutoCompleteTextView

And you can find a nice tutorial here

Then if you want to implement exact design you have to implement ActionBar and if you want to implement to lower version u need to implement ActionBarCombat instead of ActionBar

Solution 4

I've successfully used Michaels reply (https://stackoverflow.com/a/18894726/2408033) but I didn't like how manual it was, inflating the views and adding it to the actionbar, toggling its state etc.

I modified it to to use an ActionBar ActionView instead of adding the view manually to the actionbar/toolbar.

I find this works a lot better since I don't need to manage the open/close state and hiding of views as he did in his example in the toggleSearch method in the added link. It also works perfectly with the back button.

In my 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" />

In my onCreateOptionsMenu

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

You can find a fully working version of the implementation in my project. Note there are two search views as I was using the actual SearchView to filter a listView.

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

Solution 5

You should use ListPopupWindow and anchor it to the search view widget.

Share:
21,593

Related videos on Youtube

Michael
Author by

Michael

Updated on November 15, 2020

Comments

  • Michael
    Michael over 3 years

    I am currently using SearchView widget inside ActionBarcompat to filter a list while searching.

    When the user starts entering text the ListView in the main layout updates with an Adapter to filter the results. I do this by implementing a OnQueryTextListener and filter the results on each key stroke.

    Instead, I want to create a Gmail like search box with auto suggest list generated and no changes to the underlying view

    enter image description here

    I have went through this tutorial that uses the SearchView component but it requires a searchable activity. I want the drop-down to be over the MainActivity where I have the ListView (like in the Gmail app) and not a dedicated Activity.
    Besides, implementing it the same way as in the tutorial seems like an overkill for what I want (just a dropdown)

  • Michael
    Michael over 10 years
    Thanks, forgot to mention that I was already using ActionBarCompat
  • Rohan Kandwal
    Rohan Kandwal almost 9 years
    Can you provide github link for the project?
  • Solace
    Solace over 8 years
    Newb question here: In custom_searchable_header_layout.xml, why did you wrap things in wrappers. Why didn't you have an ImageView, an EditText, and then an ImageView in a LinearLayout or RelativeLayout?
  • E. Fernandes
    E. Fernandes over 8 years
    If I am not wrong I did that for the ripple effect which is applied to the RelativeLayout.
  • NimaAzhd
    NimaAzhd over 2 years
    tnx for your answer. one thing I don't understand. why do you use content provider instead of "@Doa" and repository pattern ? because saving something in database of our app do not need using content provider. I guess the time you answered the question the "@Doa" and repository pattern were not used or presented in android.