RecyclerViewと要素の選択について

Hi%username%!

この記事では、Android SDKの新しいウィジェットであるRecyclerView、要素の選択の実装、およびそれを使用する際のいくつかの有用な「レシピ」について少しお話したいと思います。








内容



  1. ViewHoldersについて少し
  2. RecyclerViewについて簡単に
  3. 要素を選択
  4. 結論+ボーナス
  5. 便利なリンク


1. ViewHoldersについて少し



Android SDK 5.0 Lollipopがリリースされる前は、リストとテーブルを表示するため ListViewウィジェットとGridViewウィジェットが使用されていました。 このウィジェットを使用する際の一般的な推奨事項は、ViewHolderパターンを使用することでした。 パターンの本質は、リストの各要素に対して、要素内の個々のビューへのリンクを保存するオブジェクトが作成されることです。 したがって、要素を作成するときに、 findViewById(int)の呼び出しを1回だけ行う必要があります。



Googleマニュアルから直接の典型的なViewHolderの例:
static class ViewHolder { TextView text; TextView timestamp; ImageView icon; ProgressBar progress; int position; }
      
      





各要素のこのようなホルダーへのリンクは、 setTag(int、Object)メソッドを使用してルートレイアウトに格納されます(私の観点からは、これはまだ松葉杖です)。



2. RecyclerViewについて簡単に



Android SDK 5.0 Lollipopのリリースまでに、Googleの開発者は、上記の2つのウィジェットは道徳的に時代遅れであり、それらをよりスタイリッシュでファッショナブルで若々しいものに置き換える必要があるという考えをようやく得ました。 古いウィジェットをやり直すのではなく、新しいウィジェットを書くことにしました。 そこで、 RecyclerViewが誕生しました。 では、違いは何ですか?



主なものの簡単な要約を提供します。より完全な開示のために、ハブに関するこの記事に精通することをお勧めします だから:
  1. ウィジェット自体は、要素を配置する責任を負いません。 このために、 LayoutManagersが登場しました。
  2. ViewHolderパターン必須になりました。 さらに、ウィジェットは、作成済みのViewHoldersを再利用し、未使用のもの(名前の由来)を削除することを学習しました。
  3. アニメーションを操作する新しい便利な方法。


私はそれを試してみましたが、ウィジェットは矛盾した印象を残しました。 一方で、はい、ViewHolderの使用が必須になったことは素晴らしいことです。また、動作が速くなり、メモリの消費も少なくなりました。 一方、ウィジェットの複雑さと不完全さには問題があります。



複雑さとはどういう意味ですか? ListViewで機能しなかった(または意図したとおりに機能しなかった)場合は、いつでもソースにアクセスしてエラーの原因を突き止め、修正し、松葉杖をあちこちに押し込めば、すべてが機能し始めます。 RecyclerViewは、作業のロジックの点ではるかに複雑であり、理解している間に脳を破壊します。 試しましたが、見捨てられました。これには時間と労力がかかりすぎます。



2番目の問題は、ListViewおよびGridViewに存在する機能の平凡な欠如です。 例-要素を選択するための標準機能(この記事の別のトピック)、要素間のインデント-に目を向ける必要はありません。 以前は、これをすべて追加するには、数行のコードを記述する必要がありましたが、今では何十行もかかります。 アニメーションがありますが、要素の追加/削除/編集専用です。 たとえば、要素の部分的な変化をアニメーション化する場合、オブロミンゴ鳥はすでにあなたのドアをノックしています。 ウィジェットは要素の一部のアニメーションをサポートしていません。外部から(たとえば、アダプターから)要素をアニメーション化する場合は、これを行わない方が良いです-そのような操作は、ウィジェット要素(同じViewHolders)を未定義の状態のままにします。



要約-プロジェクトで古いウィジェットを使用し、アニメーションを使用しない場合は、そのままにして、不足している機能でウィジェットがいっぱいになるまで待機することをお勧めします。 シンプルなアニメーションが必要であると同時に、ウィジェットとのユーザーインタラクションがシンプルであることが暗示されている場合は、RecyclerViewを試してください。



3.要素を選択します



だから、私たちは主なものに渡す-記事の技術的な部分に。 RecyclerViewでアイテムを選択する方法について話しましょう。 すぐに予約します-実装のアイデアは、Bill PhillipsのRecyclerViewに関する一連のすばらしい記事( 最後のリンク )から集められているため、以下のすべてを無料の要約と見なすことができます。

ListViewでは、 setChoiceMode(int)メソッドを使用して要素を選択しましたが、RecyclerViewは要素を割り当てることができないため、アダプターにこれを教える必要があります。



