Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Master of RecyclerView

  • Be the first to comment

Master of RecyclerView

  1. 1. Master of RecyclerView Yuki Anzai @ ABC 2015 Summer
  2. 2. • blog : Y.A.M の雑記帳 • y-anz-m.blogspot.com • twitter : @yanzm (やんざむ) • uPhyca Inc. (株式会社ウフィカ) あんざいゆき
  3. 3. New 今日話す内容は TechBoosterの 夏コミ本に 執筆してます http://techbooster.github.io/c88/ 事前のお知らせ • 今日の資料 → 公開されます • 今日の公演 → 録画&公開されます
  4. 4. RecyclerView A flexible view for providing a limited window into a large data set.
  5. 5. RecyclerView 大規模なデータセットに、 限定されたウィンドウを 提供するための柔軟なビュー
  6. 6. RecyclerView 大規模なデータセットの一部を ビューを再利用しながら 表示するためのコンポーネント
  7. 7. RecyclerView ListViewとかGridView みたいなやつ
  8. 8. RecyclerViewの構成 • RecyclerView • データを表示するためのスクロール可能な View
  9. 9. • RecyclerView • データを表示するためのスクロール可能な View • RecyclerView.LayoutManager • アイテム用のビューのサイズを計算し、配 置する RecyclerViewの構成
  10. 10. • RecyclerView.Adapter • RecyclerViewに表示するデータセットを 管理し、アイテム用のViewにデータを紐 づける RecyclerViewの構成
  11. 11. • RecyclerView.Adapter • RecyclerViewに表示するデータセットを 管理し、アイテム用のViewにデータを紐 づける • RecyclerView.ViewHolder • アイテム用のビューとメタデータを保持す る RecyclerViewの構成
  12. 12. RecyclerViewの構成 RecyclerView LayoutManager Adapter 子ビュー配置 子ビュー (ViewHolder) にデータ紐付 Recycler ViewHolderを再利用 再利用する ViewHolder
  13. 13. ListViewの構成 RecyclerView LayoutManager Adapter 子ビュー配置 子ビュー (ViewHolder) にデータ紐付 Recycler ViewHolderを再利用 再利用する ViewHolder ListView ListAdapter
  14. 14. ListViewの構成 ListView = RecyclerView + LayoutManager + Recycler + その他もろもろの機能 ListAdapter = Adapter + ViewHolder
  15. 15. ListViewの構成 ListView = RecyclerView + LayoutManager + Recycler + その他もろもろの機能 ListAdapter = Adapter + ViewHolder
  16. 16. ListView vs RecyclerView ListView RecyclerView 区切り線 ⚪ 自分で実装 listSelector ⚪ onItemClick ⚪ 自分で実装 choiceMode ⚪
  17. 17. ListView vs RecyclerView ListView RecyclerView Filter ⚪ 自分で実装 FadingEdge ⚪ 自分で実装 Header, Footer ⚪ 自分で実装 StaggeredGrid ⚪
  18. 18. ListView vs RecyclerView ListView RecyclerView 追加・削除の アニメーション 自分で実装 ⚪ Swipe to Dismiss 自分で実装 ⚪ Drag & Drop 自分で実装 ⚪ 横スクロール配置 ⚪
  19. 19. RecyclerViewの 一番シンプルな 使い方
  20. 20. 最低限必要なもの • RecyclerView • RecyclerView.LayoutManager • RecyclerView.Adapter • RecyclerView.ViewHolder
  21. 21. 最低限必要なもの • RecyclerView • RecyclerView.LayoutManager • RecyclerView.Adapter • RecyclerView.ViewHolder
  22. 22. <?xml version="1.0" encoding="utf-8"?>
 <android.support.v7.widget.RecyclerView 
 xmlns:android="http://schemas.android.com/apk/res/a 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
  23. 23. <?xml version="1.0" encoding="utf-8"?>
 <android.support.v7.widget.RecyclerView
 xmlns:android="http://schemas.android.com/apk/res/a 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
  24. 24. RecyclerView + LayoutManager <?xml version="1.0" encoding="utf-8"?>
 <android.support.v7.widget.RecyclerView
 xmlns:android="http://schemas.android.com/apk/res/a 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” />
  25. 25. 最低限必要なもの • RecyclerView • RecyclerView.LayoutManager • RecyclerView.Adapter • RecyclerView.ViewHolder
  26. 26. 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
  27. 27. 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
 Adapter
  28. 28. 
 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
  29. 29. 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 =
 Activity
  30. 30. 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
  31. 31. LinearLayoutManager GridLayoutManager StaggeredGridLayoutManager
  32. 32. LinearLayoutManager • 設定項目 • orientation • reverseLayout • stackFromEnd
  33. 33. 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
  34. 34. app:reverseLayout="true" app:stackFromEnd="true"
  35. 35. GridLayoutManager • 設定項目 • spanCount • orientation • reverseLayout • stackFromEnd
  36. 36. 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
  37. 37. StaggeredGridLayoutManager • 設定項目 • spanCount • orientation • reverseLayout
  38. 38. ItemDecorationで Dividerをつける
  39. 39. ItemDecoration • 装飾を行うためのクラス • アイテム用のViewのoffsetを指定 • onDraw()でRecyclerViewの下に描画 • onDrawOver()でRecyclerVieの上に描画
  40. 40. アイテム用の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);
  41. 41. アイテム用の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);
  42. 42. 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();
 Dividerを描画
  43. 43. 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を描画
  44. 44. @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を描画
  45. 45. 
 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を描画
  46. 46. onItemClick
  47. 47. OnItemClick • 方法がいくつかある • ViewHolderのitemViewに View.setOnClickListener • ItemDecorationでタップされたアイテムの 位置にselectorを描画
  48. 48. OnItemClick • 方法がいくつかある • ViewHolderのitemViewに View.setOnClickListener • ItemDecorationでタップされたアイテムの 位置にselectorを描画 v17 leanback library はこっち
  49. 49. // 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(backgroundRes viewHolder.itemView.setOnClickListener( OnItemClick
  50. 50. 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(backgroundRes 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
  51. 51. データの追加・削除・変更
  52. 52. notify** • ArrayAdapterに相当するものは用意されていない • データの追加・削除・変更時にはnotifyItem**()を呼ぶ • notifyDataSetChanged()はそれ以外のときだけにする
  53. 53. notify** • notifyItemChanged(int position) : positionの位置のアイテ ムの変更された • notifyItemInserted(int position) : posiitonの位置にアイテム が追加された • notifyItemRemoved(int position) : positionの位置のアイテ ムが削除された • notifyItemMoved(int fromPosition, int toPosition) : fromPositionにあったアイテムがtoPositionに移動した
  54. 54. notify** • notifyItemRangeChanged(int positionStart, int itemCount) : positionStartからitemCount個のアイテムが変更された • notifyItemRangeInserted(int positionStart, int itemCount) : positionStartにitemCount個のアイテムが追加された • notifyItemRangeRemoved(int positionStart, int itemCount) : positionStartからitemCount個のアイテムが削 除された • notifyDataSetChanged() : データセットが変更された
  55. 55. 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();
 ArrayAdapter的なRecyclerView用Adapter
  56. 56. 
 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> collecti 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) {
 ArrayAdapter的なRecyclerView用Adapter
  57. 57. 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
  58. 58. SwipeToDismiss と Drag & Drop
  59. 59. ItemTouchHelper • RecyclerViewに swipe to dismiss と drag & drop による並 び替え機能を追加するためにユーティリティクラス ItemTouchHelper.Callback callback = …;
 ItemTouchHelper itemTouchHelper = new ItemTouchHelper(callback);
 itemTouchHelper.attachToRecyclerView(recyclerView);
  60. 60. swipe to dismiss • ItemTouchHelper.Callbackのコンストラクタの第2引数で スワイプ方向を指定 • スワイプされたらonSwiped()が呼ばれる
  61. 61. 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));
 }
 };
  62. 62. drag and drop • ItemTouchHelper.Callbackのコンストラクタの第1引数で ドラッグ方向を指定 • ドロップされたらonMove()が呼ばれる • ドラッグが開始できるようになったタイミングで onSelectedChanged()が呼ばれる • ドラッグを終了するときにclearView()が呼ばれる
  63. 63. 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
  64. 64. 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 viewHo int actionState) {
 if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
 viewHolder.itemView.setBackgroundColor(Color.LTGRAY) }
 drag and drop
  65. 65. int to = target.getAdapterPosition();
 adapter.move(from, to);
 return true;
 }
 
 @Override
 public void onSelectedChanged(RecyclerView.ViewHolder viewHo 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
  66. 66. 独自のLayoutManager
  67. 67. 独自のLayoutManager 1. RecyclerView.LayoutManagerを継承 2. generateDefaultLayoutParams() で RecyclerView.LayoutParams() を返す 3. onLayoutChildren()で子ビューを配置 4. scrollVerticallyBy(), scrollHorizontallyBy()でスクロール分 だけ子ビューを移動&足りない分のビューを追加
  68. 68. onLayoutChildren()で子ビュー配置 1. detachAndScrapAttachedViews(recycler)で現在のビュー をリサイクル対象にする 2. recycler.getViewForPosition(i)でアイテム用のビューを取得 3. addView() 4. measureChildWithMargins()でビューのサイズを計算 5. layoutDecorated(v, left, top, right, bottom)で配置
  69. 69. 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
  70. 70. 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
  71. 71. まとめ
  72. 72. まとめ ListViewやGridViewで十分要求が満たせる場合は 無理にRecyclerViewを使う必要はありません
  73. 73. まとめ • ListViewやGridViewでは実現できない配置にしたい • スクロールの振る舞い(スピードや時間)をコントロール したい • アイテムが追加/削除されたときのアニメーションをコン トロールしたい • アイテムの選択をフォーカスでやりたい、フォーカスを細 かく制御したい
  74. 74. まとめ • ListViewやGridViewでは実現できない配置にしたい • スクロールの振る舞い(スピードや時間)をコントロール したい • アイテムが追加/削除されたときのアニメーションをコン トロールしたい • アイテムの選択をフォーカスでやりたい、フォーカスを細 かく制御したい RecyclerView ならそれ簡単にできますよ
  75. 75. New TechBoosterの 夏コミ本で RecyclerViewに ついて 書きます。 ありがとうございました。 http://techbooster.github.io/c88/

×