master of recyclerview
TRANSCRIPT
• blog : Y.A.M の雑記帳
• y-anz-m.blogspot.com
• twitter : @yanzm (やんざむ)
• uPhyca Inc. (株式会社ウフィカ)
あんざいゆき
New
今日話す内容はTechBoosterの 夏コミ本に 執筆してます
http://techbooster.github.io/c88/
事前のお知らせ
• 今日の資料 → 公開されます
• 今日の公演 → 録画&公開されます
• RecyclerView
• データを表示するためのスクロール可能なView
• RecyclerView.LayoutManager
• アイテム用のビューのサイズを計算し、配置する
RecyclerViewの構成
• RecyclerView.Adapter
• RecyclerViewに表示するデータセットを管理し、アイテム用のViewにデータを紐づける
• RecyclerView.ViewHolder
• アイテム用のビューとメタデータを保持する
RecyclerViewの構成
RecyclerViewの構成
RecyclerView LayoutManager
Adapter
子ビュー配置
子ビュー (ViewHolder) にデータ紐付
Recycler
ViewHolderを再利用
再利用するViewHolder
ListViewの構成
RecyclerView LayoutManager
Adapter
子ビュー配置
子ビュー (ViewHolder) にデータ紐付
Recycler
ViewHolderを再利用
再利用するViewHolder
ListView
ListAdapter
ListViewの構成
ListView =
RecyclerView + LayoutManager + Recycler
+ その他もろもろの機能
ListAdapter = Adapter + ViewHolder
ListViewの構成
ListView =
RecyclerView + LayoutManager + Recycler
+ その他もろもろの機能
ListAdapter = Adapter + ViewHolder
ListView vs RecyclerView
ListView RecyclerView
区切り線 ⚪ 自分で実装
listSelector ⚪ ×
onItemClick ⚪ 自分で実装
choiceMode ⚪ ×
ListView vs RecyclerView
ListView RecyclerView
Filter ⚪ 自分で実装
FadingEdge ⚪ 自分で実装
Header, Footer ⚪ 自分で実装
StaggeredGrid × ⚪
ListView vs RecyclerView
ListView RecyclerView
追加・削除の アニメーション
自分で実装 ⚪
Swipe to Dismiss 自分で実装 ⚪
Drag & Drop 自分で実装 ⚪
横スクロール配置 × ⚪
最低限必要なもの
• RecyclerView
• RecyclerView.LayoutManager
• RecyclerView.Adapter
• RecyclerView.ViewHolder
最低限必要なもの
• RecyclerView
• RecyclerView.LayoutManager
• RecyclerView.Adapter
• RecyclerView.ViewHolder
<?xml version="1.0" encoding="utf-8"?> <android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent" app:layoutManager="LinearLayoutManager" />
RecyclerView + LayoutManager
<?xml version="1.0" encoding="utf-8"?> <android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent" app:layoutManager ="android.support.v7.widget.LinearLayoutManager" />
RecyclerView + LayoutManager
RecyclerView + LayoutManager
<?xml version="1.0" encoding="utf-8"?> <android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent" app:layoutManager =“net.yanzm.sample.MyLayoutManager” />
最低限必要なもの
• RecyclerView
• RecyclerView.LayoutManager
• RecyclerView.Adapter
• RecyclerView.ViewHolder
private static class ViewHolder extends RecyclerView.ViewHolder {
static final int LAYOUT_ID = android.R.layout.simple_list_item_1; final TextView textView; public ViewHolder(View itemView) { super(itemView); textView = (TextView) itemView.findViewById(android.R.id.text1); }}
ViewHolder
private static class SimpleAdapter extends RecyclerView.Adapter<ViewHolder> { private final LayoutInflater inflater; private final List<String> data; private SimpleAdapter(Context context, List<String> data) { this.inflater = LayoutInflater.from(context); this.data = data; } @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return new ViewHolder(inflater .inflate(ViewHolder.LAYOUT_ID, parent, false)); } @Override public void onBindViewHolder(ViewHolder holder,
Adapter
private SimpleAdapter(Context context, List<String> data) { this.inflater = LayoutInflater.from(context); this.data = data; } @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return new ViewHolder(inflater .inflate(ViewHolder.LAYOUT_ID, parent, false)); } @Override public void onBindViewHolder(ViewHolder holder, int position) { String text = data.get(position); holder.textView.setText(text); } @Override public int getItemCount() { return data.size(); }}
Adapter
public class SimpleSampleActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_simple_sample); RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view); recyclerView.setHasFixedSize(true); List<String> data = new ArrayList<>(); for (int i = 0; i < 30; i++) { data.add("Item : " + i); } final SimpleAdapter adapter = new SimpleAdapter(this, data);
Activity
setContentView(R.layout.activity_simple_sample); RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view); recyclerView.setHasFixedSize(true); List<String> data = new ArrayList<>(); for (int i = 0; i < 30; i++) { data.add("Item : " + i); } final SimpleAdapter adapter = new SimpleAdapter(this, data); recyclerView.setAdapter(adapter); } private static class ViewHolder extends RecyclerView.ViewHolder {…}
private static class SimpleAdapter extends RecyclerView.Adapter<ViewHolder> {…} }
Activity
orientation
• デフォルトはLinearLayoutManager.VERTICAL
<?xml version="1.0" encoding="utf-8"?><android.support.v7.widget.RecyclerView … android:orientation="horizontal" app:layoutManager="LinearLayoutManager" />
new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false);
layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
or
or
spanCount
• 列数、デフォルトは1
<?xml version="1.0" encoding="utf-8"?><android.support.v7.widget.RecyclerView … app:layoutManager="GridLayoutManager" app:spanCount="2" />
new GridLayoutManager(this, 2);
layoutManager.setSpanCount(2);
or
or
ItemDecoration
• 装飾を行うためのクラス
• アイテム用のViewのoffsetを指定
• onDraw()でRecyclerViewの下に描画
• onDrawOver()でRecyclerVieの上に描画
アイテム用のViewのOffsetを指定
final int offset = (int) (8 * getResources().getDisplayMetrics().density);final RecyclerView.ItemDecoration itemDecoration = new RecyclerView.ItemDecoration() { @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { outRect.set(offset, offset, offset, offset); } };
recyclerView.addItemDecoration(itemDecoration);
アイテム用のViewのOffsetを指定
final RecyclerView.ItemDecoration itemDecoration = new RecyclerView.ItemDecoration() { @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
int position = ((RecyclerView.LayoutParams) view.getLayoutParams()).getViewLayoutPosition(); if (position == 0) { outRect.set(offset, offset, offset, offset); } else { outRect.set(offset, 0, offset, offset); } } };
recyclerView.addItemDecoration(itemDecoration);
private static class DividerDecoration extends RecyclerView.ItemDecoration { private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); private final int dividerHeight; public DividerDecoration(Resources res) { paint.setColor(Color.GRAY); dividerHeight = (int) (4 * res.getDisplayMetrics().density); } @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
int position = ((RecyclerView.LayoutParams) view.getLayoutParams()).getViewLayoutPosition(); // 位置が2番目以降なら上部にdividerを描画したいので、
Dividerを描画
paint.setColor(Color.GRAY); dividerHeight = (int) (4 * res.getDisplayMetrics().density); } @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
int position = ((RecyclerView.LayoutParams) view.getLayoutParams()).getViewLayoutPosition(); // 位置が2番目以降なら上部にdividerを描画したいので、 // divider分だけ上をあける int top = position == 0 ? 0 : dividerHeight; outRect.set(0, top, 0, 0); } @Override public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { super.onDrawOver(c, parent, state); // アイテムのビューより上に描画される }
Dividerを描画
@Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { super.onDraw(c, parent, state); // アイテムのビューより下に描画される final RecyclerView.LayoutManager manager = parent.getLayoutManager(); final int left = parent.getPaddingLeft(); final int right = parent.getWidth() - parent.getPaddingRight();
final int childCount = parent.getChildCount(); for (int i = 1; i < childCount; i++) { final View child = parent.getChildAt(i); final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams(); if (params.getViewLayoutPosition() == 0) { continue; } // ViewCompat.getTranslationY()を入れないと
Dividerを描画
final RecyclerView.LayoutManager manager = parent.getLayoutManager(); final int left = parent.getPaddingLeft(); final int right = parent.getWidth() - parent.getPaddingRight();
final int childCount = parent.getChildCount(); for (int i = 1; i < childCount; i++) { final View child = parent.getChildAt(i); final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams(); if (params.getViewLayoutPosition() == 0) { continue; } // ViewCompat.getTranslationY()を入れないと // 追加・削除のアニメーション時の位置が変になる final int top = manager.getDecoratedTop(child) - params.topMargin + Math.round(ViewCompat.getTranslationY(child)); final int bottom = top + dividerHeight; c.drawRect(left, top, right, bottom, paint); } }}
Dividerを描画
OnItemClick
• 方法がいくつかある
• ViewHolderのitemViewにView.setOnClickListener
• ItemDecorationでタップされたアイテムの位置にselectorを描画
OnItemClick
• 方法がいくつかある
• ViewHolderのitemViewにView.setOnClickListener
• ItemDecorationでタップされたアイテムの位置にselectorを描画
v17 leanback library はこっち
// selectableItemBackgroundに指定されている // リソースIDの値を取得しておくTypedValue val = new TypedValue();if (getTheme() != null) { getTheme().resolveAttribute( android.R.attr.selectableItemBackground, val, true); } final int backgroundResId = val.resourceId; final SimpleAdapter adapter = new SimpleAdapter(this, data) { @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { final ViewHolder viewHolder = super.onCreateViewHolder(parent, viewType);
viewHolder.itemView.setBackgroundResource(backgroundResId); viewHolder.itemView.setOnClickListener( new View.OnClickListener() {
OnItemClick
final SimpleAdapter adapter = new SimpleAdapter(this, data) { @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { final ViewHolder viewHolder = super.onCreateViewHolder(parent, viewType);
viewHolder.itemView.setBackgroundResource(backgroundResId); viewHolder.itemView.setOnClickListener( new View.OnClickListener() { @Override public void onClick(View v) { int position = viewHolder .getAdapterPosition();
Toast.makeText(v.getContext(), "Position = " + position, Toast.LENGTH_SHORT).show(); } }); return viewHolder; }}; recyclerView.setAdapter(adapter);
OnItemClick
notify**
• ArrayAdapterに相当するものは用意されていない
• データの追加・削除・変更時にはnotifyItem**()を呼ぶ
• notifyDataSetChanged()はそれ以外のときだけにする
notify**
• notifyItemChanged(int position) : positionの位置のアイテムの変更された
• notifyItemInserted(int position) : posiitonの位置にアイテムが追加された
• notifyItemRemoved(int position) : positionの位置のアイテムが削除された
• notifyItemMoved(int fromPosition, int toPosition) : fromPositionにあったアイテムがtoPositionに移動した
notify**
• notifyItemRangeChanged(int positionStart, int itemCount) : positionStartからitemCount個のアイテムが変更された
• notifyItemRangeInserted(int positionStart, int itemCount) : positionStartにitemCount個のアイテムが追加された
• notifyItemRangeRemoved(int positionStart, int itemCount) : positionStartからitemCount個のアイテムが削除された
• notifyDataSetChanged() : データセットが変更された
public abstract class RecyclerArrayAdapter<T, VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> { private final Object lock = new Object(); private final Context context; private final List<T> objects; public RecyclerArrayAdapter(Context context) { this(context, new ArrayList<T>()); } public RecyclerArrayAdapter(Context context, List<T> objects) { this.context = context; this.objects = objects; } public void add(@NonNull T object) { final int position = objects.size(); synchronized (lock) {
ArrayAdapter的なRecyclerView用Adapter
} public RecyclerArrayAdapter(Context context, List<T> objects) { this.context = context; this.objects = objects; } public void add(@NonNull T object) { final int position = objects.size(); synchronized (lock) { objects.add(object); } notifyItemInserted(position); } public void addAll(@NonNull Collection<? extends T> collection) { final int positionStart = objects.size(); final int itemCount = collection.size(); synchronized (lock) { objects.addAll(collection); } notifyItemRangeInserted(positionStart, itemCount); } public void insert(@NonNull T object, int index) { synchronized (lock) { objects.add(index, object);
ArrayAdapter的なRecyclerView用Adapter
objects.addAll(collection); } notifyItemRangeInserted(positionStart, itemCount); } public void insert(@NonNull T object, int index) { synchronized (lock) { objects.add(index, object); } notifyItemInserted(index); } public void remove(@NonNull T object) { int position = getPosition(object); synchronized (lock) { objects.remove(object); } notifyItemRemoved(position); } public void clear() { final int itemCount = objects.size(); synchronized (lock) { objects.clear(); } notifyItemRangeRemoved(0, itemCount); }
ArrayAdapter的なRecyclerView用Adapter
ItemTouchHelper
• RecyclerViewに swipe to dismiss と drag & drop による並び替え機能を追加するためにユーティリティクラス
ItemTouchHelper.Callback callback = …;
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(callback);
itemTouchHelper.attachToRecyclerView(recyclerView);
swipe to dismiss
int swipeDirs = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT; ItemTouchHelper.Callback callback = new ItemTouchHelper.SimpleCallback(0, swipeDirs) {
… @Override public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { int position = viewHolder.getAdapterPosition(); adapter.remove(adapter.getItem(position)); }};
drag and drop
• ItemTouchHelper.Callbackのコンストラクタの第1引数でドラッグ方向を指定
• ドロップされたらonMove()が呼ばれる
• ドラッグが開始できるようになったタイミングでonSelectedChanged()が呼ばれる
• ドラッグを終了するときにclearView()が呼ばれる
int dragDirs = ItemTouchHelper.UP | ItemTouchHelper.DOWN; ItemTouchHelper.Callback callback = new ItemTouchHelper.SimpleCallback(dragDirs, 0) { @Override public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
int from = viewHolder.getAdapterPosition(); int to = target.getAdapterPosition(); adapter.move(from, to); return true; } … };
drag and drop
int dragDirs = ItemTouchHelper.UP | ItemTouchHelper.DOWN; ItemTouchHelper.Callback callback = new ItemTouchHelper.SimpleCallback(dragDirs, 0) { @Override public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { int from = viewHolder.getAdapterPosition(); int to = target.getAdapterPosition(); adapter.move(from, to); return true; } @Override public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) { if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) { viewHolder.itemView.setBackgroundColor(Color.LTGRAY); } super.onSelectedChanged(viewHolder, actionState);
drag and drop
int from = viewHolder.getAdapterPosition(); int to = target.getAdapterPosition(); adapter.move(from, to); return true; } @Override public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) { if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) { viewHolder.itemView.setBackgroundColor(Color.LTGRAY); } super.onSelectedChanged(viewHolder, actionState); } @Override public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { super.clearView(recyclerView, viewHolder); viewHolder.itemView.setBackgroundColor(Color.TRANSPARENT); } … }};
drag and drop
独自のLayoutManager
1. RecyclerView.LayoutManagerを継承
2. generateDefaultLayoutParams() で
RecyclerView.LayoutParams() を返す
3. onLayoutChildren()で子ビューを配置
4. scrollVerticallyBy(), scrollHorizontallyBy()でスクロール分だけ子ビューを移動&足りない分のビューを追加
onLayoutChildren()で子ビュー配置
1. detachAndScrapAttachedViews(recycler)で現在のビューをリサイクル対象にする
2. recycler.getViewForPosition(i)でアイテム用のビューを取得
3. addView()
4. measureChildWithMargins()でビューのサイズを計算
5. layoutDecorated(v, left, top, right, bottom)で配置
public class SimpleListLayoutManager extends RecyclerView.LayoutManager { @Override public RecyclerView.LayoutParams generateDefaultLayoutParams() { return new RecyclerView.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); } @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { // 現在表示されている一番上のビューの位置を保持しておく final View lastTopView = getChildCount() > 0 ? getChildAt(0) : null; final int lastTop = lastTopView != null ? lastTopView.getTop() : getPaddingTop(); final int firstPosition = lastTopView != null ? getPosition(lastTopView) : 0; // 現在のビューをスクラップにする
独自のLayoutManager
getChildAt(0) : null; final int lastTop = lastTopView != null ? lastTopView.getTop() : getPaddingTop(); final int firstPosition = lastTopView != null ? getPosition(lastTopView) : 0; // 現在のビューをスクラップにする detachAndScrapAttachedViews(recycler); int top = lastTop; int bottom; final int parentLeft = getPaddingLeft(); final int parentRight = getWidth() - getPaddingRight(); final int parentBottom = getHeight() - getPaddingBottom(); final int count = state.getItemCount(); for (int i = 0; firstPosition + i < count && top < parentBottom; i++, top = bottom) { View v = recycler.getViewForPosition(firstPosition + i); addView(v, i); measureChildWithMargins(v, 0, 0); bottom = top + getDecoratedMeasuredHeight(v); layoutDecorated(v, parentLeft, top, parentRight, bottom); } }}
独自のLayoutManager
まとめ
• ListViewやGridViewでは実現できない配置にしたい
• スクロールの振る舞い(スピードや時間)をコントロールしたい
• アイテムが追加/削除されたときのアニメーションをコントロールしたい
• アイテムの選択をフォーカスでやりたい、フォーカスを細かく制御したい
まとめ
• ListViewやGridViewでは実現できない配置にしたい
• スクロールの振る舞い(スピードや時間)をコントロールしたい
• アイテムが追加/削除されたときのアニメーションをコントロールしたい
• アイテムの選択をフォーカスでやりたい、フォーカスを細かく制御したい
RecyclerView ならそれ簡単にできますよ