I have enabled fast scrolling in a ListView, which has a not selectable headerview. If you scroll down the list and drag the fast scroll thumb to the top, the list only scrolls back to the first element, but not to the header view. Dragging the list, works as expected.
Screenshot1: The red area in the screenshot is the header view.
Screenshot2: If you drag the thumb to the top you get only to the first element and the header view is still above.
ListView lv = (ListView) findViewById(R.id.listView);
lv.addHeaderView(getLayoutInflater().inflate(R.layout.view,null), null, false);
<ListView
android:layout_height="fill_parent"
android:id="@+id/listView"
android:layout_width="fill_parent"
android:fastScrollEnabled="true"
></ListView>
I have created a demo project: https://github.com/mikegr/fastscroll-bug
Why does dragging the thumb not scroll back to the top?
This is deliberate behavior of the FastScroller
. When you call setAdapter
on your ListView
, the adapter is wrapped in a HeaderViewListAdapter
if there are any headers set; this is why you must call addHeaderView
before setAdapter
. Then, in the FastScroller
code, we see:
if (adapter instanceof HeaderViewListAdapter) {
mListOffset = ((HeaderViewListAdapter)adapter).getHeadersCount();
adapter = ((HeaderViewListAdapter)adapter).getWrappedAdapter();
}
That is, get an offset and use the underlying adapter. mListOffset
is then used to set the top position to scroll to with the fast scroller. So where does this wrapping actually happen? Up to, as expected, ListView.addHeaderView
, where we see:
if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter);
} else {
mAdapter = adapter;
}
So we're definitely looking around the right place. Now, it sounds like your goal is to NOT have the offset behavior for list headers for your fast thumb, but otherwise have a normal list with header. To do this, it's sufficient (based on what we've seen of the code) to have FastScroller.mListOffset = 0
. This is set only in getSectionsFromIndexer
, which is called unconditionally in init
, and conditionally in several other functions only when mListAdapter == null
. mListAdapter
is only null if onSectionsChanged
gets called, so let's ignore that path for now.
After a lot of digging around, and playing with various reflection hooks, I can say that there's no way to do this that will be even slightly future-compatible. You can use reflection to swap out the HeaderViewListAdapter for one that lies about its header count, etc; but that's quite fragile. Similarly, you can subclass the (package visible) FastScroller with one with your own behavior; but mListOffset
is referenced widely and not through a getter, so this is even uglier than usual. Basically, you're running up against the fact that the system doesn't work quite the way you want.
I hesitate to call this a bug, since it's so clear from the code that it's deliberate behavior. Have you considered making the first element of the list just a special first element (possibly using a custom WrapperListAdapter
much like HeaderViewListAdapter
if desired for book-keeping), rather than using the header mechanism?
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With