RecyclerView - Scroll To Position Not Working Every Time - android

I have implemented a horizontal scrollable RecyclerView. My RecyclerView uses a LinearLayoutManager, and the problem I am facing is that when I try to use scrollToPosition(position) or smoothScrollToPosition(position) or from LinearLayoutManager's scrollToPositionWithOffset(position). Neither works for me. Either a scroll call doesn't scroll to the desired location or it doesn't invoke the OnScrollListener.
So far I have tried so many different combinations of code that I cannot post them all here. Following is the one that works for me (But only partially):
public void smoothUserScrollTo(final int position) {
if (position < 0 || position > getAdapter().getItemCount()) {
Log.e(TAG, "An attempt to scroll out of adapter size has been stopped.");
return;
}
if (getLayoutManager() == null) {
Log.e(TAG, "Cannot scroll to position a LayoutManager is not set. " +
"Call setLayoutManager with a non-null layout.");
return;
}
if (getChildAdapterPosition(getCenterView()) == position) {
return;
}
stopScroll();
scrollToPosition(position);
if (lastScrollPosition == position) {
addOnLayoutChangeListener(new OnLayoutChangeListener() {
#Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
if (left == oldLeft && right == oldRight && top == oldTop && bottom == oldBottom) {
removeOnLayoutChangeListener(this);
updateViews();
// removing the following line causes a position - 3 effect.
scrollToView(getChildAt(0));
}
}
});
}
lastScrollPosition = position;
}
#Override
public void scrollToPosition(int position) {
if (position < 0 || position > getAdapter().getItemCount()) {
Log.e(TAG, "An attempt to scroll out of adapter size has been stopped.");
return;
}
if (getLayoutManager() == null) {
Log.e(TAG, "Cannot scroll to position a LayoutManager is not set. " +
"Call setLayoutManager with a non-null layout.");
return;
}
// stopScroll();
((LinearLayoutManager) getLayoutManager()).scrollToPositionWithOffset(position, 0);
// getLayoutManager().scrollToPosition(position);
}
I opted for scrollToPositionWithOffset() because of this but the case perhaps is different as I use a LinearLayoutManager instead of GridLayoutManager. But the solution does work for me too, but as I said earlier only partially.
When the call to scroll is from 0th position to totalSize - 7 scroll works like a charm.
When scroll is from totalSize - 7 to totalSize - 3, First time I only scroll to 7th last item in the list. The second time however I can scroll fine
When scrolling from totalSize - 3 to totalSize, I start getting unexpected behavior.
If anyone has found a work around I'd Appreciate it. Here's the gist to my code of custom ReyclerView.

I had the same issue some weeks ago, and found only a really bad solution to solve it. Had to use a postDelayed with 200-300ms.
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
yourList.scrollToPosition(position);
}
}, 200);
If you found a better solution, please let me know! Good luck!

Turns out I was having a similar issue until I utilized
myRecyclerview.scrollToPosition(objectlist.size()-1)
It would always stay at the top when only putting in the objectlist size. This was until i decided to set the size equal to a variable. Again, that didn't work. Then I assumed that perhaps it was handling an outofboundsexception without telling me. So I subtracted it by 1. Then it worked.

The accepted answer will work, but it may also break. The main reason for this issue is that the recycler view may not be ready by the time you ask it to scroll. The best solution for the same is to wait for the recycler view to be ready and then scroll. Luckily android has provided one such option. Below solution is for Kotlin, you can try the java alternative for the same, it will work.
newsRecyclerView.post {
layoutManager?.scrollToPosition(viewModel.selectedItemPosition)
}
The post runnable method is available for every View elements and will execute once the view is ready, hence ensuring the code is executed exactly when required.

You can use LinearSmoothScroller this worked every time in my case:
First create an instance of LinearSmoothScroller:
LinearSmoothScroller smoothScroller=new LinearSmoothScroller(activity){
#Override
protected int getVerticalSnapPreference() {
return LinearSmoothScroller.SNAP_TO_START;
}
};
And then when you want to scroll recycler view to any position do this:
smoothScroller.setTargetPosition(pos); // pos on which item you want to scroll recycler view
recyclerView.getLayoutManager().startSmoothScroll(smoothScroller);
Done.

