RecyclerView fast scroll thumb height too small for large data set - android

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.

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.

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:
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:
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.

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.

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);

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.

Related

Is there a method for customizing the positioning and size (width AND height) of a scrollbar for any View?

It seems that every angle I manage to find doesn't end up working in the way I need it to. My goal is to be able to customize the positioning and size of any scrollbar on any view, be it a recyclerview, gridview, or listview. I've tried using layer-list xmls to adjust the height and positioning, a Seekbar turned vertically, as well as trying to create my own scrollbar thumb and track using imageviews.
In terms of the layer-list, it just didn't have an effect on the scrollbar at all. The other two attempts at a solution (using a Seekbar, using individual imageviews) were nearly effective, except I needed the current scrolled position (getScrollY()) to be able to make the scrollbars I made actually accurate instead of just visually being a scrollbar. However, even though getScrollY() is defined for recyclerview, gridview and more, it always returns a 0, so I am unable to get that information (except for scrollviews, perhaps; I believe that's the only view type that properly returns a getScrollY() value).
Is it even possible to customize the scrollbar in this manner? I'd be keen to see references or documentation that can point me in the right direction. It feels like this is generally a non-issue for most developers on Android, or at least in general isn't something many people have asked for.
Edit
To assist in visualizing what I have and what I desire, here's a screenshot of the scrollbar as it is right now:
The following image is marked up to show what my intended outcome for this scrollbar would be:
Views have the capability for a scrollbar but a lot don't show them by default.
So any View has a whole load of XML attributes to customise the appearance, size and position.
But these are useless if not shown.
A lot of ViewGroups sub classes setWillNotDraw to be true and this removes the capability to draw the built in scrollbars of the View.
So to get any view to show it's built in scrollbars you need to the setWillNotDraw(false)
Getting any View to show it's built in scrollbars is Step 1 but again not all Views Calculate automatically the length and position of scroll hence they return 0 for the scroll position.
The View has to implement the following methods and return the appropriate numbers for the scroll position to be correct and things like getScrollY to return more than 0
// Length of scrollbar track
#Override
protected int computeHorizontalScrollRange() {
return (int) value;
}
// Position from thumb from the left of view
#Override
protected int computeHorizontalScrollOffset() {
return (int) value;
}
#Override
protected int computeVerticalScrollRange() {
return (int) value;
}
#Override
protected int computeVerticalScrollOffset() {
return (int) value;
}
Off Course some View sub classes don't use the built in ones but draw there own.

How to get an offset in RecyclerView ItemDecorator

I have written two ItemDecorator's for RecyclerView. Each adds some top offset in getItemOffsets(). Let's say:
First decorator adds 20dp top offset
Second decrator adds 30dp top offset
Now, when I add both of them to RecyclerView, each item is correctly offsetted by 50dp, that's good.
But here comes the question:
How do I get this offset in onDraw/onDrawOver?
Usually decorators draw their stuff by traversing parent.getChildAt(i) stuff and getting child.getTop() for example to draw above child view of RecyclerView.
But in this case, doing so would mix up the drawing of other decorator, because it would also use child.getTop().
So at the moment it seems like both decorators need to know about each other and each other's height.
Am I missing something here? I hope I am.
EDIT: I reported an issue to Android issue tracker and it seems this will be worked on. Star it to keep track of progress: https://code.google.com/p/android/issues/detail?id=195746
tl;dr No you are not missing anything. But you can get the values needed in getItemOffsets, albeit it seems a little bit dirty to me.
Basically there is only one option of getting the decorated height other than managing decorations yourself: LayoutManager.getDecoratedTop();
onDraw
In onDraw you get the whole canvas of the recyclerView, c.getClipBounds() does not hold any information. Albeit the javadoc of adding decorations says that decorations should just draw withing their bounds.
Also, getting parent.getLayoutManager().getDecoratedTop() will give you the same values in every decoration, since it's already too late here, those values are for layouting purposes.
We are too late, layouting is done and we missed it.
getItemOffsets
Please note that I tested the following with a LinearLayoutManager and it might as well not work with the others (Most definitely not with most custom ones). Since I am relying on measuring happening between those calls, which is not documented, the given solution might break with some future version.
I just felt I needed that disclaimer. I found a working solution (watch the mOffset):
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
mOffset = parent.getLayoutManager().getTopDecorationHeight(view);
outRect.set(0, 10, 0, 0);
}
This works, because the recyclerview, when calculating the total inset of the child, is updating the views package local LayoutParams variable. While we cannot access the layout parameter variable itself, calling getTopDecorationHeight actually uses the (currently) dirty inset, giving us the proper value.
Hence, you can get the offset of the previous decorations before applying your own!
To apply it, just remove the other decorations offset when drawing your decoration:
c.drawRect(child.getLeft() + child.getTranslationX(),
mOffset + layoutManager.getDecoratedTop(child) + child.getTranslationY(),
child.getRight() + child.getTranslationX(),
child.getBottom() + child.getTranslationY(), paint);
This will actually apply the other decorations offset, then draw your own from the correct top.
Some Problems
This is now a basic, working solution for the default usecase. BUT. If you have different offsets depending on the item VIEW_TYPE or the adapter position things get tricky.
You will either duplicate your logic and keep various offsets for various view types or you have to store / retrieve the offset for every view or viewtype.
To do so, you could either add some custom tag with view.setTag(key, object) or doing something similar with the RecyclerView.State object that's getting passed around.

