Добавляем поисковый фильтр в RecyclerView в Android

Разработка: Добавляем поисковый фильтр в RecyclerView в Android

Сегодня мы разберём пример добавления поиска в RecyclerView. Это несложно и мы используем поисковый виджет из Toolbar для ввода поискового запроса. Для наглядности, я создам приложение, аналогичное адресной книге, в котором мы сможем искать нужный контакт по имени или номеру телефона.

1. Фильтр поиска RecyclerView – getFilter()

В Android есть класс Filterable для отбора данных по условию. Обычно для этого переопределяется метод getFilter() в классе адаптера, в котором по условию отфильтровываются только нужные данные из списка. Ниже приведён пример метода getFilter() для поиска контакта по имени или номеру телефона из списка контактов.


@Override
public Filter getFilter() {
return new Filter() {
@Override
protected FilterResults performFiltering(CharSequence charSequence) {
String charString = charSequence.toString();
if (charString.isEmpty()) {
contactListFiltered = contactList;
} else {
List filteredList = new ArrayList<>();
for (Contact row : contactList) {

// здесь мы отбираем нужные данные
if (row.getName().toLowerCase().contains(charString.toLowerCase()) || row.getPhone().contains(charSequence)) {
filteredList.add(row);
}
}

contactListFiltered = filteredList;
}

FilterResults filterResults = new FilterResults();
filterResults.values = contactListFiltered;
return filterResults;
}

@Override
protected void publishResults(CharSequence charSequence, FilterResults filterResults) {
contactListFiltered = (ArrayList) filterResults.values;

// обновляем список отфильтрованных данных
notifyDataSetChanged();
}
};
}

2. JSON примера

Для этого примера я буду использовать json с адреса https://api.androidhive.info/json/contacts.json . Он содержит список контактов, каждый из которых содержит имя, номер телефона и изображение профиля.


[{
"name": "Tom Hardy",
"image": "https://api.androidhive.info/json/images/tom_hardy.jpg",
"phone": "(541) 754-3010"
},
{
"name": "Johnny Depp",
"image": "https://api.androidhive.info/json/images/johnny.jpg",
"phone": "(452) 839-1210"
}
]

Создаём проект

1. Создайте новый проект в Android Studio (File ⇒ New Project) и выберите Basic Activity в списке шаблонов.

2. Откройте build.gradle из папки app и добавьте зависимости RecyclerView, Glide и Volley:

build.gradle
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
// ...

// recycler view
implementation 'com.android.support:recyclerview-v7:26.1.0'

// библиотека для работы с изображениями glide
implementation 'com.github.bumptech.glide:glide:4.3.1'

// http библиотека volley
implementation 'com.android.volley:volley:1.0.0'
implementation 'com.google.code.gson:gson:2.6.2'

}
3. Замените файлы ресурсов strings.xml, dimens.xml, colors.xml таким образом:

strings.xml


RecyclerView Search
Settings
Contacts
Search
Type name…

dimens.xml


16dp
16dp
40dp
10dp
15dp
12dp

colors.xml



#111 #FFF #ea3732 #333333 #8c8c8c

4. Скачайте файл res.zip и добавьте изображения из него в папку res. В этих папках также присутствует иконка поиска в тулбар.

5. Создайте класс MyApplication.java, расширяющий класс Application:


package info.androidhive.recyclerviewsearch;

import android.app.Application;
import android.text.TextUtils;

import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.toolbox.Volley;

public class MyApplication extends Application {

public static final String TAG = MyApplication.class
.getSimpleName();

private RequestQueue mRequestQueue;

private static MyApplication mInstance;

@Override
public void onCreate() {
super.onCreate();
mInstance = this;
}

public static synchronized MyApplication getInstance() {
return mInstance;
}

public RequestQueue getRequestQueue() {
if (mRequestQueue == null) {
mRequestQueue = Volley.newRequestQueue(getApplicationContext());
}

return mRequestQueue;
}

public void addToRequestQueue(Request req, String tag) {
// зададим tag по-умолчанию, если он пуст
req.setTag(TextUtils.isEmpty(tag) ? TAG : tag);
getRequestQueue().add(req);
}

public void addToRequestQueue(Request req) {
req.setTag(TAG);
getRequestQueue().add(req);
}

public void cancelPendingRequests(Object tag) {
if (mRequestQueue != null) {
mRequestQueue.cancelAll(tag);
}
}
}