So the problem for me was that I had a RecyclerView in a NestedScrollView. Took me some time to figure out this was the problem. The solution for this is (Kotlin):
val childY = recycler_view.y + recycler_view.getChildAt(position).y
nested_scrollview.smoothScrollTo(0, childY.toInt())
Java (credits to Himagi https://stackoverflow.com/a/50367883/2917564)
float y = recyclerView.getY() + recyclerView.getChildAt(selectedPosition).getY();
scrollView.smoothScrollTo(0, (int) y);
The trick is to scroll the nested scrollview to the Y instead of the RecyclerView. This works decently at Android 5.0 Samsung J5 and Huawei P30 pro with Android 9.

I also faced a similar problem (having to scroll to the top when the list is getting updated), but none of the above options worked 100%
However I finally found a working solution at https://dev.to/aldok/how-to-scroll-recyclerview-to-a-certain-position-5ck4 archive link
Summary
scrollToPosition only seems to work when the underlying dataset is ready.
So therefore postDelay works (randomly) but it's depending on the speed of the device/app. If the timeout is too short it fails. smoothScrollToPosition also only works if the adapter is not too busy (see https://stackoverflow.com/a/61403576/11649486)
To observe when the dataset is ready, a AdapterDataObserver can be added and certain methods overridden.
The code that fixed my problem:
adapter.registerAdapterDataObserver( object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(
positionStart: Int,
itemCount: Int
) {
// This will scroll to the top when new data was inserted
recyclerView.scrollToPosition(0)
}
}

None of the methods seems to be working for me. Only the below single line of code worked
((LinearLayoutManager)mRecyclerView.getLayoutManager()).scrollToPositionWithOffset(adapter.currentPosition(),200);
The second parameter refers to offset, which is actually the distance (in pixels) between the start edge of the item view and start edge of the RecyclerView. I have supplied it with a constant value to make the top items also visible.
Check for more reference over here

Using Kotlin Coroutines in Fragment or Activity, and also using the lifecycleScope since any coroutine launched in this scope is canceled when the Lifecycle is destroyed.
lifecycleScope.launch {
delay(100)
recyclerView.scrollToPosition(0)

This worked for me
Handler().postDelayed({
(recyclerView.getLayoutManager() as LinearLayoutManager).scrollToPositionWithOffset( 0, 0)
}, 100)

I had the same issue while creating a cyclic/circular adapter, where I could only scroll downward but not upward considering the position initialises to 0. I first considered using Robert's approach, but it was too unreliable as the Handler only fired once, and if I was unlucky the position wouldn't get initialised in some cases.
To resolve this, I create an interval Observable that checks every XXX amount of time to see whether the initialisation succeeded and afterward disposes of it. This approach worked very reliably for my use case.
private fun initialisePositionToAllowBidirectionalScrolling(layoutManager: LinearLayoutManager, realItemCount: Int) {
val compositeDisposable = CompositeDisposable() // Added here for clarity, make this into a private global variable and clear in onDetach()/onPause() in case auto-disposal wouldn't ever occur here
val initPosition = realItemCount * 1000
Observable.interval(INIT_DELAY_MS, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe ({
if (layoutManager.findFirstVisibleItemPosition() == 0) {
layoutManager.scrollToPositionWithOffset(initPosition, 0)
if (layoutManager.findFirstCompletelyVisibleItemPosition() == initPosition) {
Timber.d("Adapter initialised, setting position to $initPosition and disposing interval subscription!")
compositeDisposable.clear()
}
}
}, {
Timber.e("Failed to initialise position!\n$it")
compositeDisposable.clear()
}).let { compositeDisposable.add(it) }
}

This worked perfectly for when scrolling to last item in the recycler
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
#Override
public void run() {
if (((LinearLayoutManager) recyclerView.getLayoutManager())
.findLastVisibleItemPosition() != adapter.getItemCount() - 1) {
recyclerView.scrollToPosition(adapter.getItemCount() - 1);
handler.postDelayed(this, 200);
}
}
}, 200 /* change it if you want*/);

Pretty weird bug, anyway I managed to work around it without post or post delayed as follow:
list.scrollToPosition(position - 1)
list.smoothScrollBy(1, 0)
Hopefully, it helps someone too.

Had the same issue. My problem was, that I refilled the view with data in an async task, after I tried to scroll. From onPostExecute ofc fixed this problem. A Delay fixed this issue too, because when the scroll executed, the list had already been refilled.

I use below solution to make the selected item in recycler view visible after the recycler view is reloaded (orientation change, etc). It overrides LinearLayoutManager and uses onSaveInstanceState to save current recycler position. Then in onRestoreInstanceState the saved position is restored. Finaly, in onLayoutCompleted, scrollToPosition(mRecyclerPosition) is used to make the previously selected recycler position visible again, but as Robert Banyai stated, for it to work reliably a certain delay must be inserted. I guess it is needed to provide enough time for adapter to load the data before scrollToPosition is called.
private class MyLayoutManager extends LinearLayoutManager{
private boolean isRestored;
public MyLayoutManager(Context context) {
super(context);
}
public MyLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
}
public MyLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
#Override
public void onLayoutCompleted(RecyclerView.State state) {
super.onLayoutCompleted(state);
if(isRestored && mRecyclerPosition >-1) {
Handler handler=new Handler();
handler.postDelayed(new Runnable() {
#Override
public void run() {
MyLayoutManager.this.scrollToPosition(mRecyclerPosition);
}
},200);
}
isRestored=false;
}
#Override
public Parcelable onSaveInstanceState() {
Parcelable savedInstanceState = super.onSaveInstanceState();
Bundle bundle=new Bundle();
bundle.putParcelable("saved_state",savedInstanceState);
bundle.putInt("position", mRecyclerPosition);
return bundle;
}
#Override
public void onRestoreInstanceState(Parcelable state) {
Parcelable savedState = ((Bundle)state).getParcelable("saved_state");
mRecyclerPosition = ((Bundle)state).getInt("position",-1);
isRestored=true;
super.onRestoreInstanceState(savedState);
}
}

