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);
}
}
});
Related
I have a recyclerview with horizontal layout and only one view is visible at a time:
mRecyclerView = findViewById(R.id.rvmain);
mRecyclerView.setOnFlingListener(null);
final SnapHelper snapHelper = new LinearSnapHelper();
snapHelper.attachToRecyclerView(mRecyclerView);
mLayoutManager = new LinearLayoutManager(this,LinearLayoutManager.HORIZONTAL,false);
mRecyclerView.setLayoutManager(mLayoutManager);
mAdapter = new MainActivityRVAdapter(postsModels,MainActivity.this);
mRecyclerView.setAdapter(mAdapter);
using onScrolllistener, everytime I scroll I want to know the starting position and end position. I am using the below code:
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if(count == 0) {
View centerView = snapHelper.findSnapView(mLayoutManager);
if(centerView != null){
initial_position = mLayoutManager.getPosition(centerView);
//initial_position2 = ((LinearLayoutManager)mRecyclerView.getLayoutManager()).findFirstVisibleItemPosition();
Log.e("Initial Item Position:",""+initial_position);
//Log.e("Initial Item Position2:",""+initial_position2);
}
count ++;
}
// get newstate position
if(newState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
View centerView = snapHelper.findSnapView(mLayoutManager);
if(centerView != null){
int pos = mLayoutManager.getPosition(centerView);
count = 0; // in idle state clear the count again
Log.e("Snapped Item Position:",""+pos);
}
}
}
The result i get is:
E/Initial Item Position:: 0
E/Snapped Item Position:: 1
E/Initial Item Position:: 1
E/Snapped Item Position:: 1
E/Initial Item Position:: 1
E/Snapped Item Position:: 1
E/Initial Item Position:: 1
E/Snapped Item Position:: 1
And it returns positions multiple times. I wanted to check the difference between final and initial positions.
I wanted only the start and end so that i can compare and check i.e:
E/Initial Item Position:: 0 and
E/Snapped Item Position:: 1
I've met the same problem. And what i found:
RecyclerView.OnScrollListener calls onScrolled(...) multiple times while in SCROLL_STATE_DRAGGING or SCROLL_STATE_SETTLING.
I started listen to a second calback:
onScrollStateChanged(#NonNull RecyclerView recyclerView, int newState)
We are interesting in a final state SCROLL_STATE_IDLE.
So in this case we have to override onScrollStateChanged(...), check and ignore all states except SCROLL_STATE_IDLE and get final position while idle.
But as described in docs, onScrolled(...)
will also be called if visible item range changes after a layout
calculation. In that case, dx and dy will be 0.
On practice i found that if we call adapter.notifyDataSetChanged() and visible position is "0" (ie. on first start), onScrollStateChanged(...) will not be called and onScrolled(...) will be called once with dx == dy == 0.
A final variant could be as following:
private int recyclerVisiblePosition;
#Override
public void onScrollStateChanged(#NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState != RecyclerView.SCROLL_STATE_IDLE) {
return;
}
getNewPosition(recyclerView);
}
#Override
public void onScrolled(#NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (dx == 0 && dy == 0) {
getNewPosition(recyclerView);
}
}
private void getNewPosition(#NonNull final RecyclerView recyclerView) {
final LinearLayoutManager layoutManager = ((LinearLayoutManager)recyclerView.getLayoutManager());
if (layoutManager != null) {
recyclerVisiblePosition = layoutManager.findLastVisibleItemPosition();
}
}
Reason:
When you scroll on the RecyclerView, it will trigger a SCROLL_STATE_IDLE event at the end, this is due to your action that you fling, and then you let go the RecyclerView.
The SnapHelper will listen to RecyclerView's first SCROLL_STATE_IDLE event so that it can prepare to calculate the remaining distance that IT needs to scroll to the target View, when it does that, it will trigger a SCROLL_STATE_IDLE event the second time, since SnapHelper is "helping" you to scroll it.
===============================================================
Solution:
You can create a global variable and use it as a "flag", this will avoid it from being called multiple times. The final code will look something like this:
private boolean isFirstTimeCall = true;
recyclerViewExample.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(#NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
if (isFirstTimeCall) {
isFirstTimeCall = false;
// Do your stuff here...
}
}
if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
isFirstTimeCall = true;
}
}
});
I had the same issue and I recognized that it was the snap helper which causes the issue. I guess it is because snap helper triggers a small second movement after your scrolling is complete. So onScrollStateChanged is called twice.
Fortunately, now there is ViewPager2. So I didn't have to go nuts trying to listen snapping. As I had also one visible view at a time like you, I replaced Recyclerview with ViewPager2 and instead of Recyclerview.OnScrollListener, I used ViewPager2.OnPageChangeCallback. That solved the issue.
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 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.
I want to scroll multiple RecyclerView at a time how to achieve that Exp- I have 3 RecyclerView in horizontal and when i scroll 1st RecyclerView then second and third shoul also scroll how to do that ?
The answer is very simple, you have to get scroll feedback from one recycleview and pass it to other recycleviews. But very carefully.
you need to keep referance of which recyclerview starts giving scroll feedback, so that it won't enter into continious loop.
so create a global variable
private int draggingView = -1;
Add scrollListener to all your recyclerviews
mRecyclerView1.addOnScrollListener(scrollListener);
mRecyclerView2.addOnScrollListener(scrollListener);
mRecyclerView3.addOnScrollListener(scrollListener);
your scroll listener should be in this structure. It should decide which recyclerview is giving scroll input and which is receiving.
private RecyclerView.OnScrollListener scrollListener = new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (mRecyclerView1 == recyclerView && newState == RecyclerView.SCROLL_STATE_DRAGGING) {
draggingView = 1;
} else if (mRecyclerView2 == recyclerView && newState == RecyclerView.SCROLL_STATE_DRAGGING) {
draggingView = 2;
} else if (mRecyclerView3 == 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 == mRecyclerView1) {
mRecyclerView2.scrollBy(dx, dy);
} else if (draggingView == 2 && recyclerView == mRecyclerView2) {
mRecyclerView1.scrollBy(dx, dy);
} else if (draggingView == 3 && recyclerView == mRecyclerView3) {
mRecyclerView1.scrollBy(dx, dy);
}
}
};
Thats all, your recyclerview is ready to scroll. If you scroll one recycleview, it will scroll all other recyclerviews.
Moving is not a problem, stopping is. If user flinged a list, at the same time if he stopped other list, that list will only be stopped, So you need to cover that case too. I hope this explanation and code will solve your problem.
I am using Navigation Drawer in my app, that contains some Fragments just like in the below picture.
Every Fragment Contains another ViewPager that is an ImageSlider, and below that is a Listview and at the top I am using the SwipeRefreshLayout. My problem is the image slider works well on devices that has Android version 3.0 or higher but the swipe left or right doesn't works on devices 2.3 and lower, instead it invokes the Parent ViewPager's swipe that is it navigates the fragment. I am using support Version 4 library for this purpose to support devices lower than 3.0. All functions works quite well on 2.3 devices except that one. I have googled it but I haven't found any help anywhere. So to make it scroll what should I do for this, any idea/help will be highly appreciated.
You can use this ViewPager as your parent ViewPager. This allows the child ViewPager to scroll.
public class CustomViewPager extends ViewPager {
public CustomViewPager(Context context) {
super(context);
}
public CustomViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
try {
//Handle the issue only in lower versions of android
if (v != this && v instanceof ViewPager && CJRAppCommonUtility.getOSVersion() < android.os.Build.VERSION_CODES.HONEYCOMB) {
ViewPager viewPager = (ViewPager) v;
int currentPage = viewPager.getCurrentItem();
int size = viewPager.getAdapter().getCount();
//if ViewPager has reached its end and if user tries to swipe left allow the parent to scroll
if (currentPage == (size - 1) && dx < 0) {
return false;
}
//if ViewPager has reached its start and if user tries to swipe right allow the parent to scroll
else if (currentPage == 0 && dx > 0) {
return false;
}
//Allow the child to scroll hence blocking parent scroll
else {
return true;
}
}
return super.canScroll(v, checkV, dx, x, y);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
}
Android developers site has a nice explanation about handling touch events in a Viewgroup. You can refer it here: http://developer.android.com/training/gestures/viewgroup.html
Hope it helps!!
In older version of Android requestDisallowInterceptTouchEvent doesn't work that great. The solution here is to extend view pager and override the onInterceptTouchEvent and store a list of children that are scrollable. Then, when onInterceptTouchEvent is called you can iterate through the list of scrollable children, get their hit rect, and see if the touch event is inside the hit rect. If it is, you can just return false to not handle it and let the child take it.
Something like this:
#Override
public boolean onInterceptTouchEvent(MotionEvent ev)
{
for (View view : scrollableChildren)
{
// get the hit rectangle for the view
Rect rect = new Rect();
view.getHitRect(rect);
// check to see if the click was inside this child
if (rect.contains((int) ev.getX(), (int) ev.getY()))
{
return false;
}
}
return super.onInterceptTouchEvent(ev);
}