Vertical Recyclerview only allow scrolling down but block scrolling up - android

How can I limit the scrolling ability of a vertical Recyclerview to only allow scrolling down?
I want to make somehow a "list with no return to the top".
EDIT: It's not a duplicate. I don't want to disable scrolling vertically. I just want to disable scrolling upwards.

I figured out a solution using an OnItemTouchListener.
The scrolling event consists of 3 MotionEvents : ACTION_DOWN , ACTION_MOVE and ACTION_UP.
So on ACTION_DOWN we get the vertical position of the cursor (Y) and the on ACTION_MOVE we compare the new position to the old one.
By returning true, the method onInterceptTouchEvent() makes sure we intercept the scrolling event.
float lastY;
recyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
#Override
public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent event) {
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN) {
lastY = event.getY();
}
if (action == MotionEvent.ACTION_MOVE && event.getY() > lastY) {
return true;
}
return false;
}
...

Related

How to revoke `requestDisallowInterceptTouchEvent(false)` without lifting finger (Action.UP or Cancel)?

I have WebView(s) inside the RecyclerView. In order to get smooth scrolling experience such that when user scrolls, the RecyclerView will be responsible for scrolling (WebView should not scroll) I called getParent().requestDisallowInterceptTouchEvent(false); inside webview#onTouchEvent(event) when there is only one touch point and is moving vertcially (scrolling up and down).
private void handleSingleFingerTouch(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
x1 = ev.getX();
y1 = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
x2 = ev.getX();
y2 = ev.getY();
// -ve param for canScrollHorizontally is to check scroll left. +ve otherwise
if (Math.abs(x1 - x2) >= Math.abs(y1 - y2)
&& canScrollHorizontally((int) (x1 - x2))) {
// scrolling horizontally. Retain the touch inside the webView.
getParent().requestDisallowInterceptTouchEvent(true);
} else {
// scrolling vertically. Share the touch event with the parent.
getParent().requestDisallowInterceptTouchEvent(false);
}
x1 = x2;
y1 = y2;
}
}
#Override
public boolean onTouchEvent(MotionEvent ev) {
boolean multiTouch = ev.getPointerCount() > 1;
if (multiTouch) {
getParent().requestDisallowInterceptTouchEvent(true);
} else {
handleSingleFingerTouch(ev);
}
return super.onTouchEvent(ev);
}
It works as expected with just one bug, I found that while RecyclerView(and webview) scrolling and I touch inside the WebView, then RecyclerView stops scrolling as expected, then if I don't lift up my finger but keep finger on the screen and try to zoom, the webview would not zoom and actually it wouldn't receive touch event at all. I have to lift my fingers and touch again to zoom. I know this is because getParent().requestDisallowInterceptTouchEvent(false); won't cancel unless UI receive CANCEL or UP event. I tried to implement an interface that call getParent().requestDisallowInterceptTouchEvent(true); when multi-touch happen. Though it did get called, but seems it doesn't work. Zoom still not happen and onTouchEvent inside the WebView still not get triggered. Any idea to solve this?
So basically the solution is to override the onInterceptTouchEvent of the recyclerView
override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
// When recyclerview is scrolling this will stop scrolling and allow touch event passed to child views.
if (e.action == MotionEvent.ACTION_DOWN && this.scrollState == RecyclerView.SCROLL_STATE_SETTLING) {
this.stopScroll()
}
return super.onInterceptTouchEvent(e)
}

What motion event would I use for this situation?

I'm using an on touch listener to display and hide some volume controls, on ACTION_DOWN the controls are displayed and on ACTION_UP they are hidden. I want to be able to touch the controls without lifting my finger, I tried using the ACTION_MOVE motion and was unable to get it to work as the event is never triggered. I thought about drag event but I am unsure if it would be appropriate for this.
public boolean onTouch(View v, MotionEvent e)
{
if(v == audioControls)
{
if(e.getAction() == MotionEvent.ACTION_DOWN)
showVolumeControls();
else if(e.getAction() == MotionEvent.ACTION_UP)
hideVolumeControls();
}
else if(e.getAction() == MotionEvent.ACTION_MOVE)
{
if(v == mute)
//Do stuff with this volume control
}
return true;
}
#Demand answer, read my comment - here is the code:
public boolean onTouch(View v, MotionEvent e)
{
if(v == mute && e.getAction() == MotionEvent.ACTION_MOVE)
{
Toast.makeText(getApplicationContext(), "Muted.", Toast.LENGTH_SHORT).show();
hideVolumeControls();
return true;
}
else
return false;
}
So, you need to uderstand how android touch events work. If you touch down on View1, set onTouchListener for that view and return true for that event - other view will never get motion events from same chain.
For you it's mean that if you touch down on "audioControls" then no other views can catch motion events until you release your finger.
You can return false in your onTouch method. In this case parentView for audioControls will also catch all motionEvents. But views, which is not parent for audioControls in the view hierarchy will not catch motionEvent.
You need to catch all motion events in the container for your views and dispatch them for your self. This is the only way to catch motionEvents from one chain in defferent views.
UPDATE:
I will try to explain a little bit more.
Imagine you have layout like this:
<LinearLayout id="#+id/container">
<View id="#+id/view1"/>
<View id="#+id/view2"/>
</LinearLayout>
And you want to touch down on view1 and move your finger to view2. Android touch event flow can't do this. Only one view can catch whole event chain.
So you need to add onTouchListener to your container and do something like this.
public boolean onTouch(MotionEvent event) {
float x = event.getX();
float y = event.getY();
for (int i = 0; i<container.getChildCount(); i++) {
View child = container.getChildAt(i);
if (x>child.getLeft() && x < child.getRight() && y < child.getBottom() && y > child.getTop()) {
/*do whatever you want with this view. Child will be view1 or view2, depends on coords;*/
break;
}
}
}
Please note, I wrote this code right there and could make some mistake. I've tried to show the idea.

