Cómo filtrar un RecyclerView con un SearchView

319

Estoy tratando de implementar SearchViewdesde la biblioteca de soporte. Quiero que el usuario utilice SearchViewpara filtrar una Listde las películas en unRecyclerView .

He seguido algunos tutoriales hasta ahora y he agregado el SearchViewal ActionBar, pero no estoy muy seguro de a dónde ir desde aquí. He visto algunos ejemplos, pero ninguno de ellos muestra resultados cuando comienzas a escribir.

Este 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);
        }
    }
}
Jacques Krause
fuente

Respuestas:

913

Introducción

Como no está realmente claro en su pregunta con qué está teniendo problemas exactamente, 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í:

imagen de demostración

Si primero quieres jugar con la aplicación de demostración, puedes instalarla desde Play Store:

Consiguelo en google play

De todos modos, comencemos.


Configurar el SearchView

En la carpeta res/menucree un nuevo archivo llamado main_menu.xml. En él, agregue un elemento y establezca el actionViewClassen 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 actionViewClassatributo. 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 Fragmento Activitytiene que inflar este menú xml como de costumbre, luego puede buscar el MenuItemque contiene SearchViewe implementar el OnQueryTextListenerque 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 SearchViewestá listo para ser utilizado. Implementaremos la lógica del filtro más adelante 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 el 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 ViewHolderpara la ExampleModelclase:

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 deAdapter y, en cambio, me voy a concentrar en las partes que son relevantes para esta respuesta.

Pero primero hay una cosa de la que tenemos que hablar: la SortedListclase.


SortedList

El SortedListes una herramienta totalmente increíble que es parte de la RecyclerViewbiblioteca. Se encarga de notificar los Adaptercambios sobre el conjunto de datos y lo hace de una manera muy eficiente. Lo único que debe hacer es especificar un orden de los elementos. Debe hacer eso implementando un compare()método que compare dos elementos de la SortedListmisma manera que a Comparator. Pero en lugar de ordenar un List, se usa para ordenar los elementos en el RecyclerView!

El SortedListinteractúa con a Adaptertravés de una Callbackclase 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. usted tiene que llamar el equivalente método de su notificar Adapter. Los tres métodos en la parte inferior compare, areContentsTheSameyareItemsTheSame tiene que 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 compare()método del que hablé anteriormente. En este ejemplo, solo estoy pasando la llamada a una Comparatorque 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. Esto lo SortedListutiliza para determinar si es necesario invocar un evento de cambio, en otras palabras, si el RecyclerViewfundido cruza la versión antigua y la nueva. Si ustedes, las clases tienen un modelo correcto equals()y hashCode()la aplicación se puede aplicar por lo general sólo se siente más arriba. Si agregamos una equals()e hashCode()implementación a la ExampleModelclase, 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 equals()e hashCode()implementaciones para usted con solo presionar un botón! Por lo tanto, no tiene que implementarlos usted mismo. ¡Mira 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();
}

El SortedListutiliza este método para comprobar si dos artículos se refieren a la misma cosa. En términos más simples (sin explicar cómo SortedListfunciona) esto se usa para determinar si un objeto ya está contenido en elList animación y si es necesario agregar, mover o cambiar la animación. Si sus modelos tienen una identificación, generalmente compararía solo la identificación en este método. Si no lo hacen, necesita 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 la SortedList.Callbackimplementación correcta, podemos crear una instancia de SortedList:

final SortedList<ExampleModel> list = new SortedList<>(ExampleModel.class, mCallback);

Como primer parámetro en el constructor del SortedList, debe pasar la clase de sus modelos. El otro parámetro es solo el SortedList.Callbackque definimos anteriormente.

Ahora pasemos a los negocios: si implementamos el Adaptercon un SortedListdebería ser algo como esto:

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

Lo que se Comparatorusa para ordenar el elemento se pasa a través del constructor para que podamos usar el mismo Adapterincluso si se supone que los elementos se muestran en un orden diferente.

¡Ya casi hemos terminado! Pero primero necesitamos una forma de agregar o eliminar elementos al Adapter. Para este propósito, podemos agregar métodos Adapterque nos permiten agregar y eliminar elementos a 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 SortedListya 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 algunos Listde los modelos. Dado que SortedListsolo tiene un método de eliminación que puede eliminar un solo objeto, necesitamos recorrer la lista y eliminar los modelos uno por uno. Llamar beginBatchedUpdates()al principio agrupa todos los cambios que haremos SortedListjuntos y mejora el rendimiento. Cuando llamamos a endBatchedUpdates()la RecyclerViewque se notifique acerca de todos los cambios a la vez.

Además, lo que debe comprender es que si agrega un objeto al SortedListy ya está en el SortedList, no se agregará nuevamente. En su lugar, SortedListutiliza el areContentsTheSame()método para determinar si el objeto ha cambiado, y si tiene el elemento RecyclerView, se actualizará.

