I'm trying to create a RecyclerView with pagination and I have a problem with showing progress bar when I try to scroll down being already at the very end of the list. There's a callback RecyclerView.OnScrollListener which has a method onScrolled for handling scroll events, but it's not working when no actual scrolling has happened.
There's onScrollStateChanged method that works when I try to scroll down from the bottom, but my logic requires me to know direction of the gesture (up/down), so this method is not helpfull in my situation.
I currently trying to do it somewhat like this (which is obviously not working):
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
fetchDataFromServer();
}
});
EDIT: I tried to detect if the end is reached in onScrolled and it kinda worked, but sometimes when I try to fetch the data from the server I don't receive anything, so after such a request I'm still at the end of the list and I want to scroll down to try to update the list again, but I can't since onScrolled is not getting called.
Any ideas on how to fix that? Is there another callback that I can use?
I guess to show progress at the end of the recyclerview, you just need to make sure that end has been reached.
This is how it goes -
private int visibleThreshold = 1; // trigger just one item before the end
private int lastVisibleItem, totalItemCount;
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
totalItemCount = mLayoutManager.getItemCount();
lastVisibleItem = mLayoutManager.findLastVisibleItemPosition();
if (totalItemCount <= (lastVisibleItem + visibleThreshold) {
// ... end has been reached...
//... do your stuff over here...
}
}
});
}
EDIT:
sometimes when I try to fetch the data from the server I don't receive
anything
Simple! If your network request fails, try to catch an exception. If it is SocketTimeOutException, Retry doing the same request again. If there is an another exception(could be from the back-end or front-end), Get it fixed.
Check if your recycler view is nested inside NestedScrollView or something...
Remove the nested scroll view and it will work.
You can achieve any kind of feed by just using recycler view and use the VIEW_TYPE concept of recycler view
If you developing for min sdk level 23 then you can use setOnScrollChangeListener
recyclerView.setOnScrollChangeListener(new View.OnScrollChangeListener() {
#Override
public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
}
});
Related
I have a RecyclerView (say, rootRecyclerView) that can have different kinds of rows depending on some API response. I implemented one of them is a horizontal ViewPager2 and another one is implemented with horizontal RecyclerView (say, childRecyclerView).
The rootRecyclerView swipes vertically whereas the viewPager2 and childRecyclerView swipes horizontally.
The Problem:
When I swipe on the screen, if the swipe is on the the viewPager2 or childRecyclerView, the swipe MUST go perfectly straight horizontally. Otherwise, they won't scroll horizontally; the swipe is taken by the rootRecyclerView and so the you would see vertical movement.
So, this happens because your thumb would move in a curved/circular direction creating movement in both the X axis and Y axis, and the so the rootRecyclerView intercepts the swipe creating this unpleasant user experience.
I did try to solve the issue, such as adding an OnItemTouchListener to the childRecyclerView like this:
private float Y_BUFFER = ViewConfiguration.get(getContext())
.getScaledPagingTouchSlop(); // 10;
private float preX = 0f;
private float preY = 0f;
childRecyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
#Override
public boolean onInterceptTouchEvent(#NonNull RecyclerView rv, #NonNull MotionEvent e) {
if(e.getAction()==MotionEvent.ACTION_DOWN){
childRecyclerView.getParent().requestDisallowInterceptTouchEvent(true);
}
if(e.getAction() == MotionEvent.ACTION_MOVE){
if (Math.abs(e.getX() - preX) > Math.abs(e.getY() - preY)) {
childRecyclerView.getParent().requestDisallowInterceptTouchEvent(true);
} else if (Math.abs(e.getY() - preY) > Y_BUFFER) {
childRecyclerView.getParent().requestDisallowInterceptTouchEvent(false);
}
}
preX = e.getX();
preY = e.getY();
return false;
}
// ... rest of the code
It solves the problem only for the childRecyclerView, but I could not solve it for the ViewPager2.
I have also tried to use GestureDetector as described in this answer link, and some other combinations of code, but I could not make it work.
Could anyone help me?
Okay, so after some research, I came to the conclusion of substituting my ViewPager2 with a recyclerView that will 'behave like' a viewPager :/ .
First I replaced my viewPager2 with a horizontal recyclerView. To make it behave like a viewpager, use SnapHelper.
RecyclerView childRecyclerView2 = findViewById(R.id.previously_viewPager);
// other init like setup layout manager, adapter etc
SnapHelper snapHelper = new PagerSnapHelper();
snapHelper.attachToRecyclerView(replacedRecyclerView); // <-- this makes out rv behave like a viewPager
After that, you have to add an OnItemTouchListener and override onInterceptTouchEvent just like the code segment in my question:
childRecyclerView2.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
#Override
public boolean onInterceptTouchEvent(#NonNull RecyclerView rv, #NonNull MotionEvent e) {
// same as the code segment in the question,
//so skipping this part.
//just copy it from my question
}
// ...
}
Optional:
In viewPager2, you can get the current focus with getCurrentItem(), but since we have replaced out viewpager2 with recyclerview, we don't have that method. So, we need to implement our own equivalent version. If you are a Kotlin guy, you can directly jump to the reference 2 and skip this part. Here is the java version if you need, I'll skip the explanation though.
Create SnapHelperExt.java
public class SnapHelperExt {
public SnapHelperExt(){}
public int getSnapPosition(RecyclerView recyclerView, SnapHelper snapHelper){
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
View snapView = snapHelper.findSnapView(layoutManager);
if (snapView != null) {
return layoutManager.getPosition(snapView);
}else{
return -1;
}
}
}
Next create an interface OnSnapPositionChangeListener as our listener :
public interface OnSnapPositionChangeListener {
void onSnapPositionChange(int position);
}
After that, create SnapOnScrollListener.java:
public class SnapOnScrollListener extends RecyclerView.OnScrollListener {
public enum Behavior {
NOTIFY_ON_SCROLL,
NOTIFY_ON_SCROLL_STATE_IDLE
}
private SnapHelperExt snapHelperExt;
private SnapHelper snapHelper;
private Behavior behavior;
private OnSnapPositionChangeListener onSnapPositionChangeListener;
private int snapPosition = RecyclerView.NO_POSITION;
public SnapOnScrollListener(SnapHelper snapHelper, Behavior behavior, OnSnapPositionChangeListener onSnapPositionChangeListener){
this.snapHelper = snapHelper;
this.behavior = behavior;
this.onSnapPositionChangeListener = onSnapPositionChangeListener;
this.snapHelperExt = new SnapHelperExt();
}
#Override
public void onScrolled(#NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (behavior == Behavior.NOTIFY_ON_SCROLL) {
maybeNotifySnapPositionChange(recyclerView);
}
}
#Override
public void onScrollStateChanged(#NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (behavior == Behavior.NOTIFY_ON_SCROLL_STATE_IDLE
&& newState == RecyclerView.SCROLL_STATE_IDLE) {
maybeNotifySnapPositionChange(recyclerView);
}
}
private void maybeNotifySnapPositionChange(RecyclerView recyclerView){
int prevPosition = this.snapHelperExt.getSnapPosition(recyclerView, snapHelper);
boolean snapPositionIsChanged = (this.snapPosition!=prevPosition);
if(snapPositionIsChanged){
onSnapPositionChangeListener.onSnapPositionChange(prevPosition);
this.snapPosition = prevPosition;
}
}
}
Finally, use it in this way:
SnapOnScrollListener snapOnScrollListener = new SnapOnScrollListener(
snapHelper,
SnapOnScrollListener.Behavior.NOTIFY_ON_SCROLL,
position -> {
Log.e(TAG, "currently focused page no = "+position);
// your code here, do whatever you want
}
);
childRecyclerView2.addOnScrollListener(snapOnScrollListener);
References:
create-viewpager-using-recyclerview
detecting-snap-changes-with-androids-recyclerview
I am trying to hide a RelativeLayout when I scroll up and show it when I scroll down. onScroll works fine and is invoked every time until View is set to GONE.
final RelativeLayout placeHeaderMain = findViewById(R.id.place_header_main);
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (dy > 0) {
// Scrolling up
placeHeaderMain.setVisibility(View.GONE);
} else {
// Scrolling down
placeHeaderMain.setVisibility(View.VISIBLE);
}
}
});
I want my listener to continue working after setting the View to Gone in order to make it Visible when scrolled down.
Thanks in advance.
Are there enough items to be scrolled?
That code above won't be triggered if dy == 0. It could be not enough items to make the scroll and it will return dy equal to 0, father more it won't to call onScroll(...)
What dy do you have when RelativeLayout has hidden?
Try to check that method below:
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
Try to set the view to INVISIBLE and not to GONE. when you set any view to View.GONE he is invisible and it doesn't take any space inside your layout , but when you set a view to View.INVISIBLE like before he will be invisible but unlike View.GONE your view still takes up space inside the layout.
I want to add animations to the itemView, however they only run if the item is currently moving. For example the item starts to fadeOut until the alpha is 0, but if the item stops moving, the animation pauses and the item only fades out halfway. After that, the item needs to start moving again for the animation to complete.
I know onDraw typically only gets called when something changes, but I guess the OS doesn't realize the style of the View is changed as well, so it doesn't call it.
I tried adding invalidate() at the end so I can force an update but it doesn't do anything.
How can I fix this?
#Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
if (actionState == ACTION_STATE_SWIPE) {
if (optionsState == OptionState.UPVOTE) {
drawUpvoteOption(c, viewHolder.itemView, dX);
} else if (optionsState == OptionState.DOWNVOTE) {
if (!isRightSwipeMaxed(dX)) drawDownvoteOption(c, viewHolder.itemView, dX);
else drawDownvoteOption(c, viewHolder.itemView, MAX_RIGHT_SWIPE_DX);
}
setTouchListener(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
if (dX == 0) {
//This only works if the item is moving
viewHolder.itemView.animate().alpha(1f);
optionsState = OptionState.NONE;
}
if (!isRightSwipeMaxed(dX)) {
viewHolder.itemView.setTranslationX(dX);
} else {
viewHolder.itemView.setTranslationX(MAX_RIGHT_SWIPE_DX);
//This only works if the item is moving
viewHolder.itemView.animate().alpha(0);
}
//Doesn't do anything
viewHolder.itemView.invalidate();
}
I had the same issue, invalidate() on the itemView did nothing.
You have to do the invalidate() on the parent recyclerView for that to work :
viewHolder.animatedRecyclerView.invalidate()
I need to get current position of scrollbar on screen. I supposed that getVerticalScrolbarPosition would do the trick, but for some reason it gives me only zeroes. Here is the code I use:
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
(...)
getRecyclerView().setOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
int srollBarPosition = getRecyclerView().getVerticalScrollbarPosition();
}
});
What I am doing wrong?
Try using getScrollY(), here is the documentation :)
EDIT: hm, if for you can work anyway getFirstVisiblePosition() gives you not the y scrolls but the index of the first visible item of the list(might be the same, depending on what you need to do)
EDIT 2: i found this question, try reading it and probably you can solve the problem
How to get the scrollposition in Recyclerview or the Layoutmanager?
I can measure the scrollposition by adding an OnScrollListener, but when the Orientation changes scrollDy is 0.
mRecyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() {
int scrollDy = 0;
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
scrollDy += dy;
}
});
For future use, If you are switching between Fragments within the same activity and all you want to do is save scroll-position for recyclerview and then restore recyclerview to the same scroll-position, you can do as follows:
In your onStop()/onDestroyView()/onPause() whatever callback is more appropriate, do this:
Parcelable recylerViewState = recyclerView.getLayoutManager().onSaveInstanceState();
And In your onStart()/onCreateView()/onResume() whatever callback is more appropriate, do this:
recyclerView.getLayoutManager().onRestoreInstanceState(recylerViewState);
This way you can successfully keep your recyclerView's state in a parcelable and restore it whenever you want.
You cannot get it because it does not really exist. LayoutManager only knows about the views on screen, it does not know the views before, what their size is etc.
The number you can count using the scroll listener is not reliable because if data changes, RecyclerView will do a fresh layout calculation and will not try to re-calculate a real offset (you'll receive an onScroll(0, 0) if views moved).
RecyclerView estimates this value for scrollbars, you can use the same methods from View class.
computeHorizontalScrollExtent
computeHorizontalScrollRange
computeHorizontalScrollOffset
These methods have their vertical counterparts as well.
I came to the question just wanting to get the item position index that is currently scrolled to. For others who want to do the same, you can use the following:
LinearLayoutManager myLayoutManager = myRecyclerView.getLayoutManager();
int scrollPosition = myLayoutManager.findFirstVisibleItemPosition();
You can also get these other positions:
findLastVisibleItemPosition()
findFirstCompletelyVisibleItemPosition()
findLastCompletelyVisibleItemPosition()
Thanks to this answer for help with this. It also shows how to save and restore the scroll position.
recyclerView.computeVerticalScrollOffset() does the trick.
Found a work around to getting last scrolled position within the RecyclerView with credits to Suragch solution...
Declare a globally accessible LinearLayoutManager so a to be able to access it within inner methods...
When ever the populateChatAdapter method is called, if its the first call the scroll to position will be the last sent message and if the user had scrolled to view previous messages and method is called again to update new messages then they will retain their last scrolled to position...
LinearLayoutManager linearLayoutManager;
int lastScrollPosition = 0;
RecyclerView messages_recycler;
Initiate them...
messages_recycler = findViewById(R.id.messages_recycler);
linearLayoutManager = new LinearLayoutManager(this);
linearLayoutManager.setStackFromEnd(true);
messages_recycler.setLayoutManager(linearLayoutManager);
On your populate adapter method
private void populateChatAdapter(ObservableList<ChatMessages> msgs) {
if (lastScrollPosition==0) lastScrollPosition = msgs.size()-1;
ChatMessagesRecycler adapter = new ChatMessagesRecycler(msgs);
messages_recycler.setAdapter(adapter);
messages_recycler.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
lastScrollPosition = linearLayoutManager.findLastCompletelyVisibleItemPosition();
}
});
messages_recycler.scrollToPosition(lastScrollPosition);
}
You have to read scrollDy in
public void onScrollStateChanged(RecyclerView recyclerView, int newState)
then you can get recyclerView's scroll position
To get ScroolPosition try this:
You will need this:
mLayoutManager = new LinearLayoutManager(getApplicationContext(), LinearLayoutManager.HORIZONTAL, false);
And then get the position of the mLayoutManager (like this):
recyclerView
.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(#NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
int pos = mLayoutManager.getPosition(v);
RecyclerView.ViewHolder viewHolder = recyclerView.findViewHolderForAdapterPosition(pos);
}
});
With this "pos" you get the position, startin with the 0.
int pos = mLayoutManager.getPosition(v);