What is the VerticalScrollExtent in Android ScrollView?

from an answer to one of my other questions I found an Google Demo of a ListView subclass that allows item reorder.
The demo works great, but I am having some trouble to understand how the it works:
When an item is dragged above/below the bounds of the ListView, the ListView starts scrolling up/down to reveal new items. The necessary calculation uses different parameters of the underling ScrollView:
public boolean handleMobileCellScroll(Rect r) {
int offset = computeVerticalScrollOffset();
int height = getHeight();
int extent = computeVerticalScrollExtent();
int range = computeVerticalScrollRange();
int hoverViewTop = r.top;
int hoverHeight = r.height();
if (hoverViewTop <= 0 && offset > 0) {
smoothScrollBy(-mSmoothScrollAmountAtEdge, 0);
return true;
}
if (hoverViewTop + hoverHeight >= height && (offset + extent) < range) {
smoothScrollBy(mSmoothScrollAmountAtEdge, 0);
return true;
}
return false;
}
heightis the height of the ListView itself
offsetis the scroll position = how many units/pixels have been scrolled up/down
rangeis the height of the complete content.
extent - well, what is this?
ListView inherits computeVerticalScrollExtent() from View and the docu says:
Compute the vertical offset of the vertical scrollbar's thumb within
the horizontal range. This value is used to compute the position of
the thumb within the scrollbar's track.
If one looks at the code computeVerticalScrollExtent() is not implemented by one of the sublasses but only directly by View: It simply returns the height of the view.
This makes sense: If the ListView/ScrollView has a height of 500, the part of the scroll content that is visible at a time is also 500. Is this the meaning of the ScrollExtent? Why is ScrollExtent necessary? Why not simply use getHeight() directly?
I think I am missing something and I am happy about any hint!
compute*ScrollOffset - Defines the distance between the start of the scrollable area and the top of the current view window inside the scrollable area. So for example, if your list has 10 items and you've scrolled down so the 3rd item is at the top-most visible item, then the offset is 3 (or 3*itemHeight, see below).
compute*ScrollExtent - Defines the size of the current view window inside the scrollable area. So for example, if your list has 10 items and you can currently see 5 of those items, then the extent is 5 (or 5*itemHeight, see below).
compute*ScrollRange - Defines the size of the current scrollable area. So for example, if your list has 10 items then the range is 10 (or 10*itemHeight, see below).
Note that all these methods can return values in different units depending on their implementation, so for the examples above, I am using the indices, but in some cases these methods will return pixel values equivalent to the width or height of the items.
In particular, the LinearLayoutManager of the RecyclerView will return indices if the 'smooth scrollbar' feature is disabled, otherwise it will return pixel values. See ScrollbarHelper in the support library for more information.
Additional reading: https://groups.google.com/forum/#!topic/android-developers/Hxe-bjtQVvk
It's kinda late, but hope it's ok.
1) The method actually is implemented by the subclasses. For example, this is what it looks like for AbsListView (ListView's superclass).
2) View's height can be different from its vertical scroll's height - just imagine a View with weird top/bottom padding .
These two points pretty much make other questions irrelevant :)
This is a sample code which might help you to understand as to how to get scrollBar top and bottom using computeVerticalScrollExtent:
scrollbarTop = (float)this.computeVerticalScrollExtent()/this.computeVerticalScrollRange()*this‌​.computeVerticalScrollOffset();
scrollbarBottom = (float)this.computeVerticalScrollExtent()/this.computeVerticalScrollRange()*(thi‌​s.computeVerticalScrollExtent()+this.computeVerticalScrollOffset());
According to the article from here:
I found this explanation correct.
ListView with 30 items has scrollRange equals to 3000, that is due to scrollRange = numberOfItems * 100, thus scrollExtent = numberOfVisibleItems * 100 and scrollOffset = numberOfScrolledItems * 100. You can find evidance of these words in the source code of AbsListView

Synchronous scrolling of a partial ListView Header