6. Откройте файл AndroidManifest.xml и добавьте MyApplication в тег . Также добавьте разрешение INTERNET, т.к. нам необходимо будет делать запросы по http.









7. Для разбора json нам понадобится класс POJO для сериализации. Создайте класс Contact.java, добавьте в него поля “имя” (name), “картинка” (image) и “номер телефона” (phone).


package info.androidhive.recyclerviewsearch;

public class Contact {
String name;
String image;
String phone;

public Contact() {
}

public String getName() {
return name;
}

public String getImage() {
return image;
}

public String getPhone() {
return phone;
}
}

8. Создайте класс MyDividerItemDecoration.java. В нём мы добавим отступ слева к линии разделения элементов списка.


package info.androidhive.recyclerviewsearch;

import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.TypedValue;
import android.view.View;

/**
* Created by ravi on 17/11/17.
*/

public class MyDividerItemDecoration extends RecyclerView.ItemDecoration {

private static final int[] ATTRS = new int[]{
android.R.attr.listDivider
};

public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;

public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;

private Drawable mDivider;
private int mOrientation;
private Context context;
private int margin;

public MyDividerItemDecoration(Context context, int orientation, int margin) {
this.context = context;
this.margin = margin;
final TypedArray a = context.obtainStyledAttributes(ATTRS);
mDivider = a.getDrawable(0);
a.recycle();
setOrientation(orientation);
}

public void setOrientation(int orientation) {
if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
throw new IllegalArgumentException("invalid orientation");
}
mOrientation = orientation;
}

@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
if (mOrientation == VERTICAL_LIST) {
drawVertical(c, parent);
} else {
drawHorizontal(c, parent);
}
}

public void drawVertical(Canvas c, RecyclerView parent) {
final int left = parent.getPaddingLeft();
final int right = parent.getWidth() - parent.getPaddingRight();

final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) { final View child = parent.getChildAt(i); final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child .getLayoutParams(); final int top = child.getBottom() + params.bottomMargin; final int bottom = top + mDivider.getIntrinsicHeight(); mDivider.setBounds(left + dpToPx(margin), top, right, bottom); mDivider.draw(c); } } public void drawHorizontal(Canvas c, RecyclerView parent) { final int top = parent.getPaddingTop(); final int bottom = parent.getHeight() - parent.getPaddingBottom(); final int childCount = parent.getChildCount(); for (int i = 0; i < childCount; i++) { final View child = parent.getChildAt(i); final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child .getLayoutParams(); final int left = child.getRight() + params.rightMargin; final int right = left + mDivider.getIntrinsicHeight(); mDivider.setBounds(left, top + dpToPx(margin), right, bottom - dpToPx(margin)); mDivider.draw(c); } } @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { if (mOrientation == VERTICAL_LIST) { outRect.set(0, 0, 0, mDivider.getIntrinsicHeight()); } else { outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0); } } private int dpToPx(int dp) { Resources r = context.getResources(); return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, r.getDisplayMetrics())); } }

Создаём Adapter с фильтром

Итак, ресурсы готовы, приступим к написанию класса адаптера. Это главный компонент нашей статьи, будьте внимательны.

9. Создайте представление (layout) user_row_item.xml с описанной ниже разметкой. Это представление отображает один контакт списка. В нём два TextView для отображения имени и номера телефона и ImageView для вывода картинки контакта.


user_row_item.xml



10. Создайте класс ContactsAdapter.java, реализующий Filterable, в котором вас попросят переопределить метод getFilter().

