RecyclerView with items of different height: Scrollbar - android

I have a RecyclerView with items of varying heights with a scrollbar.
Because of the different heights of the items, the scrollbar changes it's vertical size, dependent on which items are currently displayed (see screenshots).
I have created an example project that displays the problem here.
Has anyone had the same problem and fixed it?
How can I override the calculation of the scrollbar height and position to come up with an own implementation?
EDIT: The scrollbar's position and height can be controlled by overriding RecyclerViews computeVerticalScrollOffset, computeVerticalScrollRange and computeVerticalScrollExtent.
I have no idea though on how to implement these to make the scrollbar work properly with dynamic item heights.
The problem, I reckon, is that RecyclerView estimates the total height of all items based on the items currently visible and sets position and height of the scrollbar accordingly. One way to solve this might be to give a better estimation of the total height of all items.

The best way to handle this situation may be to somehow calculate the scroll bar range based on the size of each item. That may not be practical or desirable. In lieu of that, here is a simple implementation of a custom RecyclerView that you can play with to try to get what you want. It will show you how you can use the various scroll methods to control the scroll bar. It will stick the size of the thumb to an initial size based upon the number of items displayed. The key thing to remember is that the scroll range is arbitrary but all other measurements (extent, offset) must use the same units.
See the documentation for computeVerticalScrollRange().
Here is a video of the result.
Update: The code has been updated to correct a few issues: The movement of the thumb is less jerky and the thumb will now come to rest at the bottom as the RecyclerView scrolls to the bottom. There are also a few caveats that are given after the code.
MyRecyclerView.java (updated)
public class MyRecyclerView extends RecyclerView {
// The size of the scroll bar thumb in our units.
private int mThumbHeight = UNDEFINED;
// Where the RecyclerView cuts off the views when the RecyclerView is scrolled to top.
// For example, if 1/4 of the view at position 9 is displayed at the bottom of the RecyclerView,
// mTopCutOff will equal 9.25. This value is used to compute the scroll offset.
private float mTopCutoff = UNDEFINED;
public MyRecyclerView(Context context) {
super(context);
}
public MyRecyclerView(Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
}
public MyRecyclerView(Context context, #Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
/**
* Retrieves the size of the scroll bar thumb in our arbitrary units.
*
* #return Scroll bar thumb height
*/
#Override
public int computeVerticalScrollExtent() {
return (mThumbHeight == UNDEFINED) ? 0 : mThumbHeight;
}
/**
* Compute the offset of the scroll bar thumb in our scroll bar range.
*
* #return Offset in scroll bar range.
*/
#Override
public int computeVerticalScrollOffset() {
return (mTopCutoff == UNDEFINED) ? 0 : (int) ((getCutoff() - mTopCutoff) * ITEM_HEIGHT);
}
/**
* Computes the scroll bar range. It will simply be the number of items in the adapter
* multiplied by the given item height. The scroll extent size is also computed since it
* will not vary. Note: The RecyclerView must be positioned at the top or this method
* will throw an IllegalStateException.
*
* #return The scroll bar range
*/
#Override
public int computeVerticalScrollRange() {
if (mThumbHeight == UNDEFINED) {
LinearLayoutManager lm = (LinearLayoutManager) getLayoutManager();
int firstCompletePositionw = lm.findFirstCompletelyVisibleItemPosition();
if (firstCompletePositionw != RecyclerView.NO_POSITION) {
if (firstCompletePositionw != 0) {
throw (new IllegalStateException(ERROR_NOT_AT_TOP_OF_RANGE));
} else {
mTopCutoff = getCutoff();
mThumbHeight = (int) (mTopCutoff * ITEM_HEIGHT);
}
}
}
return getAdapter().getItemCount() * ITEM_HEIGHT;
}
/**
* Determine where the RecyclerVIew display cuts off the list of views. The range is
* zero through (getAdapter().getItemCount() - 1) inclusive.
*
* #return The position in the RecyclerView where the displayed views are cut off. If the
* bottom view is partially displayed, this will be a fractional number.
*/
private float getCutoff() {
LinearLayoutManager lm = (LinearLayoutManager) getLayoutManager();
int lastVisibleItemPosition = lm.findLastVisibleItemPosition();
if (lastVisibleItemPosition == RecyclerView.NO_POSITION) {
return 0f;
}
View view = lm.findViewByPosition(lastVisibleItemPosition);
float fractionOfView;
if (view.getBottom() < getHeight()) { // last visible position is fully visible
fractionOfView = 0f;
} else { // last view is cut off and partially displayed
fractionOfView = (float) (getHeight() - view.getTop()) / (float) view.getHeight();
}
return lastVisibleItemPosition + fractionOfView;
}
private static final int ITEM_HEIGHT = 1000; // Arbitrary, make largish for smoother scrolling
private static final int UNDEFINED = -1;
private static final String ERROR_NOT_AT_TOP_OF_RANGE
= "RecyclerView must be positioned at the top of its range.";
}
Caveats
The following issues may need to be addressed depending on the implementation.
The sample code works only for vertical scrolling. The sample code also assumes that the contents of the RecyclerView are static. Any updates to the data backing the RecyclerView may cause scrolling issues. If any changes are made that effect the height of any view displayed on the first full screen of the RecyclerView, the scrolling will be off. Changes below that will probably work OK. This is due to how the code calculates the scrolling offset.
To determine the base value for the scrolling offset, (variable mTopCutOff), the RecyclerView must be scrolled to the top the first time computeVerticalScrollRange() is invoked so views can be measured; otherwise, the code will stop with an "IllegalStateException". This is especially troublesome on an orientation change if the RecyclerView is scrolled at all. A simple way around this would be to inhibit restoration of the scrolling position so it defaults to the top on an orientation change.
(The following is probably not the best solution...)
var lm: LinearLayoutManager = object : LinearLayoutManager(this) {
override fun onRestoreInstanceState(state: Parcelable?) {
// Don't restore
}
}
I hope this helps. (btw, your MCVE made this a lot easier.)

Use item positions as metric of scroll progress. This will cause your scroll indicator to become a bit jumpy, but at least it will remain fixed-sized.
There are multiple implementations of custom scroll indicators for RecyclerView. Most double as fast scrollers.
Here is my own implementation, based on RecyclerViewFastScroller library. Basically, one have to create a custom View subclass, that will be animated, similarly to ScrollView and DrawerLayout:
Store current offset
During animation offset position of thumb View via View#offset* calls
During layout set position based on current offset.
You probably don't want to start learning all that magic now, just use some existing fast scrolling library (RecyclerViewFastScroller or one of it's clones).

