Jak filtrować podgląd RecyclerView za pomocą widoku wyszukiwania

Próbuję zaimplementować SearchView z biblioteki wsparcia. Chcę, aby użytkownik był używać SearchView do filtrowania List filmów w RecyclerView.

Do tej pory wykonałem kilka samouczków i dodałem SearchView do ActionBar, ale nie jestem do końca pewien, dokąd się stąd udać. Widziałem kilka przykładów, ale żaden z nich nie pokazuje wyników podczas pisania.

To jest moje 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);
    }
}

A to jest moje 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);
        }
    }
}
Author: Xaver Kapeller, 2015-05-22

7 answers

Wprowadzenie

Ponieważ nie jest to bardzo jasne forma pytania z czym dokładnie masz problemy, napisałem ten krótki przegląd o tym, jak wdrożyć tę funkcję, jeśli nadal masz pytania, nie krępuj się zadać.

Mam działający przykład wszystkiego, o czym mówię tutaj w tym repozytorium GitHub.
Jeśli chcesz dowiedzieć się więcej o przykładowym projekcie odwiedź projekt Strona główna.

W każdym przypadku wynik powinien wyglądać mniej więcej tak:

obraz demo

Jeśli chcesz najpierw pobawić się z aplikacją demo, możesz ją zainstalować w Sklepie Play:]}

Pobierz w Google Play

W każdym razie zaczynajmy.


Tworzenie SearchView

W folderze res/menu utwórz nowy plik o nazwie main_menu.xml. W nim Dodaj element i ustaw actionViewClass na android.support.v7.widget.SearchView. Ponieważ korzystasz z pomocy technicznej biblioteka musisz użyć przestrzeni nazw biblioteki wsparcia, aby ustawić atrybut actionViewClass. Twój plik xml powinien wyglądać mniej więcej tak:

<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>

W twoim Fragment lub Activity musisz napompować to Menu XML jak zwykle, następnie możesz poszukać MenuItem, który zawiera SearchView i zaimplementować OnQueryTextListener, którego użyjemy do nasłuchania zmian w tekście wprowadzonym do 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) MenuItemCompat.getActionView(searchItem);
    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;
}

A teraz {[25] } jest gotowy do użycia. Zaimplementujemy logikę filtra później w onQueryTextChange() Po zakończeniu implementacji Adapter.


Tworzenie Adapter

Przede wszystkim jest to klasa modelu, której użyję w tym przykładzie:

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

To tylko twój podstawowy model, który wyświetli tekst w RecyclerView. Jest to układ, którego użyję do wyświetlenia tekstu:

<?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>

Jak widzisz używam wiązania danych. Jeśli nigdy wcześniej nie pracowałeś z powiązaniami danych, nie zniechęcaj się! Jest bardzo proste i potężne, jednak nie potrafię wyjaśnić, jak to działa w zakresie tej odpowiedzi.

To jest ViewHolder dla ExampleModel klasy:

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);
    }
}
Znowu nic specjalnego. Po prostu używa wiązania danych, aby powiązać klasę modelu z tym układem, jak zdefiniowaliśmy w powyższym układzie XML.

Teraz możemy wreszcie przejść do naprawdę interesującej części: pisania adaptera. Mam zamiar pominąć podstawową implementację Adapter i zamiast tego zamierzam skoncentrować się na części, które są istotne dla tej odpowiedzi.

Ale najpierw jest jedna rzecz, o której musimy porozmawiać: SortedList klasy.


SortedList

SortedList jest całkowicie niesamowitym narzędziem, które jest częścią RecyclerView biblioteki. Dba o powiadomienie Adapter o zmianach w zbiorze danych i robi to w bardzo skuteczny sposób. Jedyne, co musisz zrobić, to określić kolejność elementów. Musisz to zrobić poprzez wdrożenie compare() metoda, która porównuje dwa elementy w SortedList tak jak Comparator. Ale zamiast sortowania List jest on używany do sortowania elementów w RecyclerView!