スキームは次のとおりです。

図では、オブジェクト間の接続を模式的に示しています。 点線の矢印はリンクであり、残りはメソッド呼び出しです。 緑色では、割り当てロジックを直接実装するオブジェクトを指定しています。



動作原理は次のとおりです。
  1. ViewHolderWrapperは、それ自体をViewHolderのルートビューのClickListenerとして設定し、onClickおよびonLongClickイベントの受信を開始します。 実装に応じて、これらのイベントをHolderClickObservable(ViewHolderClickWrapper)で単純にプロキシするか、 SelectionHelperの現在のステータスに基づいて、setItemSelected(ViewHolder、boolean)(ViewHolderMultiSelectionWrapper)を呼び出して要素を選択できます。
  2. SelectionHelperは、選択された要素に関する情報を保存し、選択の変更についてリスナー( SelectionObserver )に通知します。
  3. リスナー(この場合はアダプター)は、要素の選択の視覚的な表示と、要素との対話(図ではActivityの startActionModeを呼び出します)を担当します。


アダプタ自体で次の変更を行う必要があります。



1. SelectionHelperを作成し、リスナーを登録します(この場合はアダプター自体ですが、たとえばActivityにすることもできます)
 mSelectionHelper = new SelectionHelper(mHolderTracker); mSelectionHelper.registerSelectionObserver(this);
      
      





2.作成したViewHoldersを目的のタイプのViewHolderWrapperでラップします。
 @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int position) { LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext()); ImageViewHolder holder = new ImageViewHolder( inflater.inflate(R.layout.gallery_item, viewGroup, false)); return mSelectionHelper.wrapSelectable(holder); }
      
      



SelectionHelperのメソッドwrapSelectable(ViewHolder):
 public <H extends RecyclerView.ViewHolder> H wrapSelectable(H holder) { new ViewHolderMultiSelectionWrapper(holder); return holder; }
      
      





3.アダプターのonBindViewHolder(ViewHolder、int)メソッドで、ViewHoldersを SelectionHelperにアタッチすることを忘れないでください!
 @Override public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) { Image image = mDataSet.getItem(position); ImageViewHolder imageViewHolder = (ImageViewHolder) viewHolder; imageViewHolder.bindInfo(image); Checkable view = (Checkable) viewHolder.itemView; view.setChecked(mSelectionHelper.isItemSelected(position)); mHolderTracker.bindHolder(imageViewHolder, position); }
      
      





これは、RecyclerViewから現在使用されているViewHoldersのリストを取得する他の方法がないため必要です。 それらを保持しない場合、必要に応じて、選択したすべての要素の選択範囲のレンダリングを更新します(たとえば、ユーザーがActionModeを閉じた場合)、SelectionHelperはこれを実行できません。 実際には選択されていない場合でも、ビューは選択されたままになります。



「setItemSelected(ViewHolder、boolean)メソッドで強調表示されたViewHoldersを覚えているだけではどうですか?」 これは、RecyclerViewの機能が実際に影響する場所です。新しく作成されたViewHoldersを使用します。



次のようになります。
  1. アプリケーションを開きます。 画面には10個の要素があります-10個のViewHolderが作成されます。
  2. ActionModeを開始し、要素の選択を開始します-1,2,3。
  3. ビューを下にスクロールすると、10から20までの要素が表示されます。現在、20個のViewHoldersが記憶に残っていると思いますか? どんなに! データの一部について、RecyclerViewは新しいViewHoldersを作成し、別のデータについては既存のものを再利用します。 そして、それはどのような順序で知られていません。
  4. ビューを上にスクロールすると、10個のViewHolderの一部が破棄され、代わりに新しいものが作成されます。 残りは再利用され、同じポジションには必要ありません。
  5. ActionModeをキャンセルします。 SelectionHelperは、各要素のViewHolderを示す要素の変更された選択についてリスナーに通知する必要がありますが、実際のデータはなく、すべてのホルダーが変更されています!


その結果、一部の要素が選択状態で表示されたままになるという事実につながります。



そして、ここでもう1つの重要なポイントが明らかになります-ViewHoldersへの強力なリンクを維持することはできません! 月の満ち欠けやラリーペイジの左ヒールの欲求に応じて、RecyclerViewから削除できます。 この場合、それらへの厳密な参照を保持すると、メモリリークが発生します。 したがって、ViewHolderWrapperおよびWeakHolderTrackerにリンクを格納するためにWeakReferenceのみ使用されます。
 private abstract class ViewHolderWrapper implements android.view.View.OnClickListener { protected final WeakReference<RecyclerView.ViewHolder> mWrappedHolderRef; protected ViewHolderWrapper(RecyclerView.ViewHolder holder) { mWrappedHolderRef = new WeakReference<>(holder); } }
      
      