If you use recyclerview in nestedScrollView you must scroll nestScrollview
nestedScrollview.smoothScrollTo(0,0)

Maybe It's not so elegant way to do it, But this always works for me. Add a new method to the RecyclerView and use it insted of scrollToPosition:
public void myScrollTo(int pos){
stopScroll();
((LinearLayoutManager)getLayoutManager()).scrollToPositionWithOffset(pos,0);
}

The answer is to use the Post Method, it will guarantee correct execution for any action

This is the ultimate solution using kotlin in this date ... if you navigate to another fragment and go back and your recyclerview resets to the first position just add this line in onCreateView or wherever you need can call the adapter...
pagingAdapter.stateRestorationPolicy=RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY
BTW pagingAdapter is my adapter with diffUtil.

I had a similar issue, (but not the same), I try to explain it, maybe be could help someone else:
By the time I call to 'scrollToPosition' dataset is already set but some content like images loaded async (using Glide library) and probably when RecyclerView tries to compute the height amount to scroll down, image should return 0 as no loaded yet. So that gives an inaccurate scroll down I could solve it that way:
fun LinearLayoutManager.accurateScrollToPosition(position: Int) {
this.scrollToPosition(position)
this.postOnAnimation {
val realPosition = this.findFirstVisibleItemPosition()
if (position != realPosition) {
this.accurateScrollToPosition(position)
} else {
this.scrollToPosition(position) // this looks redunadant or inecessary but must be call to ensure accurate scroll
}
}
}
PD: In my case was not possible to know the size of the image to be loaded, if you know or you can resize the image you can add a placeholder on glide with de image size or override de size so recyclerView can compute the size correctly and don't need the above walkaraound.

