I am currently detecting when a specific view becomes visible to the user or goes out of sight and stopping the RecyclerView scroll at that view position. So far I have been able to achieve this with the help of the OnScrollListener together with RecyclerView.stopScroll and RecyclerView.scrollTo, but the only issue now is that if the user flings the list it will get past that view position since the fling is still "acting" after the scroll to position has been called, causing the list to overshoot the item position.
How can I end the user fling action programmatically? Something like recyclerView.fling(0) exists?
I ended up using LinearLayoutManager.scrollToPositionWithOffset with the position of the target and 0 as the offset after calling RecyclerView.stopScroll.
So far the fling behavior is no longer causing the list scroll to overshoot the intended position when using these two together.
public class CustomLayoutManager extends LinearLayoutManager {
private boolean isScrollEnabled = true;
public CustomLayoutManager (Context context) {
super(context);
}
public void setScrollEnabled(boolean flag) {
this.isScrollEnabled = flag;
}
#Override
public boolean canScrollVertically() {
//Similarly you can customize "canScrollHorizontally()" for managing horizontal scroll
return isScrollEnabled && super.canScrollVertically();
}
}
Set this as your recyclerview layout manager and toggle the scrolling with setScrollEnabled(true/false) with the layoutManager object
Assuming your Recyclerview rv
CustomLayoutManager lm=new CustomLayoutManager(context of your activity);
rv.setLayoutManager(lm);
lm.setScrollEnabled(true/false);
Related
In my onScrolled() method of a RecyclerView.OnScrollListener, I need to be able to distinguish between two different sources of scroll events:
The user scrolling the RecyclerView using touch events
Code scrolling the RecyclerView using scrollToPosition()
There is no parameter to onScrolled() that would appear to cover this. Is there a recipe for making this distinction?
FWIW, my scenario: I have two RecyclerView widgets. One is the master, and shows a traditional vertical list. The other is serving as a ViewPager replacement, to allow for swiping through details of the same items that are in the vertical list. In a large-screen environment, both RecyclerView widgets will be visible at once (master-detail pattern), and I need to keep them synchronized. If the user swipes the pager RecyclerView, I need to change an indicator on the list RecyclerView rows to match. If the user taps on a row in the list RecyclerView, I need to update the current page in the pager RecyclerView. To listen for swipe events on the pager, I am using OnScrollListener, but that also is getting triggered when I scroll the pager in response to list row clicks.
Gabe Sechan's suggestion of using the scroll-state change is a good one.
Unfortunately, there is no listener for the scroll state — it requires that you subclass RecyclerView. So, I whipped up a RecyclerViewEx to add a listener API for that event:
public class RecyclerViewEx extends RecyclerView {
interface OnScrollStateChangedListener {
void onScrollStateChanged(int state);
}
final private ArrayList<OnScrollStateChangedListener> listeners=new ArrayList<>();
public RecyclerViewEx(Context context) {
super(context);
}
public RecyclerViewEx(Context context, #Nullable
AttributeSet attrs) {
super(context, attrs);
}
public RecyclerViewEx(Context context, #Nullable AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
}
public void addOnScrollStateChangedListener(OnScrollStateChangedListener listener) {
listeners.add(listener);
}
public void removeOnScrollStateChangedListener(OnScrollStateChangedListener listener) {
listeners.remove(listener);
}
#Override
public void onScrollStateChanged(int state) {
super.onScrollStateChanged(state);
for (OnScrollStateChangedListener listener : listeners) {
listener.onScrollStateChanged(state);
}
}
}
scrollToPosition() does not trigger a scroll-state change, whereas user swipes do. So, I can distinguish between the two scenarios that way.
rve.addOnScrollStateChangedListener(
scrollState -> {
if (scrollState==RecyclerView.SCROLL_STATE_IDLE) {
// do something cool, now that the user swipe is complete
}
});
Given this, I do not need a flag or any other state, which helps keep the logic simpler.
The accepted answer will not work if you are using smoothScrollBy() or smoothScrollToPosition(). The only way to tell if the scroll was from user is to set a flag when the user is dragging, when this happen the RecyclerView state would be changed to RecyclerView.SCROLL_STATE_DRAGGING. Then, when the state changed to RecyclerView.SCROLL_STATE_IDLE, you need to clear the flag.
var scrolledByUser = false
rv.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(
recyclerView: RecyclerView,
newState: Int
) {
when (newState) {
RecyclerView.SCROLL_STATE_DRAGGING -> {
scrolledByUser = true
}
RecyclerView.SCROLL_STATE_IDLE -> {
if (scrolledByUser) {
// fromUser
} else {
// from smoothScroll
}
scrolledByUser = false
}
}
}
})
I am using a RecyclerView with a Horizontal LinearLayoutManager.
recyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL,false));
For the adapter items to snap at the center, I've attached a LinearSnapHelper to the recyclerview.
SnapHelper helper = new LinearSnapHelper();
helper.attachToRecyclerView(recyclerView);
Now, I have two scenarios where I would want an item to come to center
When my activity launches, it should launch with a specific item snapped to center.
On tapping on an item in the recyclerview, it should get centered. For this, I've overridden the OnClick method in adapter's ViewHolder.
For both scenarios, I'm using
recyclerView.smoothScrollToPosition(position);
and the item gets centered. This however happens with a bouncy animation where a little extra scroll happens first and post that the item bounces back.
How can I disable this bouncy animation to get a smooth scroll?
Things I've tried -
Used below APIs in place of smoothScrollToPosition
LinearLayoutManager.scrollToPosition()
LinearLayoutManager.scrollToPositionWithOffset()
Both the above APIs don't give a smooth scroll, plus the item doesn't get centered properly (since it's difficult to figure out the correct offset value for items that are not yet created/recycled back during the API call)
I couldn't find any method to disable/override animations in RecyclerView's Documentation. Could someone please help..
The solution is to use extended LinearLayoutManager:
import android.content.Context;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.LinearSmoothScroller;
import android.support.v7.widget.RecyclerView;
public class NoBounceLinearLayoutManager extends LinearLayoutManager {
public NoBounceLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
}
#Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, final int position) {
LinearSmoothScroller linearSmoothScroller = new LinearSmoothScroller(recyclerView.getContext()) {
#Override
protected int getHorizontalSnapPreference() {
return position > findFirstVisibleItemPosition() ? SNAP_TO_START : SNAP_TO_END;
}
};
linearSmoothScroller.setTargetPosition(position);
startSmoothScroll(linearSmoothScroller);
}
}
I have a fragment that has a RecyclerView inside its layout file. The RecyclerView holds messages in a chat. So naturally, I need the RecyclerView to scroll to the bottom when the chat fragment gets opened.
I tried scrolling directly on the RecyclerView:
var mRecyclerView = view.FindViewById<RecyclerView>
mRecyclerView.ScrollToPosition(mMessages.Count-1);
Second method:
LinearLayoutManager mLinearLayoutManager = new LinearLayoutManager(Application.Context);
mRecyclerView.SetLayoutManager(mLinearLayoutManager);
mLinearLayoutManager.ScrollToPosition(mMessages.Count - 1);
Third method:
LinearLayoutManager mLinearLayoutManager = new LinearLayoutManager(Application.Context);
mRecyclerView.SetLayoutManager(mLinearLayoutManager);
mLinearLayoutManager.ScrollToPositionWithOffset(mMessages.Count - 1, 0);
Unfortunately, nothing happens in either case. Any suggestions would be greatly appreciated!
Please use smoothScrollToPosition to fix your issue.
I always use smoothScrollToPosition for redirecting to any position.
Make sure, mMessages size is good as you thinking.
Example,
RecyclerView rv = (RecyclerView)findViewById(R.id.recyclerView);
rv.smoothScrollToPosition(mMessages.count-1);
You can set setStackFromEnd=true that will set the view to show the last element, the layout direction will remain the same.
After edit I have updated: just like this:
LinearLayoutManager mLinearLayoutManager = new LinearLayoutManager(Application.Context);
mLinearLayoutManager.setStackFromEnd(true);
mRecyclerView.SetLayoutManager(mLinearLayoutManager);
mRecyclerView.scrollToPosition(mMessages.Count-1);
please check the documentation.
Edit: the problem is you are calling scrollToPosition without setting any layout manager to the recyclerview.
Considering to the scrollToPosition function in RecyclerView class, your case makes sense.
/**
* Convenience method to scroll to a certain position.
*
* RecyclerView does not implement scrolling logic, rather forwards the call to
* {#link android.support.v7.widget.RecyclerView.LayoutManager#scrollToPosition(int)}
* #param position Scroll to this adapter position
* #see android.support.v7.widget.RecyclerView.LayoutManager#scrollToPosition(int)
*/
public void scrollToPosition(int position) {
if (mLayoutFrozen) {
return;
}
stopScroll();
if (mLayout == null) {
Log.e(TAG, "Cannot scroll to position a LayoutManager set. " +
"Call setLayoutManager with a non-null argument.");
return;
}
mLayout.scrollToPosition(position);
awakenScrollBars();
}
Cannot scroll to position a LayoutManager set. Call setLayoutManager with a non-null argument.
It's Simple
change your code with
RecyclerView recyclerView = findViewById(R.id.recyclerView);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
layoutManager.setStackFromEnd(true);
recyclerView.setLayoutManager(layoutManager);
recyclerView.setAdapter(mAdapter);
recyclerView.smoothScrollToPosition(mAdapter.getItemCount());
If You have given ScrollView or NestedScrollView in the parent of RecyclerView then it will not work. Because it only works when RecyclerView doesn't have any parent layout like ScrollView. hope it will fix your error.
I believe you are not calling it in right way. You have to pass LinearLayoutManager as well along with recycler view to make it work. Here how I have made it work
mLayoutManager.smoothScrollToPosition(mRecyclerView, null, array.size() - 1);
I found the issue, set RecyclerView scrollToPosition or smoothScrollToPosition after setting the LayoutManager.
RecyclerView recyclerView = findViewById(R.id.recyclerView);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.scrollToPosition(mMessages.count - 1);
Hopes it help you.
If you want to directly scroll in the last item you can just use this code.
binding.recyclerView.smoothScrollToPosition(adapter.itemCount-1)
If you want to add continuous scrolling for the recyclerview, you can use this class.
public class AutoScrollRecyclerView extends RecyclerView {
private static long delayTime = 45;// How long after the interval to perform scrolling
AutoPollTask autoPollTask;// Scroll thread
private boolean running; // Is it rolling?
private boolean canRun;// Can it be automatically scrolled, depending on whether the data exceeds the screen
public AutoScrollRecyclerView(Context context) {
super(context);
}
public AutoScrollRecyclerView(Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
autoPollTask = new AutoPollTask(this);// Instantiate the scroll refresh thread
}
static class AutoPollTask implements Runnable {
private final WeakReference<AutoScrollRecyclerView> mReference;
// Use weak references to hold external class references to prevent memory leaks
public AutoPollTask(AutoScrollRecyclerView reference) {
this.mReference = new WeakReference<>(reference);
}
#Override
public void run() {
AutoScrollRecyclerView recyclerView = mReference.get();// Get the recyclerview object
if (recyclerView != null && recyclerView.running && recyclerView.canRun) {
recyclerView.scrollBy(2, 2);// Note the difference between scrollBy and scrollTo
//delayed to send
recyclerView.postDelayed(recyclerView.autoPollTask, delayTime);
}
}
}
#Override
public boolean onTouchEvent(MotionEvent e) {
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN:
/*if (running)
stop();*/
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_OUTSIDE:
/*if (canRun)
start();*/
break;
}
return super.onTouchEvent(e);
}
//Open: If it is running, stop first -> then open
public void start() {
if (running)
stop();
canRun = true;
running = true;
postDelayed(autoPollTask, delayTime);
}
public void stop() {
running = false;
removeCallbacks(autoPollTask);
}
}
If You have given ScrollView or NestedScrollView in the parent of recyclerView then try this:
parentNestedScrollView.smoothScrollTo(0, parentNestedScrollView.bottom)
Sometimes this may not work, as an attempt to update the scroll position will be made before the adapter displays the new item, so we need to use delay:
popupView.postDelayed({
parentNestedScrollView.smoothScrollTo(0, parentNestedScrollView.bottom)
}, 200)
I use smoothScrollToPosition to scroll RecyclerView. It does scroll every time a new entry is inserted; but to the top, not to the bottom, which is the direction i want.
list_chat = (RecyclerView) findViewById(R.id.list_chat);
//Set up Layout Manager
linearLayoutManager = new LinearLayoutManager(this);
linearLayoutManager.setStackFromEnd(true);
list_chat.setLayoutManager(linearLayoutManager);
//set adapter
list_chat.setAdapter(adapter);
//set scroll
list_chat.post(new Runnable() {
#Override
public void run() {
list_chat.smoothScrollToPosition(adapter.getItemCount());
}
});
The adapter is from Firebase
adapter = new FirebaseRecyclerAdapter<ChatItem, ChatRecylerViewHolder>(ChatItem.class,R.layout.chat_item
,ChatRecylerViewHolder.class,queryChat ) {
#Override
protected void populateViewHolder(ChatRecylerViewHolder viewHolder, ChatItem model, int position) {
viewHolder.tvAuthorChat.setText(model.chatAuthor);
viewHolder.tvContentChat.setText(model.chatContent);
}
};
You do notice you are using linearLayoutManager.setStackFromEnd(true); this mean you first position is at the bottom. I suggest you a better option.
RecycleView didn't work the way listView work, you can scroll it with your layout manager something like this
linearLayoutManager.scrollToPositionWithOffset(position,offset);
Which position is the position you want to scroll to, offset is the offset within the current position. You could just use with one parameter as well.
linearLayoutManager.scrollToPosition(position);
Ok. I found the answer.
First, the old problem with my question: i thought list_chat.post is called whenever an item is inserted (turn out that is wrong). The reason for it keeps scrolling top is linearLayoutManager.setStackFromEnd(true);
Thus, the question comes down to Where to call the scrolling ?
The answer is : Since adapter manages data, it makes sense to guess that adapter will notify the insertion.
Here is the code
adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
#Override
public void onItemRangeInserted(int positionStart, int itemCount) {
super.onItemRangeInserted(positionStart, itemCount);
list_chat.smoothScrollToPosition(adapter.getItemCount());
}
});
Hi I have vertical list (Custom ArrayAdapter) where each element has a horizontal RecyclerView, to give me a table like format.
What I want to do is, if one row is scrolled horizontally, rest rows too should be. I tried putting them just inside horizontal scroll view and various tricks available, but got no luck.
If possible, I would also like this synchronization keeping first element fixed.
Make an instance of RecyclerView.OnScrollListener. This instance will be common to all your horizontal recycler views. When onScrolled( ) gets called, scroll all the views except the one on which this got called.
Adding more to Little Child's answer, it was trickier than expected, I had to create a custom listener that was in my main Activity that was called by onScroll. This is because onScroll was in a getView method and did not get access to other views in listview, but my mainactivty could access them.
In my listadapter, I added
public interface RecycleListener {
public void onScroll(int scroll_x);
}
Then in getView
mRecyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
listener.onScroll(recyclerView.computeHorizontalScrollOffset());
}
});
and finally in MainActivity
dayAdapter.setRecycleListener(new DayAdapter.RecycleListener() {
int busy = 0;
#Override
public void onScroll(int scroll_x)
{
if(busy == 1) return;
busy = 1;
for(int i = 0;i<days.size();i++)
{
View view = lv.getChildAt(i);
if(view == null) continue;
RecyclerView r = (RecyclerView) view.findViewById(R.id.timetable_row_recycler);
LinearLayoutManager layoutManager = ( LinearLayoutManager) r.getLayoutManager();
layoutManager.scrollToPositionWithOffset(0,0-scroll_x);
}
busy = 0;
}
});
Note I found out that you can easily get scroll position by using int scroll_x = recyclerView.computeHorizontalScrollOffset() however there is no direct method to appy it back. The method layoutManager.scrollToPosition(i) scrolls to ith element and not the scroll position, so the only correct possible way is
layoutManager.scrollToPositionWithOffset(0,0-scroll_x);
or (in case of custom layoutmanager)
((LinearLayoutManager) layoutManager).scrollToPositionWithOffset(0,0-scroll_x);
We basically apply negative offset to the 0th position, so instead of scrolling to offset before 0th, it scrolls to our desired scroll_x.
UPDATE
this worked but caused a lot of lag, so later I ended up switching to android.widget.TableLayout, and inserted rows (android.widget.TableRow) like I was trying with recyclerview.