SortedList współdziała z Adapter poprzez Callback klasę, którą musisz zaimplementować:

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

W metodach na górze wywołania zwrotnego jak onMoved, onInserted, itd. musisz wywołać równoważną metodę powiadamiania Adapter. Trzy metody na dole compare, areContentsTheSame i areItemsTheSame musisz wdrożyć zgodnie do jakiego rodzaju obiektów chcesz wyświetlić i w jakiej kolejności obiekty te powinny pojawić się na ekranie.

Przejdźmy przez te metody jeden po drugim:

@Override
public int compare(ExampleModel a, ExampleModel b) {
    return mComparator.compare(a, b);
}
To jest Metoda, o której mówiłem wcześniej. W tym przykładzie po prostu przekazuję wywołanie Comparator, które porównuje oba modele. Jeśli chcesz, aby elementy były wyświetlane w porządku alfabetycznym na ekranie. Ten komparator może wyglądać tak:
private static final Comparator<ExampleModel> ALPHABETICAL_COMPARATOR = new Comparator<ExampleModel>() {
    @Override
    public int compare(ExampleModel a, ExampleModel b) {
        return a.getText().compareTo(b.getText());
    }
};

Teraz spójrzmy na następny metoda:

@Override
public boolean areContentsTheSame(ExampleModel oldItem, ExampleModel newItem) {
    return oldItem.equals(newItem);
}

Celem tej metody jest ustalenie, czy zawartość modelu uległa zmianie. SortedList używa tego do określenia, czy zdarzenie change musi zostać wywołane - innymi słowy, jeśli {[41] } powinno skrzyżować starą i nową wersję. Jeśli modelowe klasy mają poprawną implementację equals() i hashCode(), zazwyczaj możesz ją zaimplementować tak jak powyżej. Jeśli dodamy implementację equals() i hashCode() do klasy ExampleModel powinna ona wyglądać tak: to:

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

Szybka Uwaga: Większość IDE, takich jak Android Studio, IntelliJ i Eclipse mają funkcjonalność do generowania equals() i {68]} implementacje dla Ciebie za naciśnięciem przycisku! Więc nie musisz ich wdrażać samodzielnie. Sprawdź w Internecie, jak to działa w IDE!

Przyjrzyjmy się teraz ostatniej metodzie:]}
@Override
public boolean areItemsTheSame(ExampleModel item1, ExampleModel item2) {
    return item1.getId() == item2.getId();
}

SortedList używa tej metody, aby sprawdzić, czy dwa elementy odnoszą się do tej samej rzeczy. W najprostszych słowach (bez wyjaśnienia, jak SortedList Działa) służy do określenia, czy obiekt jest już zawarty w List i czy należy odtworzyć animację dodaj, Przenieś lub zmień. Jeśli twoje modele mają identyfikator, Zwykle porównujesz tylko identyfikator w tej metodzie. Jeśli nie musisz wymyślić innego sposobu, aby to sprawdzić, ale jednak w końcu wdrożysz to zależy od konkretnej aplikacji. Zazwyczaj najprostszą opcją jest nadanie wszystkim modelom identyfikatora - który może być na przykład głównym polem klucza, jeśli odpytywanie danych z bazy danych.

Z SortedList.Callback poprawnie zaimplementowanym możemy utworzyć instancję SortedList:

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

Jako pierwszy parametr w konstruktorze SortedList musisz przejść klasę swoich Modeli. Innym parametrem jest SortedList.Callback zdefiniowany powyżej.

Przejdźmy do rzeczy: jeśli zaimplementujemy Adapter z SortedList powinno to wyglądać mniej więcej tak:
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();
    }
}

Comparator używane do sortowania elementu jest przekazywane in through the constructor, więc możemy użyć tego samego Adapter, nawet jeśli elementy mają być wyświetlane w innej kolejności.

