RecyclerView fast scroll thumb height too small for large data set
Asked Answered
G

6

26

I am using the default RecyclerView fast scroll and I followed this guide to support it.

Now, the problem is that the thumb resizes its height as per the size of the data set. For large items like 100 and above, the thumb becomes very small and almost becomes difficult to respond to dragging.

Please is there any way I can set minimum height for the fast scroll thumb.

Gamma answered 16/12, 2017 at 14:53 Comment(8)
Same problem in my App. Can we define a fix thumb, which is not getting small depending on the list size?Rabbin
Same problem in my AppCalais
The above answer is very limited. Here's an example of what you want to do.Tremulant
@BurakCakir, what do you mean by saying "the above answer"?Varicotomy
@Varicotomy the SO answer when you click this guide.Tremulant
Same problem in my app. is fix your the problem?Linage
@BurakCakir this is not the answer people looking for here. Anybody found solutions to this yet?Magnitude
I made a library that fixes it, here: github.com/AndroidDeveloperLB/FastScrollerAndRecyclerViewFixes . Explanation here: https://mcmap.net/q/188384/-android-add-spacing-below-last-element-in-recyclerview-with-gridlayoutmanagerFacile
B
6

This is only a partial answer; I'm missing (at least) one piece of the puzzle, but hopefully someone else can figure it out.

Once you've added the necessary attributes to your <RecyclerView> tag (as mentioned in the answer linked in OP's question), the sizing/positioning of the scrollbar thumb is controlled by three methods inside LinearLayoutManager:

  • int computeVerticalScrollRange(): The size of the scrollbar's track.

  • int computeVerticalScrollExtent(): The size of the scrollbar's thumb.

  • int computeVerticalScrollOffset(): The distance between the top of the scrollbar's track and the top of the scrollbar's thumb.

The units for these methods is arbitrary; you can use anything you'd like as long as all three methods share the same units. By default, LinearLayoutManager will use one of two sets of units:

  • mSmoothScrollbarEnabled == true: Use units based on the pixel sizes of the visible items in the RecyclerView.

  • mSmoothScrollbarEnabled == false: Use units based on the positions of the visible items in the RecyclerView's adapter.

To control the size of the scrollbar's thumb yourself, you'll have to override these methods... but here's the piece I'm missing: In all of my experimentation, computeVerticalScrollExtent() is never called by the system. That said, we can still show some progress here.

First, I've created a simple adapter that shows 500 CardViews with the item's position inside. I've enabled fast scrolling with some really simple (but ugly) drawables. Here's what the scrollbar looks like with just a default LinearLayoutManager implementation:

enter image description here

As you've found, with 500 (small) items, the scrollbar thumb is really small and quite hard to tap on. We can make the scrollbar dramatically larger by overriding computeVerticalScrollRange() to just return a fixed constant... I picked 5000 essentially at random just to show the major change:

enter image description here

Of course, now the scrollbar doesn't work like you'd expect; scrolling the list by dragging on it as normal moves the thumb much more than it should, and fast scrolling the list by dragging on the thumb moves the list much less than it should.

On my device, with the randomly-chosen range of 5000, overriding computeVerticalScrollOffset() as follows makes the scrollbar thumb move perfectly as I scroll the list by dragging on it:

@Override
public int computeVerticalScrollRange(RecyclerView.State state) {
    return 5000;
}

@Override
public int computeVerticalScrollOffset(RecyclerView.State state) {
    return (int) (super.computeVerticalScrollOffset(state) / 23.5f);
}

However, this still doesn't fix the second issue: dragging on the thumb itself doesn't correctly scroll the list. As I mentioned above, it would seem like the appropriate thing to do here would be to override computeVerticalScrollExtent(), but the system never invokes this method. I've even overridden it to simply throw an exception, and my app never crashes.

Hopefully this at least helps point people in the right direction for a full solution.

PS: The implementations of computeVerticalScrollRange() and computeVerticalScrollOffset() I've included in this answer are intentionally simple (read: bogus). "Real" implementations would be much more complex; the default LinearLayoutManager implementations take into account device orientation, the first and last visible items in the list, the number of items off-screen in both directions, smooth scrolling, various layout flags, and so on.

Beechnut answered 8/1, 2018 at 20:46 Comment(3)
computeVerticalScrollExtent() is called if scrollbars are set e.g. android:scrollbars="vertical". That's not helping fast scroll though.Abstractionism
Did you solve this? Can you please share full source code of the solution? Maybe on Github?Facile
Not Helping Kindly post the full class or git link .Lissome
B
6

I solved this problem by copying the FastScroller class from android.support.v7.widget.FastScroller

