studio recyclerview android android-recyclerview searchview android-filterable

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étodo bind() que el Adapter 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 extienda SortedListAdapter.ViewHolder . El parámetro type debería ser el modelo que debería estar vinculado a este ViewHolder , en este caso ExampleModel . performBind() vincular datos a sus modelos en performBind() lugar de bind() .

    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.