WeakHolderTracker
 public class WeakHolderTracker { private final SparseArray<WeakReference<RecyclerView.ViewHolder>> mHoldersByPosition = new SparseArray<>(); public void bindHolder(RecyclerView.ViewHolder holder, int position) { mHoldersByPosition.put(position, new WeakReference<>(holder)); } @Nullable private RecyclerView.ViewHolder getHolder(int position) { WeakReference<RecyclerView.ViewHolder> holderRef = mHoldersByPosition.get(position); if (holderRef == null) { mHoldersByPosition.remove(position); return null; } RecyclerView.ViewHolder holder = holderRef.get(); if (holder == null || (holder.getAdapterPosition() != position && holder.getAdapterPosition() != RecyclerView.NO_POSITION)) { mHoldersByPosition.remove(position); return null; } return holder; } public List<RecyclerView.ViewHolder> getTrackedHolders() { List<RecyclerView.ViewHolder> holders = new ArrayList<>(); for (int i = 0; i < mHoldersByPosition.size(); i++) { int key = mHoldersByPosition.keyAt(i); RecyclerView.ViewHolder holder = getHolder(key); if (holder != null) { holders.add(holder); } } return holders; } }
      
      





4.また、onBindViewHolder(ViewHolder、int)で選択範囲が視覚的に表示されることを忘れないことも重要です(そうでない場合は、削除することを忘れないでください!)。 選択されていない要素には、以前に選択されていない要素に使用されていたViewHolderを使用できることを覚えていますか?

次のように実装しています。



4.1。 SelectableRecyclerViewAdapter.onBindViewHolder(ViewHolder、int)
 Checkable view = (Checkable) viewHolder.itemView; view.setChecked(mSelectionHelper.isItemSelected(position));
      
      





4.2。 要素のレイアウトファイル
 <com.bejibx.android.recyclerview.example.ui.widget.CheckableAutofitHeightFrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="80dp" android:layout_height="80dp" android:background="#AAA" android:foreground="@drawable/gallery_item_foreground" tools:ignore="ContentDescription,RtlHardcoded"> <ImageView android:id="@+id/image" android:layout_width="match_parent" android:layout_height="match_parent" /> </com.bejibx.android.recyclerview.example.ui.widget.CheckableAutofitHeightFrameLayout>
      
      





