Mastering RecyclerView Layouts

Mastering RecyclerView Layouts

Walkthrough of a (hopefully) sane method of creating a custom LayoutManager for Android's RecyclerView.

674ae12d99324148551d77de28bc4dbe?s=128

Dave Smith

April 10, 2015
Tweet

Transcript

  1. Mastering RecyclerView Layouts Dave Smith • NewCircle, Inc. @devunwired +DaveSmithDev

  2. None
  3. RecyclerView recyclerView = new RecyclerView(this);
 recyclerView.setLayoutManager(…);
 
 setContentView(recyclerView);

  4. LinearLayoutManager LinearLayoutManager manager = new LinearLayoutManager( this, LinearLayoutManager.VERTICAL, false);

  5. GridLayoutManager GridLayoutManager manager = new GridLayoutManager( this,
 2, /* Number

    of grid columns */
 GridLayoutManager.VERTICAL,
 false);
  6. GridLayoutManager GridLayoutManager manager = new GridLayoutManager( this, 2, /* Number

    of grid columns */ GridLayoutManager.VERTICAL, false); manager.setSpanSizeLookup(
 new GridLayoutManager.SpanSizeLookup() {
 @Override
 public int getSpanSize(int position) {
 //Stagger every other row
 return (position % 3 == 0 ? 2 : 1);
 }
 });
  7. StaggeredGridLayoutManager StaggeredGridLayoutManager manager = new StaggeredGridLayoutManager(
 2, /* Number of

    grid columns */
 StaggeredGridLayoutManager.VERTICAL);
  8. StaggeredGridLayoutManager StaggeredGridLayoutManager manager = new StaggeredGridLayoutManager(
 2, /* Number of

    grid columns */
 StaggeredGridLayoutManager.VERTICAL); manager.setGapStrategy( StaggeredGridLayoutManager.GAP_HANDLING_NONE);
  9. Case Study: FixedGridLayoutManager 1 2 3 4 5 6 7

    8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
  10. Case Study: FixedGridLayoutManager 1 2 3 4 5 6 7

    8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
  11. The Recycler LayoutManager Adapter ?

  12. The Recycler LayoutManager Recycler Adapter Obtain View Recycle View Bind

  13. Recycler The Recycler Scrap Heap detachAndScrapView() Recycle Pool removeAndRecycleView()

  14. Handling Decorations Item View getDecoratedLeft() getDecoratedTop() getDecoratedRight() getDecoratedBottom()

  15. Handling Decorations Item View getDecoratedWidth() getDecoratedHeight()

  16. Handling Decorations Item View layoutDecorated() measureChild()

  17. The Fill Technique fillGaps() 1. Discover first visible position/location 2.

    Find layout gaps (at the edges) 3. Scrap everything 4. Lay out all visible positions 5. Recycle remaining views Save as little state as possible.
  18. private void fillGrid(int direction, …,
 RecyclerView.Recycler recycler,
 RecyclerView.State state) {


    //…Obtain the first visible item position… findFirstVisiblePosition(); 
 detachAndScrapAttachedViews(recycler);
 
 for (…) {
 int nextPosition = …;
 
 View View = recycler.getViewForPosition(nextPosition); addView(view);
 
 measureChildWithMargins(view, …);
 layoutDecorated(view, …);
 }
 
 //Remove anything that is left behind final List<RecyclerView.ViewHolder> scrapList = recycler.getScrapList();
 for (int i=0; i < scrapList.size(); i++) {
 final View removingView = scrapList.get(i);
 recycler.recycleView(removingView);
 }
 }
  19. private void fillGrid(int direction, …,
 RecyclerView.Recycler recycler,
 RecyclerView.State state) {


    //…Obtain the first visible item position… findFirstVisiblePosition(); 
 detachAndScrapAttachedViews(recycler);
 
 for (…) {
 int nextPosition = …;
 
 View View = recycler.getViewForPosition(nextPosition); addView(view);
 
 measureChildWithMargins(view, …);
 layoutDecorated(view, …);
 }
 
 //Remove anything that is left behind final List<RecyclerView.ViewHolder> scrapList = recycler.getScrapList();
 for (int i=0; i < scrapList.size(); i++) {
 final View removingView = scrapList.get(i);
 recycler.recycleView(removingView);
 }
 }
  20. private void fillGrid(int direction, …,
 RecyclerView.Recycler recycler,
 RecyclerView.State state) {


    //…Obtain the first visible item position… findFirstVisiblePosition(); 
 detachAndScrapAttachedViews(recycler);
 
 for (…) {
 int nextPosition = …;
 
 View View = recycler.getViewForPosition(nextPosition); addView(view);
 
 measureChildWithMargins(view, …);
 layoutDecorated(view, …);
 }
 
 //Remove anything that is left behind final List<RecyclerView.ViewHolder> scrapList = recycler.getScrapList();
 for (int i=0; i < scrapList.size(); i++) {
 final View removingView = scrapList.get(i);
 recycler.recycleView(removingView);
 }
 }
  21. private void fillGrid(int direction, …,
 RecyclerView.Recycler recycler,
 RecyclerView.State state) {


    //…Obtain the first visible item position… findFirstVisiblePosition(); 
 detachAndScrapAttachedViews(recycler);
 
 for (…) {
 int nextPosition = …;
 
 View View = recycler.getViewForPosition(nextPosition); addView(view);
 
 measureChildWithMargins(view, …);
 layoutDecorated(view, …);
 }
 
 //Remove anything that is left behind final List<RecyclerView.ViewHolder> scrapList = recycler.getScrapList();
 for (int i=0; i < scrapList.size(); i++) {
 final View removingView = scrapList.get(i);
 recycler.recycleView(removingView);
 }
 }
  22. Level 1: Make It Work onLayoutChildren() Run a FILL operation

    canScrollVertically() canScrollHorizontally() Which axes enable scrolling? scrollHorizontallyBy() scrollVerticallyBy() Clamp supplied delta against boundary conditions Shift all views in the layout Trigger a FILL operation Report back actual distance scrolled
  23. Level 1: Make It Work public int scrollHorizontallyBy(int dx, RecyclerView.Recycler

    recycler, RecyclerView.State state) {
 …
 
 int delta;
 if (dx > 0) { // Contents are scrolling left
 delta = …;
 } else { // Contents are scrolling right
 delta = …;
 }
 
 offsetChildrenHorizontal(delta);
 
 if (dx > 0) {
 fillGrid(DIRECTION_START, …, recycler, state);
 } else {
 fillGrid(DIRECTION_END, …, recycler, state);
 }
 
 return -delta;
 }
  24. Level 2: Data Set Changes onAdapterChanged() removeAllViews() notifyDataSetChanged() -> onLayoutChildren()

    Treat as a new layout, and just run a FILL…
  25. Level 3: Add Some Flair scrollToPosition() Track Requested Position Trigger

    requestLayout() smoothScrollToPosition() Create a SmoothScroller instance Set the target position Invoke startSmoothScroll()
  26. Level 3: Add Some Flair LinearSmoothScroller scroller = new LinearSmoothScroller(recyclerView.getContext())

    {
 
 @Override
 public PointF computeScrollVectorForPosition(int targetPosition) {
 findFirstVisiblePosition(); final int rowOffset = …;
 final int columnOffset = …;
 
 return new PointF(columnOffset * stepWidth, rowOffset * stepHeight);
 }
 }; 
 scroller.setTargetPosition(position);
 startSmoothScroll(scroller);
  27. Level 4: Zen Master supportsPredictiveItemAnimations() We have more to say

    about animations…
  28. Default Item Animations

  29. Default Item Animations

  30. Level 4: Zen Master supportsPredictiveItemAnimations() We have more to say

    about animations… onLayoutChildren() [isPreLayout() == true] Note any removed views -> LayoutParams.isViewRemoved() Add extra views during FILL to cover space left behind onLayoutChildren() Place disappearing views off-screen
  31. public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
 … 
 if

    (state.isPreLayout()) {
 final View child = getChildAt(…); final LayoutParams lp = (LayoutParams) child.getLayoutParams();
 if (lp.isItemRemoved()) { //Track and count view removals… }
 }
 
 //Clear all attached views into the recycle bin
 detachAndScrapAttachedViews(recycler);
 
 //Fill the grid for the initial layout of views
 fillGrid(…);
 
 //Anything left? Lay out for disappearing animation
 if (!state.isPreLayout() && !recycler.getScrapList().isEmpty()) {
 final List<RecyclerView.ViewHolder> scrapList = recycler.getScrapList();
 //Laying out a scrap item removes it from the list… //Beware concurrent modification! 
 …
 }
 }
  32. public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
 … 
 if

    (state.isPreLayout()) {
 final View child = getChildAt(…); final LayoutParams lp = (LayoutParams) child.getLayoutParams();
 if (lp.isItemRemoved()) { //Track and count view removals… }
 }
 
 //Clear all attached views into the recycle bin
 detachAndScrapAttachedViews(recycler);
 
 //Fill the grid for the initial layout of views…account for extras
 fillGrid(…);
 
 //Anything left? Lay out for disappearing animation
 if (!state.isPreLayout() && !recycler.getScrapList().isEmpty()) {
 final List<RecyclerView.ViewHolder> scrapList = recycler.getScrapList();
 //Laying out a scrap item removes it from the list… //Beware concurrent modification! 
 …
 }
 }
  33. public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
 … 
 if

    (state.isPreLayout()) {
 final View child = getChildAt(…); final LayoutParams lp = (LayoutParams) child.getLayoutParams();
 if (lp.isItemRemoved()) { //Track and count view removals… }
 }
 
 //Clear all attached views into the recycle bin
 detachAndScrapAttachedViews(recycler);
 
 //Fill the grid for the initial layout of views
 fillGrid(…);
 
 //Anything left? Lay out for disappearing animation
 if (!state.isPreLayout() && !recycler.getScrapList().isEmpty()) {
 final List<RecyclerView.ViewHolder> scrapList = recycler.getScrapList();
 //Laying out a scrap item removes it from the list… //Beware concurrent modification! 
 …
 }
 }
  34. Predictive Item Animations

  35. Predictive Item Animations

  36. Resources RecyclerView Playground http://milehighandroid.com/ Building a RecyclerView LayoutManager http://wiresareobsolete.com/ Redux

    - Handling Disconnected Ranges Fixing LinearSmoothScroller https://blog.stylingandroid.com/scrolling-recyclerview-part-1/ Android SDK Reference https://developer.android.com/training/material/lists-cards.html