What is the VerticalScrollExtent in Android ScrollView? - android

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

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.

RecyclerView fast scroll thumb height too small for large data set

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.

ListView in page without messing up scrolling

What I'm trying to achieve:
--- Stack Layout / Relative Layout---
- some widget (e.g. Label) -
- some widget (e.g. Label) -
- ListView -
-------------------------------------
However, I also want the following scroll behaviour:
Top widgets disappear first then ListView starts scrolling. Basically, I want a "natural" scrolling behaviour.
One way I can achieve this is by making the whole page a ListView and putting the widgets as a Header for the ListView
But that has one problem... which I think is a bug in Xamarin.Forms:
If you have a long Label (what else to hold text?), it will not display all of it. It will actually make it scrollable and display only part of it at a time. What makes this even worse is that you cannot scroll the Label "easily", you have to try multiple times to make it scroll the label instead of the page, it's obviously bugged. That happens even if the page itself has hit the end (i.e. can't scroll any more), the Label still can't be scrolled easily.
Is there another way or a workaround to achieve what I want?
As one of the comments suggests, the best is to set the HeightRequest of the Label to the needed value.
Here is how I measure the height of the text on Android (you'll need DependencyService, if you want to call this function from Xamarin.Forms):
double measureString(string text, string font, double fontSize, double width)
{
var textView = new TextView(global::Android.App.Application.Context);
textView.Typeface = Android.Graphics.Typeface.Create(font, Android.Graphics.TypefaceStyle.Normal);
textView.SetText(text, TextView.BufferType.Normal);
textView.SetTextSize(Android.Util.ComplexUnitType.Px, (float)(fontSize * Xamarin.Forms.Forms.Context.Resources.DisplayMetrics.ScaledDensity));
int widthMeasureSpec = Android.Views.View.MeasureSpec.MakeMeasureSpec((int)(width * Xamarin.Forms.Forms.Context.Resources.DisplayMetrics.Density), width == 0 ? Android.Views.MeasureSpecMode.Unspecified : Android.Views.MeasureSpecMode.Exactly);
int heightMeasureSpec = Android.Views.View.MeasureSpec.MakeMeasureSpec(0, Android.Views.MeasureSpecMode.Unspecified);
textView.Measure(widthMeasureSpec, heightMeasureSpec);
textView.SetIncludeFontPadding(false);
return (width == 0 ? textView.MeasuredWidth : textView.MeasuredHeight) / Xamarin.Forms.Forms.Context.Resources.DisplayMetrics.Density;
}

How can I get the y position of an item in an Android listview in the overriden 'getView' function?

I have a customlist in which I overload the getView function and I need to know the y position of the current item that is being drawn. And I need it in relation to the entire list and not just the ones visible on screen. How can I find this out?
Edit: The Items have different heights.
Since you want the one relative to the top of the list (not the top of the visible screen, you can calculate it using:
int yPos = view.getHeight() * position;
Edit:
Different heights? That probably hurts performance, since the recycler isn't helping much.
I don't think there's a magic method for this case. Realistically what you have to do is keep an array of list position and y offsets, so for instance
// Item at position 0, top starts at 0, height is 15
pos[0][0] = 0
pos[0][1] = 15
// Next item starts one pixel down from previous item's top, is 10 pixels tall
pos[1][0] = 16
pos[1][1] = 10
And just populate from within getView. Note that this solution tracks individual height as well as absolute offset for each item- since the previous item's position + height is required to determine the next item's position.

Center a ListView on its current selection

Does anyone know of a way to center a ListView based on its current selection or selection set with setSelection?
I did see this other StackOverflow question without any answers: Android ListView center selection
Thanks,
Kevin
First, get the height of the ListView using getHeight, which returns the height of the ListView in pixels.
Then, get the height of the row's View using the same method.
Then, use setSelectionFromTop and pass in half of the ListView's height minus half of the row's height.
Something like:
int h1 = mListView.getHeight();
int h2 = v.getHeight();
mListView.setSelectionFromTop(position, h1/2 - h2/2);
Or, instead of doing the math, you might just pick a constant for the offset from the top, but I would think it might be more fragile on different devices since the second argument for setSelectionFromTop appears to be in pixels rather than device independent pixels.
I haven't tested this code, but it should work as long as your rows are all roughly the same height.
You will need to have the scroll view and the view of the item selected. Then you can simply do:
scrollView.smoothScrollTo(0, selectedView.getTop() - (scrollView.getHeight() / 2) + (selectedView.getHeight() / 2), 0);
This will center the scroll view exactly on selectedView
I haven't tried any of this but based on the current selection could you use public void smoothScrollByOffset (int offset) to get the view to scroll to where you want so that your selection is in the middle of the view?

Categories

Resources