4.3。 CheckableAutofitHeightFrameLayoutウィジェット
 public class CheckableAutofitHeightFrameLayout extends FrameLayout implements Checkable { private static final int[] CHECKED_STATE_SET = {android.R.attr.state_checked}; private boolean mIsChecked; private boolean mIsCheckable; public CheckableAutofitHeightFrameLayout(Context context) { super(context); } public CheckableAutofitHeightFrameLayout(Context context, AttributeSet attrs) { super(context, attrs); } public CheckableAutofitHeightFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public CheckableAutofitHeightFrameLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } @Override protected int[] onCreateDrawableState(int extraSpace) { final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); if (isChecked()) { mergeDrawableStates(drawableState, CHECKED_STATE_SET); } return drawableState; } @Override public boolean isCheckable() { return mIsCheckable; } @Override public void setCheckable(boolean isCheckable) { boolean wasCheckable = isCheckable(); mIsCheckable = isCheckable; if (!isCheckable && isChecked()) { setChecked(false); } else if (wasCheckable ^ mIsCheckable) { refreshDrawableState(); } } @Override public void setChecked(boolean isChecked) { boolean wasChecked = isChecked(); mIsChecked = isCheckable() && isChecked; if (wasChecked ^ mIsChecked) { refreshDrawableState(); } } @Override public boolean isChecked() { return mIsChecked; } @Override public void toggle() { setChecked(!mIsChecked); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //noinspection SuspiciousNameCombination super.onMeasure(widthMeasureSpec, widthMeasureSpec); } }
      
      





CheckableAutofitHeightFrameLayoutは、 FrameLayoutに 2つだけを追加します 。最初は常に正方形(onMeasure(int、int)を参照)で、次にstate_checked状態をDrawableStates (xmlで使用されるもの)に追加します。 その結果、このようなレイアウトのハイライトを表示するには、次のようなものでStateListDrawableを使用できます。
 <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_pressed="true"> <shape xmlns:android="http://schemas.android.com/apk/res/android"> <stroke android:color="@color/accent" android:width="1dp" /> </shape> </item> <item android:state_checked="true"> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <stroke android:color="@color/accent" android:width="1dp" /> <solid android:color="@color/accent_alpha" /> </shape> </item> <item android:drawable="@android:color/transparent" /> </selector>
      
      



また、表示の詳細はすべてxml-kiに忍び込みます。Javaでは、適切な状態を設定するだけで済みます。



5. onSelectableChanged(boolean)イベントをアクティビティに渡し、ActionModeを実行します。



アダプター内
 @Override public void onSelectableChanged(boolean isSelectable) { if (isSelectable) { mActivity.startActionMode(); } }
      
      





アクティビティでのActionModeの実行
 public class GalleryActivity extends Activity { private final ActionModeCallback mActionModeCallback = new ActionModeCallback(); private SelectableRecyclerViewAdapter mAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); RecyclerView recyclerView = (RecyclerView) findViewById(R.id.gallery); int columnWidth = getResources().getDimensionPixelSize(R.dimen.column_width); int vSpacing = getResources().getDimensionPixelSize(R.dimen.grid_spacing_vertical); int hSpacing = getResources().getDimensionPixelSize(R.dimen.grid_spacing_horizontal); recyclerView.setLayoutManager(new GridAutofitLayoutManager(this, columnWidth)); recyclerView.addItemDecoration(new GridSimpleSpacingDecoration(hSpacing, vSpacing)); DataSet<Image> dataSet = new DummyImagesDataSet(); mAdapter = new SelectableRecyclerViewAdapter(this, dataSet); recyclerView.setAdapter(mAdapter); } public void startActionMode() { startActionMode(mActionModeCallback); } private class ActionModeCallback implements ActionMode.Callback, SelectionObserver { private ActionMode mActionMode; @Override public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) { return false; } @Override public void onDestroyActionMode(ActionMode actionMode) { SelectionHelper selectionHelper = mAdapter.getSelectionHelper(); selectionHelper.unregisterSelectionObserver(this); mActionMode = null; selectionHelper.setSelectable(false); } @Override public boolean onCreateActionMode(ActionMode actionMode, Menu menu) { mActionMode = actionMode; mActionMode.getMenuInflater().inflate(R.menu.gallery_selection, menu); mAdapter.getSelectionHelper().registerSelectionObserver(this); return true; } @Override public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) { switch (menuItem.getItemId()) { case R.id.menu_toast: Toast.makeText(GalleryActivity.this, R.string.text_simple_toast, Toast.LENGTH_SHORT).show(); break; } return true; } @Override public void onSelectedChanged(RecyclerView.ViewHolder holder, boolean isSelected) { if (mActionMode != null) { int checkedImagesCount = mAdapter.getSelectionHelper().getSelectedItemsCount(); mActionMode.setTitle(String.valueOf(checkedImagesCount)); } } @Override public void onSelectableChanged(boolean isSelectable) { if (!isSelectable) { mActionMode.finish(); } } } }
      
      





ご覧のとおり、ActionModeが起動すると、SelectionObserverとして登録されます。 したがって、ヘッダー内の選択された要素の数を更新できます。 閉じるときは、unregisterSelectionObserver(SelectionObserver)を呼び出すことを忘れないでください!



4.結論+ボーナス



割り当てを整理したようです。 すべてのソースコードはGitHubでも表示できます



結論として、RecyclerViewを使用するためのいくつかの機能を簡単にリストします。これらの機能は例にあります。



