8

I've got a CoordinatorLayout which contains a CollapsingToolbarLayout and a RecyclerView. Everything looks the way it's supposed to, except that when I try to scroll to the last item programmatically, it doesn't go all the way to the bottom. Instead, it does this:

enter image description here

I don't think this is a clipping problem, since the bottom item is fully there if I scroll down:

enter image description here

Here's the main layout:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
                                                 xmlns:app="http://schemas.android.com/apk/res-auto"
                                                 android:layout_width="match_parent"
                                                 android:layout_height="match_parent"
                                                 android:fitsSystemWindows="true">
    <android.support.design.widget.AppBarLayout
            android:id="@+id/app_bar"
            android:layout_width="match_parent"
            android:layout_height="150dp"
            android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.design.widget.CollapsingToolbarLayout
                android:id="@+id/toolbar_layout"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:fitsSystemWindows="true"
                app:contentScrim="?attr/colorPrimary"
                app:expandedTitleMarginStart="16dp"
                app:layout_scrollFlags="scroll|exitUntilCollapsed"
                app:toolbarId="@+id/toolbar">

            <android.support.v7.widget.Toolbar
                    android:id="@+id/toolbar"
                    android:layout_width="match_parent"
                    android:layout_height="?attr/actionBarSize"
                    app:contentInsetStart="72dp"
                    app:popupTheme="@style/AppTheme.PopupOverlay"/>

        </android.support.design.widget.CollapsingToolbarLayout>
    </android.support.design.widget.AppBarLayout>

    <android.support.v7.widget.RecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginBottom="@dimen/recyclerview_bottom_margin"
            app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</android.support.design.widget.CoordinatorLayout>

And here's the code that goes with the screencaps above:

class TestActivity : AppCompatActivity() {

    private val itemNames = listOf("top item", "next item", "yada", "yada yada", "yada yada yada", "second last item", "last item")

    private val selectedPosition = itemNames.size - 1

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.recyclerview_with_collapsing_toolbar)
        setSupportActionBar(toolbar)
        supportActionBar?.setTitle(R.string.some_title)

        val recyclerView  = findViewById<RecyclerView>(R.id.recycler_view)
        recyclerView.setHasFixedSize(true)
        val layoutManager = LinearLayoutManager(this)
        recyclerView.layoutManager = layoutManager
        recyclerView.adapter = MyAdapter()

        // try to scroll to the initial selected position

        recyclerView.scrollToPosition(selectedPosition)

//        layoutManager.scrollToPosition(selectedPosition)

//        layoutManager.scrollToPositionWithOffset(selectedPosition, resources.getDimensionPixelOffset(R.dimen.item_height))

//            recyclerView.post {
//                recyclerView.smoothScrollToPosition(selectedPosition)
//            }

    }

    inner class MyAdapter: RecyclerView.Adapter<MyViewHolder>() {
        override fun onCreateViewHolder(parent: ViewGroup, itemType: Int): MyViewHolder {
            val view = LayoutInflater.from(parent.context).inflate(R.layout.item_layout, parent, false)
            return MyViewHolder(view)
        }

        override fun getItemCount(): Int {
            return itemNames.size
        }

        override fun onBindViewHolder(vh: MyViewHolder, position: Int) {
            vh.words.text = itemNames[position]
            if (selectedPosition == position) {
                vh.parent.setBackgroundColor(Color.MAGENTA)
            } else {
                vh.parent.setBackgroundColor(Color.BLACK)
            }
        }
    }

    inner class MyViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
        val parent = itemView
        val words: TextView = itemView.findViewById(R.id.some_text)
    }
}

Additional notes:

  • If I get rid of the CollapsingToolbarLayout then it does show the entire last item.

  • I've left some of my other attempts in the code above (commented out). None of them worked.

  • This example just involves a short static list and always scrolls to the same item, but the code I'm actually working on is a bit more complicated.

  • The designer really wants everything to look exactly as designed, and I'm not free to change the visual design.

How can I scroll to the last item in a RecyclerView that's inside a layout with a collapsing toolbar?

Rapunzel Van Winkle
  • 5,380
  • 6
  • 31
  • 48
  • 1
    @Rapunazel I think you have to add an image view before the toolbar in the collapsing toolbar. – Gourav Jan 23 '19 at 14:48

2 Answers2

9