Related

scrollToPositionWithOffset from LinearLayoutManager on RecyclerView not working

I'm trying to make an horizontal list of sticky images with RecyclerView and I'd like to move them by pixels' offset with scrollToPositionWithOffset. I thought passing 0 as position and the pixels I want to move to right / left as offset.
But it doesn't work, the list remains untouched, unscrolled, it doesn't move. This is my implementation:
final LargeImageAdapter mLargeImageAdapter = new LargeImageAdapter(this);
linearLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false);
recyclerView.setLayoutManager(linearLayoutManager);
recyclerView.setAdapter(mLargeImageAdapter);
seekBar = (SeekBar)findViewById(R.id.seekBar);
seekBar.setMax(7000);
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
#Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
int scrollToDX = progress;
((LinearLayoutManager)recyclerView.getLayoutManager()).scrollToPositionWithOffset(0, scrollToDX);
// tried invoking also linearLayoutManager instead getLayoutManager.
}
#Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
#Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
what am I doing wrong?
Thank you very much.
Regards.
Rafael.
I finally used:
recyclerView.scrollBy(int offsetX, int offsetY); setting offsetY = 0 and it works now.
I don't understand what's the utility of the function scrollToPositionWithOffset.
I had a similar issue. My problem was that my recyclerview wasn't of the same size of its parent layout. I solved it by setting the recycler view width and height to match_parent. I don't know why this happens in this case.
A late answer to your first question, and an addition to your answer:
Your method works better for your personal needs, because scrollToPositionWithOffset is not intended to do what you want.
As the doc says here:
[...]Resolved layout start depends on [...]
getLayoutDirection(android.view.View) [...]
Which means it would offset the scroll target position in the layout direction, vertically in your case.
I don't understand what's the utility of the function
scrollToPositionWithOffset.
it allows to not only scroll to a given item in the list, but also position it at a more "visible" or otherwise convenient place.
recently I encountered this problem too, I invoke scrollToPositionWithOffset when onScrolled() directly, but nothing change, with that I turn to scrollToPosition() even scrollBy() but not help, finally I attempt to delay that so it work, first time I delay 50ms, but two weeks later I found that's not enough, so I increase to 100ms with no approachs in my hands, of course it work, just feel a little unsettled.
val layoutManager = LinearLayoutManager(hostActivity, VERTICAL, false)
fileRv.layoutManager = layoutManager
fileRv.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (dx == 0 && dy == 0) {
scrollToLastPosition()
}
}
private fun scrollToLastPosition() {
val lastScrollPosition = viewModel.consumeLastScrollPosition()
if (lastScrollPosition > 0) {
Handler().postDelayed({ layoutManager.scrollToPositionWithOffset(lastScrollPosition, 0) }, 100)
}
}
})
override fun onItemClick(position: Int) {
layoutManager.findFirstVisibleItemPosition().let {
if (it >= 0) viewModel.markLastScrollPosition(it)
}
}
fun markLastScrollPosition(position: Int) {
currentFolderListData.value?.lastOrNull()?.lastScrollPosition = position
}
fun consumeLastScrollPosition(): Int {
currentFolderListData.value?.lastOrNull()?.run {
return lastScrollPosition.apply { lastScrollPosition = -1 }
}
return 0
}
I find a solution.
Coz I am the developer of DNA Launcher. When I use RecyclerView to display A-Z App List, I found that the function scrollToPositionWithOffset is not working. I track the problem for almost one day and I figured it out.
When the RecyclerView display again, just let the parent of RecyclerView do requestLayout.
It works for me.
And I know how to make the function scrollToPositionWithOffset not working. You just need to add a view on it and make it gone then.

Listview: smooth scroll and row update at the same time