При этом в методе getFilter() строка поиска будет передана методу performFiltering(). Интерфейс ContactsAdapterListener содержит вызов метода onContactSelected() при выборе контакта из списка.


package info.androidhive.recyclerviewsearch;

import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Filter;
import android.widget.Filterable;
import android.widget.ImageView;
import android.widget.TextView;

import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;

import java.util.ArrayList;
import java.util.List;

public class ContactsAdapter extends RecyclerView.Adapter
implements Filterable {
private Context context;
private List contactList;
private List contactListFiltered;
private ContactsAdapterListener listener;

public class MyViewHolder extends RecyclerView.ViewHolder {
public TextView name, phone;
public ImageView thumbnail;

public MyViewHolder(View view) {
super(view);
name = view.findViewById(R.id.name);
phone = view.findViewById(R.id.phone);
thumbnail = view.findViewById(R.id.thumbnail);

view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// отправим выбранный контакт в callback
listener.onContactSelected(contactListFiltered.get(getAdapterPosition()));
}
});
}
}

public ContactsAdapter(Context context, List contactList, ContactsAdapterListener listener) {
this.context = context;
this.listener = listener;
this.contactList = contactList;
this.contactListFiltered = contactList;
}

@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.user_row_item, parent, false);

return new MyViewHolder(itemView);
}

@Override
public void onBindViewHolder(MyViewHolder holder, final int position) {
final Contact contact = contactListFiltered.get(position);
holder.name.setText(contact.getName());
holder.phone.setText(contact.getPhone());

Glide.with(context)
.load(contact.getImage())
.apply(RequestOptions.circleCropTransform())
.into(holder.thumbnail);
}

@Override
public int getItemCount() {
return contactListFiltered.size();
}

@Override
public Filter getFilter() {
return new Filter() {
@Override
protected FilterResults performFiltering(CharSequence charSequence) {
String charString = charSequence.toString();
if (charString.isEmpty()) {
contactListFiltered = contactList;
} else {
List filteredList = new ArrayList<>();
for (Contact row : contactList) {

if (row.getName().toLowerCase().contains(charString.toLowerCase()) || row.getPhone().contains(charSequence)) {
filteredList.add(row);
}
}

contactListFiltered = filteredList;
}

FilterResults filterResults = new FilterResults();
filterResults.values = contactListFiltered;
return filterResults;
}

@Override
protected void publishResults(CharSequence charSequence, FilterResults filterResults) {
contactListFiltered = (ArrayList) filterResults.values;
notifyDataSetChanged();
}
};
}

public interface ContactsAdapterListener {
void onContactSelected(Contact contact);
}
}

5. Добавляем виджет поиска

Нам осталось добавить SearchView в Toolbar, отрисовать RecyclerView, прочитав json и передать поисковый запрос в adapter.

11. Откройте / создайте файл menu_main.xml в папке res ⇒ menus и добавьте в него виджет SearchView, также сделаем его видимым всегда.



12. В папке res ⇒ xml создайте xml файл searchable.xml (если папки xml нет, создайте её)




13. Откройте файл AndroidManifest.xml и настройте поиск:











14. Откройте файлы главной activity activity_main.xml и content_main.xml и добавьте элемент RecyclerView.


activity_main.xml




content_main.xml



15. Откройте файл MainActivity.java и добавьте нижеописанный код.

> В методе fetchContacts() запрос Volley получает json. Полученный json сериализуется с помощью Gson и все контакты из него добавляются в список. Вызов mAdapter.notifyDataSetChanged() перерисовывает RecyclerView.

> В onCreateOptionsMenu() создаётся меню и выводится SearchView.

> searchView.setOnQueryTextListener() ожидает ввод текста в поле поиска от пользователя. Введённый запрос затем передаётся адаптеру через mAdapter.getFilter().filter(query), затем RecyclerView обновляется, показывая только отфильтрованные данные.

> onContactSelected() вызывается при выборе контакта в списке.


MainActivity.java

package info.androidhive.recyclerviewsearch;

