master of recyclerview

82
Master of RecyclerView Yuki Anzai @ ABC 2015 Summer

Upload: yuki-anzai

Post on 14-Aug-2015

2.923 views

Category:

Technology


1 download

TRANSCRIPT

Master of RecyclerView

Yuki Anzai @ ABC 2015 Summer

• blog : Y.A.M の雑記帳

• y-anz-m.blogspot.com

• twitter : @yanzm (やんざむ)

• uPhyca Inc. (株式会社ウフィカ)

あんざいゆき

New

今日話す内容はTechBoosterの 夏コミ本に 執筆してます

http://techbooster.github.io/c88/

事前のお知らせ

• 今日の資料 → 公開されます

• 今日の公演 → 録画&公開されます

RecyclerView

A flexible view for providing a limited window

into a large data set.

RecyclerView

大規模なデータセットに、 限定されたウィンドウを 提供するための柔軟なビュー

RecyclerView

大規模なデータセットの一部を ビューを再利用しながら

表示するためのコンポーネント

RecyclerView

ListViewとかGridView みたいなやつ

RecyclerViewの構成

• RecyclerView

• データを表示するためのスクロール可能なView

• RecyclerView

• データを表示するためのスクロール可能なView

• RecyclerView.LayoutManager

• アイテム用のビューのサイズを計算し、配置する

RecyclerViewの構成

• RecyclerView.Adapter

• RecyclerViewに表示するデータセットを管理し、アイテム用のViewにデータを紐づける

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

• 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

LinearLayoutManager

GridLayoutManager

StaggeredGridLayoutManager

LinearLayoutManager

• 設定項目

• orientation

• reverseLayout

• stackFromEnd

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

app:reverseLayout="true" app:stackFromEnd="true"

GridLayoutManager

• 設定項目

• spanCount

• orientation

• reverseLayout

• stackFromEnd

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

StaggeredGridLayoutManager

• 設定項目

• spanCount

• orientation

• reverseLayout

ItemDecorationで

Dividerをつける

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

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

SwipeToDismiss と Drag & Drop

ItemTouchHelper

• RecyclerViewに swipe to dismiss と drag & drop による並び替え機能を追加するためにユーティリティクラス

ItemTouchHelper.Callback callback = …;

ItemTouchHelper itemTouchHelper = new ItemTouchHelper(callback);

itemTouchHelper.attachToRecyclerView(recyclerView);

swipe to dismiss

• ItemTouchHelper.Callbackのコンストラクタの第2引数でスワイプ方向を指定

• スワイプされたらonSwiped()が呼ばれる

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

独自の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で十分要求が満たせる場合は

無理にRecyclerViewを使う必要はありません

まとめ

• ListViewやGridViewでは実現できない配置にしたい

• スクロールの振る舞い(スピードや時間)をコントロールしたい

• アイテムが追加/削除されたときのアニメーションをコントロールしたい

• アイテムの選択をフォーカスでやりたい、フォーカスを細かく制御したい

まとめ

• ListViewやGridViewでは実現できない配置にしたい

• スクロールの振る舞い(スピードや時間)をコントロールしたい

• アイテムが追加/削除されたときのアニメーションをコントロールしたい

• アイテムの選択をフォーカスでやりたい、フォーカスを細かく制御したい

RecyclerView ならそれ簡単にできますよ

New

TechBoosterの 夏コミ本で

RecyclerViewに ついて 書きます。

ありがとうございました。

http://techbooster.github.io/c88/