I basically have to smooth scroll a listview and update a row at the same time.
I do it with a simple approach for now:
mListViewWeeks.post(new Runnable() {
#Override
public void run() {
// update the row concerned
updateItemAtPosition(rowIndex);
int duration = 200;
mListView.smoothScrollToPositionFromTop(rowIndex, 0, duration);
}
});
with the updateItemAtPosition() function:
private void updateItemAtPosition(int position) {
int visiblePosition = mListView.getFirstVisiblePosition();
View view = mListView.getChildAt(position - visiblePosition);
mListView.getAdapter().getView(position, view, mListView);
}
It's working well at a reasonable scroll speed, but when going faster (calling the first block above at a high rate) it can get a bit laggy. Is there anything that I can do to improve updating a row while scrolling smoothly?
You should'nt acces the iew dirrectly like this. Instead you should update your model object displayed by the list and call notifyDataSetChanged() in your adapter.
Well, I'm really late in the game. It looks like one of the reason why RecyclerView was introduced. I'm gonna try to use this component from now on.
A combination of layoutManager.scrollToPosition(position);
and adapter.notifyItemChanged(position); does the job. Everything runs smoothly!!

(Smooth)ScrollToPosition doesn't work properly with RecyclerView

I'm using basic RecyclerView with GridLayoutManager. I observed that nor smoothScrollToPosition nor scrollToPosition works properly.
a) when using smoothScrollToPosition I often receive error from RecyclerView
"RecyclerView﹕ Passed over target position while smooth scrolling."
and RecyclerView is not scrolled properly (often it misses the targeted row). This is observed mostly when I'm trying to scroll to the 1st item of some row
b) when using scrollToPosition it seems to work quite ok but most of the time I can see only the 1st item of the row and the rest are not displayed.
Can you give me some hints how to make work properly at least one of the methods?
Thanks a lot!
Finally I was able to make it work! LinearLayoutManager.scrollToPositionWithOffset(int, int) did the trick.
I also have same issue, but managed to fix the issue by Customizing SmoothScroller
let Custom LayoutManager as below
public class CustomLayoutManager extends LinearLayoutManager {
private static final float MILLISECONDS_PER_INCH = 50f;
private Context mContext;
public CustomLayoutManager(Context context) {
super(context);
mContext = context;
}
#Override
public void smoothScrollToPosition(RecyclerView recyclerView,
RecyclerView.State state, final int position) {
LinearSmoothScroller smoothScroller =
new LinearSmoothScroller(mContext) {
//This controls the direction in which smoothScroll looks
//for your view
#Override
public PointF computeScrollVectorForPosition
(int targetPosition) {
return CustomLayoutManager.this
.computeScrollVectorForPosition(targetPosition);
}
//This returns the milliseconds it takes to
//scroll one pixel.
#Override
protected float calculateSpeedPerPixel
(DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH/displayMetrics.densityDpi;
}
};
smoothScroller.setTargetPosition(position);
startSmoothScroll(smoothScroller);
}
}
(documentation commented inside the code given above)Please set the above LayoutManager
to the recyerview
CustomLayoutManagerlayoutManager = new CustomLayoutManager(getActivity());
recyclerView.setLayoutManager(layoutManager);
recyclerView.smoothScrollToPosition(position);
by using the custom Layout manager
scrollToPosition
also working well in my case u can use
recyclerView.scrollToPosition(position)
also if you want to adjust the speed of smoothScrollToPosition please override the
private static final float MILLISECONDS_PER_INCH = 50f;
in CustomLayoutManager.
So if we put the value as 1f the smoothScrollToPosition will be faster like scrollToPosition.increasing value make delay and decreasing will make the speed of scroll.
Hope this will useful.
In My case,
mRecyclerView.scrollToPosition(10);
also did not work.
But
mRecyclerView.smoothScrollToPosition(10);
works fine for me...
To scroll down smoothly to bottom from any position in the RecyclerView on clicking EditText.
edittext.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
rv_commentList.postDelayed(new Runnable() {
#Override
public void run() {
rv_commentList.scrollToPosition(rv_commentList.getAdapter().getItemCount() - 1);
}
}, 1000);
}
});
Another reason why any of the before mentioned solutions may not work is if your RecyclerView is embedded in a NestedScrollView. In this case you have to call the scroll action on the NestedScrollView.
for example:
nestedScrollview.smoothScrollTo(0,0)
This extension is so useful, try please.
fun RecyclerView.smoothSnapToPosition(position: Int, snapMode: Int = LinearSmoothScroller.SNAP_TO_START) {
val smoothScroller = object : LinearSmoothScroller(this.context) {
override fun getVerticalSnapPreference(): Int = snapMode
override fun getHorizontalSnapPreference(): Int = snapMode
}
smoothScroller.targetPosition = position
layoutManager?.startSmoothScroll(smoothScroller)
}
I was facing a weird issue wherein smoothScrollToPosition only worked occasionally.
After putting the smoothScrollToPosition inside Handler Post
Delayed with 1 second delay, it worked fine.
Refer to the following Kotlin example:
Handler().postDelayed({
recyclerViewObject.smoothScrollToPosition(0) // mention the position in place of 0
}, 1000) // 1000 indicates the 1 second delay.
recyclerView.getLayoutManager().smoothScrollToPosition(recyclerView,new RecyclerView.State(),currentPosition);
Try measuring item width or height and call smoothScrollBy(int dx, int dy).
How to perform smooth scrolling and save RecyclerView vertical position after device rotating:
This is the method that works for my case,
public class MainFragment extends Fragment { //OR activity it's //fragment in my case
....
#Override
public void onLoadFinished(#NonNull Loader<List<Report>> loader, List<Report> objects) { // or other method of your choice, in my case it's a Loader
RecyclerView recyclerViewRv = findViewById(........;
.....
recyclerViewRv.setAdapter(.....Your adapter);
recyclerViewRv.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(#NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
#Override
public void onScrolled(#NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
recyclerScrollY = recyclerViewRv. computeVerticalScrollOffset();
}
});
//Apply smooth vertical scroll
recyclerViewRv.smoothScrollBy(0,recyclerScrollY);
}
//Save vertical scroll position before rotating devices
#Override
public void onSaveInstanceState(#NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt("recyclerScrollY",recyclerScrollY);
}
//BackUp vertical scroll position after rotating devices
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if(savedInstanceState != null) {
recyclerScrollY = savedInstanceState.getInt("recyclerScrollY");
}
}
//If you want to perform the same operation for horizontal scrolling just add a variable called recyclerScrollX = recyclerScrollY = recyclerViewRv. computeHorizontalScrollOffset(); then save in bundle
Calling the recyclerView smoothScroll isn't effective, as the recyclerView itself doesn't handle its layout.
What you should do is calling the layout manager scroll method instead.
This should look something like this
mRecyclerView.getLayoutManager().scrollToPosition(desiredPosition);
If you are trying to do a quick scroll to a position at the top of the RecyclerView, just use LinearLayoutManager.scrollToPositionWithOffset with 0 as the offset.
Example:
mLinearLayoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(mLinearLayoutManager);
mLinearLayoutManager.scrollToPositionWithOffset(myPosition, 0);
smoothScrollToPosition is very slow. If you want something fast go with scrollToPositionWithOffset.
when you use scrollToPosition it will show it on top of the recycler view.
But if you use smoothScrollToPosition it will scroll till it come in to Window Visible. that's why while smoothScrool to item below, it will show it on bottom
Actually, if you have a RecyclerView inside a NestedScrollView, you must use both of these lines every time you want to go to the beginning of the RecyclerView:
nestedScrollView.smoothScrollTo(0, 0);
layoutManager.scrollToPositionWithOffset(0, 0);
This completely works for me.
this worked for me
Handler().postDelayed({
(recyclerView.getLayoutManager() as LinearLayoutManager).scrollToPositionWithOffset( 0, 0)
}, 100)
None of these answers worked for me. I needed to smoothScrollToPosition but #Ramz answer didn't work. I was finding it would consistently overscroll but only in one direction. I discovered that it seemed to be the item decorators throwing it off. I had a horizontal layout and I wanted to add a space after every item except the last and it didn't like that asymmetry. As soon as I included a space after every item, it worked!
nestedScroll.smoothScrollTo(0, recycler.top)
So i was looking for a solution to get back to the top with a recyclerview inside another layout that has a view on top of it (in my case I had a LinearLayout with a TextView and my recyclerview inside). Because of that the smoothscroll would go only to half the way to the first item.
Here's what I did which works really well (Kotlin solution):
back_to_top.setOnClickListener {
GlobalScope.launch(Dispatchers.Main) {
GlobalScope.launch(Dispatchers.Main) {
recyclerview.layoutManager?.smoothScrollToPosition(recyclerview, RecyclerView.State(), 0)
delay((recyclerview.layoutManager as LinearLayoutManager).findLastVisibleItemPosition() * 100L)
}.join()
recyclerview.scrollToPosition(0)
}
back_to_top.visibility = View.GONE
}
}
Here what I do is I smoothscroll to the first element and delay the scroll by 100ms times the last item visible and then call the scrollToPosition(0) (which goes to the top.correctly)

