Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Radical RecyclerView: Droidcon NYC 2016

Avatar for Lisa Wray Lisa Wray
September 29, 2016

Radical RecyclerView: Droidcon NYC 2016

Practical advice for complex RecyclerViews

Avatar for Lisa Wray

Lisa Wray

September 29, 2016
Tweet

More Decks by Lisa Wray

Other Decks in Technology

Transcript

  1. “RV Animations & Behind the Scenes” Android Dev Summit 2015

    Yigit’s talk at Android Dev Summit youtube.com/watch?v=imsr8NrIAMs “Pro RecyclerView” 360|AnDev speakerdeck.com/yigit/pro-recyclerview
  2. @lisawrayz • RecyclerView: Creates, binds, recycles • Adapter: Provides data

    • LayoutManager: Lays out & positions • ItemDecoration: Adds offsets, draws over / under • ItemAnimator: Animates changes • ItemTouchHelper: Handles drag&drop, swipe-to-delete • SnapHelper: Creates ViewPager-like scrolls & flings • DiffUtil: Calculates changes for you
  3. @lisawrayz • RecyclerView: RecyclerView • Adapter: RecyclerView.Adapter • LayoutManager: Linear-

    / GridLayoutManager • ItemDecoration: nope • ItemAnimator: DefaultItemAnimator • ItemTouchHelper: SimpleItemTouchHelper / SimpleCallback • SnapHelper: LinearSnapHelper • DiffUtil: DiffUtil
  4. @lisawrayz snapping Base class: SnapHelper LinearSnapHelper: center snapping SnapHelper snapHelper

    = new GravitySnapHelper(Gravity.START);
 snapHelper.attachToRecyclerView(recyclerView); GravitySnapHelper: rubensousa.github.io/2016/08/recyclerviewsnap
  5. private TouchCallback touchCallback = new SwipeTouchCallback(); ItemTouchHelper itemTouchHelper = new

    ItemTouchHelper(touchCallback); 
 itemTouchHelper.attachToRecyclerView(recyclerView);
  6. public class SwipeTouchCallback extends ItemTouchHelper.SimpleCallback {
 
 public SwipeTouchCallback() {


    super(0, 0);
 }
 
 @Override public int getSwipeDirs(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { 
 if (viewHolder.getItemViewType() == R.layout.item_card) {
 return ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
 } else {
 return super.getSwipeDirs(recyclerView, viewHolder);
 }
 }
 }
  7. public class SwipeTouchCallback extends ItemTouchHelper.SimpleCallback {
 
 …
 
 @Override

    public void onSwiped( RecyclerView.ViewHolder viewHolder, int direction) { 
 int position = viewHolder.getAdapterPosition();
 // remove & notify
 }
 
 @Override public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
 View child = viewHolder.itemView;
 
 // Fade out the item
 child.setAlpha(1 - (Math.abs(dX) / child.getWidth()));
 
 super.onChildDraw(…);
 }
 }
  8. 
 DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff( new Callback(items, newItems)); // Actually

    change adapter
 adapter.clear();
 adapter.addAll(newItems); // Notify
 diffResult.dispatchUpdatesTo(adapter);
  9. private class Callback extends DiffUtil.Callback { …
 
 @Override public

    boolean areItemsTheSame( int oldItemPosition, int newItemPosition) {} 
 
 @Override public boolean areContentsTheSame( int oldItemPosition, int newItemPosition) {}
 }
  10. public class ViewTypes {
 
 public static final int HEADER

    = 0;
 public static final int CARD = 1;
 public static final int FULL_BLEED_CARD = 2;
 public static final int SQUARE_CARD = 3;
 public static final int SMALL_CARD = 4;
 
 } naïve way
  11. @Override public RecyclerView.ViewHolder 
 onCreateViewHolder(ViewGroup parent, int viewType) {
 LayoutInflater

    inflater = … ;
 
 View view;
 switch (viewType) {
 case HEADER:
 view = inflater.inflate(
 R.layout.item_header, parent, false);
 return new HeaderViewHolder(view);
 case CARD:
 view = inflater.inflate(
 R.layout.item_card, parent, false);
 return new CardViewHolder(view);
 case FULL_BLEED_CARD:
 case SQUARE_CARD:
 case SMALL_CARD:
 …
 }
 } item creation
  12. @Override public void onBindViewHolder( RecyclerView.ViewHolder viewHolder, int position) { 


    Model model = models.get(position);
 switch (viewHolder.getItemViewType()) {
 case HEADER:
 HeaderViewHolder headerVH = (HeaderViewHolder) viewHolder;
 headerVH.title.setText(model.getTitle());
 if (model.getSubtitle() != null) {
 headerVH.subtitle.setText(model.getSubtitle());
 }
 headerVH.subtitle.setVisibility(
 model.getSubtitle() != null ? View.VISIBLE : View.GONE);
 headerVH.icon.setImageDrawable(model.getIcon());
 break;
 case CARD:
 CardViewHolder cardVH = (CardViewHolder) viewHolder; …
 break;
 case FULL_BLEED_CARD:
 case SQUARE_CARD:
 …
 }
 } item bind
  13. public class Adapter extends RecyclerView.Adapter { List<AdapterDelegate> delegates; public Adapter()

    { delegates.add(new HeaderDelegate()); delegates.add(new CardDelegate()); } @Override public void onBindViewHolder( RecyclerView.ViewHolder viewHolder, int position) { for (AdapterDelegate delegate : delegates) { if (delegate.handles(viewHolder)) { delegate.onBind(viewHolder, position); } } } } boiler plate
  14. public class ItemTypes {
 
 public static final int HEADER

    = 0;
 public static final int CARD = 1;
 public static final int FULL_BLEED_CARD = 2;
 public static final int SQUARE_CARD = 3;
 public static final int SMALL_CARD = 4;
 
 } better way
  15. public class SongItem extends Item {
 private final Song song;


    
 public SongItem(Song song) {
 this.song = song;
 }
 
 @Override public void bind(ViewHolder viewHolder, int position) { // binding logic here
 }
 
 @Override public int getLayout() {
 return R.layout.song;
 }
 } Item
  16. public class PhotoModel extends EpoxyModel<PhotoView> { private final Photo photo;

    public PhotoModel(Photo photo) { this.photo = photo; } @LayoutRes public int getDefaultLayout() { return R.layout.view_model_photo; } @Override public void bind(PhotoView photoView) { photoView.setUrl(photo.getUrl()); } } Epoxy
  17. public class PhotoModel extends EpoxyModel<PhotoView> { private final Photo photo;

    public PhotoModel(Photo photo) { this.photo = photo; } @LayoutRes public int getDefaultLayout() { return R.layout.view_model_photo; } @Override public void bind(PhotoView photoView) { photoView.setUrl(photo.getUrl()); } } Epoxy
  18. public class PhotoModel extends EpoxyModel<PhotoView> { private final Photo photo;

    public PhotoModel(Photo photo) { this.photo = photo; } @LayoutRes public int getDefaultLayout() { return R.layout.view_model_photo; } @Override public void bind(PhotoView photoView) { photoView.setUrl(photo.getUrl()); } } Epoxy
  19. public class PhotoModel extends EpoxyModel<PhotoView> { private final Photo photo;

    public PhotoModel(Photo photo) { this.photo = photo; } @LayoutRes public int getDefaultLayout() { return R.layout.view_model_photo; } @Override public void bind(PhotoView photoView) { photoView.setUrl(photo.getUrl()); } } Epoxy need a custom view or view holder for each item
  20. consider data binding /MyAdapter.java @Override public RecyclerView.ViewHolder onCreateViewHolder( ViewGroup parent,

    int layoutResId) {
 LayoutInflater inflater = LayoutInflater.from( parent.getContext());
 ViewDataBinding binding = DataBindingUtil.inflate( inflater, layoutResId, parent, false);
 return new ViewHolder<>(binding);
 } public class ViewHolder<T extends ViewDataBinding> extends RecyclerView.ViewHolder {
 public final T binding;
 
 public ViewHolder(T binding) {
 super(binding.getRoot());
 this.binding = binding;
 }
 }
  21. consider data binding /MyAdapter.java @Override public RecyclerView.ViewHolder onCreateViewHolder( ViewGroup parent,

    int layoutResId) {
 LayoutInflater inflater = LayoutInflater.from( parent.getContext());
 ViewDataBinding binding = DataBindingUtil.inflate( inflater, layoutResId, parent, false);
 return new ViewHolder<>(binding);
 } public class ViewHolder<T extends ViewDataBinding> extends RecyclerView.ViewHolder {
 public final T binding;
 
 public ViewHolder(T binding) {
 super(binding.getRoot());
 this.binding = binding;
 }
 }
  22. @lisawrayz choosing a spanCount • Least common multiple (LCM) of

    all your desired column splits • I want single, double, triple & quad columns
 LCM(1, 2, 3, 4) = 12 • No performance hit from having a large num of columns. (Large num of items might be)
  23. final int spanCount = 12;
 layoutManager = new GridLayoutManager(this, spanCount);


    layoutManager.setSpanSizeLookup( new GridLayoutManager.SpanSizeLookup() {
 @Override public int getSpanSize(int position) {
 int viewType = adapter.getItemViewType(position);
 switch (viewType) {
 case HEADER:
 return spanCount;
 case CARD:
 return spanCount / 2;
 case FULL_BLEED_CARD:
 return spanCount;
 case SMALL_CARD:
 return spanCount / 3;
 default:
 return 1;
 }
 }
 span size lookup
  24. final int spanCount = 12;
 layoutManager = new GridLayoutManager(this, spanCount);


    layoutManager.setSpanSizeLookup( new GridLayoutManager.SpanSizeLookup() {
 @Override public int getSpanSize(int position) {
 Item item = groupAdapter.getItem(position);
 return item.getSpanSize(spanCount, position);
 }
 });
  25. public class SongItem extends Item {
 private final Song song;


    
 public SongItem(Song song) {
 this.song = song;
 }
 
 @Override public void bind(ViewHolder viewHolder, int position) {…}
 
 @Override public int getLayout() {
 return R.layout.song;
 } @Override public int getSpanSize(int spanCount, int position) {
 // individual item’s span size
 }
 } Item
  26. public class SongItem extends Item {
 private final Song song;


    
 public SongItem(Song song) {
 this.song = song;
 }
 
 @Override public void bind(ViewHolder viewHolder, int position) {…}
 
 @Override public int getLayout() {
 return R.layout.song;
 } @Override public int getSpanSize(int spanCount, int position) {
 // individual item’s span size
 }
 } Item
  27. Header commentHeader; List<Comment> comments; int index = adapter.getPosition(commentHeader) + 1;

    adapter.addAll(index, comments); adapter.notifyInsert(index, comments.size()); Don’t hold adapter position!
  28. Header commentHeader; List<Comment> comments; int index = adapter.getPosition(commentHeader) + 1;

    adapter.addAll(index, comments); adapter.notifyInsert(index, comments.size()); Use references List.indexOf()
  29. Header commentHeader; List<Comment> comments; int index = adapter.getPosition(commentHeader) + 1;

    adapter.addAll(index, comments); adapter.notifyInsert(index, comments.size()); Use references
  30. EpoxyModel commentHeaderModel; List<EpoxyModel> commentModels; for (int i = commentModels.size -

    1; i >= 0 ; i--) { expoxyAdapter.insertModelAfter(commentHeaderModel); } Epoxy
  31. EpoxyModel commentHeaderModel; List<EpoxyModel> commentModels; for (int i = commentModels.size -

    1; i >= 0 ; i--) { expoxyAdapter.insertModelAfter(commentHeaderModel); } Epoxy
  32. EpoxyModel commentHeaderModel; List<EpoxyModel> commentModels; for (int i = commentModels.size -

    1; i >= 0 ; i--) { expoxyAdapter.insertModelAfter(commentHeaderModel); } epoxyAdapter.hideModels(commentModels); Epoxy
  33. GroupAdapter groupAdapter; Item item = new TitleItem(); groupAdapter.add(item); HeaderItem header

    = new HeaderItem(“Comments”); ExpandableGroup commentGroup = new ExpandableGroup(header); groupAdapter.add(commentGroup); groupie
  34. GroupAdapter groupAdapter; Item item = new TitleItem(); groupAdapter.add(item); HeaderItem header

    = new HeaderItem(“Comments”); ExpandableGroup commentGroup = new ExpandableGroup(header); groupAdapter.add(commentGroup); groupie
  35. GroupAdapter groupAdapter; Item item = new TitleItem(); groupAdapter.add(item); HeaderItem header

    = new HeaderItem(“Comments”); ExpandableGroup commentGroup = new ExpandableGroup(header); groupAdapter.add(commentGroup); groupie
  36. public class ItemDecoration {
 
 public void getItemOffsets(…) {}
 


    public void onDraw(…) {}
 
 public void onDrawOver(…) {} 
 }
  37. public class ItemDecoration {
 
 public void getItemOffsets(…) {}
 


    public void onDraw(…) {}
 
 public void onDrawOver(…) {} 
 }
  38. Is this item on the left edge, right edge, middle,

    …? @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { 
 RecyclerView.ViewHolder viewHolder = parent.getChildViewHolder(view);
 
 GridLayoutManager.LayoutParams layoutParams = view.getLayoutParams();
 GridLayoutManager gridLayoutManager = parent.getLayoutManager(); 
 int spanSize = layoutParams.getSpanSize();
 int totalSpanSize = gridLayoutManager.getSpanCount();
 
 if (spanSize + layoutParams.getSpanIndex() == totalSpanSize) {
 // Item reaches to right edge of list
 outRect.right = padding;
 }
 if (layoutParams.getSpanIndex() == 0) {
 // Item's left edge is on left edge of list
 outRect.left = padding;
 }
 }
  39. Is this item on the left edge, right edge, middle,

    …? @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { 
 RecyclerView.ViewHolder viewHolder = parent.getChildViewHolder(view);
 
 GridLayoutManager.LayoutParams layoutParams = view.getLayoutParams();
 GridLayoutManager gridLayoutManager = parent.getLayoutManager(); 
 int spanSize = layoutParams.getSpanSize();
 int totalSpanSize = gridLayoutManager.getSpanCount();
 
 if (spanSize + layoutParams.getSpanIndex() == totalSpanSize) {
 // Item reaches to right edge of list
 outRect.right = padding;
 }
 if (layoutParams.getSpanIndex() == 0) {
 // Item's left edge is on left edge of list
 outRect.left = padding;
 }
 }
  40. Is this item on the left edge, right edge, middle,

    …? @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { 
 RecyclerView.ViewHolder viewHolder = parent.getChildViewHolder(view);
 
 GridLayoutManager.LayoutParams layoutParams = view.getLayoutParams();
 GridLayoutManager gridLayoutManager = parent.getLayoutManager(); 
 int spanSize = layoutParams.getSpanSize();
 int totalSpanSize = gridLayoutManager.getSpanCount();
 
 if (spanSize + layoutParams.getSpanIndex() == totalSpanSize) {
 // Item reaches to right edge of list
 outRect.right = padding;
 }
 if (layoutParams.getSpanIndex() == 0) {
 // Item's left edge is on left edge of list
 outRect.left = padding;
 }
 }
  41. Is this item on the left edge, right edge, middle,

    …? @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { 
 RecyclerView.ViewHolder viewHolder = parent.getChildViewHolder(view);
 
 GridLayoutManager.LayoutParams layoutParams = view.getLayoutParams();
 GridLayoutManager gridLayoutManager = parent.getLayoutManager(); 
 int spanSize = layoutParams.getSpanSize();
 int totalSpanSize = gridLayoutManager.getSpanCount();
 
 if (spanSize + layoutParams.getSpanIndex() == totalSpanSize) {
 // Item reaches to right edge of list
 outRect.right = padding;
 }
 if (layoutParams.getSpanIndex() == 0) {
 // Item's left edge is on left edge of list
 outRect.left = padding;
 }
 }
  42. Is this item on the left edge, right edge, middle,

    …? @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { 
 RecyclerView.ViewHolder viewHolder = parent.getChildViewHolder(view);
 
 GridLayoutManager.LayoutParams layoutParams = view.getLayoutParams();
 GridLayoutManager gridLayoutManager = parent.getLayoutManager(); 
 int spanSize = layoutParams.getSpanSize();
 int totalSpanSize = gridLayoutManager.getSpanCount();
 
 if (spanSize + layoutParams.getSpanIndex() == totalSpanSize) {
 // Item reaches to right edge of list
 outRect.right = padding;
 }
 if (layoutParams.getSpanIndex() == 0) {
 // Item's left edge is on left edge of list
 outRect.left = padding;
 }
 }
  43. @lisawrayz eek! different size squares!! 2x offsets on edges? uneven

    item widths — offsets don’t change measured item width
  44. @lisawrayz each item needs same total padding … just differently

    distributed “DebugItemDecoration” in example project
  45. even column padding @Override public void getItemOffsets(Rect outRect, View view,

    RecyclerView parent, RecyclerView.State state) {
 
 GridLayoutManager.LayoutParams layoutParams = view.getLayoutParams();
 GridLayoutManager gridLayoutManager = parent.getLayoutManager();
 float spanSize = layoutParams.getSpanSize();
 float totalSpanSize = gridLayoutManager.getSpanCount();
 
 float n = totalSpanSize / spanSize; // num columns
 float c = layoutParams.getSpanIndex() / spanSize; // column index
 
 float leftPadding = padding * ((n - c) / n);
 float rightPadding = padding * ((c + 1) / n);
 
 outRect.left = (int) leftPadding;
 outRect.right = (int) rightPadding;
 }
  46. even column padding @Override public void getItemOffsets(Rect outRect, View view,

    RecyclerView parent, RecyclerView.State state) {
 
 GridLayoutManager.LayoutParams layoutParams = view.getLayoutParams();
 GridLayoutManager gridLayoutManager = parent.getLayoutManager();
 float spanSize = layoutParams.getSpanSize();
 float totalSpanSize = gridLayoutManager.getSpanCount();
 
 float n = totalSpanSize / spanSize; // num columns
 float c = layoutParams.getSpanIndex() / spanSize; // column index
 
 float leftPadding = padding * ((n - c) / n);
 float rightPadding = padding * ((c + 1) / n);
 
 outRect.left = (int) leftPadding;
 outRect.right = (int) rightPadding;
 }
  47. even column padding @Override public void getItemOffsets(Rect outRect, View view,

    RecyclerView parent, RecyclerView.State state) {
 
 GridLayoutManager.LayoutParams layoutParams = view.getLayoutParams();
 GridLayoutManager gridLayoutManager = parent.getLayoutManager();
 float spanSize = layoutParams.getSpanSize();
 float totalSpanSize = gridLayoutManager.getSpanCount();
 
 float n = totalSpanSize / spanSize; // num columns
 float c = layoutParams.getSpanIndex() / spanSize; // column index
 
 float leftPadding = padding * ((n - c) / n);
 float rightPadding = padding * ((c + 1) / n);
 
 outRect.left = (int) leftPadding;
 outRect.right = (int) rightPadding;
 }
  48. public class ItemDecoration {
 
 public void getItemOffsets(…) {}
 


    public void onDraw(…) {}
 
 public void onDrawOver(…) {} 
 }
  49. @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {

    
 for (int i = 0; i < parent.getChildCount(); i++) {
 View child = parent.getChildAt(i);
 if (!isHeader(child, parent)) continue;
 
 float top = child.getTop();
 float bottom = child.getBottom();
 float right = child.getRight();
 float left = child.getLeft();
 c.drawRect(left, top, right, bottom, paint);
 }
 }
  50. @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {

    
 for (int i = 0; i < parent.getChildCount(); i++) {
 View child = parent.getChildAt(i);
 if (!isHeader(child, parent)) continue;
 
 float top = child.getTop();
 float bottom = child.getBottom();
 float right = child.getRight();
 float left = child.getLeft();
 c.drawRect(left, top, right, bottom, paint);
 }
 }
  51. @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {

    
 for (int i = 0; i < parent.getChildCount(); i++) {
 View child = parent.getChildAt(i);
 if (!isHeader(child, parent)) continue;
 
 float top = child.getTop();
 float bottom = child.getBottom();
 float right = child.getRight();
 float left = child.getLeft();
 c.drawRect(left, top, right, bottom, paint);
 }
 }
  52. @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {

    
 for (int i = 0; i < parent.getChildCount(); i++) {
 View child = parent.getChildAt(i);
 if (!isHeader(child, parent)) continue;
 
 float top = child.getTop();
 float bottom = child.getBottom();
 float right = child.getRight();
 float left = child.getLeft();
 c.drawRect(left, top, right, bottom, paint);
 }
 }
  53. @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {


    for (int i = 0; i < parent.getChildCount(); i++) {
 View child = parent.getChildAt(i);
 if (!isHeader(child, parent)) continue;
 
 RecyclerView.LayoutManager lm = parent.getLayoutManager();
 
 float top = lm.getDecoratedTop(child);
 float bottom = lm.getDecoratedBottom(child);
 float right = lm.getDecoratedRight(child);
 float left = lm.getDecoratedLeft(child);
 c.drawRect(left, top, right, bottom, paint);
 }
 } use decorated bounds
  54. @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {


    for (int i = 0; i < parent.getChildCount(); i++) {
 View child = parent.getChildAt(i);
 if (!isHeader(child, parent)) continue;
 
 RecyclerView.LayoutManager lm = parent.getLayoutManager();
 
 float top = lm.getDecoratedTop(child) + child.getTranslationY();
 float bottom = lm.getDecoratedBottom(child) + child.getTranslationY();
 float right = lm.getDecoratedRight(child) + child.getTranslationX();
 float left = lm.getDecoratedLeft(child) + child.getTranslationX();
 c.drawRect(left, top, right, bottom, paint);
 }
 use translation
  55. @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {


    for (int i = 0; i < parent.getChildCount(); i++) {
 View child = parent.getChildAt(i);
 if (!isHeader(child, parent)) continue;
 
 RecyclerView.LayoutManager lm = parent.getLayoutManager();
 
 float top = lm.getDecoratedTop(child) + child.getTranslationY();
 float bottom = lm.getDecoratedBottom(child) + child.getTranslationY();
 float right = lm.getDecoratedRight(child) + child.getTranslationX();
 float left = lm.getDecoratedLeft(child) + child.getTranslationX();
 c.drawRect(left, top, right, bottom, paint);
 }
 use translation
  56. @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {


    for (int i = 0; i < parent.getChildCount(); i++) {
 View child = parent.getChildAt(i);
 
 int position = parent.getChildAdapterPosition(child); }
 } use layout position
  57. @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {


    for (int i = 0; i < parent.getChildCount(); i++) {
 View child = parent.getChildAt(i);
 
 int position = parent.getChildAdapterPosition(child); }
 } use layout position can be NO_POSITION during animation
  58. @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {


    for (int i = 0; i < parent.getChildCount(); i++) {
 View child = parent.getChildAt(i);
 int position = parent.getChildLayoutPosition(child); }
 } use layout position