import android.app.SearchManager;
import android.content.Context;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.DividerItemDecoration;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.SearchView;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;

import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.JsonArrayRequest;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

import org.json.JSONArray;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity implements ContactsAdapter.ContactsAdapterListener {
private static final String TAG = MainActivity.class.getSimpleName();
private RecyclerView recyclerView;
private List contactList;
private ContactsAdapter mAdapter;
private SearchView searchView;

// url для получения контактов в json
private static final String URL = "https://api.androidhive.info/json/contacts.json";

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);

// делаем красивым toolbar
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle(R.string.toolbar_title);

recyclerView = findViewById(R.id.recycler_view);
contactList = new ArrayList<>();
mAdapter = new ContactsAdapter(this, contactList, this);

// белый фон строки состояния
whiteNotificationBar(recyclerView);

RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getApplicationContext());
recyclerView.setLayoutManager(mLayoutManager);
recyclerView.setItemAnimator(new DefaultItemAnimator());
recyclerView.addItemDecoration(new MyDividerItemDecoration(this, DividerItemDecoration.VERTICAL, 36));
recyclerView.setAdapter(mAdapter);

fetchContacts();
}

/**
* получает json посредством http-запроса
*/
private void fetchContacts() {
JsonArrayRequest request = new JsonArrayRequest(URL,
new Response.Listener() {
@Override
public void onResponse(JSONArray response) {
if (response == null) {
Toast.makeText(getApplicationContext(), "Couldn't fetch the contacts! Pleas try again.", Toast.LENGTH_LONG).show();
return;
}

List items = new Gson().fromJson(response.toString(), new TypeToken>() {
}.getType());

// добавляем контакты в список
contactList.clear();
contactList.addAll(items);

// обновляем recycler view
mAdapter.notifyDataSetChanged();
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
// ошибка в получении json
Log.e(TAG, "Error: " + error.getMessage());
Toast.makeText(getApplicationContext(), "Error: " + error.getMessage(), Toast.LENGTH_SHORT).show();
}
});

MyApplication.getInstance().addToRequestQueue(request);
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main, menu);

// Ассоциируем настройку поиска с SearchView
SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
searchView = (SearchView) menu.findItem(R.id.action_search)
.getActionView();
searchView.setSearchableInfo(searchManager
.getSearchableInfo(getComponentName()));
searchView.setMaxWidth(Integer.MAX_VALUE);

// отслеживаем изменения текста в поисковом поле
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
// фильтруем recycler view при окончании ввода
mAdapter.getFilter().filter(query);
return false;
}

@Override
public boolean onQueryTextChange(String query) {
// фильтруем recycler view при изменении текста
mAdapter.getFilter().filter(query);
return false;
}
});
return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Обрабатываем меню здесь. Нажатия на Home/Up
// будут автоматически обработаны так, как указано в
// родительской activity в AndroidManifest.xml.
int id = item.getItemId();

//noinspection SimplifiableIfStatement
if (id == R.id.action_search) {
return true;
}

return super.onOptionsItemSelected(item);
}

@Override
public void onBackPressed() {
// при нажатии кнопки "назад" закрываем поиск
if (!searchView.isIconified()) {
searchView.setIconified(true);
return;
}
super.onBackPressed();
}

private void whiteNotificationBar(View view) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
int flags = view.getSystemUiVisibility();
flags |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
view.setSystemUiVisibility(flags);
getWindow().setStatusBarColor(Color.WHITE);
}
}

@Override
public void onContactSelected(Contact contact) {
Toast.makeText(getApplicationContext(), "Selected: " + contact.getName() + ", " + contact.getPhone(), Toast.LENGTH_LONG).show();
}
}

Разработка: Добавляем поисковый фильтр в RecyclerView в Android

Надеюсь, я понятно описал как фильтровать данные RecyclerView. Если у вас, всё ещё есть вопросы, пишите их в комментариях.

Happy Coding!

Источник: "Android RecyclerView adding Search Filter"

Leave a Comment