1.要素を選択する必要はなく、ViewHolderMultiSelectionWrapperではなくクリックのみを処理する必要がある場合は、wrapClickable(ViewHolder)メソッドを使用してViewHolderClickWrapperで要素をラップします。 この場合のアダプター自体は次のようになります。
非表示のテキスト
 public class SelectableRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements SelectionObserver { private final SelectionHelper mSelectionHelper; public SelectableRecyclerViewAdapter() { mSelectionHelper = new SelectionHelper(); mSelectionHelper.registerSelectionObserver(this); } public SelectionHelper getSelectionHelper() { return mSelectionHelper; } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int position) { LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext()); ImageViewHolder holder = new ImageViewHolder( inflater.inflate(R.layout.gallery_item, viewGroup, false)); return mSelectionHelper.wrapClickable(holder); } @Override public void onHolderClick(RecyclerView.ViewHolder holder) { // perform item click } @Override public boolean onHolderLongClick(RecyclerView.ViewHolder holder) { // perform item long click return false; } //... }
      
      





2. GridLayoutManagerは 、コンテンツの幅に応じて列数を自動的に選択することはできません。 この機能を追加しました
GridAutofitLayoutManager
 public class GridAutofitLayoutManager extends GridLayoutManager { private int mColumnWidth; private boolean mColumnWidthChanged = true; public GridAutofitLayoutManager(Context context, int columnWidth) { /* Initially set spanCount to 1, will be changed automatically later. */ super(context, 1); setColumnWidth(checkedColumnWidth(context, columnWidth)); } public GridAutofitLayoutManager(Context context, int columnWidth, int orientation, boolean reverseLayout) { /* Initially set spanCount to 1, will be changed automatically later. */ super(context, 1, orientation, reverseLayout); setColumnWidth(checkedColumnWidth(context, columnWidth)); } private int checkedColumnWidth(Context context, int columnWidth) { if (columnWidth <= 0) { context.getResources().getDimensionPixelSize(R.dimen.rv_def_column_width); } return columnWidth; } public void setColumnWidth(int newColumnWidth) { if (newColumnWidth > 0 && newColumnWidth != mColumnWidth) { mColumnWidth = newColumnWidth; mColumnWidthChanged = true; } } /* I don't actually remember why I choose to set span count in onLayoutChildren, I wrote this class some time ago. But the point is we need to do so after view get measured, so we can get its height and width. */ @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { if (mColumnWidthChanged && mColumnWidth > 0) { int totalSpace; if (getOrientation() == VERTICAL) { totalSpace = getWidth() - getPaddingRight() - getPaddingLeft(); } else { totalSpace = getHeight() - getPaddingTop() - getPaddingBottom(); } int spanCount = Math.max(1, totalSpace / mColumnWidth); setSpanCount(spanCount); mColumnWidthChanged = false; } super.onLayoutChildren(recycler, state); } }
      
      





ウィジェットは、columnWidthパラメーターに応じて列の幅を選択します。 重要な点:利用可能な幅が330ピクセルで、目的の幅を100に渡すと、結果としてテーブルには110ピクセルの3列があり、要素はこの幅になります。 これが、幅に応じてCheckableAutofitHeightFrameLayoutが高さを自動的に変更するようにした理由です。



3.要素間にパディングを追加するには、RecyclerViewにpaddingTop / Leftを、要素にmarginRight / Bottomを設定できますが、松葉杖のように見えます。 推奨される方法は、 ItemDecorationをRecyclerViewに追加することです 。 例にいくつかあります。 通常のGridLayoutManagerにパディングを追加するには(「通常」とは、標準のSpanSizeLookupを持つGridLayoutManagerを意味し、各要素は1スパンかかります)、使用できます
GridSimpleSpacingDecoration
 public class GalleryActivity extends Activity { private final ActionModeCallback mActionModeCallback = new ActionModeCallback(); private SelectableRecyclerViewAdapter mAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); RecyclerView recyclerView = (RecyclerView) findViewById(R.id.gallery); int columnWidth = getResources().getDimensionPixelSize(R.dimen.column_width); int vSpacing = getResources().getDimensionPixelSize(R.dimen.grid_spacing_vertical); int hSpacing = getResources().getDimensionPixelSize(R.dimen.grid_spacing_horizontal); recyclerView.setLayoutManager(new GridAutofitLayoutManager(this, columnWidth)); recyclerView.addItemDecoration(new GridSimpleSpacingDecoration(hSpacing, vSpacing)); DataSet<Image> dataSet = new DummyImagesDataSet(); mAdapter = new SelectableRecyclerViewAdapter(this, dataSet); recyclerView.setAdapter(mAdapter); } //...
      
      





それがすべてのようです。 ご清聴ありがとうございました。リストが遅くなることはありません。



5.便利なリンク



  1. Bill PhillipsのRecyclerViewに関する記事の最初の部分 基本情報(英語)
  2. RecyclerViewに関するビル・フィリップスの記事の第2部。 アイテムの強調表示について(英語)
  3. RecyclerViewレビュー記事(英語)
  4. ListViewのViewHolderパターン情報(英語)
  5. RecyclerViewで視差ヘッダーを作成する
  6. RecyclerViewおよびCardViewに関する基本情報
  7. RecyclerViewへの切り替えについて
  8. Javaのリンクの種類
  9. RecyclerViewを使用するための便利なライブラリ (ユーザーartemgapchenkoに感謝)


PS。 出版物の準備にご協力いただいQayatriユーザーに感謝します! また、貴重なコメントをありがとうartemgapchenkoに感謝します!



All Articles