前書き
何が問題になっているのかは質問からは明確ではないので、この機能を実装する方法について簡単に説明しました。それでも質問がある場合は、遠慮なく質問してください。
このGitHubリポジトリには、ここで話しているすべての実例があります。
サンプルプロジェクトについて詳しく知りたい場合は、プロジェクトのホームページにアクセスしてください。
いずれにしても、結果は次のようになります。
最初にデモアプリをいじりたい場合は、Playストアからインストールできます。
とにかく始めましょう。
のセットアップ SearchView
フォルダ内にres/menu
という新しいファイルを作成しますmain_menu.xml
。その中にアイテムを追加し、をに設定actionViewClass
しandroid.support.v7.widget.SearchView
ます。サポートライブラリを使用しているので、サポートライブラリの名前空間を使用してactionViewClass
属性を設定する必要があります。xmlファイルは次のようになります。
<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>
あなたにFragment
またはActivity
あなたがいつものように、このメニューXMLを膨らませる必要があり、その後、あなたはを探すことができますMenuItem
含まれているSearchView
と実装OnQueryTextListener
我々が締結したテキストへの変更をリッスンするために使用しようとしています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;
}
これで、SearchView
使用する準備が整いました。の実装onQueryTextChange()
が完了したら、後でフィルターロジックを実装しAdapter
ます。
のセットアップ Adapter
何よりもまず、これはこの例で使用するモデルクラスです。
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;
}
}
にテキストを表示するのは、単なる基本モデルRecyclerView
です。これは、テキストを表示するために使用するレイアウトです。
<?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>
ご覧のとおり、データバインディングを使用しています。これまでにデータバインディングを使用したことがない場合は、落胆しないでください。それは非常にシンプルで強力ですが、この回答の範囲内でどのように機能するかは説明できません。
これがあるViewHolder
ためExampleModel
、クラス:
public class ExampleViewHolder extends RecyclerView.ViewHolder {
private final ItemExampleBinding mBinding;
public ExampleViewHolder(ItemExampleBinding binding) {
super(binding.getRoot());
mBinding = binding;
}
public void bind(ExampleModel item) {
mBinding.setModel(item);
}
}
ここでも特別なことは何もありません。上記のレイアウトxmlで定義したように、データバインディングを使用してモデルクラスをこのレイアウトにバインドするだけです。
これで、ようやく本当に興味深い部分であるアダプターの作成にたどり着くことができます。私はの基本的な実装をスキップして、Adapter
代わりにこの回答に関連する部分に集中します。
しかし、最初に私たちが話し合わなければならないことが1つありSortedList
ます。それはクラスです。
SortedList
SortedList
一部であり、完全に素晴らしいツールであるRecyclerView
ライブラリ。これはAdapter
、データセットの変更に関する通知を処理し、非常に効率的な方法で通知します。必要なのは、要素の順序を指定することだけです。のようにcompare()
2つの要素を比較するメソッドを実装することにより、これを行う必要があります。ただし、aをソートする代わりに、!内のアイテムをソートするために使用されます。SortedList
Comparator
List
RecyclerView
SortedList
相互作用Adapter
を通じてCallback
、あなたが実装する必要がクラス:
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();
}
}
以下のようなコールバックの上部の方法ではonMoved
、onInserted
など、あなたの方法を通知同等のものを呼び出す必要がありますAdapter
。3つの下部の方法compare
、areContentsTheSame
そしてareItemsTheSame
あなたが表示し、これらのオブジェクトは、画面に表示されます注文何にするオブジェクトの種類に応じて実装する必要があります。
これらの方法を1つずつ見ていきましょう。
@Override
public int compare(ExampleModel a, ExampleModel b) {
return mComparator.compare(a, b);
}
これは、compare()
先ほどお話しした方法です。この例ではComparator
、2つのモデルを比較するaに呼び出しを渡しています。画面にアイテムをアルファベット順に表示する場合。このコンパレータは次のようになります。
private static final Comparator<ExampleModel> ALPHABETICAL_COMPARATOR = new Comparator<ExampleModel>() {
@Override
public int compare(ExampleModel a, ExampleModel b) {
return a.getText().compareTo(b.getText());
}
};
次のメソッドを見てみましょう:
@Override
public boolean areContentsTheSame(ExampleModel oldItem, ExampleModel newItem) {
return oldItem.equals(newItem);
}
このメソッドの目的は、モデルのコンテンツが変更されたかどうかを判断することです。SortedList
他の言葉であれば-変更イベントが呼び出される必要があるかどうかを判断するために、これを使用してRecyclerView
古いものと新しいバージョンをクロスフェードする必要があります。モデルクラスが正しく実装されている場合equals()
、hashCode()
通常は上記のように実装できます。クラスにequals()
とhashCode()
実装を追加すると、ExampleModel
次のようになります。
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;
}
}
簡単な補足:Android Studio、IntelliJ、EclipseなどのほとんどのIDEには、ボタンを押すだけで生成equals()
およびhashCode()
実装する機能があります。したがって、自分で実装する必要はありません。IDEでの動作をインターネットで調べてください。
次に、最後のメソッドを見てみましょう。
@Override
public boolean areItemsTheSame(ExampleModel item1, ExampleModel item2) {
return item1.getId() == item2.getId();
}
SortedList
2つの項目が同じものを指しているかどうかを確認するために、このメソッドを使用しています。簡単に言えば(SortedList
動作の説明はありません)、これは、オブジェクトが既にに含まれてList
いるかどうか、および追加、移動、または変更のアニメーションを再生する必要があるかどうかを判断するために使用されます。モデルにIDがある場合は、通常、このメソッドでIDのみを比較します。そうでない場合は、他の方法でこれを確認する必要がありますが、実装は最終的には特定のアプリによって異なります。通常、すべてのモデルにIDを与えるのが最も簡単なオプションです。たとえば、データベースからデータをクエリする場合、これは主キーフィールドになる可能性があります。
ではSortedList.Callback
正しく実装我々はのインスタンスを作成することができますSortedList
。
final SortedList<ExampleModel> list = new SortedList<>(ExampleModel.class, mCallback);
のコンストラクタの最初のパラメータとして、SortedList
モデルのクラスを渡す必要があります。もう1つのパラメーターは、上SortedList.Callback
で定義したとおりです。
さて、ビジネスに取り掛かりましょう:Adapter
with を実装すると、SortedList
次のようになります。
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
私たちは同じを使用できるようにアイテムをソートするために使用さは、コンストラクタ経由で渡されるAdapter
項目が異なる順序で表示されるようになっている場合でも。
これでほぼ完了です。ただし、最初にに項目を追加または削除する方法が必要Adapter
です。この目的のために、にメソッドAdapter
を追加して、アイテムをに追加および削除できるようにすることができます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();
}
我々はので、ここで任意の通知メソッドを呼び出す必要はありませんSortedList
すでに通過のためにこれを行いますSortedList.Callback
!それを除けば、これらのメソッドの実装は、1つの例外を除いて非常に単純ですList
。モデルのaを削除するremoveメソッドです。にSortedList
は、単一のオブジェクトを削除できるremoveメソッドが1つしかないため、リストをループしてモデルを1つずつ削除する必要があります。beginBatchedUpdates()
最初に呼び出すと、SortedList
一緒に行うすべての変更がバッチ処理され、パフォーマンスが向上します。を呼び出すとendBatchedUpdates()
、RecyclerView
すべての変更が一度に通知されます。
さらに、理解する必要があるのは、オブジェクトをに追加し、SortedList
それがすでにSortedList
そこにある場合、再度追加されないことです。代わりに、SortedList
はareContentsTheSame()
メソッドを使用して、オブジェクトが変更されたかどうか、およびオブジェクトがにあるかどうかをRecyclerView
更新します。
とにかく、私が通常好むのはRecyclerView
、一度にすべてのアイテムを交換できる1つの方法です。にないものをすべて削除し、List
不足しているすべての項目をに追加します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();
}
この方法でも、すべての更新をバッチ処理してパフォーマンスを向上させます。最初のループは逆です。最初にアイテムを削除すると、その後に表示されるすべてのアイテムのインデックスが混乱し、場合によってはデータの不整合などの問題が発生する可能性があるためです。後私達はちょうど追加することList
にSortedList
使用addAll()
してまだ入っていないすべての項目を追加するSortedList
と-私は上記と同じように-更新に既にあるすべての項目SortedList
が、変更されています。
これAdapter
で完了です。全体は次のようになります。
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();
}
}
今欠けている唯一のものはフィルタリングを実装することです!
フィルターロジックの実装
フィルターロジックを実装するには、まずList
すべての可能なモデルを定義する必要があります。この例では、私は作成List
のExampleModel
映画の配列からインスタンスを:
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);
}
ここでは特別なことは何もせず、をインスタンス化してAdapter
に設定しRecyclerView
ます。その後List
、MOVIES
配列の映画名からモデルのを作成します。次に、すべてのモデルをに追加しますSortedList
。
これで、onQueryTextChange()
前に定義したものに戻り、フィルターロジックの実装を開始できます。
@Override
public boolean onQueryTextChange(String query) {
final List<ExampleModel> filteredModelList = filter(mModels, query);
mAdapter.replaceAll(filteredModelList);
mBinding.recyclerView.scrollToPosition(0);
return true;
}
これもまた非常に簡単です。メソッドを呼び出し、ofとクエリ文字列filter()
を渡します。次に、を呼び出して、から返されたフィルター済みのフィルターを渡します。また、呼び出す必要がありますに何かを検索する際に、ユーザは常にすべての項目を見ることができることを保証します。そうしないと、はフィルタリング中にスクロールダウン位置にとどまり、その後いくつかの項目を非表示にする可能性があります。上にスクロールすると、検索中のユーザーエクスペリエンスが向上します。List
ExampleModel
replaceAll()
Adapter
List
filter()
scrollToPosition(0)
RecyclerView
RecyclerView
あとは、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;
}
ここで最初にすることはtoLowerCase()
、クエリ文字列の呼び出しです。検索機能で大文字と小文字を区別したくないので、toLowerCase()
比較するすべての文字列を呼び出すことで、大文字と小文字を区別せずに同じ結果を返すことができます。次に、List
渡されたのすべてのモデルを反復処理し、クエリ文字列がモデルのテキストに含まれているかどうかを確認します。そうである場合、モデルはフィルターに追加されList
ます。
以上です!上記のコードはAPIレベル7以上で実行され、APIレベル11以降、アイテムのアニメーションを無料で入手できます!
これは非常に詳細な説明であるため、おそらくこの全体が実際よりも複雑に見えますが、この全体の問題を一般化して、にAdapter
基づいSortedList
てをより簡単に実装できるようにする方法があります。
問題の一般化とアダプターの簡素化
このセクションでは、詳細には触れません。スタックオーバーフローでの回答の文字数制限に達しているためですが、そのほとんどがすでに上記で説明されているためですが、変更を要約すると、次のようになります。基本Adapter
クラスを実装できます。これは、SortedList
モデルをViewHolder
インスタンスにバインドするだけでなく、の処理もすでに処理しており、にAdapter
基づいてを実装する便利な方法を提供しますSortedList
。そのためには、2つのことを行う必要があります。
ViewModel
すべてのモデルクラスが実装しなければならないインターフェースを作成する必要があります
- モデルを自動的にバインドするために使用できるメソッド
ViewHolder
を定義するサブクラスを作成する必要があります。bind()
Adapter
これによりRecyclerView
、モデルとそれに対応するViewHolder
実装を実装するだけで、に表示されることになっているコンテンツに焦点を合わせることができます。この基本クラスを使用するAdapter
と、とそのの複雑な詳細について心配する必要はありませんSortedList
。
SortedListAdapter
このため、基本クラスを実装するか、ここでも完全なソースコードを追加していますが、この基本クラスの完全なソースコードを見つけることができるの各ステップを経ることができないStackOverflowの私の答えの文字数制限の-私はそれを呼ばれるSortedListAdapter
-これでGitHubの要旨。
あなたの人生をシンプルにするために、SortedListAdapter
!を含むライブラリをjCenterに公開しました。使用したい場合は、この依存関係をアプリのbuild.gradleファイルに追加するだけです。
compile 'com.github.wrdlbrnft:sorted-list-adapter:0.2.0.1'
このライブラリの詳細については、ライブラリホームページをご覧ください。
SortedListAdapterの使用
を使用するSortedListAdapter
には、2つの変更を行う必要があります。
ViewHolder
拡張するように変更しますSortedListAdapter.ViewHolder
。typeパラメータは、これにバインドされるモデルでなければなりませんViewHolder
-この場合はExampleModel
。performBind()
ではなく、モデルにデータをバインドする必要があります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);
}
}
すべてのモデルがViewModel
インターフェースを実装していることを確認してください:
public class ExampleModel implements SortedListAdapter.ViewModel {
...
}
その後は、を更新して、不要になったすべてをExampleAdapter
拡張SortedListAdapter
して削除する必要があります。typeパラメータは、使用しているモデルのタイプである必要があります-この場合はExampleModel
。ただし、さまざまなタイプのモデルを使用している場合は、typeパラメータをに設定します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);
}
}
その後、完了です!言及するしかし最後の一つです:SortedListAdapter
同じ持っていないadd()
、remove()
またはreplaceAll()
独自の方法ExampleAdapter
でしたが。別のEditor
オブジェクトを使用して、edit()
メソッドを介してアクセスできるリストの項目を変更します。したがって、呼び出す必要がある項目を削除または追加する場合はedit()
、このEditor
インスタンスで項目を追加および削除し、完了したら、呼び出しcommit()
て変更をに適用しますSortedList
。
mAdapter.edit()
.remove(modelToRemove)
.add(listOfModelsToAdd)
.commit();
この方法で行うすべての変更は、パフォーマンスを向上させるためにバッチ処理されます。replaceAll()
我々は上記の章で実装される方法はまた、この上に存在するEditor
オブジェクト:
mAdapter.edit()
.replaceAll(mModels)
.commit();
呼び出すのを忘れた場合commit()
、変更は適用されません。