De todos modos, lo que generalmente prefiero es un método que me permite reemplazar todos los elementos a la RecyclerViewvez. Elimine todo lo que no esté en el Listy agregue todos los elementos que faltan en 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 el Listal SortedListuso addAll()para agregar todos los elementos que aún no están en el SortedListy, tal como describí anteriormente, actualizar todos los elementos que ya están en el SortedListpero que han cambiado.

Y con eso Adapterse completa. 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 del filtro, primero tenemos que definir uno Listde todos los modelos posibles. Para este ejemplo se crea una Listde ExampleModellas instancias de una serie 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 Adaptery lo configuramos en RecyclerView. Después de eso creamos una Listde modelos a partir de los nombres de las películas en la MOVIESmatriz. Luego agregamos todos los modelos al SortedList.

Ahora podemos volver a onQueryTextChange()lo 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 el pase en el Listde ExampleModels, así como la cadena de consulta. Luego llamamos replaceAll()al Adaptery pasamos el filtrado Listdevuelto por filter(). También tenemos que llamar scrollToPosition(0)al RecyclerViewasegurar que el usuario siempre puede ver todos los elementos en la búsqueda de algo. De lo contrario, RecyclerViewpodría permanecer en una posición desplazada hacia abajo mientras se filtra y posteriormente ocultar algunos elementos. Desplazarse hacia arriba asegura una mejor experiencia de usuario mientras busca.

Lo único que queda por hacer ahora es implementarse filter():

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 toLowerCase()a la cadena de consulta. No queremos que nuestra función de búsqueda distinga entre mayúsculas y minúsculas y, al invocar toLowerCase()todas las cadenas que comparamos, podemos asegurarnos de que arrojemos los mismos resultados independientemente del caso. Luego solo itera a través de todos los modelos Listque pasamos y verifica si la cadena de consulta está contenida en el texto del modelo. Si es así, el modelo se agrega al filtrado List.

¡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 de forma gratuita!

Soy consciente 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 que podemos generalizar todo este problema y hacer que la implementación de una Adapterbase a una SortedListmucho 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 Stack Overflow, pero también porque la mayoría ya se explicó anteriormente, pero para resumir los cambios: podemos implementar una Adapterclase base que ya se encarga de tratar SortedListtanto los modelos vinculantes como los de ViewHolderinstancias y proporciona una forma conveniente de implementar un Adapterbasado en a SortedList. Para eso tenemos que hacer dos cosas:

  • Necesitamos crear una ViewModelinterfaz que todas las clases de modelos tengan que implementar
  • Necesitamos crear una ViewHoldersubclase que defina un bind()método que Adapterpueda usar para vincular modelos automáticamente.

Esto nos permite centrarnos solo en el contenido que se supone que se muestra en la RecyclerViewimplementación de los modelos y las ViewHolderimplementaciones correspondientes . Al usar esta clase base, no tenemos que preocuparnos por los intrincados detalles del Adaptery sus SortedList.

SortedListAdapter

Debido al límite de caracteres para las respuestas en StackOverflow, no puedo seguir cada paso de la implementación de esta clase base o incluso agregar el código fuente completo aquí, pero puedes 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 el SortedListAdaptertenemos que hacer dos cambios:

  • Cambia el ViewHolderpara que se extienda SortedListAdapter.ViewHolder. El parámetro tipo debería ser el modelo que debería estar vinculado a esto ViewHolder, en este caso ExampleModel. Debe 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 ViewModelinterfaz:

    public class ExampleModel implements SortedListAdapter.ViewModel {
        ...
    }

Después de eso, solo tenemos que actualizar ExampleAdapterpara ampliar SortedListAdaptery eliminar todo lo que ya no necesitamos. El parámetro de tipo debe 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: el SortedListAdapterno tiene el mismo add(), remove()o los replaceAll()métodos que ExampleAdaptertenía nuestro original . Utiliza un Editorobjeto separado para modificar los elementos de la lista a los que se puede acceder a través del edit()método. Entonces, si desea eliminar o agregar elementos a los que tiene que llamar edit(), agregue y elimine los elementos en esta Editorinstancia y una vez que haya terminado, invoque 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 replaceAll()método que implementamos en los capítulos anteriores también está presente en este Editorobjeto:

mAdapter.edit()
        .replaceAll(mModels)
        .commit();

Si olvida llamar commit(), ¡ninguno de sus cambios se aplicará!