Już prawie skończyliśmy! Ale najpierw potrzebujemy sposobu, aby dodać lub usunąć elementy do Adapter. W tym celu możemy dodać metody do Adapter, które pozwalają nam dodawać i usuwać elementy do 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();
}

Nie musimy tutaj wywoływać żadnych metod powiadamiania, ponieważ SortedList robi to już za pośrednictwem SortedList.Callback! Poza tym realizacja metody te są dość proste z jednym wyjątkiem: metoda remove, która usuwa List modeli. Ponieważ SortedList ma tylko jedną metodę remove, która może usunąć pojedynczy obiekt, musimy zapętlić listę i usunąć modele jeden po drugim. Wywołanie beginBatchedUpdates() na początku partii wszystkich zmian, które zamierzamy wprowadzić do SortedList razem i poprawia wydajność. Kiedy wywołujemy endBatchedUpdates() {[41] } jest powiadamiany o wszystkich zmianach na raz.

Dodatkowo to, co masz aby zrozumieć, że jeśli dodasz obiekt do SortedList i jest już w SortedList, nie zostanie on dodany ponownie. Zamiast tego SortedList używa metody areContentsTheSame(), aby dowiedzieć się, czy obiekt się zmienił - i czy posiada element w RecyclerView zostanie zaktualizowany.

W każdym razie, Zwykle preferuję jedną metodę, która pozwala mi zastąpić wszystkie przedmioty w RecyclerView naraz. Usuń wszystko, czego nie ma w List i dodaj wszystkie elementy, których brakuje w 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();
}

To metoda ponownie grupuje wszystkie aktualizacje razem, aby zwiększyć wydajność. Pierwsza pętla jest odwrotna, ponieważ usunięcie elementu na początku spowoduje bałagan w indeksach wszystkich elementów, które pojawiają się po nim, a to może prowadzić w niektórych przypadkach do problemów, takich jak niespójności danych. Następnie dodajemy List do SortedList używając addAll(), aby dodać wszystkie elementy, które nie są jeszcze w SortedList i - tak jak opisałem powyżej-aktualizujemy wszystkie elementy, które są już w SortedList, ale mają zmieniony.

I z tym Adapter jest kompletna. Całość powinna wyglądać mniej więcej tak:

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

Jedyne czego teraz brakuje to zaimplementowanie filtrowania!


Implementacja logiki filtra

Aby zaimplementować logikę filtra musimy najpierw zdefiniować List wszystkich możliwych modeli. W tym przykładzie tworzę List Z ExampleModel instancji z tablicy filmów:

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

Nic specjalnego się tu nie dzieje, po prostu Utwórz instancję Adapter i ustaw ją na RecyclerView. Następnie tworzymy List modeli z nazw filmów w tablicy MOVIES. Następnie dodajemy wszystkie modele do SortedList.

Teraz możemy wrócić do onQueryTextChange(), które zdefiniowaliśmy wcześniej i rozpocząć implementację logiki filtra:

@Override
public boolean onQueryTextChange(String query) {
    final List<ExampleModel> filteredModelList = filter(mModels, query);
    mAdapter.replaceAll(filteredModelList);
    mBinding.recyclerView.scrollToPosition(0);
    return true;
}

To znowu całkiem proste. Wywołujemy metodę filter() i przekazujemy w List z ExampleModels, jak również łańcuch zapytania. Następnie wywołujemy {[122] } na Adapter i przekazujemy w filtrowanym List zwrócone przez filter(). Musimy również zadzwonić scrollToPosition(0) na RecyclerView, aby upewnić się, że użytkownik zawsze może zobaczyć wszystkie elementy podczas wyszukiwania czegoś. W przeciwnym razie RecyclerView może pozostać w pozycji przewijanej w dół podczas filtrowania, a następnie ukryć kilka elementów. Przewijanie do góry zapewnia lepsze wrażenia użytkownika podczas wyszukiwania.

