android - studio - Cómo filtrar un RecyclerView con un SearchView
searchable recyclerview (9)
Estoy tratando de implementar
SearchView
desde la biblioteca de soporte.
Quiero que el usuario use
SearchView
para filtrar una
List
de películas en un
RecyclerView
.
He seguido algunos tutoriales hasta ahora y he agregado
SearchView
a
ActionBar
, pero no estoy realmente seguro de a dónde ir desde aquí.
He visto algunos ejemplos, pero ninguno de ellos muestra resultados cuando comienzas a escribir.
Esta es mi
MainActivity
:
public class MainActivity extends ActionBarActivity {
RecyclerView mRecyclerView;
RecyclerView.LayoutManager mLayoutManager;
RecyclerView.Adapter mAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_recycler_view);
mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
mRecyclerView.setHasFixedSize(true);
mLayoutManager = new LinearLayoutManager(this);
mRecyclerView.setLayoutManager(mLayoutManager);
mAdapter = new CardAdapter() {
@Override
public Filter getFilter() {
return null;
}
};
mRecyclerView.setAdapter(mAdapter);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
SearchView searchView = (SearchView) menu.findItem(R.id.menu_search).getActionView();
searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
}
Y este es mi
Adapter
:
public abstract class CardAdapter extends RecyclerView.Adapter<CardAdapter.ViewHolder> implements Filterable {
List<Movie> mItems;
public CardAdapter() {
super();
mItems = new ArrayList<Movie>();
Movie movie = new Movie();
movie.setName("Spiderman");
movie.setRating("92");
mItems.add(movie);
movie = new Movie();
movie.setName("Doom 3");
movie.setRating("91");
mItems.add(movie);
movie = new Movie();
movie.setName("Transformers");
movie.setRating("88");
mItems.add(movie);
movie = new Movie();
movie.setName("Transformers 2");
movie.setRating("87");
mItems.add(movie);
movie = new Movie();
movie.setName("Transformers 3");
movie.setRating("86");
mItems.add(movie);
movie = new Movie();
movie.setName("Noah");
movie.setRating("86");
mItems.add(movie);
movie = new Movie();
movie.setName("Ironman");
movie.setRating("86");
mItems.add(movie);
movie = new Movie();
movie.setName("Ironman 2");
movie.setRating("86");
mItems.add(movie);
movie = new Movie();
movie.setName("Ironman 3");
movie.setRating("86");
mItems.add(movie);
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.recycler_view_card_item, viewGroup, false);
return new ViewHolder(v);
}
@Override
public void onBindViewHolder(ViewHolder viewHolder, int i) {
Movie movie = mItems.get(i);
viewHolder.tvMovie.setText(movie.getName());
viewHolder.tvMovieRating.setText(movie.getRating());
}
@Override
public int getItemCount() {
return mItems.size();
}
class ViewHolder extends RecyclerView.ViewHolder{
public TextView tvMovie;
public TextView tvMovieRating;
public ViewHolder(View itemView) {
super(itemView);
tvMovie = (TextView)itemView.findViewById(R.id.movieName);
tvMovieRating = (TextView)itemView.findViewById(R.id.movieRating);
}
}
}
simplemente cree dos listas en el adaptador, una original y una temporal e implemente Filtrable .
@Override
public Filter getFilter() {
return new Filter() {
@Override
protected FilterResults performFiltering(CharSequence constraint) {
final FilterResults oReturn = new FilterResults();
final ArrayList<T> results = new ArrayList<>();
if (origList == null)
origList = new ArrayList<>(itemList);
if (constraint != null && constraint.length() > 0) {
if (origList != null && origList.size() > 0) {
for (final T cd : origList) {
if (cd.getAttributeToSearch().toLowerCase()
.contains(constraint.toString().toLowerCase()))
results.add(cd);
}
}
oReturn.values = results;
oReturn.count = results.size();//newly Aded by ZA
} else {
oReturn.values = origList;
oReturn.count = origList.size();//newly added by ZA
}
return oReturn;
}
@SuppressWarnings("unchecked")
@Override
protected void publishResults(final CharSequence constraint,
FilterResults results) {
itemList = new ArrayList<>((ArrayList<T>) results.values);
// FIXME: 8/16/2017 implement Comparable with sort below
///Collections.sort(itemList);
notifyDataSetChanged();
}
};
}
dónde
public GenericBaseAdapter(Context mContext, List<T> itemList) {
this.mContext = mContext;
this.itemList = itemList;
this.origList = itemList;
}
Introducción
Como no está realmente claro en su pregunta con qué tiene exactamente problemas, escribí este tutorial rápido sobre cómo implementar esta función; Si todavía tiene preguntas, no dude en preguntar.
Tengo un ejemplo práctico de todo lo que estoy hablando aquí en este
repositorio de GitHub
.
Si desea saber más sobre el proyecto de ejemplo, visite la
página de inicio del proyecto
.
En cualquier caso, el resultado debería verse así:
Si primero quieres jugar con la aplicación de demostración, puedes instalarla desde Play Store:
De todos modos, comencemos.
Configurar el
SearchView
En la carpeta
res/menu
cree un nuevo archivo llamado
main_menu.xml
.
En él, agregue un elemento y configure
actionViewClass
en
android.support.v7.widget.SearchView
.
Como está utilizando la biblioteca de soporte, debe usar el espacio de nombres de la biblioteca de soporte para establecer el atributo
actionViewClass
.
Su archivo xml debería verse así:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/action_search"
android:title="@string/action_search"
app:actionViewClass="android.support.v7.widget.SearchView"
app:showAsAction="always"/>
</menu>
En su
Fragment
o
Activity
, debe inflar este menú xml como de costumbre, luego puede buscar el
MenuItem
que contiene
SearchView
e implementar
OnQueryTextListener
que vamos a usar para escuchar los cambios en el texto ingresado en
SearchView
:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main, menu);
final MenuItem searchItem = menu.findItem(R.id.action_search);
final SearchView searchView = (SearchView) searchItem.getActionView();
searchView.setOnQueryTextListener(this);
return true;
}
@Override
public boolean onQueryTextChange(String query) {
// Here is where we are going to implement the filter logic
return false;
}
@Override
public boolean onQueryTextSubmit(String query) {
return false;
}
Y ahora el
SearchView
está listo para ser utilizado.
Implementaremos la lógica del filtro más adelante en
onQueryTextChange()
una vez que hayamos terminado de implementar el
Adapter
.
Configurar el
Adapter
En primer lugar, esta es la clase de modelo que voy a usar para este ejemplo:
public class ExampleModel {
private final long mId;
private final String mText;
public ExampleModel(long id, String text) {
mId = id;
mText = text;
}
public long getId() {
return mId;
}
public String getText() {
return mText;
}
}
Es solo su modelo básico que mostrará un texto en
RecyclerView
.
Este es el diseño que voy a usar para mostrar el texto:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="model"
type="com.github.wrdlbrnft.searchablerecyclerviewdemo.ui.models.ExampleModel"/>
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:text="@{model.text}"/>
</FrameLayout>
</layout>
Como puede ver, uso el enlace de datos. Si nunca antes ha trabajado con el enlace de datos, ¡no se desanime! Es muy simple y poderoso, sin embargo, no puedo explicar cómo funciona en el alcance de esta respuesta.
Este es el
ViewHolder
para la clase
ExampleModel
:
public class ExampleViewHolder extends RecyclerView.ViewHolder {
private final ItemExampleBinding mBinding;
public ExampleViewHolder(ItemExampleBinding binding) {
super(binding.getRoot());
mBinding = binding;
}
public void bind(ExampleModel item) {
mBinding.setModel(item);
}
}
De nuevo nada especial. Solo usa el enlace de datos para vincular la clase de modelo a este diseño como hemos definido en el diseño xml anterior.
Ahora finalmente podemos llegar a la parte realmente interesante: escribir el adaptador.
Voy a omitir la implementación básica del
Adapter
y, en cambio, me concentraré en las partes que son relevantes para esta respuesta.
Pero primero hay una cosa de la que tenemos que hablar: la clase
SortedList
.
SortedList
SortedList
es una herramienta completamente sorprendente que forma parte de la biblioteca
RecyclerView
.
Se encarga de notificar al
Adapter
sobre los cambios en el conjunto de datos y lo hace de una manera muy eficiente.
Lo único que debe hacer es especificar un orden de los elementos.
SortedList
hacerlo implementando un método
compare()
que compare dos elementos en
SortedList
como un
Comparator
.
¡Pero en lugar de ordenar una
List
, se usa para ordenar los elementos en
RecyclerView
!
SortedList
interactúa con el
Adapter
través de una clase de
Callback
que debe implementar:
private final SortedList.Callback<ExampleModel> mCallback = new SortedList.Callback<ExampleModel>() {
@Override
public void onInserted(int position, int count) {
mAdapter.notifyItemRangeInserted(position, count);
}
@Override
public void onRemoved(int position, int count) {
mAdapter.notifyItemRangeRemoved(position, count);
}
@Override
public void onMoved(int fromPosition, int toPosition) {
mAdapter.notifyItemMoved(fromPosition, toPosition);
}
@Override
public void onChanged(int position, int count) {
mAdapter.notifyItemRangeChanged(position, count);
}
@Override
public int compare(ExampleModel a, ExampleModel b) {
return mComparator.compare(a, b);
}
@Override
public boolean areContentsTheSame(ExampleModel oldItem, ExampleModel newItem) {
return oldItem.equals(newItem);
}
@Override
public boolean areItemsTheSame(ExampleModel item1, ExampleModel item2) {
return item1.getId() == item2.getId();
}
}
En los métodos en la parte superior de la devolución de llamada, como
onMoved
,
onInserted
, etc., debe llamar al método de notificación equivalente de su
Adapter
.
Los tres métodos en la parte inferior
compare
,
areContentsTheSame
y
areItemsTheSame
que debe implementar de acuerdo con el tipo de objetos que desea mostrar y en qué orden deben aparecer estos objetos en la pantalla.
Veamos estos métodos uno por uno:
@Override
public int compare(ExampleModel a, ExampleModel b) {
return mComparator.compare(a, b);
}
Este es el método
compare()
que hablé anteriormente.
En este ejemplo, solo estoy pasando la llamada a un
Comparator
que compara los dos modelos.
Si desea que los elementos aparezcan en orden alfabético en la pantalla.
Este comparador podría verse así:
private static final Comparator<ExampleModel> ALPHABETICAL_COMPARATOR = new Comparator<ExampleModel>() {
@Override
public int compare(ExampleModel a, ExampleModel b) {
return a.getText().compareTo(b.getText());
}
};
Ahora echemos un vistazo al siguiente método:
@Override
public boolean areContentsTheSame(ExampleModel oldItem, ExampleModel newItem) {
return oldItem.equals(newItem);
}
El propósito de este método es determinar si el contenido de un modelo ha cambiado.
SortedList
usa esto para determinar si se debe invocar un evento de cambio, en otras palabras, si
RecyclerView
debe
SortedList
versión antigua y la nueva.
Si las clases de modelo tienen una implementación correcta de
equals()
y
hashCode()
, generalmente puede implementarla como se
hashCode()
anteriormente.
Si agregamos una implementación
equals()
y
hashCode()
a la clase
ExampleModel
, debería verse así:
public class ExampleModel implements SortedListAdapter.ViewModel {
private final long mId;
private final String mText;
public ExampleModel(long id, String text) {
mId = id;
mText = text;
}
public long getId() {
return mId;
}
public String getText() {
return mText;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ExampleModel model = (ExampleModel) o;
if (mId != model.mId) return false;
return mText != null ? mText.equals(model.mText) : model.mText == null;
}
@Override
public int hashCode() {
int result = (int) (mId ^ (mId >>> 32));
result = 31 * result + (mText != null ? mText.hashCode() : 0);
return result;
}
}
Nota al margen: ¡La mayoría de los IDE como Android Studio, IntelliJ y Eclipse tienen funcionalidad para generar implementaciones
equals()
y
hashCode()
con solo presionar un botón!
Por lo tanto, no tiene que implementarlos usted mismo.
¡Busca en Internet cómo funciona en tu IDE!
Ahora echemos un vistazo al último método:
@Override
public boolean areItemsTheSame(ExampleModel item1, ExampleModel item2) {
return item1.getId() == item2.getId();
}
SortedList
utiliza este método para verificar si dos elementos hacen referencia a la misma cosa.
En términos más simples (sin explicar cómo funciona
SortedList
), esto se utiliza para determinar si un objeto ya está contenido en la
List
y si es necesario reproducir una animación de agregar, mover o cambiar.
Si sus modelos tienen una identificación, generalmente compararía solo la identificación en este método.
Si no lo hacen, debe encontrar otra forma de verificar esto, pero, sin embargo, termina implementando esto dependiendo de su aplicación específica.
Por lo general, es la opción más simple para dar a todos los modelos una identificación, que podría ser, por ejemplo, el campo de clave principal si está consultando los datos de una base de datos.
Con
SortedList.Callback
implementado correctamente, podemos crear una instancia de
SortedList
:
final SortedList<ExampleModel> list = new SortedList<>(ExampleModel.class, mCallback);
Como primer parámetro en el constructor de
SortedList
, debe pasar la clase de sus modelos.
El otro parámetro es solo el
SortedList.Callback
que definimos anteriormente.
Ahora pasemos a los negocios: si implementamos el
Adapter
con una
SortedList
, debería verse así:
public class ExampleAdapter extends RecyclerView.Adapter<ExampleViewHolder> {
private final SortedList<ExampleModel> mSortedList = new SortedList<>(ExampleModel.class, new SortedList.Callback<ExampleModel>() {
@Override
public int compare(ExampleModel a, ExampleModel b) {
return mComparator.compare(a, b);
}
@Override
public void onInserted(int position, int count) {
notifyItemRangeInserted(position, count);
}
@Override
public void onRemoved(int position, int count) {
notifyItemRangeRemoved(position, count);
}
@Override
public void onMoved(int fromPosition, int toPosition) {
notifyItemMoved(fromPosition, toPosition);
}
@Override
public void onChanged(int position, int count) {
notifyItemRangeChanged(position, count);
}
@Override
public boolean areContentsTheSame(ExampleModel oldItem, ExampleModel newItem) {
return oldItem.equals(newItem);
}
@Override
public boolean areItemsTheSame(ExampleModel item1, ExampleModel item2) {
return item1.getId() == item2.getId();
}
});
private final LayoutInflater mInflater;
private final Comparator<ExampleModel> mComparator;
public ExampleAdapter(Context context, Comparator<ExampleModel> comparator) {
mInflater = LayoutInflater.from(context);
mComparator = comparator;
}
@Override
public ExampleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
final ItemExampleBinding binding = ItemExampleBinding.inflate(inflater, parent, false);
return new ExampleViewHolder(binding);
}
@Override
public void onBindViewHolder(ExampleViewHolder holder, int position) {
final ExampleModel model = mSortedList.get(position);
holder.bind(model);
}
@Override
public int getItemCount() {
return mSortedList.size();
}
}
El
Comparator
utilizado para ordenar el elemento se pasa a través del constructor para que podamos usar el mismo
Adapter
incluso si se supone que los elementos se muestran en un orden diferente.
¡Ya casi hemos terminado!
Pero primero necesitamos una forma de agregar o quitar elementos al
Adapter
.
Para este propósito, podemos agregar métodos al
Adapter
que nos permiten agregar y eliminar elementos a la lista
SortedList
:
public void add(ExampleModel model) {
mSortedList.add(model);
}
public void remove(ExampleModel model) {
mSortedList.remove(model);
}
public void add(List<ExampleModel> models) {
mSortedList.addAll(models);
}
public void remove(List<ExampleModel> models) {
mSortedList.beginBatchedUpdates();
for (ExampleModel model : models) {
mSortedList.remove(model);
}
mSortedList.endBatchedUpdates();
}
No necesitamos llamar a ningún método de notificación aquí porque
SortedList
ya lo hace a través de
SortedList.Callback
!
Aparte de eso, la implementación de estos métodos es bastante sencilla, con una excepción: el método remove que elimina una
List
de modelos.
Dado que
SortedList
tiene solo un método de eliminación que puede eliminar un solo objeto, necesitamos recorrer la lista y eliminar los modelos uno por uno.
Llamar a
beginBatchedUpdates()
al principio agrupa todos los cambios que haremos en
SortedList
y mejora el rendimiento.
Cuando llamamos a
endBatchedUpdates()
RecyclerView
es notificado sobre todos los cambios a la vez.
Además, lo que debe comprender es que si agrega un objeto a
SortedList
y ya está en
SortedList
, no se volverá a agregar.
En su lugar,
SortedList
utiliza el método
areContentsTheSame()
para determinar si el objeto ha cambiado, y si tiene el elemento en
RecyclerView
se actualizará.
De todos modos, lo que generalmente prefiero es un método que me permite reemplazar todos los elementos en
RecyclerView
a la vez.
Elimine todo lo que no esté en la
List
y agregue todos los elementos que faltan en la
SortedList
:
public void replaceAll(List<ExampleModel> models) {
mSortedList.beginBatchedUpdates();
for (int i = mSortedList.size() - 1; i >= 0; i--) {
final ExampleModel model = mSortedList.get(i);
if (!models.contains(model)) {
mSortedList.remove(model);
}
}
mSortedList.addAll(models);
mSortedList.endBatchedUpdates();
}
Este método vuelve a agrupar todas las actualizaciones para aumentar el rendimiento.
El primer bucle es inverso, ya que eliminar un elemento al principio estropearía los índices de todos los elementos que aparecen después y esto puede conducir en algunos casos a problemas como inconsistencias de datos.
Después de eso, simplemente agregamos la
List
a
SortedList
usando
addAll()
para agregar todos los elementos que aún no están en
SortedList
y, tal como describí anteriormente, actualiza todos los elementos que ya están en
SortedList
pero que han cambiado.
Y con eso el
Adapter
está completo.
Todo debería verse así:
public class ExampleAdapter extends RecyclerView.Adapter<ExampleViewHolder> {
private final SortedList<ExampleModel> mSortedList = new SortedList<>(ExampleModel.class, new SortedList.Callback<ExampleModel>() {
@Override
public int compare(ExampleModel a, ExampleModel b) {
return mComparator.compare(a, b);
}
@Override
public void onInserted(int position, int count) {
notifyItemRangeInserted(position, count);
}
@Override
public void onRemoved(int position, int count) {
notifyItemRangeRemoved(position, count);
}
@Override
public void onMoved(int fromPosition, int toPosition) {
notifyItemMoved(fromPosition, toPosition);
}
@Override
public void onChanged(int position, int count) {
notifyItemRangeChanged(position, count);
}
@Override
public boolean areContentsTheSame(ExampleModel oldItem, ExampleModel newItem) {
return oldItem.equals(newItem);
}
@Override
public boolean areItemsTheSame(ExampleModel item1, ExampleModel item2) {
return item1 == item2;
}
});
private final Comparator<ExampleModel> mComparator;
private final LayoutInflater mInflater;
public ExampleAdapter(Context context, Comparator<ExampleModel> comparator) {
mInflater = LayoutInflater.from(context);
mComparator = comparator;
}
@Override
public ExampleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
final ItemExampleBinding binding = ItemExampleBinding.inflate(mInflater, parent, false);
return new ExampleViewHolder(binding);
}
@Override
public void onBindViewHolder(ExampleViewHolder holder, int position) {
final ExampleModel model = mSortedList.get(position);
holder.bind(model);
}
public void add(ExampleModel model) {
mSortedList.add(model);
}
public void remove(ExampleModel model) {
mSortedList.remove(model);
}
public void add(List<ExampleModel> models) {
mSortedList.addAll(models);
}
public void remove(List<ExampleModel> models) {
mSortedList.beginBatchedUpdates();
for (ExampleModel model : models) {
mSortedList.remove(model);
}
mSortedList.endBatchedUpdates();
}
public void replaceAll(List<ExampleModel> models) {
mSortedList.beginBatchedUpdates();
for (int i = mSortedList.size() - 1; i >= 0; i--) {
final ExampleModel model = mSortedList.get(i);
if (!models.contains(model)) {
mSortedList.remove(model);
}
}
mSortedList.addAll(models);
mSortedList.endBatchedUpdates();
}
@Override
public int getItemCount() {
return mSortedList.size();
}
}
¡Lo único que falta ahora es implementar el filtrado!
Implementando la lógica del filtro
Para implementar la lógica de filtro, primero tenemos que definir una
List
de todos los modelos posibles.
Para este ejemplo, creo una
List
de instancias de
ExampleModel
partir de una matriz de películas:
private static final String[] MOVIES = new String[]{
...
};
private static final Comparator<ExampleModel> ALPHABETICAL_COMPARATOR = new Comparator<ExampleModel>() {
@Override
public int compare(ExampleModel a, ExampleModel b) {
return a.getText().compareTo(b.getText());
}
};
private ExampleAdapter mAdapter;
private List<ExampleModel> mModels;
private RecyclerView mRecyclerView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
mAdapter = new ExampleAdapter(this, ALPHABETICAL_COMPARATOR);
mBinding.recyclerView.setLayoutManager(new LinearLayoutManager(this));
mBinding.recyclerView.setAdapter(mAdapter);
mModels = new ArrayList<>();
for (String movie : MOVIES) {
mModels.add(new ExampleModel(movie));
}
mAdapter.add(mModels);
}
No pasa nada especial aquí, solo instanciamos el
Adapter
y lo configuramos en
RecyclerView
.
Después de eso, creamos una
List
de modelos a partir de los nombres de las películas en la matriz
MOVIES
.
Luego agregamos todos los modelos a
SortedList
.
Ahora podemos volver a
onQueryTextChange()
que definimos anteriormente y comenzar a implementar la lógica de filtro:
@Override
public boolean onQueryTextChange(String query) {
final List<ExampleModel> filteredModelList = filter(mModels, query);
mAdapter.replaceAll(filteredModelList);
mBinding.recyclerView.scrollToPosition(0);
return true;
}
Esto es nuevamente bastante sencillo.
Llamamos al método
filter()
y pasamos la
List
de
ExampleModel
s, así como la cadena de consulta.
Luego llamamos
replaceAll()
en el
Adapter
y pasamos la
List
filtrada que devuelve
filter()
.
También tenemos que llamar a
scrollToPosition(0)
en
RecyclerView
para asegurarnos de que el usuario siempre pueda ver todos los elementos al buscar algo.
De lo contrario,
RecyclerView
podría permanecer desplazado hacia abajo mientras se filtra y posteriormente ocultar algunos elementos.
Desplazarse hacia la parte superior garantiza una mejor experiencia de usuario durante la búsqueda.
Lo único que queda por hacer ahora es implementar
filter()
sí:
private static List<ExampleModel> filter(List<ExampleModel> models, String query) {
final String lowerCaseQuery = query.toLowerCase();
final List<ExampleModel> filteredModelList = new ArrayList<>();
for (ExampleModel model : models) {
final String text = model.getText().toLowerCase();
if (text.contains(lowerCaseQuery)) {
filteredModelList.add(model);
}
}
return filteredModelList;
}
Lo primero que hacemos aquí es llamar a
toLowerCase()
en la cadena de consulta.
No queremos que nuestra función de búsqueda
toLowerCase()
mayúsculas y minúsculas y al llamar a
toLowerCase()
en todas las cadenas que comparamos, podemos asegurarnos de que devolvemos los mismos resultados independientemente del caso.
Luego solo recorre todos los modelos de la
List
que pasamos y verifica si la cadena de consulta está contenida en el texto del modelo.
Si es así, el modelo se agrega a la
List
filtrada.
¡Y eso es! El código anterior se ejecutará en el nivel de API 7 y superior y, a partir del nivel de API 11, ¡obtendrá animaciones de elementos gratis!
Me doy cuenta de que esta es una descripción muy detallada que probablemente hace que todo esto parezca más complicado de lo que realmente es, pero hay una manera en que podemos generalizar todo este problema y hacer que la implementación de un
Adapter
basado en una lista
SortedList
mucho más simple.
Generalizando el problema y simplificando el adaptador
En esta sección no voy a entrar en muchos detalles, en parte porque me estoy enfrentando al límite de caracteres para las respuestas en , pero también porque la mayor parte ya se explicó anteriormente, pero para resumir los cambios: podemos implementar un
Adapter
base clase que ya se ocupa de manejar tanto la
SortedList
como los modelos vinculantes para
ViewHolder
instancias de
ViewHolder
y proporciona una forma conveniente de implementar un
Adapter
basado en una
SortedList
.
Para eso tenemos que hacer dos cosas:
-
Necesitamos crear una interfaz
ViewModel
que todas las clases de modelos tengan que implementar -
Necesitamos crear una subclase
ViewHolder
que defina un métodobind()
que elAdapter
pueda usar para vincular modelos automáticamente.
Esto nos permite centrarnos en el contenido que se supone que se mostrará en
RecyclerView
simplemente implementando los modelos y las implementaciones de
ViewHolder
correspondientes.
Al usar esta clase base, no tenemos que preocuparnos por los intrincados detalles del
Adapter
y su
SortedList
.
SortedListAdapter
Debido al límite de caracteres para las respuestas en , no puedo seguir cada paso de implementación de esta clase base o incluso agregar el código fuente completo aquí, pero puede encontrar el código fuente completo de esta clase base, lo llamé
SortedListAdapter
, en este
GitHub Gist
.
Para simplificar su vida, ¡he publicado una biblioteca en jCenter que contiene el
SortedListAdapter
!
Si desea usarlo, todo lo que necesita hacer es agregar esta dependencia al archivo build.gradle de su aplicación:
compile ''com.github.wrdlbrnft:sorted-list-adapter:0.2.0.1''
Puede encontrar más información sobre esta biblioteca en la página de inicio de la biblioteca .
Usando el SortedListAdapter
Para usar
SortedListAdapter
tenemos que hacer dos cambios:
-
Cambie el
ViewHolder
para que se extiendaSortedListAdapter.ViewHolder
. El parámetro type debería ser el modelo que debería estar vinculado a esteViewHolder
, en este casoExampleModel
.performBind()
vincular datos a sus modelos enperformBind()
lugar debind()
.public class ExampleViewHolder extends SortedListAdapter.ViewHolder<ExampleModel> { private final ItemExampleBinding mBinding; public ExampleViewHolder(ItemExampleBinding binding) { super(binding.getRoot()); mBinding = binding; } @Override protected void performBind(ExampleModel item) { mBinding.setModel(item); } }
-
Asegúrese de que todos sus modelos implementen la interfaz
ViewModel
:public class ExampleModel implements SortedListAdapter.ViewModel { ... }
Después de eso, solo tenemos que actualizar el
ExampleAdapter
para extender
SortedListAdapter
y eliminar todo lo que ya no necesitamos.
El parámetro type debería ser el tipo de modelo con el que está trabajando, en este caso
ExampleModel
.
Pero si está trabajando con diferentes tipos de modelos, configure el parámetro de tipo en
ViewModel
.
public class ExampleAdapter extends SortedListAdapter<ExampleModel> {
public ExampleAdapter(Context context, Comparator<ExampleModel> comparator) {
super(context, ExampleModel.class, comparator);
}
@Override
protected ViewHolder<? extends ExampleModel> onCreateViewHolder(LayoutInflater inflater, ViewGroup parent, int viewType) {
final ItemExampleBinding binding = ItemExampleBinding.inflate(inflater, parent, false);
return new ExampleViewHolder(binding);
}
@Override
protected boolean areItemsTheSame(ExampleModel item1, ExampleModel item2) {
return item1.getId() == item2.getId();
}
@Override
protected boolean areItemContentsTheSame(ExampleModel oldItem, ExampleModel newItem) {
return oldItem.equals(newItem);
}
}
Después de eso hemos terminado!
Sin embargo, una última cosa para mencionar:
SortedListAdapter
no tiene los mismos métodos
add()
,
remove()
o
replaceAll()
que tenía nuestro
ExampleAdapter
original.
Utiliza un objeto
Editor
separado para modificar los elementos de la lista a los que se puede acceder mediante el método
edit()
.
Entonces, si desea eliminar o agregar elementos, debe llamar a
edit()
luego agregue y elimine los elementos en esta instancia del
Editor
y una vez que haya terminado, llame a
commit()
para aplicar los cambios a
SortedList
:
mAdapter.edit()
.remove(modelToRemove)
.add(listOfModelsToAdd)
.commit();
Todos los cambios que realice de esta manera se agrupan para aumentar el rendimiento.
El método
replaceAll()
que implementamos en los capítulos anteriores también está presente en este objeto
Editor
:
mAdapter.edit()
.replaceAll(mModels)
.commit();
Si olvida llamar a
commit()
, ¡ninguno de sus cambios se aplicará!
Con los componentes de arquitectura de Android mediante el uso de LiveData, esto se puede implementar fácilmente con cualquier tipo de adaptador . Simplemente tiene que hacer los siguientes pasos:
1. Configure sus datos para regresar de la base de datos de la sala como LiveData como en el siguiente ejemplo:
@Dao
public interface CustomDAO{
@Query("SELECT * FROM words_table WHERE column LIKE :searchquery")
public LiveData<List<Word>> searchFor(String searchquery);
}
2. Cree un objeto ViewModel para actualizar sus datos en vivo a través de un método que conectará su DAO y su IU
public class CustomViewModel extends AndroidViewModel {
private final AppDatabase mAppDatabase;
public WordListViewModel(@NonNull Application application) {
super(application);
this.mAppDatabase = AppDatabase.getInstance(application.getApplicationContext());
}
public LiveData<List<Word>> searchQuery(String query) {
return mAppDatabase.mWordDAO().searchFor(query);
}
}
3. Llame sus datos desde ViewModel sobre la marcha pasando la consulta a través de onQueryTextListener como se muestra a continuación:
Dentro
onCreateOptionsMenu
configure a su oyente de la siguiente manera
searchView.setOnQueryTextListener(onQueryTextListener);
Configure su escucha de consulta en algún lugar de su clase SearchActivity de la siguiente manera
private android.support.v7.widget.SearchView.OnQueryTextListener onQueryTextListener =
new android.support.v7.widget.SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
getResults(query);
return true;
}
@Override
public boolean onQueryTextChange(String newText) {
getResults(newText);
return true;
}
private void getResults(String newText) {
String queryText = "%" + newText + "%";
mCustomViewModel.searchQuery(queryText).observe(
SearchResultsActivity.this, new Observer<List<Word>>() {
@Override
public void onChanged(@Nullable List<Word> words) {
if (words == null) return;
searchAdapter.submitList(words);
}
});
}
};
Nota : Los pasos (1.) y (2.) son la implementación estándar de AAC ViewModel y DAO , la única "magia" real que está sucediendo aquí está en OnQueryTextListener, que actualizará los resultados de su lista dinámicamente a medida que cambie el texto de la consulta.
Si necesita más aclaraciones sobre el asunto, no dude en preguntar. Espero que esto haya ayudado :).
En adaptador:
public void setFilter(List<Channel> newList){
mChannels = new ArrayList<>();
mChannels.addAll(newList);
notifyDataSetChanged();
}
En actividad:
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
return false;
}
@Override
public boolean onQueryTextChange(String newText) {
newText = newText.toLowerCase();
ArrayList<Channel> newList = new ArrayList<>();
for (Channel channel: channels){
String channelName = channel.getmChannelName().toLowerCase();
if (channelName.contains(newText)){
newList.add(channel);
}
}
mAdapter.setFilter(newList);
return true;
}
});
Esta es mi opinión sobre la expansión de la respuesta @klimat para no perder la animación de filtrado.
public void filter(String query){
int completeListIndex = 0;
int filteredListIndex = 0;
while (completeListIndex < completeList.size()){
Movie item = completeList.get(completeListIndex);
if(item.getName().toLowerCase().contains(query)){
if(filteredListIndex < filteredList.size()) {
Movie filter = filteredList.get(filteredListIndex);
if (!item.getName().equals(filter.getName())) {
filteredList.add(filteredListIndex, item);
notifyItemInserted(filteredListIndex);
}
}else{
filteredList.add(filteredListIndex, item);
notifyItemInserted(filteredListIndex);
}
filteredListIndex++;
}
else if(filteredListIndex < filteredList.size()){
Movie filter = filteredList.get(filteredListIndex);
if (item.getName().equals(filter.getName())) {
filteredList.remove(filteredListIndex);
notifyItemRemoved(filteredListIndex);
}
}
completeListIndex++;
}
}
Básicamente, lo que hace es mirar a través de una lista completa y agregar / eliminar elementos a una lista filtrada uno por uno.
He resuelto el mismo problema usando el enlace con algunas modificaciones. Filtro de búsqueda en RecyclerView con tarjetas. ¿Es posible? (espero que esto ayude).
Aquí está mi clase de adaptador
public class ContactListRecyclerAdapter extends RecyclerView.Adapter<ContactListRecyclerAdapter.ContactViewHolder> implements Filterable {
Context mContext;
ArrayList<Contact> customerList;
ArrayList<Contact> parentCustomerList;
public ContactListRecyclerAdapter(Context context,ArrayList<Contact> customerList)
{
this.mContext=context;
this.customerList=customerList;
if(customerList!=null)
parentCustomerList=new ArrayList<>(customerList);
}
// other overrided methods
@Override
public Filter getFilter() {
return new FilterCustomerSearch(this,parentCustomerList);
}
}
// clase de filtro
import android.widget.Filter;
import java.util.ArrayList;
public class FilterCustomerSearch extends Filter
{
private final ContactListRecyclerAdapter mAdapter;
ArrayList<Contact> contactList;
ArrayList<Contact> filteredList;
public FilterCustomerSearch(ContactListRecyclerAdapter mAdapter,ArrayList<Contact> contactList) {
this.mAdapter = mAdapter;
this.contactList=contactList;
filteredList=new ArrayList<>();
}
@Override
protected FilterResults performFiltering(CharSequence constraint) {
filteredList.clear();
final FilterResults results = new FilterResults();
if (constraint.length() == 0) {
filteredList.addAll(contactList);
} else {
final String filterPattern = constraint.toString().toLowerCase().trim();
for (final Contact contact : contactList) {
if (contact.customerName.contains(constraint)) {
filteredList.add(contact);
}
else if (contact.emailId.contains(constraint))
{
filteredList.add(contact);
}
else if(contact.phoneNumber.contains(constraint))
filteredList.add(contact);
}
}
results.values = filteredList;
results.count = filteredList.size();
return results;
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
mAdapter.customerList.clear();
mAdapter.customerList.addAll((ArrayList<Contact>) results.values);
mAdapter.notifyDataSetChanged();
}
}
// clase de actividad
public class HomeCrossFadeActivity extends AppCompatActivity implements View.OnClickListener,OnFragmentInteractionListener,OnTaskCompletedListner
{
Fragment fragment;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_homecrossfadeslidingpane2);CardView mCard;
setContentView(R.layout.your_main_xml);}
//other overrided methods
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
MenuInflater inflater = getMenuInflater();
// Inflate menu to add items to action bar if it is present.
inflater.inflate(R.menu.menu_customer_view_and_search, menu);
// Associate searchable configuration with the SearchView
SearchManager searchManager =
(SearchManager) getSystemService(Context.SEARCH_SERVICE);
SearchView searchView =
(SearchView) menu.findItem(R.id.menu_search).getActionView();
searchView.setQueryHint("Search Customer");
searchView.setSearchableInfo(
searchManager.getSearchableInfo(getComponentName()));
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
return false;
}
@Override
public boolean onQueryTextChange(String newText) {
if(fragment instanceof CustomerDetailsViewWithModifyAndSearch)
((CustomerDetailsViewWithModifyAndSearch)fragment).adapter.getFilter().filter(newText);
return false;
}
});
return true;
}
}
En el método OnQueryTextChangeListener () use su adaptador. Lo he fragmentado ya que mi adpter está fragmentado. Puede usar el adaptador directamente si está en su clase de actividad.
Recomiendo modificar la solución de @Xaver Kapeller con 2 cosas a continuación para evitar un problema después de que borró el texto buscado (el filtro ya no funciona) debido a que la lista de la parte posterior del adaptador tiene un tamaño menor que la lista de filtros y sucedió la IndexOutOfBoundsException. Entonces, el código debe modificarse como se muestra a continuación
public void addItem(int position, ExampleModel model) {
if(position >= mModel.size()) {
mModel.add(model);
notifyItemInserted(mModel.size()-1);
} else {
mModels.add(position, model);
notifyItemInserted(position);
}
}
Y modificar también en la funcionalidad moveItem
public void moveItem(int fromPosition, int toPosition) {
final ExampleModel model = mModels.remove(fromPosition);
if(toPosition >= mModels.size()) {
mModels.add(model);
notifyItemMoved(fromPosition, mModels.size()-1);
} else {
mModels.add(toPosition, model);
notifyItemMoved(fromPosition, toPosition);
}
}
¡Espero que pueda ayudarte!
Siguiendo a @Shruthi Kamoji de una manera más limpia, podemos usar un filtro, es para eso:
public abstract class GenericRecycleAdapter<E> extends RecyclerView.Adapter implements Filterable
{
protected List<E> list;
protected List<E> originalList;
protected Context context;
public GenericRecycleAdapter(Context context,
List<E> list)
{
this.originalList = list;
this.list = list;
this.context = context;
}
...
@Override
public Filter getFilter() {
return new Filter() {
@SuppressWarnings("unchecked")
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
list = (List<E>) results.values;
notifyDataSetChanged();
}
@Override
protected FilterResults performFiltering(CharSequence constraint) {
List<E> filteredResults = null;
if (constraint.length() == 0) {
filteredResults = originalList;
} else {
filteredResults = getFilteredResults(constraint.toString().toLowerCase());
}
FilterResults results = new FilterResults();
results.values = filteredResults;
return results;
}
};
}
protected List<E> getFilteredResults(String constraint) {
List<E> results = new ArrayList<>();
for (E item : originalList) {
if (item.getName().toLowerCase().contains(constraint)) {
results.add(item);
}
}
return results;
}
}
La E aquí es un tipo genérico, puede extenderla usando su clase:
public class customerAdapter extends GenericRecycleAdapter<CustomerModel>
O simplemente cambie la E al tipo que desee (
<CustomerModel>
por ejemplo)
Luego, desde searchView (el widget que puedes poner en menu.xml):
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String text) {
return false;
}
@Override
public boolean onQueryTextChange(String text) {
yourAdapter.getFilter().filter(text);
return true;
}
});
Todo lo que necesita hacer es agregar un método de
filter
en
RecyclerView.Adapter
:
public void filter(String text) {
items.clear();
if(text.isEmpty()){
items.addAll(itemsCopy);
} else{
text = text.toLowerCase();
for(PhoneBookItem item: itemsCopy){
if(item.name.toLowerCase().contains(text) || item.phone.toLowerCase().contains(text)){
items.add(item);
}
}
}
notifyDataSetChanged();
}
itemsCopy
se inicializa en el constructor del adaptador como
itemsCopy.addAll(items)
.
Si lo hace, simplemente llame al
filter
desde
OnQueryTextListener
:
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
adapter.filter(query);
return true;
}
@Override
public boolean onQueryTextChange(String newText) {
adapter.filter(newText);
return true;
}
});
Es un ejemplo de filtrar mi agenda por nombre y número de teléfono.