Inspired by Cheticamp's solution I managed to spin my own extension of RecyclerView which doesn't have the computeVerticalScrollRange limitations.
In fact, this alternative solution doesn't require extending computeVerticalScrollRange at all.
By reasoning with things in terms of spans I managed to think of a solution that doesn't depend on calculating the height of any items in the RecyclerView.
Each item in the list has a span of 1, and I am fixing the scrollbar thumb size to a certain number of spans (meaning the scrollbar doesn't change its height as the user scrolls).
Now consider the following things:
rangeSpanCount to be the number of spans a.k.a the number of items in the adapter
firstSpan to be the position of the first visible span (first completely visible if any, otherwise the first partially visible)
lastSpan to be the position of the last visible span (last completely visible if any, otherwise the last partially visible)
visibleSpanCount, equal to lastSpan - firstSpan, to be the number of spans currently visible in the screen
remainingSpanCount, equal to rangeSpanCount - 1 - visibleSpanCount, to be the number of spans remaining in the RecyclerView
Then for the sake of the explanation assume we have a list of 9 spans, and only 3 of them can be visible at any given time (although the logic holds even if the number of visible spans at a given moment is dynamic):
0 1 2 3 4 5 6 7 8 9
0 1 2{------------}
| the size of this range is:
{--}2 3 4 |===========> rangeSpanCount - 1 - visibleSpanCount
{-----}3 4 5
{-------}4 5 6
{-----------}6 7 8
{-------------}7 8 9
| you can see that this range is simply computed as:
|===========> firstSpan - 0
Then notice how we can use the range that grows as the scrolling from top to bottom happens and the range of spans that is left out of sight at any given moment to calculate the progress of the scrolling throughout the RecyclerView.
First we figure out how much has the growing range grown:
partialProgress = (firstSpan - 0) / remainingSpanCount
(From 0% all the way to 100% when firstSpan == remainingSpanCount)
Then we calculate which span among the visible ones better represent the progress of the scrolling throughout the RecyclerView. Basically, we want to make sure the first span (of position 0) is chosen when RecyclerView is at the very top and the last span (of position rangeSpanCount - 1) to be chosen when we reach the very bottom. This is important otherwise your scrolling will be off when reaching these edges.
progressSpan = firstSpan + (visibleSpanCount * partialProgress)
And finally, you can use the position of this chosen span and the total number of spans to figure out the actual progress percentage across the RecyclerView, and use the real computed scroll range to determine the best offset for the scrollbar:
scrollProgress = progressSpan / rangeSpanCount
scrollOffset = scrollProgress * super.computeVerticalScrollRange()
And that's it! This solution can be adapted to support the horizontal axis, so it carries none of the caveats from Cheticamp's alternative.
It has one caveat, though: the movement of the scrollbar thumb is discrete, not continuous along the axis, meaning the jumping from one position to the next is noticeable. It is consistent, though, never "shaking" itself / going back and forth while the user performs a scroll to any direction.
This caveat can probably be solved by working with a much higher number of spans in respect to the number of items in the adapter (e.g. having multiple spans per item) but I didn't give it too much thought right now.
I hope my explanation is reasonably clear... and I thank you all for helping me with your answers, it really helped point me to the right direction!
Below you can check out the complete solution and source code:
package cz.nn.calllog.view.utils.recyclerview
import android.content.Context
import android.util.AttributeSet
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
class SmartScrollbarRecyclerView(
context: Context,
attributeSet: AttributeSet?,
defaultStyleAttribute: Int
) : RecyclerView(context, attributeSet, defaultStyleAttribute) {
constructor(
context: Context,
attributeSet: AttributeSet
) : this(context, attributeSet, 0)
constructor(
context: Context
) : this(context, null, 0)
override fun computeVerticalScrollExtent(): Int {
return checkCalculationPrerequisites(
onFailure = {
super.computeVerticalScrollExtent()
},
onSuccess = { _, rangeSpan, scrollRange ->
val extentSpanCount = 1.5F
val scrollExtent = (extentSpanCount / rangeSpan)
(scrollExtent * scrollRange).toInt()
}
)
}
override fun computeVerticalScrollOffset(): Int {
return checkCalculationPrerequisites(
onFailure = {
super.computeVerticalScrollOffset()
},
onSuccess = { layoutManager, rangeSpanCount, scrollRange ->
val firstSpanPosition = calculateFirstVisibleItemPosition(layoutManager)
val lastSpanPosition = calculateLastVisibleItemPosition(layoutManager)
val visibleSpanCount = lastSpanPosition - firstSpanPosition
val remainingSpanCount = rangeSpanCount - 1 - visibleSpanCount
val partialProgress = (firstSpanPosition / remainingSpanCount)
val progressSpanPosition = firstSpanPosition + (visibleSpanCount * partialProgress)
val scrollProgress = progressSpanPosition / rangeSpanCount
(scrollProgress * scrollRange).toInt()
}
)
}
private fun calculateFirstVisibleItemPosition(layoutManager: LinearLayoutManager): Int {
val firstCompletelyVisibleItemPosition = layoutManager.findFirstCompletelyVisibleItemPosition()
return if (firstCompletelyVisibleItemPosition == -1) {
layoutManager.findFirstVisibleItemPosition()
} else {
firstCompletelyVisibleItemPosition
}
}
private fun calculateLastVisibleItemPosition(layoutManager: LinearLayoutManager): Int {
val lastCompletelyVisibleItemPosition = layoutManager.findLastCompletelyVisibleItemPosition()
return if (lastCompletelyVisibleItemPosition == -1) {
layoutManager.findLastVisibleItemPosition()
} else {
lastCompletelyVisibleItemPosition
}
}
private fun checkCalculationPrerequisites(
onFailure: () -> Int,
onSuccess: (LinearLayoutManager, Float, Int) -> Int
): Int {
val layoutManager = layoutManager
if (layoutManager !is LinearLayoutManager) {
return onFailure.invoke()
}
val scrollRange = computeVerticalScrollRange()
if (scrollRange < height) {
return 0
}
val rangeSpanCount = calculateRangeSpanCount()
if (rangeSpanCount == 0F) {
return 0
}
return onSuccess.invoke(layoutManager, rangeSpanCount, scrollRange)
}
private fun calculateRangeSpanCount(): Float {
val recyclerAdapter = adapter ?: return 0F
return recyclerAdapter.itemCount.toFloat()
}
}

If I'm not mistaken the attribute android:scollBarSize="Xdp" should work for you. Add it to your RecyclerView xml.
That way you decide the size, and it will remain fixed.

Related

Scroll to an item in a RecyclerView, placing it at the center of the screen

I want to scroll to a particular item in my RecyclerView, but using LinearLayoutManager.scrollToPosition(position: Int), the item is sometimes at the top of the screen and sometimes at the bottom.
I want it to scroll to this item and place it at the center of the screen.
I can achieve this with a smooth scroller using other answers, but I often want to jump very far in the list, so waiting for the smooth scroll to complete is unacceptable.
I found many answers for achieving this with a smooth scroller, but in case you want to achieve this without smooth scrolling (to immediately jump a large distance for example), you can do it with a normal LinearLayoutManager/GridLayoutManager by calculating the offset yourself.
I subclassed LinearLayoutManager so that I could just use the scrollToPosition function and have it always scroll to that position with the item in the center, but you can just use the scrollToPositionWithOffset function of the LinearLayoutManager with a manual offset each time if you prefer.
class CenterScrollLayoutManager(context: Context, orientation: Int, reverseLayout: Boolean): LinearLayoutManager(context, orientation, reverseLayout) {
override fun scrollToPosition(position: Int) {
//this will place the top of the item at the center of the screen
val height = getApplicationContext().resources.displayMetrics.heightPixels
val offset = height/2
//if you know the item height, you can place the center of the item at the center of the screen
// by subtracting half the height of that item from the offset:
// val height = getApplicationContext().resources.displayMetrics.heightPixels
// //(say item is 40dp tall)
// val itemHeight = 40F * getApplicationContext().resources.displayMetrics.scaledDensity
// val offset = height/2 - itemHeight/2
//depending on if you have a toolbar or other headers above the RecyclerView,
// you may want to subtract their height as well:
// val height = getApplicationContext().resources.displayMetrics.heightPixels
// //(say item is 40dp tall):
// val itemHeight = 40F * getApplicationContext().resources.displayMetrics.scaledDensity
// //(say toolbar is 56dp tall, which is the default action bar height for portrait mode)
// val toolbarHeight = 56F * getApplicationContext().resources.displayMetrics.scaledDensity
// val offset = height/2 - itemHeight/2 - toolbarHeight
//call scrollToPositionWithOffset with the desired offset
super.scrollToPositionWithOffset(position, offset)
}
}
Then, to use it, you need to first set this as the layout manager for your RecyclerView, likely inside OnCreate (if in an activity) or OnViewCreated (if in a fragment):
yourRecyclerView.apply {
adapter = yourRecyclerViewAdapter
layoutManager = CenterScrollLayoutManager(context, LinearLayoutManager.VERTICAL, false)
}
Then, just scroll to a desired position, and the item should be in the center:
yourRecyclerView.scrollToPosition(desiredPosition)
This also works with a GridLayoutManager.

How to set recycler height to highest item in recyclerView?

I need to make sure that horizontal recyclerView height is the same as the height of the biggest item.
Items can have a different height (Item = always the same image + title + subtitle, title and subtitle could have infinite length).
When I set wrap_content for my recyclerView it would resize, basing on the height of visible items which makes content below recyclerView jump, and that's something I want to avoid.
What I want to achieve:
The gray area is visible viewport.
So basically I would like to get somehow hight of the biggest item, then put recyclerView height to that number.
What I already tried is approximation high of items based on length of title + subtitle but it's very inaccurate because for example even if two titles have the same text length they could have different width because of font that I use which is not a monospace font.
I just had this issue as well. My solution is:
Wrap the RecyclerView inside a ConstraintLayout.
Set the ConstraintLayout's layout_height to wrap_content.
Add an item view to the ConstraintLayout and populate it with the data of the item you expect to be the highest based on the length of its title for example.
Set the item view's visibility to invisible.
Set the RecyclerView's layout_height to zero, and make its top and bottom constraints match that of the item view.
Too late for an answer, but maybe this will help someone.
I struggled with the same issue and couldn't find an acceptable solution.
Solved by following:
First, you need to override onMeasure from the RecyclerView to save the largest element height:
class CustomRecycleView(ctx: Context, attrs: AttributeSet) : RecyclerView(ctx, attrs) {
private var biggestHeight: Int = 0
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
for (i in 0 until childCount) {
val child = getChildAt(i)
child.measure(widthSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED))
val h = child.measuredHeight
if (h > biggestHeight) biggestHeight = h
}
super.onMeasure(widthSpec, MeasureSpec.makeMeasureSpec(biggestHeight, MeasureSpec.EXACTLY))
}
}
In you layout replace RecycleView with this CustomRecycleView:
onMeasure is called when a new element in the list is visible, and if the element is the highest, then we save this value. For example: if the first element has lowest height but lates has highest then at start RecycleView will be have height match to first element but after scrolling it will stay match to highest.
If you don't need to make RecycleView height match to highest item at start then you can stop here.
To do this at the beginning, you must make a hack (based on #MidasLefko suggestion):
To find out initially what the height of the highest element will be, you need to add a scroll mechanism to the end and the beginning. I did it as follows:
private fun initRecycleView(items: ArrayList<Object>) {
val adapter = Adapter()
rv.visibility = View.INVISIBLE
rv.vadapter = adapter
rv.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
rv.setHasFixedSize(true)
rv.smoothScrollToPosition(pinnedPosts.size)
Handler().postDelayed({
rv.smoothScrollToPosition(0)
}, 300)
Handler().postDelayed({
rv.visibility = View.VISIBLE
}, 700)
}
Set the visibility of Recycle view to INVISIBLE and after 700 milliseconds to VISIBLE to make this process invisible for user. Also, scrolling to start is performed with a delay of 300 milliseconds, because without some delay it can work incorrectly. In my case, this is needed for a list of 3 elements, and these delays is optimal for me.
Also remember to remove all Handler callbacks in onStop ()
I don't think that this is possible out of the box.
Let's think for a minute about how a RecyclerView works. In order to save memory it reuses the same View objects and just binds them to new data from the list as the user scrolls. So, for example, if the user sees item's 0 and 1 then the system has only measured and laid out 2 items (and perhaps one or two more to help scroll performance).
But let's say that your tall item is number 50 in the list, when the RecyclerView binds the first few items it has no idea at all that item 50 even exists, let alone how tall it will be.
However, you can do something a bit hacky. For example, you can measure each items height after it is bound, keep track of the tallest, and then manually set the RecyclerView height to that size. With that mechanism in place you can make the RecyclerView be hidden, then manually scroll to the end of the list, scroll back to the beginning of the list, then show the RecyclerView.
Not the most elegant solution, but it should work.
Created a method to calculate the projected height of textView by trying all the description in the list to get the highest height.
public static int getHeightOfLargestDescription(final Context context, final CharSequence text, TextView textView) {
final WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
final Point displaySize = new Point();
wm.getDefaultDisplay().getSize(displaySize);
final int deviceWidth = displaySize.x;
textView.setTypeface(Typeface.DEFAULT);
textView.setText(text, TextView.BufferType.SPANNABLE);
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(deviceWidth, View.MeasureSpec.AT_MOST);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
textView.measure(widthMeasureSpec, heightMeasureSpec);
return textView.getMeasuredHeight();
}
then used this method to in onCreateViewHolder to get ready with the highest height to be used while binding the view.
MyViewHolder myViewHolder = new MyViewHolder(itemView);
for (Model m : modelList) {
currentItemHeight = getHeightOfLargestDescription(context, m.description, myViewHolder.description);
if (currentItemHeight > highestHeight) {
highestHeight = currentItemHeight;
}
}
Then used this highestHeight in onBindViewHolder` to set the height of the description TexView, so that all the views always have the same height that is equal to the highest height.
viewHolder.description.setHeight(highestHeight);
Code is committed in the
https://github.com/dk19121991/HorizontalRecyclerWithDynamicHeight
Let me know if this solves your problem, if you have some more question feel free to ask.
Thanks
To view a full discussion on this solution please see below
https://stackoverflow.com/a/67403898/4828650
You may try this:
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(#NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
final int newHeight = recyclerView.getMeasuredHeight();
if (0 != newHeight && minHeight < newHeight) {
// keep track the height and prevent recycler view optimizing by resizing
minHeight = newHeight;
recyclerView.setMinimumHeight(minHeight);
}
}
});
you should try with different item_view type
Try this
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = mLayoutInflater.inflate(R.layout.view_item, parent, false);
// work here if you need to control height of your items
// keep in mind that parent is RecyclerView in this case
int height = parent.getMeasuredHeight() / 4;
itemView.setMinimumHeight(height);
return new ItemViewHolder(itemView);
}
Or you can try this also
#Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View itemView = inflater.inflate(R.layout.itemview, parent, false);
ViewGroup.LayoutParams layoutParams = itemView.getLayoutParams();
layoutParams.height = (int) (parent.getHeight() * 0.3);
itemView.setLayoutParams(layoutParams);
return new MyViewHolder(itemView);
}
You can also set your itemView with fixed height.
I disabled the recycling in recycler view and it solved the issue.
recyclerView.getRecycledViewPool().setMaxRecycledViews(TYPE_CAROUSEL, 0);
this solution may have a performance issue if there are a lot of items but will work fine for a few items lets say 5 to 20 which was case for me.
recyclerViewHorizontal.setMinimumHeight(maxItemHeight) has worked well for me.

How do I "Save State" of my Recyclerview ItemDecorations & Offsets?

I have a RecyclerView of items and a layoutManager that is of type StaggeredGridLayoutManager. I was in an interesting situation, I wanted my items staggered to look like this:
but my views are all the same size, so they would not stagger. To correct the problem I needed to add an offset at the start of the 2nd column. Since I was also creating my own custom decorator class I figured the best way to accomplish this was to just add an offset for the first right column item in my list using the getItemsOffsets method.
Here is the relevant code for my decorator class:
public class StampListDecoration extends RecyclerView.ItemDecoration {
...
#Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
// good example here: https://stackoverflow.com/questions/29666598/android-recyclerview-finding-out-first-and-last-view-on-itemdecoration/30404499#30404499
/**
* Special case. Te first right side item in the list should have an extra 50% top
* offset so that these equal sized views are perfectly staggered.
*/
if (parent.getChildAdapterPosition(view) == 1) {
/**
* We would normally do a outRect.top = view.getHeight()/2 to create a 50% top offset on the first right item in the list.
* However, problems would arise if we paused the app when the top right item was scrolled off screen.
* In this situation, when we re-inflated the recyclerview since the view was off screen
* Android would say the height of the view was zero. So instead I added code that
* looked for the height of the top most view that was visible (and would therefore
* have a height.
*
* see https://stackoverflow.com/questions/29463560/findfirstvisibleitempositions-doesnt-work-for-recycleview-android
* because as a staggeredGrid layout you have a special case first visible method
* findFirstVisibleItemPositions that returns an array of (notice the S on the end of
* the method name.
*/
StaggeredGridLayoutManager layoutMngr = ((StaggeredGridLayoutManager) parent.getLayoutManager());
int firstVisibleItemPosition = layoutMngr.findFirstVisibleItemPositions(null)[0];
int topPos = 0;
try {
topPos = parent.getChildAt(firstVisibleItemPosition).getMeasuredHeight()/2;
} catch (Exception e) {
e.printStackTrace();
}
outRect.set(0, topPos, 0, 0);
} else {
outRect.set(0, 0, 0, 0);
}
}
}
my problem is that these offsets are not getting saved to state when my activity pauses/resumes. So when I switch to another app and switch back, the right column in my RecyclerView slides back to the top...and I lose my stagger.
Can someone show me how to save my offset state? Where are offsets supposed to be saved? I was assuming the LayoutManager would save this information, and I'm saving the LayoutManager state, but that does not seem to be working.

scrollBy doesn't work properly in nested recyclerview

I have a vertically scrolling RecyclerView with horizontally scrolling inner RecyclerViews just like this.
With this implementation, users can scroll each horizontal recyclerview synchronously. However, when a user scroll vertically to the parent recyclerView, a new horizontal recyclerview which has just attached on window doesn't display on same scroll x position. This is normal. Because it has just created.
So, I had tried to scroll to the scrolled position before it was displayed. Just like this:
Note: this is in adapter of the parent recyclerview whose orientation is vertical.
#Override
public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
super.onViewAttachedToWindow(holder);
CellColumnViewHolder viewHolder = (CellColumnViewHolder) holder;
if (m_nXPosition != 0) {
// this doesn't work properly
viewHolder.m_jRecyclerView.scrollBy(m_nXPosition, 0);
}
}
As you can see, scrollBy doesn't effect for row 10, row 11, row 12 and row 13 After that, I debugged the code to be able find out find out what's happening. When I set scroll position using scrollBy, childCount() return zero for row 10, row 11, row 12 and row 13 So they don't scroll. But why ? and Why others work ?
How can I fix this ?
Is onViewAttachedToWindow right place to scroll new attached recyclervViews ?
Note: I have also test scrollToPosition(), it doesn't get any problem like this. But I can't use it at my case. Because users can scroll to the any x position which may not the exact position. So I need to set scroll position using x value instead of the position.
Edit: You can check The source code
I found a solution that is use scrollToPositionWithOffset method instead using scrollBy. Even if both of two scroll another position, they have really different work process in back side.
For example: if you try to use scrollBy to scroll any pixel position and your recyclerView had not been set any adapter which means there is no any data to display and so it has no any items yet, then scrollBy doesn't work. RecyclerView uses its layoutManager's scrollBy method. So in my case, I am using LinearLayoutManager to the horizontal recyclerViews.
Lets see what it's doing :
int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getChildCount() == 0 || dy == 0) {
return 0;
}
mLayoutState.mRecycle = true;
ensureLayoutState();
final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
final int absDy = Math.abs(dy);
updateLayoutState(layoutDirection, absDy, true, state);
final int consumed = mLayoutState.mScrollingOffset
+ fill(recycler, mLayoutState, state, false);
if (consumed < 0) {
if (DEBUG) {
Log.d(TAG, "Don't have any more elements to scroll");
}
return 0;
}
final int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
mOrientationHelper.offsetChildren(-scrolled);
if (DEBUG) {
Log.d(TAG, "scroll req: " + dy + " scrolled: " + scrolled);
}
mLayoutState.mLastScrollDelta = scrolled;
return scrolled;
}
As you can see scrollBy ignores the scroll intentions if there is no any child at that time.
if (getChildCount() == 0 || dy == 0) {
return 0;
}
On the other hand scrollToPosition can work perfectly even if there is no any set data yet.
According to the Pro RecyclerView slide, the below sample works perfectly. However you can not do that with scrollBy.
void onCreate(SavedInstanceState state) {
....
mRecyclerView.scrollToPosition(selectedPosition);
mRecyclerView.setAdapter(myAdapter);
}
As a result, I have changed little thing to use scrollToPositionWithOffset().
Before this implementation I was calculating the exact scroll x position as a pixel.
After that, when the scroll came idle state, calculating the first complete visible position to the first parameter of the scrollToPositionWithOffset().
For second parameter which is the offset, I am getting the value using view.getLeft() function which helps to get left position of this view relative to its parent.
And it works perfectly!!

Check if ScrollView is higher than screen / scrollable

How can I check if a ScrollView is higher than the screen? When the content of a ScrollView fits on the screen, the ScrollView isn't scrollable, when it's contents exceed the screen height it's scrollable.
How can I check the condition of a ScrollView in that regard?
This is the code from ScrollView, which is private, but can be adapted to be used outside of the class itself
/**
* #return Returns true this ScrollView can be scrolled
*/
private boolean canScroll() {
View child = getChildAt(0);
if (child != null) {
int childHeight = child.getHeight();
return getHeight() < childHeight + mPaddingTop + mPaddingBottom;
}
return false;
}
Too late, but I'm using the following code and it looks more safe for me:
if (view.canScrollVertically(1) || view.canScrollVertically(-1)) {
// you code here
}
A ScrollView always has 1 child. All you need to do is get the height of the child
int scrollViewHeight = scrollView.getChildAt(0).getHeight();
and Calculate Height of Your Screen
if both are equal(or scrollView Height is more) then it fits on your screen.
In my case, I was checking to see if my scrollView(which contained text) was scrollable vertically when the activity was created. On phones, it would scroll but on tablets it couldn't. canScrollVertically was returning me incorrect value because it couldn't be determined yet. I fixed this issue by calling it in the OnGlobalLayoutListener.
(Kotlin)
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
// Must use onGlobalLayout or else canScrollVertically will not return the correct value because the layout hasn't been made yet
scrollView.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
// If the scrollView can scroll, disable the accept menu item button
if ( scrollView.canScrollVertically(1) || scrollView.canScrollVertically(-1) )
acceptMenuItem?.isEnabled = false
// Remove itself after onGlobalLayout is first called or else it would be called about a million times per second
scrollView.viewTreeObserver.removeOnGlobalLayoutListener(this)
}
})
}
My use case was displaying terms of use. I didn't want the accept button to be enabled until the user scrolled to the bottom. I know this is late but i hope this resolves some confusion about canScrollVertically

Categories

Resources