Android: Hiding a view off the top edge of the screen

Would it be possible to hide a view off the top edge of the screen, and only have it appear if the user scrolls upwards?
My first attempt used a scrollview, but it seems that scrollTo() doesn't work unless I used postDelayed (it doesn't even work with Post()). I tried adding it to the scrollview's view tree observer onPreDraw() event and it still doesn't work unless I delay it, so there is an ugly glitch when the activity is first launched.
The second issue is that if the onscreen keyboard is minimized, the view no longer needs to scroll so hiding things by using a scroll offset no longer works. I thought about manipulating the height in code, but this seems pretty hackish.
Is there a better way to do this than by using a scrollview? Alternatively, Does anyone have any tips on the best place to place the scrollTo (the end of onCreate does not work nor the other places I have tried) so I don't need to use postDelayed? That would at least eliminate one glitch.
Thanks!
This is the code I'm using right now, which is the least glitchy but I don't understand why it doesn't work until the third time onPreDraw() is called.
mScrollView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener()
{
#Override
public boolean onPreDraw()
{
final int fieldYStart = mFieldIWantAtTheTop.getTop();
if (mFieldIWantAtTheTopYStart != fieldYStart
|| mScrollView.getScrollY() < 10)
{
mFieldIWantAtTheTopYStart = fieldYStart;
mScrollView.post(new Runnable()
{
#Override
public void run()
{
Log.v("Testing", "scrolling!");
mScrollView.scrollTo(0, mFieldIWantAtTheTopYStart);
Log.v("Testing", "scroll is now=" + mScrollView.getScrollY());
}
});
}
return true;
}
});
I also tried using a custom scrollview as mentioned below, but this does not solve the issue of the graphical glitch:
#Override
public void onMeasure(int measureWidthSpec, int measureHeightSpec) {
super.onMeasure(measureWidthSpec, measureHeightSpec);
Log.v("Testing", "Scrolling");
post(
new Runnable()
{
public void run()
{
scrollTo(0, 100);
Log.v("Testing", "ScrollY = " + getScrollY());
}
});
}
This code works as does the onPreDraw() code above but there is still a glitch when the activity is launched because the activity is first drawn with the scroll at 0.
I haven't tried this, but you may want to create a custom ScrollView and override onMeasure:
ScrollView scroll = new ScrollView(this) {
#Override
public void onMeasure(int measureWidthSpec, int measureHeightSpec) {
super.onMeasure(measureWidthSpec, measureHeightSpec);
scrollTo(...);
}
};
It seems like this would be the earliest point that scrollTo would be valid.
Edit - I found this answer, which apparently worked for the asker. Is this the method you tried?

