In the last post, we walked through the core functionality necessary for building a RecyclerView LayoutManager. In this post, we are going to add support for a few additional features that the average adapter-based view is expected to have.A reminder that the entire sample application can be found here on GitHub.
Supporting Item Decorations
RecyclerView
has a really neat feature in which an RecyclerView.ItemDecoration
instance can be supplied to do custom drawing alongside the child view content, as well as provide insets (margins) that will apply to the child views without the need for modifying layout parameters. The latter places a constraint on how the children should be laid out that the LayoutManager
implementation must support.The RecyclerPlayground repository uses a few different decorators in the examples to illustrate how they are implemented.
LayoutManager gives us helper methods to account for decorations so we don’t have to think about them:
- To get the left edge of a child view, use
getDecoratedLeft()
instead ofchild.getLeft()
- To get the top edge of a child view, use
getDecoratedTop()
instead ofchild.getTop()
- To get the right edge of a child view, use
getDecoratedRight()
instead ofchild.getRight()
- To get the bottom edge of a child view, use
getDecoratedBottom()
instead ofchild.getBottom()
- Use
measureChild()
ormeasureChildWithMargins()
instead ofchild.measure()
to measure new views coming from theRecycler
. - Use
layoutDecorated()
instead ofchild.layout()
to lay out new views coming from theRecycler
. - Use
getDecoratedMeasuredWidth()
orgetDecoratedMeasuredHeight()
instead ofchild.getMeasuredWidth()
orchild.getMeasuredHeight()
to get the measurements of a child view.
As long as you take into account using the proper methods for getting view properties and measurments, RecyclerView
will handle dealing with decorations so you don’t have to.
Data Set Changes
When the attached RecyclerView.Adapter
triggers an update via notifyDataSetChanged()
, the LayoutManager
will be responsible for updating the layout in the view. In this case, onLayoutChildren()
will be called again. To support this we need to make some adjustments to our sample to make the distinction between a fresh layout and a layout change due to an adapter update. Below is the fully fleshed out method from the 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
|
Our implementation determines if this is a new layout or an update based on whether we have child views attached already. In the case of an update, the first visible position (i.e. the top-left view, which we track continuously) and the current scrolled x/y offset give us enough information to do a new fillGrid()
while preserving that the same item position remain in the top-left.
There are a few special cases we handle as well.
- When the new data set is too small to scroll, the layout is reset with position 0 in the top-left.
- If the new data set is smaller, and preserving the current position would cause the layout to be scrolled beyond the allowed boundary (on the right and/or bottom). Here we adjust the first position so the layout aligns with the bottom-right of the grid.
onAdapterChanged()
This method provides you an additional opportunity to reset the layout in the event that the entire adapter is swapped out (i.e. setAdapter() is invoked again on the view). In this event, it’s safer to assume that the views returned will be completely different than from the previous adapter. Therefore, our example simply removes all current views (without recycling them):
1 2 3 4 5 |
|
The view removal will trigger a new layout pass, and when onLayoutChildren()
is called again, our code can perform a fresh layout since there are no longer any child views attached.
Scroll to Position
Another important feature you will likely want from your LayoutManager is the ability to tell the view to scroll to a specific position. This can be done with or without animation, and there is a callback for each.
scrollToPosition()
This method is invoked from the RecyclerView when the layout should immediately update with the given position as the first visible item. In a vertical list, the element would be placed at the top; in a horizontal list, it would generally be on the left. In our grid, the “selected” position will be placed at the top-left of the view.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
With a proper implementation of onLayoutChildren()
, this can be as simple as updating the target position and triggering a new fill.
smoothScrollToPosition()
In the case where the selection should be animated, we need to take a slightly different approach. The contract of this method is for the LayoutManager
to construct an instance of a RecyclerView.SmoothScroller
, and begin the animation by invoking startSmoothScroll()
before the method returns.
RecyclerView.SmoothScroller
is an abstract class with an API that consists of four required methods:
onStart()
: Triggered when the scroller animation begins.onStop()
: Triggered when the scroller animation ends.onSeekTargetStep()
: Invoked incrementally as the scroller searches for the target view. The implementation is responsible for reading the provided dx/dy and updating how far the view should actually scroll in both directions.- A
RecyclerView.SmoothScroller.Action
instance is passed to this method. Notify the view how it should animate the next increment by passing a new dx, dy, duration, andInterpolator
to the action’supdate()
method. - NOTE: The framework will warn you if you are taking too long to animate (i.e. your increments are too small); try to tune your animation steps to match a standard animation duration from the framework.
- A
onTargetFound()
: Called only once, after a view for the target position has been attached. This is one final chance to animate the target view to its exact position.- Internally, this uses
findViewByPosition()
from theLayoutManager
to determine when the view is attached. If yourLayoutManager
is efficient about mapping views to positions, override this method to improve performance. The default implementation iterates over all child views…all the time.
- Internally, this uses
You can provide your own scroller implementation if you really want to fine-tune your scrolling animations. We have chosen to use the framework’s LinearSmoothScroller
instead, which implements the callback work for us. We only need to implement a single method, computeScrollVectorForPosition()
, to tell the scroller the initial direction and approximate distance it needs to travel to get from its current location to the target location.
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 |
|
This implementation, similar to the existing behavior of ListView, will stop scrolling as soon as the view becomes fully visible; whether that be on the left, top, right, or bottom of the RecyclerView
.
Now What?
You mean that wasn’t enough? Things are starting to look pretty good! In fact, for many the implementation could be considered complete. But we’re going to go just one step further. In the next, and final post of this series, we will look at supporting animations for data set changes in your LayoutManager.