This question is very specific, What I am trying to do (with a list view) is described in great detail in the following article: http://www.pushing-pixels.org/2011/07/18/android-tips-and-tricks-synchronized-scrolling.html
Thanks #kaushal trivedi for the link
Details:
I have an android application I am working on that uses a list view with a custom adapter. The Listview Contains a Custom header of a non-fixed height. Also please note that the list items are also of variable height. My goal is to mimic the effect produced in the latest gmail app (as an example) where when you are viewing an email, and scroll past the header, it sticks to the top of the screen just under the action bar and the content continues to scroll under it. What I would like to do, is stick the bottom half of my header to the top of the screen.
My initial reasoning was to create an invisible view fixed in the desired location, and when the user scrolled to or past that location, make the view visible. The issue in this logic, is I need the exact pixel scroll height, which after many attempts I have determined very difficult to do. The exact issue I ran into is, it is not possible from what I can gather to retrieve the pixel level Y-scroll in an onScroll event, I have only been able to retrieve the value in the onScrollStateChanged event. Which as described above will not achieve the desired functionality.
Working with the onScroll event "int firstVisibleItem, int visibleItemCount, int totalItemCount" parameters is also not an option because of the fact that the content I want to "stick" is not the size of a list item, but a fraction of the size of the variable height header.
Is there a correct way to accomplish this effect? My current minSDK level is 10.
Update 10/10/13
I made some progress. The following code syncs the Y position floating view I have on the screen with the list view. b is the view I am setting just as an example.
NOTE: This is used in the onScroll event of the list view.
View c = view.getChildAt(0);
if (c != null) {
int currY = c.getTop();
int diffY = currY - lastY;
lastY = currY;
b.setTop(b.getTop() + diffY);
}
Now the issue is, the header of my List is a non fixed height as I said earlier. So I need to get the height of the header and apply an offset to "b" to place it at the bottom of the list header floating above the list.
This is the code I've tried so far.
header.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
The issue here us header.getMeasuredHeight(); always resolves to the same value no matter how tall the actual height is.
I understand I cannot get the height until after it is displayed. Is there a way I can get that value and set the offset after it is rendered?
Update 10/11/13
I Answered my last question as soon as I woke up this morning.
While the View.measure() code was returning a height. It appears to be the default height of the view, assuming there was no text (that would ultimately stretch the view). So I used the below event to listen for when the view is displayed, and then record its actual height (which works exactly as I had hoped :) )
ViewTreeObserver vto = header.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
b.setY(header.getMeasuredHeight() - 80); //80 is a temp offset
}
});
I have to go to work soon and being that I have still not fully achieved the desired effect, I will not mark this as answered yet. Hopefully I will be able to sit down and finish this in the next day or two. I am still open to suggestions on better ways of doing this.
Okay, so after a lot of time and research, I have found an answer to my question.
First off, Thank you #kaushal for this link: http://www.pushing-pixels.org/2011/07/18/android-tips-and-tricks-synchronized-scrolling.html
My solution ended up being somewhat complex. So instead of trying to describe it here, I made an example app and posted it here: https://github.com/gh123man/Partial-Header-ListView-Scroll-Sync
The specific file containing the code for the solution is here: https://github.com/gh123man/Partial-Header-ListView-Scroll-Sync/blob/master/src/com/example/partialheaderlistviewscrollsync/MainActivity.java

Change the behaviour of a GridView to make it scroll horizontally rather than vertically

I want to make a UI element like a GridView, I want it's complete functionality but want it to be horizontally scrollable rather than vertically.
By horizontal scroll I mean it should be built that way and not put in a HorizontalScrollView.
My would be Custom GridView will have fixed number of rows say 4-5 and the columns should be extensible based on number of items in the Adapter. You can think of it as the opposite of what the native GridView does, yet it should maintain the functionality.
I have looked at the source code of how a GridView is implemented by Google, but I am able to understand very less and starting to make the View from scratch doesn't seem to be a good idea, since I am afraid I will not be able to do justice to memory optimization's the way Google did it.
I had observed that GridView extends AbsListView, so my question is, is it AbsListView which lets a GridView scroll vertically and add items from the adapter, or is it GridView which adds the vertical scrolling ability? Should I tweak GridView or AbsListView?
It would be even better to know if there's something which already does what I want to do?
This has already been implemented in native Gallery and YouTube app of Android Honeycomb 3.1 and above. So if anyone has an idea, please elaborate.
Snapshot of Honeycomb Gallery app:
Snapshot of Honeycomb YouTube app:
There is setRotation in API 11. You'll have to rotate the gridview by 90 degrees and child views by -90 degrees.
Documentation: http://developer.android.com/reference/android/view/View.html#setRotation(float)
Update:
To get a 3d effect on views following APIs would be useful
setCameraDistance(float) - set the z axis distance(depth)
setRotationX(float) - set the horizontal axis angle
setRotationY(float) - set the vertical axis angle
Set the camera distance to half of the screen height. Then set the rotationX based on the view's location on screen. The rotation angles should be something like (20, 10, 0, -10, -20) from left to right. Later you can play with rotationY angles to get some height perception.
Do all setting in extended GridView's overriden layout method.
#override
void layout(int t, int l, int r, int b) {
super.layout(t, l, r, b);
...
int columnStart = getFirstVisiblePosition()/no_of_columns;
int columnEnd = getLastVisiblePosition()/no_of_columns;
loop from 'columnStart' to 'columnEnd' 'no_of_colmns' times {
// set the camera distance and rotationX to views
// depending on the position of a view on screen.
}
}

Categories

Resources