List view snap to item

I'm creating a list of pictures using a ListView and the photos are of a size that would fit 2 to 3 photos on the screen.
The problem that I'm having is that I would like to when the user stops scrolling that the first item of the visible list would snap to the top of screen, for example, if the scroll ends and small part of the first picture displayed, we scroll the list down so the picture is always fully displayed, if mostly of the picture is displayed, we scroll the list up so the next picture is fully visible.
Is there a way to achieve this in android with the listview?
I've found a way to do this just listening to scroll and change the position when the scroll ended by implementing ListView.OnScrollListener
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
switch (scrollState) {
case OnScrollListener.SCROLL_STATE_IDLE:
if (scrolling){
// get first visible item
View itemView = view.getChildAt(0);
int top = Math.abs(itemView.getTop()); // top is a negative value
int bottom = Math.abs(itemView.getBottom());
if (top >= bottom){
((ListView)view).setSelectionFromTop(view.getFirstVisiblePosition()+1, 0);
} else {
((ListView)view).setSelectionFromTop(view.getFirstVisiblePosition(), 0);
}
}
scrolling = false;
break;
case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
case OnScrollListener.SCROLL_STATE_FLING:
Log.i("TEST", "SCROLLING");
scrolling = true;
break;
}
}
The change is not so smooth but it works.
Utilizing a couple ideas from #nininho's solution, I got my listview to snap to the item with a smooth scroll instead of abruptly going to it. One caveat is that I've only tested this solution on a Moto X in a basic ListView with text, but it works very well on the device. Nevertheless, I'm confident about this solution, and encourage you to provide feedback.
listview.setOnScrollListener(new OnScrollListener() {
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
// TODO Auto-generated method stub
if (scrollState == SCROLL_STATE_IDLE) {
View itemView = view.getChildAt(0);
int top = Math.abs(itemView.getTop());
int bottom = Math.abs(itemView.getBottom());
int scrollBy = top >= bottom ? bottom : -top;
if (scrollBy == 0) {
return;
}
smoothScrollDeferred(scrollBy, (ListView)view);
}
}
private void smoothScrollDeferred(final int scrollByF,
final ListView viewF) {
final Handler h = new Handler();
h.post(new Runnable() {
#Override
public void run() {
// TODO Auto-generated method stub
viewF.smoothScrollBy(scrollByF, 200);
}
});
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
// TODO Auto-generated method stub
}
});
The reason I defer the smooth scrolling is because in my testing, directly calling the smoothScrollBy method in the state changed callback had problems actually scrolling. Also, I don't foresee a fully-tested, robust solution holding very much state, and in my solution below, I hold no state at all. This solution is not yet in the Google Play Store, but should serve as a good starting point.
Using #nininho 's solution,
In the onScrollStateChanged when the state changes to SCROLL_STATE_IDLE, remember the position to snap and raise a flag:
snapTo = view.getFirstVisiblePosition();
shouldSnap = true;
Then, override the computeScroll() method:
#Override
public void computeScroll() {
super.computeScroll();
if(shouldSnap){
this.smoothScrollToPositionFromTop(snapTo, 0);
shouldSnap = false;
}
}
You can do a much more smooth scrolling if you use RecyclerView. The OnScrollListener is way better.
I have made an example here: https://github.com/plattysoft/SnappingList
Well.. I know 10 years have past since this question was asked, but now we can use LinearSnapHelper:
new LinearSnapHelper().attachToRecyclerView(recyclerView);
Source:
https://proandroiddev.com/android-recyclerview-snaphelper-19eaa9598da6
Apart from trying the code above one thing you should make sure of is that your listView have a height that can fit exact number of items you want to be displayed.
e.g
If you want 4 items to be displayed after snap effect and your row height (defined in its layout) should be 1/4 of the total height of the list.
Note that after the smoothScrollBy() call, getFirstVisiblePosition() may point to the list item ABOVE the topmost one in the listview. This is especially true when view.getChildAt(0).getBottom() == 0. I had to call view.setSelection(view.getFirstVisiblePosition() + 1) to remedy this odd behavior.

Categories

Resources