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

Radical RecyclerView: Droidcon NYC 2016

Lisa Wray
September 29, 2016

Radical RecyclerView: Droidcon NYC 2016

Practical advice for complex RecyclerViews

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