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
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'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 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 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);
}
}
});
I'm running into a bit of a problem. What I'm doing: I've got a ListView which has got some images in it. To make the scrolling smoother I've disabled the images to show up when scrolling. Now there seems to be a bug in Android which sometimes causes the scroll state to not change back from SCROLL_STATE_FLING back to SCROLL_STATE_IDLE, which causes my images to not show up again.
My first thought was to set an onTouchListener and check when I get ACTION_UP, but that doesn't help because the SCROLL_STATE_FLING state is obviously being set after that. So now I've thought I could start a timer when the SCROLL_STATE_FLING state is being set and check after some time if the state is still in fling mode and then invalidate my view. But I don't think that's a very good solution.
Does anyone have a better idea on how I could do that? I've seen this reply but I need a solution for API level < 9 (plus it also sometimes happen when it's not overscrolling)
Here's my code for that:
mList.setOnScrollListener(new OnScrollListener() {
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
mListAdapter.setIsScrolling(scrollState != SCROLL_STATE_IDLE);
Log.i(this, "scrollStateChanged" + scrollState);
if (scrollState == SCROLL_STATE_IDLE) {
mList.invalidateViews();
}
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
}
});
Thanks,
Maria
I had the same problem, so my solution was to just detect if the scrollview position has reached the last page and in that case always load the images regardless of the scroll state (since the problem seems to always occur when the user flings to the end of the listview). So modifying your code you would have:
mList.setOnScrollListener(new OnScrollListener() {
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
mListAdapter.setIsScrolling(scrollState != SCROLL_STATE_IDLE);
Log.i(this, "scrollStateChanged" + scrollState);
int first = view.getFirstVisiblePosition();
int count = view.getChildCount();
if (scrollState == SCROLL_STATE_IDLE || (first + count > mListAdapter.getCount()) ) {
mList.invalidateViews();
}
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
}
});
I ran into the same issue with a RecyclerView.
Jason Burke's answer is correct, but I'll provide an alternate way of checking if you are at the beginning/end of the RecyclerView/ListView.
I do it in onScrolled instead of onScrollStateChanged since I don't trust that I get a state changed event in the case when you scroll/fling to the edge of the RecyclerView.
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
updateUi()
}
}
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
/* If the user scrolls to the edges of the recyclerview, we can't trust that we get the SCROLL_STATE_IDLE state.
* Therefore we have to update the view here in onScrolled for these cases
*/
if (!recyclerView.canScrollVertically(1) || !recyclerView.canScrollVertically(-1)) {
updateUi()
}
}
}
In my case it was actually a horizontal RecyclerView, so I had to do this instead:
if (!recyclerView.canScrollHorizontally(1) || !recyclerView.canScrollHorizontally(-1)) {
updateUi()
}
I have had this same problem and posted a workaround on the bug list:
For anybody still running into this problem (as I was last week) a
workaround that works for me is the following: If android SDKInt == 7
set a onTouchListener on the (Abs)ListView
In that onTouchListener when the OnTouch event action is
MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL you force
a onScrollStateChanged with first a SCROLL_STATE_FLING and then a
SCROLL_STATE_IDLE
Code example: In the onCreate:
if(androidSDKInt <= 7){
listViewDateSelector.setOnTouchListener(new FingerTracker(onScrollListener)); }
Then add a private class with:
private class FingerTracker implements View.OnTouchListener {
private OnScrollListener myOnScrollListener;
public FingerTracker(OnScrollListener onScrollListener){
myOnScrollListener = onScrollListener; }
public boolean onTouch(View view, MotionEvent event) {
final int action = event.getAction();
boolean mFingerUp = action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL;
if (mFingerUp) {
myOnScrollListener.onScrollStateChanged((AbsListView) view, OnScrollListener.SCROLL_STATE_FLING);
myOnScrollListener.onScrollStateChanged((AbsListView) view, OnScrollListener.SCROLL_STATE_IDLE);
}
return false; } }