Then I removed the fast scroll enabled from the xml and applied fastscroller using the below code:

StateListDrawable verticalThumbDrawable = (StateListDrawable) getResources().getDrawable(R.drawable.fastscroll_sunnah);
Drawable verticalTrackDrawable = getResources().getDrawable(R.drawable.fastscroll_line_drawable);
StateListDrawable horizontalThumbDrawable = (StateListDrawable)getResources().getDrawable(R.drawable.fastscroll_sunnah);
Drawable horizontalTrackDrawable = getResources().getDrawable(R.drawable.fastscroll_line_drawable);

Resources resources = getContext().getResources();
new FastScroller(recyclerView, verticalThumbDrawable, verticalTrackDrawable,
                horizontalThumbDrawable, horizontalTrackDrawable,
                resources.getDimensionPixelSize(R.dimen.fastscroll_default_thickness),
                resources.getDimensionPixelSize(R.dimen.fastscroll_minimum_range),
                resources.getDimensionPixelOffset(R.dimen.fastscroll_margin));

Inside the FastScroller Class I extended the defaultWidth:

FastScroller(RecyclerView recyclerView, StateListDrawable verticalThumbDrawable,
        Drawable verticalTrackDrawable, StateListDrawable horizontalThumbDrawable,
        Drawable horizontalTrackDrawable, int defaultWidth, int scrollbarMinimumRange,
        int margin) {
    ...
    this.defaultWidth = defaultWidth;
    ...

Then I updated the code in this method:

void updateScrollPosition(int offsetX, int offsetY) {
...
mVerticalThumbHeight = Math.max(defaultWidth * 4, Math.min(verticalVisibleLength,
                (verticalVisibleLength * verticalVisibleLength) / verticalContentLength));
...
...
mHorizontalThumbWidth = Math.max(defaultWidth * 4, Math.min(horizontalVisibleLength,
                (horizontalVisibleLength * horizontalVisibleLength) / horizontalContentLength));
...    
}

This ensures that the minimum thumb height/width is 4 times the default width.

Bessiebessy answered 12/3, 2018 at 11:15 Comment(3)
If your list is long, this is also an opportunity to replace the call to scrollBy in verticalScrollTo with a call to scrollToPosition (adjusting the calculation accordingly). scrollBy results in adapter calls for each and every item across the scroll range which becomes very slow with many items (as least in recyclerview:1.1.0).Abstractionism
Testing this, I've noticed that while it makes the thumb larger, it also lets it be truncated when at the top/bottom. Can you please share full source code of the solution? Maybe on Github?Facile
It does make the thumb bigger. I will see if I have the gist somewhereBessiebessy
S
4

This is a known issue, opened in August 2017: https://issuetracker.google.com/issues/64729576

Still waiting for recommendations on how to manage RecyclerView with fast scroll on large amounts of data. No reply from Google on that issue so far on recommendations :(

The answer from Nabil is similar to a workaround mentioned in the issue. Nabil's answer copies the FastScroller class and modifies it to ensure a minimum thumb size, and the workaround in the issue extends FastScroller (but it has to stay in the android.support.v7.widget package) to ensure the minimum thumb size.

Sulamith answered 10/5, 2018 at 13:7 Comment(4)
I found in this issue link to fast scroller library, which solved my problem: github.com/zhanghai/AndroidFastScrollLorenelorens
@Lorenelorens This is only from API 21, and RecyclerView supports from before...Facile
@Lorenelorens Thanks a bunch, exactly what i was looking forChiles
@Lorenelorens this one helped. thanksIodous
P
1

Nabil Mosharraf Hossain's answer solved this issue. I just wanted to post some details for future reference.

By default, instead of calling computeVerticalScrollExtent() in LinearLayoutManager, thumb size and thumb position are calculated in FastScroller class in updateScrollPosition method.

This is the formula for thumb size:

this.mVerticalThumbHeight =
Math.min(verticalVisibleLength, verticalVisibleLength * verticalVisibleLength / verticalContentLength);

verticalVisibleLength is a length of the part of the recycler's content that is visible on the screen. verticalContentLength is length of the whole content of the recycler. So the thumb takes up from the screen the same proportion as visible content takes up from the whole content. Which means that if recycler content is very long - thumb would be very small.

To prevent this, we can override mVerticalThumbHeight as Nabil Mosharraf Hossain suggested. I did it this way, so the thumb would still has the same proportional size but not lower that 100 pixels:

this.mVerticalThumbHeight = Math.max(100,
Math.min(verticalVisibleLength, verticalVisibleLength * verticalVisibleLength / verticalContentLength));

But. Overriding only the thumb size would lead to some inconsistencies in thumb movement near the top and the bottom of the screen because its position would behave the same way like the thumb is still very small. To prevent this we can also override thumb position.

Default formula for thumb position doesn't take the thumb size into consideration and just uses the fact the it is proportional to the screen size. So I found this formula to work. But I can't quite remember what does it do exactly.

this.mVerticalThumbCenterY = (int)((verticalVisibleLength - mVerticalThumbHeight) * offsetY
/ ((float)verticalContentLength - verticalVisibleLength) + mVerticalThumbHeight / 2.0);
Peraza answered 21/3, 2019 at 16:32 Comment(5)
although mVerticalThumbHeight portion works well, mVerticalThumbCenterY has some glitches as far my test...where should mVerticalThumbCenterY be? before or after mVerticalThumbHeight update? either way, its has some bugFiora
@TouhidulIslam mVerticalThumbCenterY is defined right after mVerticalThumbHeight in the same method. And you right, it still has some glitches. For example when recycler is very long if you scroll to the top or to the bottom, recycler stops not on the first or last element but the one before that. Is this what you talking about or where is something else?Peraza
sometimes the Thumb just vanishes when a single view is too big that it doesn't fit in a single screen, need scrolling. Then I print the mVerticalThumbCenterY values to see what is happening. I found, mVerticalThumbCenterY becomes negative when the stated scenario occursFiora
i solved the issue, it was just plain old integer overflow issue! instead of this.mVerticalThumbCenterY = (int)((verticalVisibleLength - mVerticalThumbHeight) * offsetY / ((float)verticalContentLength - verticalVisibleLength) + mVerticalThumbHeight / 2.0); i had to do this.mVerticalThumbCenterY = (int)((verticalVisibleLength - mVerticalThumbHeight) / ((float)verticalContentLength - verticalVisibleLength)* offsetY + mVerticalThumbHeight / 2.0);Fiora
Did you solve this? Can you please share full source code of the solution? Maybe on Github?Facile
B
0

I have found a solution to adjust the size of the scrollbar thumb, however I'm not completely there yet. Just like the answer of Ben P. I've used the default properties app:fastScrollEnabled and all the track & thumb drawable properties and created a custom LinearLayoutManager.

    class FastScrollLayoutManager(context: Context) : LinearLayoutManager(context) {

        override fun scrollVerticallyBy(dy: Int, recycler: RecyclerView.Recycler?, state: RecyclerView.State?): Int {
            return super.scrollVerticallyBy(dy * 2, recycler, state)
        }

        override fun computeVerticalScrollRange(state: RecyclerView.State): Int {
            return super.computeVerticalScrollRange(state) / 2
        }

        override fun computeVerticalScrollOffset(state: RecyclerView.State): Int {
            return super.computeVerticalScrollOffset(state) / 2
        }
    }

By dividing the result of computeVerticalScrollRange() and computeVerticalScrollOfsett() with 2, the scroll thumb will become 2x bigger. If you then multiply the scrolling speed with 2, the scroll speed will be on the right scale again. Dragging the thumb of the scrollbar will be working perfectly and the size of the thumb will be 2x bigger than before.

However, this is not yet a final solution. By increasing the scroll speed, the 'normal' scrolling (not using the scrollbar) won't be smooth anymore.

Bascule answered 14/7, 2020 at 17:20 Comment(3)
Did you solve this? Can you please share full source code of the solution? Maybe on Github?Facile
@androiddeveloper I ended up using this library: github.com/zhanghai/AndroidFastScrollBascule
It does seem very good, but the minSdkVersion is 21, while RecyclerView supports less... Do you know of a way to use it only from API 21, while using the one that RecyclerView offers on below?Facile
T
0

If you don't want to copy FastScroller class, you can intercept ACTION_DOWN touches yourself, determine the desired touch area and change the final Y coordinate:

class FastScrollerTouchesFixRecyclerView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : RecyclerView(context, attrs, defStyleAttr) {

    private val thumbSize = 48 * context.resources.displayMetrics.density

    override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
        // If necessary, add the RTL check yourself
        if (event.action == MotionEvent.ACTION_DOWN && event.x > width - thumbSize) {
            val middleScreenPos = computeVerticalScrollOffset() + height / 2
            val thumbCenter = height * middleScreenPos / computeVerticalScrollRange()

            if (abs(event.y - thumbCenter) < thumbSize / 2) {
                event.setLocation(event.x, thumbCenter.toFloat())
            }
        }
        return super.onInterceptTouchEvent(event)
    }
}
Tum answered 19/6, 2023 at 7:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.