In this tutorial, we’ll discuss and implement a search feature that displays the matched results in a drop-down beside allowing to filter the ListView results based on the searched string. This type of UI is commonly seen in Food Delivery apps that have plenty of options to choose from. The user can search based on a certain tag/category to quickly find their desired result.
At the end of this tutorial, you’ll be able to come up with a working application similar to the one given below.
Android Multi Search
For the above application, we’ll NOT be using a SearchView. Instead, we’ll be using an EditText wrapped inside a CardView. The dropdown that pops up with a list of suggestions will be a RecyclerView.
Project Structure
Code
The activity_main.xml
layout is given below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
<RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android" xmlns:card_view="https://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#FFFFFF"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#212121" android:minHeight="?attr/actionBarSize"> <TextView android:id="@+id/toolbar_title" style="@style/TextAppearance.AppCompat.Widget.ActionBar.Title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:text="@string/app_name" android:textColor="#FFF" /> </android.support.v7.widget.Toolbar> <RelativeLayout android:id="@+id/view_search" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#50000000" android:clickable="true" android:visibility="invisible"> <ProgressBar android:id="@+id/marker_progress" style="?android:attr/progressBarStyle" android:layout_width="50dp" android:layout_height="50dp" android:layout_centerInParent="true" android:indeterminate="true" android:visibility="gone" /> </RelativeLayout> <ListView android:id="@+id/listContainer" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#fff" android:clipToPadding="false" android:divider="#fff" android:paddingTop="56dp" android:visibility="gone" /> <android.support.v7.widget.CardView android:id="@+id/card_search" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="4dp" android:visibility="invisible" card_view:cardCornerRadius="2dp"> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <LinearLayout android:id="@+id/linearLayout_search" android:layout_width="match_parent" android:layout_height="48dp"> <ImageView android:id="@+id/image_search_back" android:layout_width="48dp" android:layout_height="48dp" android:background="?android:attr/selectableItemBackground" android:clickable="true" android:padding="12dp" android:src="https://www.journaldev.com/14073/@mipmap/ic_arrow_back" /> <EditText android:id="@+id/edit_text_search" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:background="#fff" android:focusable="true" android:gravity="center_vertical" android:hint="@string/search_restaurants_and_cuisines" android:imeOptions="actionSearch" android:inputType="textCapWords" android:maxLines="1" android:paddingLeft="12dp" android:paddingRight="8dp" /> </LinearLayout> <View android:id="@+id/line_divider" android:layout_width="match_parent" android:layout_height=".5dp" android:layout_below="@+id/linearLayout_search" android:background="#eee" /> <android.support.v7.widget.RecyclerView android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="250dp" android:layout_below="@+id/line_divider" android:divider="#FFFFFF" /> </RelativeLayout> </android.support.v7.widget.CardView> <TextView android:id="@+id/txtNoResultsFound" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:gravity="center" android:padding="@dimen/activity_horizontal_margin" android:text="@string/no_results_found" /> <ListView android:id="@+id/listView" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_below="@+id/toolbar" android:layout_marginBottom="@dimen/corners_small_value" android:layout_marginLeft="@dimen/corners_small_value" android:layout_marginRight="@dimen/corners_small_value"> </ListView> <View android:id="@+id/toolbar_shadow" android:layout_width="match_parent" android:layout_height="4dp" android:layout_below="@+id/toolbar" android:background="@drawable/toolbar_shadow" /> </RelativeLayout> |
In the above code, the CardView is a Custom SearchView UI with a back button on the left. The ListView would display all the restaurants from the adapter.
The Data Source for the restaurants and the type of cuisines is defined in the Model.java
file as shown below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
package com.journaldev.efficientsearch; import java.util.List; public class Model { public String name; public String id; public List cuisines; public boolean isCuisine; public int numberOfCuisine; public Model(String id, String name, List cuisines, boolean isCuisine, int numberOfCuisine) { this.name = name; this.id = id; this.cuisines = cuisines; this.isCuisine = isCuisine; this.numberOfCuisine = numberOfCuisine; } } |
The parameter that differentiates between a restaurant and a cuisine is isCuisine which is of type Boolean.
The MainActivity.java
is given below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 |
package com.journaldev.efficientsearch; import android.content.Context; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.CardView; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.Toolbar; import android.text.Editable; import android.text.TextWatcher; import android.util.Log; import android.view.KeyEvent; import android.view.MenuItem; import android.view.View; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.ImageView; import android.widget.ListView; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; public class MainActivity extends AppCompatActivity implements CuisineSearchAdapter.ItemListener { Toolbar toolbar; private ImageView image_search_back; private RelativeLayout view_search; private EditText edit_text_search; private CardView card_search; private View line_divider, toolbar_shadow; RecyclerView recyclerView; ListView listView; String text = ""; List modelsList, filterModels; List cuisinesModels; ListViewAdapter listViewAdapter; CuisineSearchAdapter cuisineSearchAdapter; ShowSearchView showSearchView; boolean editTextChangedFromClick = false; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); showSearchView = new ShowSearchView(); toolbar = (Toolbar) findViewById(R.id.toolbar); toolbar.inflateMenu(R.menu.menu_main); image_search_back = (ImageView) findViewById(R.id.image_search_back); view_search = (RelativeLayout) findViewById(R.id.view_search); edit_text_search = (EditText) findViewById(R.id.edit_text_search); card_search = (CardView) findViewById(R.id.card_search); line_divider = findViewById(R.id.line_divider); toolbar_shadow = findViewById(R.id.toolbar_shadow); recyclerView = (RecyclerView) findViewById(R.id.recyclerView); listView = (ListView) findViewById(R.id.listView); TextView no_results = (TextView) findViewById(R.id.txtNoResultsFound); listView.setEmptyView(no_results); populateRestaurantsAndCuisines(); image_search_back.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { text = ""; showSearchView.handleToolBar(MainActivity.this, card_search, toolbar, view_search, recyclerView, edit_text_search, line_divider); toolbar_shadow.setVisibility(View.VISIBLE); listViewAdapter = new ListViewAdapter(modelsList); listView.setAdapter(listViewAdapter); } }); edit_text_search.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { text = s.toString(); if (editTextChangedFromClick) { editTextChangedFromClick = false; if (recyclerView.getVisibility() == View.VISIBLE) recyclerView.setVisibility(View.GONE); } else { if (recyclerView.getVisibility() != View.VISIBLE) recyclerView.setVisibility(View.VISIBLE); if (s.toString().length() > 0) { performFiltering(filterModels); } else { CuisineSearchAdapter cuisineSearchAdapter = new CuisineSearchAdapter(cuisinesModels, modelsList, MainActivity.this, MainActivity.this, text); cuisineSearchAdapter.notifyDataSetChanged(); recyclerView.setAdapter(cuisineSearchAdapter); listViewAdapter = new ListViewAdapter(modelsList); listView.setAdapter(listViewAdapter); } } } @Override public void afterTextChanged(Editable s) { } }); edit_text_search.setOnEditorActionListener(new TextView.OnEditorActionListener() { @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { if (actionId == EditorInfo.IME_ACTION_SEARCH) { // Your piece of code on keyboard search click recyclerView.setVisibility(View.GONE); ((InputMethodManager) getApplicationContext().getSystemService(Context.INPUT_METHOD_SERVICE)).hideSoftInputFromWindow(view_search.getWindowToken(), 0); listViewAdapter.getFilter().filter(v.getText().toString()); return true; } return false; } }); toolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { int menuItem = item.getItemId(); switch (menuItem) { case R.id.action_search: recyclerView.setLayoutManager(new LinearLayoutManager(MainActivity.this)); cuisineSearchAdapter = new CuisineSearchAdapter(cuisinesModels, filterModels, MainActivity.this, MainActivity.this, text); recyclerView.setAdapter(cuisineSearchAdapter); showSearchView.handleToolBar(MainActivity.this, card_search, toolbar, view_search, recyclerView, edit_text_search, line_divider); break; default: break; } return false; } }); } private void populateRestaurantsAndCuisines() { modelsList = new ArrayList(); cuisinesModels = new ArrayList(); List cuisinesList = new ArrayList(); for (int i = 0; i < 7; i++) cuisinesList.add("Cafes"); for (int i = 0; i < 4; i++) cuisinesList.add("Burgers"); for (int i = 0; i < 4; i++) cuisinesList.add("Bars"); modelsList.add(new Model("1", "McDonalds", new ArrayList(Arrays.asList("Cafes", "Burgers")), false, -1)); modelsList.add(new Model("2", "KFC", new ArrayList(Arrays.asList("Cafes", "Burgers")), false, -1)); modelsList.add(new Model("3", "Burger King", new ArrayList(Arrays.asList("Cafes", "Burgers")), false, -1)); modelsList.add(new Model("4", "Subway", new ArrayList(Arrays.asList("Burgers")), false, -1)); modelsList.add(new Model("5", "Cafe Coffee Day", new ArrayList(Arrays.asList("Cafes")), false, -1)); modelsList.add(new Model("6", "Costa", new ArrayList(Arrays.asList("Cafes")), false, -1)); modelsList.add(new Model("7", "Coffee Beans", new ArrayList(Arrays.asList("Cafes")), false, -1)); modelsList.add(new Model("8", "Starbucks", new ArrayList(Arrays.asList("Cafes")), false, -1)); modelsList.add(new Model("9", "Blues", new ArrayList(Arrays.asList("Bars")), false, -1)); modelsList.add(new Model("10", "Hard Rock Cafe", new ArrayList(Arrays.asList("Bars", "Cafe")), false, -1)); modelsList.add(new Model("11", "The Backyard Underground", new ArrayList(Arrays.asList("Bars")), false, -1)); modelsList.add(new Model("12", "Downtown", new ArrayList(Arrays.asList("Bars")), false, -1)); Map cuisineMap = new HashMap(); for (String cuisine : cuisinesList) { Integer n = cuisineMap.get(cuisine); n = (n == null) ? 1 : ++n; cuisineMap.put(cuisine, n); } for (Map.Entry entry : cuisineMap.entrySet()) { Model model = new Model("", entry.getKey(), null, true, entry.getValue()); modelsList.add(model); cuisinesModels.add(model); } filterModels = new ArrayList(modelsList); initialiseAdapters(); } private void initialiseAdapters() { listViewAdapter = new ListViewAdapter(filterModels); listView.setAdapter(listViewAdapter); } @Override public void onItemClick(Model model) { editTextChangedFromClick = true; if (model.isCuisine) { edit_text_search.setText(model.name); listViewAdapter.getFilter().filter(model.name); } else { edit_text_search.setText(model.name); showSearchView.handleToolBar(MainActivity.this, card_search, toolbar, view_search, recyclerView, edit_text_search, line_divider); Toast.makeText(getApplicationContext(), model.name + " was selected.", Toast.LENGTH_LONG).show(); } } public void performFiltering(List filteredSuggestions) { filteredSuggestions.clear(); for (Model model : modelsList) { if (model.name.toLowerCase().contains(text.toLowerCase())) { filteredSuggestions.add(model); } } CuisineSearchAdapter cuisineSearchAdapter = new CuisineSearchAdapter(cuisinesModels, filteredSuggestions, MainActivity.this, MainActivity.this, text); cuisineSearchAdapter.notifyDataSetChanged(); recyclerView.setAdapter(cuisineSearchAdapter); } } |
The Toolbar layout is set from the menu_main.xml
file as shown below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<menu xmlns:android="https://schemas.android.com/apk/res/android" xmlns:app="https://schemas.android.com/apk/res-auto" xmlns:tools="https://schemas.android.com/tools" tools:context=".MainActivity"> <item android:id="@+id/action_search" android:icon="@mipmap/ic_action_search" android:orderInCategory="100" android:title="Search" app:showAsAction="always"/> </menu> |
Before analysing the MainActivity.java class in detail, let’s glance at the code for ListViewAdapter with a filter. Something that we’ve already implemented here.
The code for the ListViewAdapter.java
class is given below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
package com.journaldev.efficientsearch; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.Filter; import android.widget.Filterable; import android.widget.TextView; import java.util.ArrayList; import java.util.List; public class ListViewAdapter extends BaseAdapter implements Filterable { private List modelList; private List mStringFilterList; private ValueFilter valueFilter; private class ViewHolder { TextView vendorName; } public ListViewAdapter(List modelList) { this.modelList = modelList; mStringFilterList = modelList; } @Override public int getCount() { if (modelList != null) return modelList.size(); else return 0; } @Override public Model getItem(int position) { return modelList.get(position); } @Override public long getItemId(int position) { Model object = getItem(position); if (object.isCuisine) { return -1; } else return Integer.parseInt(object.id); } @Override public View getView(int position, View convertView, final ViewGroup parent) { ViewHolder holder = null; Model vendorModel = getItem(position); if (vendorModel.isCuisine) { return LayoutInflater.from(parent.getContext()).inflate(R.layout.row_null, null); } else { if (convertView == null) { convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_row_model, parent, false); holder = new ViewHolder(); holder.vendorName = ((TextView) convertView.findViewById(R.id.txt_vendor_name)); convertView.setTag(holder); } else { if (holder == null) { convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_row_model, parent, false); holder = new ViewHolder(); holder.vendorName = ((TextView) convertView.findViewById(R.id.txt_vendor_name)); convertView.setTag(holder); } else holder = (ViewHolder) convertView.getTag(); } holder.vendorName.setText(vendorModel.name); return convertView; } } @Override public Filter getFilter() { if (valueFilter == null) { valueFilter = new ValueFilter(); } return valueFilter; } private class ValueFilter extends Filter { @Override protected FilterResults performFiltering(CharSequence constraint) { FilterResults results = new FilterResults(); if (constraint != null && constraint.length() > 0) { List filterList = new ArrayList(); for (int i = 0; i < mStringFilterList.size(); i++) { if (!mStringFilterList.get(i).isCuisine) { if (mStringFilterList.get(i).cuisines.contains(constraint)) { Model model = new Model(mStringFilterList.get(i).id, mStringFilterList.get(i).name, mStringFilterList.get(i).cuisines, false, -1); filterList.add(model); } } } if (filterList.size() == 0) { for (int i = 0; i < mStringFilterList.size(); i++) { if ((mStringFilterList.get(i).name.toUpperCase()) .contains(constraint.toString().toUpperCase()) && !mStringFilterList.get(i).isCuisine) { Model model = new Model(mStringFilterList.get(i).id, mStringFilterList.get(i).name, mStringFilterList.get(i).cuisines, false, -1); if (!model.isCuisine) filterList.add(model); } } } results.count = filterList.size(); results.values = filterList; } else { results.count = mStringFilterList.size(); results.values = mStringFilterList; } return results; } @Override protected void publishResults(CharSequence constraint, FilterResults results) { modelList = (List) results.values; notifyDataSetChanged(); } } } |
In the above class, we check the isCuisine
param on each Model instance. If it’s true we add an empty row with 0 height. Else list_row_model.xml
is added. The xml code for each of the layouts is given below.
1 2 3 4 5 6 7 |
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="https://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content"> </LinearLayout> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android" android:id="@+id/rl_car" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="@dimen/activity_vertical_margin"> <TextView android:id="@+id/txt_vendor_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingBottom="5dp" android:textColor="#212121" android:textSize="20sp" android:textStyle="bold" /> </RelativeLayout> |
Clicking the search icon from the Toolbar inflates our custom Search UI and populates the RecyclerView under. To do this, the handleToolbar method is invoked on an instance from the ShowSearchView class.
The code for the ShowSearchView.java
is given below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
package com.journaldev.efficientsearch; import android.animation.Animator; import android.content.Context; import android.content.res.Resources; import android.os.Build; import android.support.v7.widget.CardView; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.Toolbar; import android.util.DisplayMetrics; import android.view.View; import android.view.ViewAnimationUtils; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; public class ShowSearchView { public static void handleToolBar(final Context context, final CardView search, Toolbar toolbarMain, final View view, final RecyclerView recyclerView, final EditText editText, final View line_divider) { final Animation fade_in = AnimationUtils.loadAnimation(context.getApplicationContext(), android.R.anim.fade_in); final Animation fade_out = AnimationUtils.loadAnimation(context.getApplicationContext(), android.R.anim.fade_out); if (search.getVisibility() == View.VISIBLE) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { final Animator animatorHide = ViewAnimationUtils.createCircularReveal(search, search.getWidth() - (int) convertDpToPixel(56, context), (int) convertDpToPixel(23, context), (float) Math.hypot(search.getWidth(), search.getHeight()), 0); animatorHide.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { view.startAnimation(fade_out); view.setVisibility(View.INVISIBLE); search.setVisibility(View.GONE); ((InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE)).hideSoftInputFromWindow(view.getWindowToken(), 0); recyclerView.setVisibility(View.GONE); } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); animatorHide.setDuration(300); animatorHide.start(); } else { ((InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE)).hideSoftInputFromWindow(view.getWindowToken(), 0); view.startAnimation(fade_out); view.setVisibility(View.INVISIBLE); search.setVisibility(View.GONE); } editText.setText(""); toolbarMain.getMenu().clear(); toolbarMain.inflateMenu(R.menu.menu_main); search.setEnabled(false); } else { toolbarMain.getMenu().clear(); toolbarMain.setNavigationIcon(null); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { final Animator animator = ViewAnimationUtils.createCircularReveal(search, search.getWidth() - (int) convertDpToPixel(56, context), (int) convertDpToPixel(23, context), 0, (float) Math.hypot(search.getWidth(), search.getHeight())); animator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { view.setVisibility(View.VISIBLE); view.startAnimation(fade_in); ((InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE)).toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY); } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); search.setVisibility(View.VISIBLE); if (search.getVisibility() == View.VISIBLE) { animator.setDuration(300); animator.start(); search.setEnabled(true); } fade_in.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { editText.requestFocus(); recyclerView.setVisibility(View.VISIBLE); } @Override public void onAnimationRepeat(Animation animation) { } }); } else { search.setVisibility(View.VISIBLE); search.setEnabled(true); recyclerView.setVisibility(View.VISIBLE); editText.requestFocus(); ((InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE)).showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT); } } } public static float convertDpToPixel(float dp, Context context) { Resources resources = context.getResources(); DisplayMetrics metrics = resources.getDisplayMetrics(); return dp * (metrics.densityDpi / 160f); } } |
The handleToolbar method shows/hides the custom UI depending upon the search icon/back arrow clicked respectively. The view animates in/out in the form of circular reveal animation.
The RecyclerView is populated with the cuisines as well as restaurants. Though only the cuisines are displayed until the user types anything.
The adapter for the RecyclerView is defined in the class CuisinesSearchAdapter.java
as shown below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
package com.journaldev.efficientsearch; import android.content.Context; import android.graphics.Color; import android.support.v7.widget.RecyclerView; import android.text.Spannable; import android.text.SpannableString; import android.text.style.ForegroundColorSpan; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import java.text.Normalizer; import java.util.List; import java.util.Locale; public class CuisineSearchAdapter extends RecyclerView.Adapter { Context mContext; ItemListener mListener; String prefix = ""; private List allVendors, cuisines; public CuisineSearchAdapter(List cuisines, List vendorModels, Context context, ItemListener itemListener, String text) { this.allVendors = vendorModels; this.cuisines = cuisines; this.mContext = context; this.mListener = itemListener; prefix = text; } public class MyViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { ImageView icon; TextView textView; View parent; Model vendorModel; MyViewHolder(View itemView) { super(itemView); this.parent = itemView.findViewById(R.id.parentView); this.textView = (TextView) itemView.findViewById(R.id.textView); this.icon = (ImageView) itemView.findViewById(R.id.imageView); itemView.setOnClickListener(this); } void setData(Model model, MyViewHolder holder) { textView = holder.textView; icon = holder.icon; this.vendorModel = model; if (prefix.length() > 0) { if (model.isCuisine) { textView.setText(highlight(prefix, model.name + " (" + model.numberOfCuisine + ")")); icon.setImageResource(R.mipmap.ic_local_offer); } else { textView.setText(highlight(prefix, model.name)); icon.setImageResource(R.mipmap.ic_local_dining); } } else { if (model.isCuisine) { textView.setText(model.name + " (" + model.numberOfCuisine + ")"); icon.setImageResource(R.mipmap.ic_local_offer); } } } @Override public void onClick(View view) { if (mListener != null) { mListener.onItemClick(vendorModel); } } } @Override public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { final View view = LayoutInflater.from(parent.getContext()) .inflate(R.layout.recyclerview_list_row, parent, false); MyViewHolder myViewHolder = new MyViewHolder(view); return myViewHolder; } @Override public void onBindViewHolder(final MyViewHolder holder, final int listPosition) { if (prefix.length() > 0) holder.setData(allVendors.get(holder.getAdapterPosition()), holder); else holder.setData(cuisines.get(holder.getAdapterPosition()), holder); } @Override public int getItemCount() { if (prefix.length() > 0) return allVendors.size(); else return cuisines.size(); } private static CharSequence highlight(String search, String originalText) { // ignore case and accents // the same thing should have been done for the search text String normalizedText = Normalizer .normalize(originalText, Normalizer.Form.NFD) .replaceAll("\p{InCombiningDiacriticalMarks}+", "") .toLowerCase(Locale.ENGLISH); int start = normalizedText.indexOf(search.toLowerCase(Locale.ENGLISH)); if (start = 0) { int spanStart = Math.min(start, originalText.length()); int spanEnd = Math.min(start + search.length(), originalText.length()); highlighted.setSpan(new ForegroundColorSpan(Color.BLUE), spanStart, spanEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); start = normalizedText.indexOf(search, spanEnd); } return highlighted; } } public interface ItemListener { void onItemClick(Model model); } } |
The layout for each row of the RecyclerView is defined in the xml code below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="https://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:id="@+id/parentView" android:background="#FFF" android:layout_height="wrap_content"> <ImageView android:layout_width="24dp" android:layout_height="24dp" android:layout_gravity="center_vertical" android:layout_marginLeft="16dp" android:layout_marginRight="16dp" android:id="@+id/imageView" android:tint="@color/text_color"/> <TextView android:id="@+id/textView" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:padding="16dp" android:textColor="@color/text_color" android:textSize="14sp"/> </LinearLayout> |
- As the user enters text, the RecyclerView data is filtered from the performFiltering method inside the
MainActivity.java
class. The substrings in the RecyclerView rows that match the typed text are then highlighted. - Clicking on a cuisine would filter the list to display all the restaurants having that cuisine as a type.
Clicking a restaurant shall display its name in a Toast message.
This brings an end to this tutorial. You can download the Android EfficientSearch Project from the link below.