I have an Activity with 3 RecyclerViews in a ConstraintLayout. One is along the left side of the activity and uses a vertical LinearLayoutManager. One is on the top side of the activity and uses a horizontal LinearLayoutManger. The last one to the right of the first and under the second and using a custom LayoutManager that implements a grid that scrolls both horizontally and vertically. This creates a spreadsheet effect, and I need the RecyclerViews to scroll simultaneously in an direction. This works for the most part, except for the case where there is a quick flinging action. When a fling is detected there are two problems:
The RecyclerView using the custom LayoutManager seems to lag and for a time no views are visible in this recycler.
I suspect this is because the size of the Views means about 45 are visible each time, so on a fast fling all of these views are being constantly recycled.
The scrolling synchronization between RecyclerViews is lost, sometimes they are off by a little but if the list is large then they can be off by quite a bit. This is the more important issue as the views must align correctly to be useful to users. The speed of the fling that causes this is much lower than problem #1
Current Code
The scroll listener attached to all 3 RecyclerViews:
private RecyclerView.OnScrollListener syncScrollListener = new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (recyclerAssignment == recyclerView && newState == RecyclerView.SCROLL_STATE_DRAGGING) {
draggingView = 1;
} else if (recyclerGrades == recyclerView && newState == RecyclerView.SCROLL_STATE_DRAGGING) {
draggingView = 2;
} else if (recyclerStudent == recyclerView && newState == RecyclerView.SCROLL_STATE_DRAGGING) {
draggingView = 3;
}
}
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (draggingView == 1 && recyclerView == recyclerAssignment) {
recyclerGrades.scrollBy(dx, dy);
} else if (draggingView == 2 && recyclerView == recyclerGrades) {
recyclerAssignment.scrollBy(dx, dy);
recyclerStudent.scrollBy(dx, dy);
} else if (draggingView == 3 && recyclerView == recyclerStudent) {
recyclerGrades.scrollBy(dx, dy);
}
}
};
The custom LayoutManager is exactly the same as this: https://github.com/devunwired/recyclerview-playground/blob/master/app/src/main/java/com/example/android/recyclerplayground/layout/FixedGridLayoutManager.java
What I've Tried
I noticed there is a possibility to add an OnFlingListener but it seems a fling calls the Recycler's scrollBy function, and they have different parameters.
I tried to detect when a scroll state changed to idle in the grid RecyclerView and then scroll the other views, but this caused an infinite loop.
Further thoughts
I thought it might make sense to extend RecyclerView to override the method that handles a fling, but I'm unsure exactly what to do in that method to make this work correctly.
I understand that to solve problem #1 I probably have to store more views out of frame. This is fine but I'm a big confused on how that logic might work in the LayoutManager.
The layout manager returns a "delta" variable when it scrolls either horizontally or vertically, and the comments mentions this is "for handling flings" but there are no further comments and I don't think this is behaving quite as intended.
Any help solving these two problems is greatly appreciated.
Related
Is there a way to detect that user is trying to scroll recyclerview and actual scrolling is not happened? For example, the vertical recyclerview is at its top position, and user tries to scroll it up.
Yeah , you can detect the scrolling behaving using onScroll function of recycler view .
recyclerView
.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView,
int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
lastVisibleItem = linearLayoutManager
.findLastVisibleItemPosition();
if (!loading
&& totalItemCount <= (lastVisibleItem + visibleThreshold)) {
// Do something
}
}
});
}
As you can see above , (linearLayoutManager.findLastVisibleItemPosition();) gives you the last visible position , once the user try to scroll the recycler view , it tends to change the last visible position .
Even dx gives you the int: The amount of horizontal scroll and dy (int) gives you the amount of vertical scroll .
This callback will also be called if visible item range changes after a layout calculation. In that case, dx and dy will be 0.
I have a GridLayout RecyclerView with a PageSnapHelper attached that essentially acts like a Vertical Linear RecyclerView (I inherited the code, not sure why this was done). The goal is to highlight the item currently centered in the view. Scrolling is done from code driven by two menu items to simulate going forward and backward from a remote. Scrolling of the view works fine. The issue seems to be that when I listen for the end of scrolling event there is animation going on which messes up my code to draw the highlight around the item. Here's the code I use to scroll the view based on the menu item:
#Override
public boolean onOptionsItemSelected(MenuItem item) {
//Get which arrow was pressed
int id = item.getItemId();
//Get count of items
int itemCount = recyclerView.getAdapter().getItemCount();
//Condition to moving down the list
if (id == R.id.next) {
//Make sure we're within bounds of the of the view from the top
if(count>=itemCount-1) {
return super.onOptionsItemSelected(item);
}
//Increment to count to next ViewHolder
count++;
//Condition for moving up list
} else if (id == R.id.prev) {
//Make sure we're within bounds
if(count == 0){
return super.onOptionsItemSelected(item);
}
//Decremennt to previous ViewHolder
count--;
}
//Scroll the RecyclerView to the position we want
layoutManager.setIsNext(id == R.id.next);
recyclerView.smoothScrollToPosition(count);
//If the item is already in view, then no scroll event is necessary and we can access the ViewHolder
HighlightViewHolder(count);
//In the event that the ViewHolder is off-screen listen for the end of the scrolling event to ensure the ViewHolder
//is created and in correct position on the screen
RecyclerView.OnScrollListener scrollListener = new RecyclerView.OnScrollListener() {
//Add a listener to that looks out for when the scrolling is complete
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
Log.i("ScrollListener", "Scrolling has ended");
HighlightViewHolder(count);
recyclerView.removeOnScrollListener(this);
}
};
recyclerView.addOnScrollListener(scrollListener);
return super.onOptionsItemSelected(item);
When the app first begins and I the forward button, the first item is highlighted just fine:
On the next selection, which engages a scrolling event is where I have an issue:
What I would expect to happen is that when the scrolling event has ended, the item is in its centered position. Based on what I'm seeing I think there's some animation event that's still happening that I should be looking for the completion of. My highlighting code relies on the view in question to be in its expected location. I'm new to android and my searches for RecyclerView animations brought me to ItemAnimator. But reading the docs I'm not sure if this is what I'm looking for. Any ideas?
I couldn't find any events related to what I was looking for so I went ahead and used a Handler to wait an arbitrary amount of time before drawing the highlight. I updated the Listener as so:
//Add a listener to that looks out for when the scrolling is complete
#Override
public void onScrolled(final RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
Handler handler = new Handler();
Runnable task = new Runnable() {
#Override
public void run() {
HighlightViewHolder(count);
}
};
handler.postDelayed(task, 500);
recyclerView.removeOnScrollListener(this);
}
I've got a recyclerView within a pull to refresh layout.
I've added an onScrollListener to keep track of the total vertical movement by adding/subtracting the delta Y (dy) to a variable named totalScrolled.
And if the recyclerview is scrolled fully to the top totalScrolled should be 0 and a view should be visible.
Problem only is that somehow there is a bug and the scroll listener is not accurate in telling me how much the list has scrolled. After scrolling down and back up again through my many items list somehow the totalScrolled doesn't return to 0.
Anyone ran into this issue as well and knows how to solve this?
Instead of using the dy from the scrollListener's onScrolled callback to keep track of the total vertical scroll which is inaccurate I use a function from the recyclerView itself - RecyclerView.computeVerticalScrollOffset() - which accurately keeps track of how many pixels the recyclerView has scrolled.
My code looks something like this:
home_screen_recycler.addOnScrollListener(object: RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val scrollOffset = home_screen_recycler.computeVerticalScrollOffset()
presenter.onListScrolled(scrollOffset)
}
})
In my project I use this logic.
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (arrayList.size() == 0)
return;
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
int itemPosition = mLinearLayoutManager.findLastCompletelyVisibleItemPosition();
if (itemPosition == (arrayList.size() - 1)) {
// here you can fetch new data from server.
}
}
}
});
I have overlay view that is animated from top. In that scene i have recycler view with elements list.
When user scrolls list to the end, scrolling further should scroll the whole recyclerview up (uncovering view beneath) and on ACTION_UP should animate recycler out of the screen.
overriding onTouch events doesn't work :
RecyclerView.setOnTouch() - causes dragging to glitch
ParentView.setOnTouch() - works only when dragging other views (not RecyclerView it self).
It's like new Tinder scroll that uncovers reactions (2017-11-17)
Any ideas how to do it?
Try this :
recyclerView.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);
}
});
In the onScrolled method you can translate the whole recyclerView.
To check if you have reached the end then use this method in the onScrolled method
recyclerView.canScrollVertically(1);
This will return a boolean. Then you can check and move the view.
Hope this helps.
OK, solved it my self. when i talked about glitching onTouch i made a mistake. I set translation to my RecyclerView, but i used MotionEvent.getX() instead of MotionEvent.getRawX() which caused this wild glitch due to inconsistent touch coordinate returned by getX()
Also
recyclerView.canScrollVertically(1);
by Sarthak Gandhi helped.
override fun onTouch(p0: View?, p1: MotionEvent): Boolean {
if (p1.action == MotionEvent.ACTION_UP && dragging) {
dragging = false
mInitailPoint = null
currentDy.invoke(0F)
return true
}
if (p1.action == MotionEvent.ACTION_DOWN || p1.action == MotionEvent.ACTION_MOVE) {
if (!dragging && !recyclerView.canScrollVertically(1)) {
mInitailPoint = Point(p1.rawX.toInt(), p1.rawY.toInt())
mCurrentPoint = Point(p1.rawX.toInt(), p1.rawY.toInt())
dragging = true
return true
} else if (dragging && p1.action == MotionEvent.ACTION_MOVE) {
mCurrentPoint = Point(p1.rawX.toInt(), p1.rawY.toInt())
val dy = mCurrentPoint!!.y.toFloat() - mInitailPoint!!.y.toFloat()
if (dy < 0) {
currentDy.invoke(dy)
} else {
currentDy.invoke(0F)
dragging = false
return false
}
return true
}
}
return false
}
at this point currentDy.invoke(dy) actually sets translation to RecyclerView of course this isn't complete code to achieve tinder stuff and there is no fling animations, but this part is what the question about.
Moral of the story: if you set OnTouchListener for the view you want to translate - use raw coordinates to calculate translation
I want to implement a recyclerview within a vertical viewpager. My current layout looks like the following
VerticalViewpager
- Fragment1
- ImageView
- Fragment2
- RecyclerView
If I swipe from Fragment1 to Fragment2 everything works fine. I am able to scroll within the recyclerview up and down. The problem occurs if I try to swipe/scroll back to Fragment1.
If the user has scrolled to the top of the recyclerview, I would like to forward the next "Up"-Scroll event to the vertical viewpager. I have tried overriding the onInterceptTouchEvent method, but unfortunately still no success. Any ideas out there? Thanks for your help :)
You need do disable the scroll. Try using recyclerView.setNestedScrollingEnabled(false);
1) You need to use support library 23.2.0 (or) above
2) and recyclerView height will be wrap_content.
3) recyclerView.setNestedScrollingEnabled(false)
But by doing this the recycler pattern don't work. (i.e all the views will be loaded at once because wrap_content needs the height of complete recyclerView so it will draw all recycler views at once. No view will be recycled). Try not to use this pattern util unless it is really required.
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled (RecyclerView recyclerView,int dx, int dy) {
int topRowVerticalPosition = (recyclerView == null || recyclerView.getChildCount() == 0) ? 0 : recyclerView.getChildAt(0).getTop();
if (topRowVerticalPosition >= 0) {
//top position
}
}
#Override
public void onScrollStateChanged (RecyclerView recyclerView,int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
});
By using this code you can find whether the user is in top position. In the position you can make the view pager to move to your desired position.
If you have access VerticalViewPager from RecyclerView, you can extend RecyclerView and check canScrollVertically(-1) and forward touch event to viewpager
I would suggest migrating to ViewPager2 first, since it natively allows for vertical scrolling, and then using the solution provided by the official documentation to supported nested scrollable elements.
Essentially, you need to add the NestedScrollableHost to your project and then wrap your RecyclerView with it, similar to below:
<NestedScrollableHost
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/my_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" />
</NestedScrollableHost>
Note that this solution only works for the layout your provided where the RecyclerView would be an immediate child of one of your ViewPager's screen. This solution won't work for other RecyclerView that are nested in the main RecyclerView.
You need to forbid nestedscroll in parent scrollview
i have new implement for this , you can try the below link https://github.com/liuxiaocong/VerticalViewpage
I had the same problem. The answer from Mr.India was helpful, but as your commented, it only worked sometimes.
What I did was
recyclerView.setOnTouchListener(new View.OnTouchListener() {
public float speed;
public VelocityTracker mVelocityTracker;
#Override
public boolean onTouch(View v, MotionEvent event) {
int index = event.getActionIndex();
int action = event.getActionMasked();
int pointerId = event.getPointerId(index);
switch (action) {
case MotionEvent.ACTION_DOWN:
if(mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
else {
mVelocityTracker.clear();
}
mVelocityTracker.addMovement(event);
break;
case MotionEvent.ACTION_MOVE:
if(mVelocityTracker != null) {
mVelocityTracker.addMovement(event);
mVelocityTracker.computeCurrentVelocity(1000);
speed = VelocityTrackerCompat.getYVelocity(mVelocityTracker, pointerId);
}
break;
case MotionEvent.ACTION_UP:
int topRowVerticalPosition = (recyclerView == null || recyclerView.getChildCount() == 0) ? 0 : recyclerView.getChildAt(0).getTop();
if(speed > 500 && topRowVerticalPosition >= 0) {
ReaderFragmentParent parent = (ReaderFragmentParent) getActivity();
ReaderPager pager = parent.getPager();
if(pager != null) {
pager.setCurrentItem(pager.getCurrentItem()-1);
}
}
return false;
case MotionEvent.ACTION_CANCEL:
mVelocityTracker.recycle();
break;
}
return false;
}
});
On the ACTION_UP I verify if the speed > 500, it means it's going up on a considered speed and the topRowVerticalPosition means it is on top of the recyclerview. Then I get the ViewPager and setCurrentItem to previous item.
it's a bit of a dirty hack, but it was the only way I found to make look like a good scrolling between pages on the Vertical ViewPager
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled (RecyclerView recyclerView,int dx, int dy) {
}
#Override
public void onScrollStateChanged (RecyclerView recyclerView,int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (!mRecyclerView.canScrollVertically(-1) && newState == 0) {
Log.e("canScrollVertically", mRecyclerView.canScrollVertically(-1)+"");
verticalViewPager.setCurrentItem(0, true);
}
}
});