Xaver Kapeller
fuente
44
@TiagoOliveira Bueno, está hecho para funcionar de inmediato: D El enlace de datos es un obstáculo para las personas que no están familiarizadas con él, pero lo incluí de todos modos porque es increíble y quiero promocionarlo. Por alguna razón, no mucha gente parece saberlo ...
Xaver Kapeller
78
Todavía no he leído toda la respuesta, tuve que pausar mi lectura en algún lugar a la mitad para escribir este comentario, ¡esta es una de las mejores respuestas que he encontrado aquí en el SO! ¡Gracias!
daneejela
16
Me encanta cómo eres: "No está claro en tu pregunta con qué tienes problemas, así que aquí hay un ejemplo completo que acabo de hacer": D
Fred
77
¡+1 solo para mostrarnos que Data Binding existe en Android! Nunca he oído hablar de eso y parece que comenzaré a usarlo. Gracias
Jorge Casariego
66
Esta solución es ridículamente larga y, en general, está sobredimensionada. Ve por el segundo.
Enrico Casini
194

Todo lo que necesitas hacer es agregar un filtermétodo 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();
}

itemsCopyse inicializa en el constructor del adaptador como itemsCopy.addAll(items).

Si lo hace, solo llame filterdesde 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.

klimat
fuente
11
Creo que esta debería ser la respuesta aceptada. Es más simple y simplemente funciona
Jose_GD
66
Simple y eficiente!
AlxDroidDev
11
Tenga en cuenta que pierde la animación si sigue este enfoque en lugar de la respuesta @Xaver Kapeller.
Humazed
23
No probé la respuesta aceptada porque es demasiado larga. Esta respuesta funciona y es fácil de implementar. No olvide agregar "app: actionViewClass =" android.support.v7.widget.SearchView "en su elemento de menú XML.
SajithK
3
¿Qué es exactamente artículos y artículos Copiar aquí?
Lucky_girl
82

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;
    }
});
sagits
fuente
Yo uso algo como esto! Funciona bien y muestra genérica!
Mateus
Hola, quién puede ayudarme paso a paso con este: stackoverflow.com/questions/40754174/…
Thorvald Olavsen
La respuesta más limpia!
adalpari
44
Esto es mucho mejor que la respuesta votada porque la operación se realiza en un subproceso de trabajo en el método performFiltering.
Hmmm
1
Pero asigna una referencia a la misma Lista a diferentes variables. Por ejemplo this.originalList = list; En su lugar, debe usar addAll o pasar la lista en el constructor ArrayList
Florian Walther
5

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;
    }
Zar E Ahmer
fuente
Buena solución Creé dos listas y usé un método de filtro simple. Parece que no puedo pasar la posición correcta del adaptador para un elemento a la siguiente Actividad. Agradecería cualquier pensamiento o idea que pueda sugerir para este: stackoverflow.com/questions/46027110/…
AJW
4

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;
            }
        });
Firoz Ahmed
fuente
3

Con Android Architecture Components 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 que regresen 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 onCreateOptionsMenuconfigure 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 :).

Panos Gr
fuente
1

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.

AhmadF
fuente
0

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 más pequeño que la lista de filtros y ocurrió la excepción 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!

toidv
fuente
Eso no es en absoluto necesario.
Xaver Kapeller
Para la respuesta original si no hace eso, se producirá la IndexOutOfBoundsException, entonces, ¿por qué no es necesario? ¿Quieres un registro? @XaverKapeller
toidv
No, la excepción solo ocurrirá si está implementando de la Adaptermanera incorrecta. Sin ver su código, supongo que el problema más probable es que no esté pasando una copia de la lista con todos los elementos al Adapter.
Xaver Kapeller
El registro de errores: W / System.err: java.lang.IndexOutOfBoundsException: Índice no válido 36, el tamaño es 35 W / System.err: at java.util.ArrayList.throwIndexOutOfBoundsException (ArrayList.java:255) W / System.err: en java.util.ArrayList.add (ArrayList.java:147) W / System.err: en com.quomodo.inploi.ui.adapter.MultipleSelectFilterAdapter.addItem (MultipleSelectFilterAdapter.java:125) W / System.err: en com .quomodo.inploi.ui.adapter.MultipleSelectFilterAdapter.applyAndAnimateAdditions (MultipleSelectFilterAdapter.java:78)
toidv
Por favor, ayuda a verificar el código fuente a continuación @XaverKapeller gist.github.com/toidv/fe71dc45169e4138271b52fdb29420c5
toidv
0

Recyclerview con searchview y clicklistener

Agregue una interfaz en su adaptador.

public interface SelectedUser{

    void selectedUser(UserModel userModel);

}

Implemente la interfaz en su mainactividad y anule el método. @Override public void selectedUser (UserModel userModel) {

    startActivity(new Intent(MainActivity.this, SelectedUserActivity.class).putExtra("data",userModel));



}

Tutorial completo y código fuente: Recyclerview con searchview y onclicklistener

Richard Kamere
fuente
-1

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.

Shruthi Kamoji
fuente