Jedyne, co pozostało do zrobienia, to zaimplementować filter() siebie:

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

Pierwszą rzeczą, którą tutaj robimy, jest wywołanie toLowerCase() na łańcuchu zapytania. My nie chcemy, aby nasza funkcja wyszukiwania uwzględniała wielkość liter i wywołując toLowerCase() na wszystkich porównywanych łańcuchach możemy zapewnić, że zwracamy te same wyniki niezależnie od wielkości liter. Następnie po prostu iteruje przez wszystkie modele w List przekazaliśmy do niego i sprawdza, czy łańcuch zapytania jest zawarty w tekście modelu. Jeśli tak, to model jest dodawany do filtrowanego List.

I to wszystko! Powyższy kod będzie działał na poziomie API 7 i wyższym, a zaczynając od poziomu API 11 otrzymasz element animacje za darmo!

Zdaję sobie sprawę, że jest to bardzo szczegółowy opis, który prawdopodobnie sprawia, że cała sprawa wydaje się bardziej skomplikowana niż w rzeczywistości jest, ale jest sposób, w jaki możemy uogólnić ten cały problem i uczynić implementację Adapter opartą na {45]} znacznie prostszym.


Uogólnienie problemu i uproszczenie adaptera

W tym dziale Nie będę wdawać się zbytnio w szczegóły-częściowo dlatego, że biegnę z limitem znaków dla odpowiedzi na temat przepełnienia stosu, ale również dlatego, że większość z nich została już wyjaśniona powyżej - ale podsumowując zmiany: możemy zaimplementować klasę base Adapter, która już zajmuje się radzeniem sobie z SortedList, jak również wiążąc modele do ViewHolder instancji i zapewnia wygodny sposób zaimplementowania Adapter w oparciu o SortedList. W tym celu musimy zrobić dwie rzeczy:

  • musimy stworzyć interfejs ViewModel, który wszystkie klasy modelu muszą zaimplementować
  • musimy stworzyć ViewHolder podklasa definiująca metodę bind(), której Adapter może używać do automatycznego wiązania modeli.

Pozwala nam to po prostu skupić się na treści, która ma być wyświetlana w RecyclerView poprzez implementację modeli i odpowiadających im implementacji {42]}. Używając tej klasy bazowej nie musimy się martwić o szczegóły Adapter i jej SortedList.

SortedListAdapter

Ze względu na limit znaków odpowiedzi na StackOverflow I nie można przejść przez każdy etap implementacji tej klasy bazowej ani nawet dodać pełnego kodu źródłowego tutaj, ale można znaleźć Pełny kod źródłowy tej klasy bazowej-nazwałem ją {149]} - w tym GitHub Gist.

Aby ułatwić Ci życie, opublikowałem bibliotekę na jCenter, która zawiera SortedListAdapter! Jeśli chcesz go użyć, wszystko, co musisz zrobić, to dodać tę zależność do kompilacji aplikacji.plik:

compile 'com.github.wrdlbrnft:sorted-list-adapter:0.2.0.1'

Możesz znaleźć więcej informacji na temat ta biblioteka na stronie głównej biblioteki.

Korzystanie z SortedListAdapter

Aby skorzystać z SortedListAdapter musimy wprowadzić dwie zmiany:

  • Zmień ViewHolder tak, aby rozszerzył SortedListAdapter.ViewHolder. Parametrem typu powinien być model, który powinien być związany z tym ViewHolder - w tym przypadku ExampleModel. Musisz powiązać dane z modelami w performBind() zamiast 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);
        }
    }
    
  • Upewnij się, że wszystkie twoje modele wdrażają ViewModel interfejs:

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

Potem musimy tylko zaktualizować ExampleAdapter, aby rozszerzyć SortedListAdapter i usunąć wszystko, czego już nie potrzebujemy. Parametr type powinien być typem modelu, z którym pracujesz-w tym przypadku ExampleModel. Ale jeśli pracujesz z różnymi typami modeli, ustaw parametr type na 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);
    }
}