The problem is that getNestedScrollingParentForType(type) in NestedScrollingChildHelper#dispatchNestedScroll returns null for the non-touch scroll, so scrolling is not dispatched when it is done programmatically.

So we need to enable that before scrolling programmatically:

if (!recyclerView.hasNestedScrollingParent(ViewCompat.TYPE_NON_TOUCH)) {
    recyclerView.startNestedScroll(View.SCROLL_AXIS_VERTICAL,ViewCompat.TYPE_NON_TOUCH);
}
// now smooth scroll your recycler view programmatically here. 
Vishal Arora
  • 2,524
  • 2
  • 11
  • 14
  • I was really optimistic that this solution would work, but it didn't seem to have any effect at all. (Specifically, I inserted your snippet into my small program just before scrolling, and it didn't seem to make any difference.) – Rapunzel Van Winkle Jan 27 '19 at 01:41
  • Let me add the complete code snippet for you. I have tried your layout code and it works. – Vishal Arora Jan 27 '19 at 07:22
  • 3
    Have created a repository to demonstrate this fix - https://github.com/arora-vishal/RecyclerScrollFix – Vishal Arora Jan 27 '19 at 09:03
  • I'm not sure why you wrote a completely different example instead of just using the code that I originally posted, but thanks for the effort. I've tried out your code, and it does scroll all the way to the bottom when a button is clicked. However, if you take that code out of the `onClick` and try to just preselect the bottom item initially (before any user interaction), it doesn't scroll all the way to the bottom. That's an interesting mystery! I'm giving you an upvote, but your solution (which apparently requires a click to work) still doesn't do what I want yet. – Rapunzel Van Winkle Jan 28 '19 at 03:04
  • Click is just to demonstrate the code which is required to scroll the content all the way to bottom. – Vishal Arora Jan 28 '19 at 07:05
  • The code in the example repository is similar to yours. I know its not exact replica of your code. But it demonstrates the use case you are trying to solve. I can help with your code if you can post on a repository. Would be convenient for me to understand your use case clearly. – Vishal Arora Jan 28 '19 at 07:07
  • I posted complete code in the original question. But you can duplicate the problem with your own code by just pulling out the `smoothScrollToPosition` so that it's done automatically when the list comes up (without any user interaction). In that case, your code works just like mine (not scrolling all the way to the bottom). – Rapunzel Van Winkle Jan 28 '19 at 07:56
  • The part where it does not scroll to bottom in first layout (in oncreate() in your case ) phase is a separate problem. To resolve this problem you can scroll to bottom once the first phase of recycler view layout is done using - `recyclerView.post(new Runnable() { @Override public void run() { // add smooth scroll code here - });` – Vishal Arora Jan 28 '19 at 08:33
  • Okay, I can finally scroll down to the last item (fully displayed) using a combination of your nested scrolling code along with a runnable. So this might work! But I'm surprised that it only goes all the way to the bottom if the selected position is `itemNames.size` (instead of `itemName.size - 1`). I would have expected `itemNames.size` to be out of bounds! So is the parameter for `smoothScrollToPosition` not zero-based? I want to be able to scroll to any selected position. – Rapunzel Van Winkle Jan 28 '19 at 10:02
  • That is not relevant here. You can look at the implementation of smoothScrollToPosition() inside RecyclerView class for further reference. – Vishal Arora Jan 28 '19 at 10:29
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/187438/discussion-between-vishal-arora-and-rapunzel-van-winkle). – Vishal Arora Jan 28 '19 at 16:42
4

A fix for this problem would be to collapse the toolbar before scrolling to the given position. This can be done by adding app_bar_layout.setExpanded(false) before scrollToPosition.

B. Plüster
  • 564
  • 3
  • 11
  • That simple fix does indeed scroll almost to the bottom, but the last bit (the height of the remaining app bar) of the selected item is still hidden. However, if I combine your trick with a `viewTreeObserver`, that does finally show the entire selected item. I gave your answer an upvote, and if I don't get any better answer, it may eventually get the bounty. But I'll wait until the end of the week to see what other answers come in, and award the bounty to the best answer. – Rapunzel Van Winkle Jan 23 '19 at 21:41
  • @RapunzelVanWinkle Perhaps you could try using `scrollToPositionWithOffset()` and use the height of your collapsed app bar as the offset. You can either hard-code that height or retreive it dynamically according to [this answer](https://stackoverflow.com/a/13216807/10928836). – B. Plüster Jan 24 '19 at 13:23