I have a container ViewGroup, lets call it screen inside a ScrollView. This container view hosts a number of other Views let's call them widgets, and some of them are interested in preventing the ScrollView from scrolling and using the MotionEvent theirselves (for example a pannable image)
I can't figure out the proper event intercept strategy to use. ScrollView always processes the event before the children, or the children process the event but scrollview is disabled.
I read about issuing getParent().requestDisableInterceptTouchEvent() in the child views if this view wants to capture the event, but their onTouchEvent is not called, I suppose because ScrollView has engulfed the event beforehand. I guess the fact that I have 2 levels of layers (container + widgets) prevents this from working, I suppose the container ViewGroup has to play an important part here, but I can't figure out which one...
Can I know, at the ScrollView's onInterceptTouchEvent level, which widget on the container viewGroup has been touched to decide if I should intercept or not?
or...
How can the 'widget' layers in the ViewGroup get the event before ScrollView so I can call getParent().onRequestDisableInterceptTouch() ... or is it getParent().getParent().onRequestDisableInterceptTouch()?
Thanks in advance
I've read related questions but no luck ...
Handle touch events in ScrollView Android
Well after a night of coca cola & debugging I managed to get this to work. I share the solution just in case it is of interest to anyone, because it took me quite a lot of time to get it running.
I didn't manage to get it running with getParent().onRequestDisableInterceptTouch(), I was close, but couldn't find a way for the child widgets to get the MotionEvents they need for scrolling once I intercepted the touch on the parent, so even though the outer scroll was prevented correctly, the inner widgets didn't scroll.
So the solution is to interceptTouchEvents in the children ONLY, and if the children is scrollable (known property), and the touch is ACTION_DOWN, then disable the scrollview two levels above. If the touch is ACTION_UP, we enable the scrollview.
To enable/disable the scrollview I just intercept the touch event and with a flag filter the event or not.
I did three auxiliary classes, one for the ScrollView, one for the Container, One for the widgets:
This class wraps every widget and, if I call setNeedsScroll(true) , then touches will be intercepted, and when it is touched, it will (tell the container to) tell the scrollview to disable itself. When the touch is released, it will re-enable the scrollview.
class WidgetWrapperLayout extends FrameLayout {
private boolean mNeedsScroll=false;
public WidgetWrapperLayout(Context context) {
super(context);
}
/** Called anytime, ie, during construction, to indicate that this
* widget uses vertical scroll, so we need to disable its container scroll
*/
public void setNeedsScroll(boolean needsScroll) {
mNeedsScroll=needsScroll;
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (mNeedsScroll) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
((SlideLayout)getParent()).setEnableScroll(false);
break;
case MotionEvent.ACTION_UP:
((SlideLayout)getParent()).setEnableScroll(true);
break;
}
return false;
}
return super.onInterceptTouchEvent(ev);
}
}
This is the container, only child of the scrollview, and holds the different widgets. It just provides methods for the children so they can enable/disable the scroll:
public class ContainerLayout extends FrameLayout {
public ContainerLayout(Context context) {
super(context);
}
public void setEnableScroll(boolean status) {
if (Conf.LOG_ON) Log.d(TAG, "Request enable scroll: "+status);
((StoppableScrollView)getParent()).setScrollEnabled(status);
}
}
and finally a scrollview capable of deactivation. It disables the scroll 'old-skool', intercepting and blocking events.
public class StoppableScrollView extends ScrollView {
private String TAG="StoppableScrollView";
private boolean mDisableScrolling=false;
public StoppableScrollView(Context context) {
super(context);
}
/** Enables or disables ScrollView scroll */
public void setScrollEnabled (boolean status) {
if (Conf.LOG_ON) Log.d(TAG, "Scroll Enabled "+status);
mDisableScrolling=!status;
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (mDisableScrolling) return false;
return super.onInterceptTouchEvent(ev);
}
}
Implement View.OnTouchListener in your Activity and add that listener to the ScrollView. Then return true in onTouchEvent(...). Before returning call the onTouch of the children you want to handle that event.
Related
So I have the following view structure:
LinearLayout
HorizontalScrollView
Other Child Views
The parent LinearLayout is clickable has a custom selector (changes color when pressed). I want to be able to touch the HorizontalScrollView within the LinearLayout and still handle the touch in the LinearLayout as long as it is not a scroll motion. If I do a scroll motion then the HorizontalScrollView should intercept the gesture and cancel the touch for the LinearLayout. Basically, I want to be able to intercept the gesture from a child view as opposed from the parent which is the standard.
I have tried to handle the MotionEvent manually by creating extension classes that do the following:
LinearLayout
public override bool OnInterceptTouchEvent(MotionEvent ev)
{
// Handle the motion event even if a child returned true for OnTouchEvent
base.OnTouchEvent(ev);
return base.OnInterceptTouchEvent(ev);
}
HorizontalScrollView
public override bool OnTouchEvent(MotionEvent e)
{
if (e.Action == MotionEventActions.Down)
{
_intialXPos = e.GetX();
}
if (e.Action == MotionEventActions.Move)
{
float xDifference = Math.Abs(e.GetX() - _intialXPos);
if (xDifference > _touchSlop)
{
// Prevent the parent OnInterceptTouchEvent from being called, thus it will no longer be able to handle motion events for this gesture
Parent.RequestDisallowInterceptTouchEvent(true);
}
}
return base.OnTouchEvent(e);
}
This almost worked. When I touch the HorizontalScrollView, the LinearLayout shows the pressed state UI and activates when the click is completed. If I touch and scroll the HorizontalScrollView then scrolling works. When I let go of the scroll, the click handler for the LinearLayout does not fire because it was intercepted. But the problem is that before I start scrolling the LinearLayout changes to the pressed state and it does not reset even after the gesture is completed. In my additional attempt to try to manually cancel the gesture for the LinearLayout I kept running into other issues. Additionally, the LinearyLayout has other buttons inside it which when clicked should not allow the parent LinearLayout to display the pressed state. Any suggestions? Is there a set pattern for intercepting touch events from a child? I'm sure it is possible if both classes know about each other, but I am trying to avoid coupling them.
The following work for me for all cases:
InterceptableLinearLayout
public override bool DispatchTouchEvent(MotionEvent e)
{
bool dispatched = base.DispatchTouchEvent(e);
// Handle the motion event even if a child returns true in OnTouchEvent
// The MotionEvent may have been canceled by the child view
base.OnTouchEvent(e);
return dispatched;
}
public override bool OnTouchEvent(MotionEvent e)
{
// We are calling OnTouchEvent manually, if OnTouchEvent propagates back to this layout do nothing as it was already handled.
return true;
}
InterceptCapableChildView
public override bool OnTouchEvent(MotionEvent e)
{
bool handledTouch = base.OnTouchEvent(e);
if ([Meets Condition to Intercept Gesture])
{
// If we are inside an interceptable viewgroup, intercept the motionevent by sending the cancel action to the parent
e.Action = MotionEventActions.Cancel;
}
return handledTouch;
}
I need to disable touch gesture on the scrim (the red highlighted part). I want to dismiss the drawer only with the swipe.
The issue is that when the drawer layout is open and I need to select an element from the ListView below the red highlighted part, what's happend is that the drawer get closed and only at this point I can select an element from the ListView.
I need to select the element from the ListView directly, also when the Drawer is opened
You have to create custom drawer for that like this
public class CustomDrawer extends DrawerLayout {
public CustomDrawer(Context context) {
super(context);
}
public CustomDrawer(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public CustomDrawer(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if(isDrawerOpen(Gravity.START)){
if(event.getX() > getChildAt(1).getWidth()){
return false;
}
}
return super.onInterceptTouchEvent(event);
}
}
Note : getChildAt(1) should be that child to whom you have given gravity as "start" and whose width determines the width of opening drawer.
I hope this should solve your problem
I asked an answered a question here:
How to vary between child and parent view group touch events
The parent (drawer) ontouchevent is being fired, rather than the child, listview.
I have also answered a similar problem here:
https://stackoverflow.com/a/28180281/3956566
You need to manage your touch events so it is handled by the child. You need to use an onInterceptTouchEvent and return false.
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// returning false means the child will handle the touch event.
return false;
}
You then manage you touchevent for the list view:
#Override
public boolean onTouchEvent(MotionEvent ev) {
// This method will only be called if the touch event was intercepted in
// onInterceptTouchEvent
// TODO Select your listview item.
}
You can also determine what type of touch event is taking place eg, scrolling and determine whether the child or parent will manage the event.
Managing Touch Events in a ViewGroup
I've added this quote from here Understanding Android Input Touch Events System Framework (dispatchTouchEvent, onInterceptTouchEvent, onTouchEvent, OnTouchListener.onTouch):
The root view starts dispatching the event down to its children. Let’s
presume that we have this hierarchy:
A – ViewGroup1 (parent of B).
B – ViewGroup2 (parent of C).
C – View (child of B)
– receives a touch/tap/click. Now the root view will call
A.dispatchTouchEvent(). Now the job of a ViewGroup.dispatchEvent()
(not View.dispatchEvent()) is to find out all the child views and view
groups whose bounds contain the touch point coordinates (using a hit
testing algorithm). When it figures out a list of relevant children,
it starts dispatching the events to them by calling their
dispatchTouchEvent().
Here’s an important piece though. Before the dispatchTouchEvent() is
called on the children, the A.dispatchTouchEvent() will first call
A.onInterceptTouchEvent() to see if the view group is interested in
intercepting the event and handling the subsequent gesture by itself
(scrolling is a good use case where a fling on B should lead to
scrolling on A). The method onInterceptTouchEvent() is only available
on view groups (as they’re the one who can be parents/containers with
the requirement to intercept touch events) that can sort of keep an
eye on the event and hijack it by returning true. If it returns false
then dispatching continues as usual, i.e., B.dispatchTouchEvent()
(child) will be called. But on returning true, this is what’ll happen:
ACTION_CANCEL will be dispatched to all the children.
All the
subsequent gesture events (till ACTION_UP/ACTION_CANCEL) will be
consumed by the event listeners (OnTouchListener.onTouch()) if
defined, else the event handler A.onTouchEvent() at A’s level.
A.onInterceptTouchEvent() itself will be never called again.
With this diagram:
Let me know if you need more explanation.
just add android:clickable="true" to drawer menu.
I have a view that covers entire screen (let's say ParentView), and child inner view ChildView that covers only portion of it.
I want to make ChildView to respond to onSingleTapUp(), while the ParentView respond to onFling(). I am trying to do so by attaching one SimpleOnGestureListener on ChildView and one SimpleOnGestureListener on ParentView.
To accept onSingleTapUp() from ChildView, its listener's onDown() has to return true.
But once I do that, the listener tied to ParentView does not hear any motion events anymore since it is taken by the ChildView's listener. Even though ChildView's onFling() returns false, the events do not flow to the ParentView's listener.
How can I make the parent view's listener catch the fling gesture while child view's listener catch tap gesture?
I don't think any source code is needed to explain the situation, but here is a snippet that sets up my ChildView listener.
ChildView.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View view, MotionEvent motionEvent) {
return singleTapGestureDetector.onTouchEvent(motionEvent);
}
});
One workaround could be to have both ParentView and ChildView's listeners to handle onFling() while only ChildView's listener handle onSingleTapUp(), but in that case, fling won't be able to happen across the ChildView (like start outside the child and then end within the child), I believe.
I don't like my solution, but I found a way to do this. Hopefully somebody else will post better answer in the future, or at least my workaround is useful to somebody otherwise.
As I described in the question, the problem lies on how gesture listener works. For child view to catch onSingleTapUp() event, you return true on onDown(). But once you do that, the subsequent series of events won't go to the parent view even after your child view's onTouch() declares it is no longer interested in the event. If you forcefully call the parent's onTouch() within the child's onTouch() when its gesture detector returns false, yes the parent's onFling() will be invoked but the first MouseEvent argument will be NULL since it was consumed by the child view's onTouch().
I must be missing something since this seems very basic gesture detection scenario. Anyway, I couldn't find a way to do this in reasonable way.
So, my workaround is to make TouchListenerService as a singleton.
Both child view and parent view have this line:
view.setOnTouchListener(TouchListenerService.Instance());
and TouchListenerService starts like this:
public class TouchListenerService
extends GestureDetector.SimpleOnGestureListener
implements View.OnTouchListener {
// some code to implement singleton
public SingleTapUpHandler SingleTapUpHandler;
public FlingHandler FlingHandler;
private View _touchingView;
GestureDetector gestureDetector;
#Override
public boolean onTouch(View view, MotionEvent motionEvent) {
if (gestureDetector == null)
gestureDetector = new GestureDetector(_touchListenerService);
_touchingView = view;
boolean result = gestureDetector.onTouchEvent(motionEvent);
_touchingView = null;
return result;
}
// and some more code
Since it is the same event handler, parent view catches onFling() event successfully while child view can set SingleTapUpHandler to process click event.
I have a Linear Layout that has a Button and a TextView on it. I have written a OnTouchEvent for the activity. The code works fine if I touch on the screen, but if I touch the button the code does not work. What is the possible solution for this?
public boolean onTouchEvent(MotionEvent event) {
int eventaction=event.getAction();
switch(eventaction)
{
case MotionEvent.ACTION_MOVE:
reg.setText("hey");
break;
}
return true;
}
The problem is the order of operations for how Android handles touch events. Each touch event follows the pattern of (simplified example):
Activity.dispatchTouchEvent()
ViewGroup.dispatchTouchEvent()
View.dispatchTouchEvent()
View.onTouchEvent()
ViewGroup.onTouchEvent()
Activity.onTouchEvent()
But events only follow the chain until they are consumed (meaning somebody returns true from onTouchEvent() or a listener). In the case where you just touch somewhere on the screen, nobody is interested in the event, so it flows all the way down to your code. However, in the case of a button (or other clickable View) it consumes the touch event because it is interested in it, so the flow stops at Line 4.
If you want to monitor all touches that go into your Activity, you need to override dispatchTouchEvent() since that what always gets called first, onTouchEvent() for an Activity gets called last, and only if nobody else captured the event. Be careful to not consume events here, though, or the child views will never get them and your buttons won't be clickable.
public boolean dispatchTouchEvent(MotionEvent event) {
int eventaction=event.getAction();
switch(eventaction) {
case MotionEvent.ACTION_MOVE:
reg.setText("hey");
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}
Another option would be to put your touch handling code into a custom ViewGroup (like LinearLayout) and use its onInterceptTouchEvent() method to allow the parent view to steal away and handle touch events when necessary. Be careful though, as this interaction is one that cannot be undone until a new touch event begins (once you steal one event, you steal them all).
HTH
Let me add one more comment to this excellent post by #Devunwired.
If you've also set an onTouchListener on your View, then its onTouch() method will be called AFTER the dispatch methods, but BEFORE any onTouchEvent() method, i.e. in between no.3 and no.4 on #Devunwired's answer.
Try to set the descendantFocusability attribute of your layout to blocksDescendants
Activity::onTouchEvent will be called only when non of the views in the Activity WIndow consumes/handles the event. If you touch the Button, the Button will consume the events, so the Activity won't be able to handle it.
Check out following articles for more about Android Touch Event handling pipeline.
http://pierrchen.blogspot.jp/2014/03/pipeline-of-android-touch-event-handling.html
you can also try onUserInteraction():
#Override
public void onUserInteraction(){
//your code here
super.onUserInteraction();
}
works well for me!
RecyclerView list_view = findViewById(R.id.list_view);
list_view.addOnItemTouchListener(new RecyclerView.SimpleOnItemTouchListener(){
#Override
public boolean onInterceptTouchEvent(#NonNull RecyclerView rv, #NonNull MotionEvent e) {
View child = rv.findChildViewUnder(e.getX(), e.getY());
Log.i("Hello", "World");
return false;
}
});
use public boolean dispatchTouchEvent(MotionEvent event) instead on onTouchEvent()
edited for clarity
I feel like this question already has an answer, but I can't find one.
I have a ScrollView in my layout, and it contains a variety of clickable views.
Under a specific condition I would like to disable clicks and events for the ScrollView and ALL of its children.
The following have not been helpful:
ScrollView.setEnabled(false)
ScrollView.setClickable(false)
ScrollView.setOnTouchListener(null)
As well as:
(parent view of the ScrollView).requestDisallowInterceptTouchEvent()
I have created a custom ScrollView with the following code:
public class StoppableScrollView extends ScrollView
{
private static boolean stopped = false;
public StoppableScrollView(Context context)
{
super(context);
}
public StoppableScrollView(Context context, AttributeSet attrs)
{
super(context, attrs);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev)
{
if(stopped)
{
return true;
}
else
{
super.onInterceptTouchEvent(ev);
return false;
}
}
#Override
public boolean onTouchEvent(MotionEvent ev)
{
if(stopped)
{
return true;
}
else
{
super.onTouchEvent(ev);
return false;
}
}
public static void setStopped(boolean inBool)
{
stopped = inBool;
}
public static boolean getStopped()
{
return stopped;
}
}
Using only onTouchEvent() will stop the scrolling, but not the clicking of child views.
Using only onInterceptTouchEvent() makes it such that when clicks work scrolling does not, and vice versa.
Using both onTouchEvent() and onInterceptTouchEvent() successfully stops unwanted clicks on child views when stopped is 'true' but it also disables scrolling regardless of the state of stopped.
Is there an easier way to get this behaviour, or is there a way to modify the StoppableScrollView class so that it will handle these touch events properly?
What probably should help is the following (because I had similar problems):
In the ScrollView you should do a RelativeLayout as Main Child (ScrollView does accept only 1 main child anyway). This RelativeLayout should of course of fill_parent in both directions.
At the really end of the RelativeLayout (after all other children), you could put now a LinearLayout with transparent background (#00FFFFFF) which has also fill_parent in both directions. This LinearLayout should have Visibility = View.GONE (by default)
Also you have to attach an empty OnClickListener to it. Now, because of zOrder if you make this LinearLayout Visibility = View.Visible it will catch all the events and avoid clicking the children above!
As scrollview allows immeditate one child say in my case i have linear layout.and in this linear layout i have other conreolls.
now our first task is to get this linear layout so what we can write is
LinearLayout l = (LinearLayout) scrollview.getChildAt(0);
now after getting this linear layour we can easily access other controlls placed inside it via this code and disable it.
for(int i =0; i<l.getChildCount(); i++)
{
Log.i(TAG,"child "+ l.getChildAt(i));
l.getChildAt(i).setEnabled(false);
}