Potem już po nas! Jednak jeszcze jedna rzecz do wspomnienia: SortedListAdapter nie ma tego samego add(), remove() lub replaceAll() metody nasze oryginalne ExampleAdapter miały. Używa oddzielnego obiektu Editor do modyfikowania pozycji na liście, do których można uzyskać dostęp za pomocą metody edit(). Jeśli chcesz usunąć lub dodać elementy, musisz wywołać edit(), a następnie dodać i usunąć elementy na tej instancji Editor, A gdy skończysz, wywołaj commit() na nim, aby zastosować zmiany do SortedList:

mAdapter.edit()
        .remove(modelToRemove)
        .add(listOfModelsToAdd)
        .commit();

Wszystkie wprowadzone w ten sposób zmiany są zestawiane razem, aby zwiększyć wydajność. Metodę replaceAll() zaimplementowaliśmy w rozdziały powyżej są również obecne na tym Editor obiekcie:

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

Jeśli zapomnisz zadzwonić commit() wtedy żadna z Twoich zmian nie zostanie zastosowana!

 794
Author: Xaver Kapeller,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-09-01 19:48:13

Wystarczy dodać filter metodę w 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 jest inicjalizowana w konstruktorze adaptera jak itemsCopy.addAll(items).

Jeśli to zrobisz, po prostu zadzwoń filter z 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;
    }
});

To przykład z filtrowania mojej książki telefonicznej po imieniu i numerze telefonu.

 143
Author: klimat,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-09-15 22:17:49

Podążając za @ Shruthi Kamoji w czystszy sposób, możemy po prostu użyć filtra, do tego jest przeznaczony:

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

E jest typem generycznym, możesz go rozszerzyć używając swojej klasy:

public class customerAdapter extends GenericRecycleAdapter<CustomerModel>

Lub po prostu zmień E na typ, który chcesz (<CustomerModel> na przykład)

Następnie z searchView (widget można umieścić w 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;
    }
});
 50
Author: sagits,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-06-21 11:25:05

Po prostu utwórz dwie listy w adapterze jedna orignal i jedna temp i implementuje Filtrowalną .

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

Gdzie

public GenericBaseAdapter(Context mContext, List<T> itemList) {
        this.mContext = mContext;
        this.itemList = itemList;
        this.origList = itemList;
    }
 4
Author: Xar E Ahmer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-08-18 11:10:42

Polecam zmodyfikować rozwiązanie @ Xaver Kapeller z 2 rzeczy poniżej, aby uniknąć problemu po wyczyszczeniu przeszukiwanego tekstu (filtr nie działa już) ze względu na liście tył adaptera ma mniejszy rozmiar niż lista filtrów i indexoutofboundsexception stało. Więc kod musi zmodyfikować jak poniżej

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

I modyfikować również w funkcjonalności 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); 
    }
}

Mam nadzieję, że to może Ci pomóc!

 0
Author: toidv,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-08-10 21:42:08

Rozwiązałem ten sam problem używając linku z pewnymi modyfikacjami w nim. Filtr wyszukiwania w RecyclerView z kartami. Czy to w ogóle możliwe? (mam nadzieję, że to pomoże).

Oto moja klasa adaptera

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

//Klasa filtra

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

}

//Klasa aktywności

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

W metodzie OnQueryTextChangeListener() użyj swojego adaptera. Rzuciłem go na fragment, ponieważ mój adpter jest we fragmentach. Możesz użyć adaptera bezpośrednio, jeśli jest w klasie aktywności.

 -1
Author: Shruthi Kamoji,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-07-09 12:41:51

W Adapterze:

public void setFilter(List<Channel> newList){
        mChannels = new ArrayList<>();
        mChannels.addAll(newList);
        notifyDataSetChanged();
    }

W Aktywności:

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;
            }
        });
 -1
Author: Firoz Ahmed,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-08-30 15:06:13