not Working ScrollView and HorizontalListView

Can some one help my with ScrolView in Android Mobile App.My problem- on my vertical ScrollView i have horizontal "hlistview»,and when i moved "hlistview" up/down/right/left play only one. How they can work together? At time,work only one.
hListView.setOnTouchListener(new HorizontalListView.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
v.getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_UP:
v.getParent().requestDisallowInterceptTouchEvent(false);
break;
}
v.onTouchEvent(event);
return true;
}
});
On nested scrollviews you must intercept the touch events. However if you just requestDisallowInterceptTouchEvent on ACTION_DOWN you are making the it's parent disable touch events the moment your finger touches the screen. That's not the wanted behavior. You need to set a threshold and only disallow the events the moment your scroll distance has hit that threshold.
On the nested scrollview ACTION_DOWN set a field saving the initial X (for horizontal swipe) like this:
mLastX = event.getX();
Next on ACTION_MOVE compare it with your threshold (50 in this example)
if (Math.abs(event.getX() - mLastX) > 50)
requestDisallowInterceptTouchEvent(true);

Android: RecyclerView item when set to clickable blocks onTouch events

Looks like setting RecyclerView's item layout to clickable="true", consume some touch events completely, particulary MotionEvent.ACTION_DOWN (ACTION_MOVE and ACTION_UP afterwards are working):
item.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/demo_item_container"
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"> <-- this what breaks touch event ACTION_DOWN
....
</LinearLayout>
Having very basic RecyclerView setup in onCreate():
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.list);
... //Standard recyclerView init stuff
//Please note that this is NOT recyclerView.addOnItemTouchListener()
recyclerView.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View view, MotionEvent motionEvent) {
Log.d("", "TOUCH --- " + motionEvent.getActionMasked());
//Will never get here ACTION_DOWN when item set to android:clickable="true"
return false;
}
});
Is this intended behaviour or bug in RecyclerView cause it is still a preview?
PS. I want this to be clickable as per docs to react on pressed state and have ripple effect on click. When set to false ACTION_DOWN is working fine but pressed state is not triggered and selectableBackground does not have any effect.
This is intended behaviour NOT a bug.
When set item clickable true, ACTION_DOWN will be consumed, recycler view
will NEVER get ACTION_DOWN.
Why are you need ACTION_DOWN in onTouch() of recycler view? Does it necessary?
if you want to set lastY in ACTION_DOWN, why not this
case MotionEvent.ACTION_MOVE:
if (linearLayoutManager.findFirstCompletelyVisibleItemPosition() == 0) {
// initial
if (lastY == -1)
lastY = y;
float dy = y - lastY;
// use dy to do your work
lastY = y;
break;
case:MotionEvent.ACTION_UP:
// reset
lastY = -1;
break;
Does it you want to? if you still want the ACTION_DOWN, try to get it in activity, such as:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN)
lastY = ev.getRawY();
return super.dispatchTouchEvent(ev);

Android View stops receiving touch events when parent scrolls

I have a custom Android view which overrides onTouchEvent(MotionEvent) to handle horizontal scrolling of content within the view. However, when the ScrollView in which this is contained scrolls vertically, the custom view stops receiving touch events. Ideally what I want is for the custom view to continue receiving events so it can handle its own horizontal scrolling, while the containing view hierarchy deals with vertical scrolling.
Is there any way to continue receiving those motion events on scroll? If not, is there any other way to get the touch events I need?
Use requestDisallowInterceptTouchEvent(true) in the childview to prevent from vertical scrolling if you want to continue doing horizontal scrolling and latter reset it when done.
private float downXpos = 0;
private float downYpos = 0;
private boolean touchcaptured = false;
#Override
public boolean onTouchEvent(MotionEvent event) {
switch(event.getAction()) {
case MotionEvent.ACTION_DOWN:
downXpos = event.getX();
downYpos = event.getY();
touchcaptured = false;
break;
case MotionEvent.ACTION_UP:
requestDisallowInterceptTouchEvent(false);
break;
case MotionEvent.ACTION_MOVE:
float xdisplacement = Math.abs(event.getX() - downXpos);
float ydisplacement = Math.abs(event.getY() - downYpos);
if( !touchcaptured && xdisplacement > ydisplacement && xdisplacement > 10) {
requestDisallowInterceptTouchEvent(true);
touchcaptured = true;
}
break;
}
super.onTouchEvent(event);
return true;
}
I'm answering my own question in case anyone else is as bad at Googling for the answer as I apparently was. :P
A workaround for this problem is to extend ScrollView and override the onInterceptTouchEvent method so that it only intercepts touch events where the Y movement is significant (greater than the X movement, according to one suggestion).

Categories

Resources