Recently, I have been working on a new feature of the core New York Times reading app, and I encountered a very challenging issue: whenever the user clicked on an item of a list, a permanent
Snackbar would appear. Since the
Snackbar should have been anchored to the lower end of the
RecyclerView, the last element of the list would always be at least partially covered, and inaccessible.
We discussed several solutions with the rest of the team: we tried to use the
CoordinatorLayout, but with the
Snackbar not being a
View, this approached revealed to be quite complicated; we talked about adding an empty
View at the end of the list, or some spacing, but we were not sold.
So, during my hack-day, I decided to poke around another solution: the generic idea was to leverage a callback, so that I could be warned when the
Snackbar was visible. The
BaseCallback<T> offers two methods to override, that will be fired after the
Snackbar is shown, and after is is dismissed.
The main idea is to leverage the showing event to raise the content — only when needed — and lower it back when the
Snackbar is not visible anymore. Since we want this to be as smooth as possible, we updated the spacing after the
Snackbar is shown, but we won’t be using the other event: this method will be fired after the
Snackbar is already gone, so we would see a glitch.
We will instead use the
Action on the
Snackbar, so that we could reset the spacing before the animation is started, masking the glitch:
The logic that powers the margin move is quite simple, but it took me a few hours to grasp completely. The first thing we have to do is checking what is the last item in the adapter.
Next step would be the actual move of the
RecyclerView: we create a new
ConstraintSet and clone it from the root
ConstraintLayout, then we apply a margin based either on the
Snackbar height or 0 (when such bar is not shown anymore), and then we apply this set to the root.
At this point, if the selected item is the last one, we scroll the list, so that every item is visible:
This code is based on a couple of assumptions, specific to our case:
- Our elements are taller than the
Snackbar, so the bar can only cover one of them — in this way we only have to care about the last item.
- In our scenario, when we press the Action button on the
Snackbar, we are brought to another screen, so the exit animation will not be seen by the users.
- We only want to scroll if the interested item is the last — since we add the margin at the bottom, the users will still be able to scroll and see everything.
We also need to create a new instance of the
SmoothScroller at every iteration, as otherwise our app would start complaining about it being reused:
W/RecyclerView: An instance of SmoothScroller was started more than once. Each instance ofSmoothScroller is intended to only be used once. You should create a new instance for each use.
The use of a
SmoothScroller, which is a small extension of the
LinearSmoothScroller, was needed so that we could define a scrolling speed that was looking good with the rest of the screen, but it can be easily elaborated further:
The final result looks pretty OK, even if I’m not 100% happy with the exit behaviour of the list:
This code is really tailored upon the issue we were facing, and it could be customised to work based on the number of visible items quite easily.
If you want to check the code and play around it — and maybe find a better solution to this issue — you can find it on GitHub.