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;
}
Related
In my layout I have a structure like that:
--RelativeLayout
|
--FrameLayout
|
--Button, EditText...
I want to handle touch events in the RelativeLayout and in the FrameLayout, so I set the onTouchListener in these two view groups. But only the touch in the RelativeLayout is captured.
To try solve this, I wrote my own CustomRelativeLayout, and override the onInterceptTouchEvent, now the click in the child ViewGroup (FrameLayout) is captured, but the click in the buttons and other views doesn't make any effect.
In my own custom layout, I have this:
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;
}
You need to override the onInterceptTouchEvent() for each child, otherwise it will remain an onTouchEvent for the parent.
Intercept Touch Events in a ViewGroup
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
/*
* This method JUST determines whether we want to intercept the motion.
* If we return true, onTouchEvent will be called and we do the actual
* scrolling there.
*/
...
// In general, we don't want to intercept touch events. They should be
// handled by the child view.
return false;
}
You need to return false to have the child handle it, otherwise you are returning it to the parent.
Your custom solution will capture touch events from anywhere in your relative layout since the overridden method is set to always throw true.
For your requirement I guess its better to use
the onClick method rather than using onTouch.
OnTouch method invokes different threads on every TouchEvent and I guess that is the cause of your problem
Rather than handling these events its better to try onClick method.
I was able to solve this problem with the following code:
Step 1: declare the EditText above the onCreate () method
public EditText etMyEdit;
Step 2: in the onResume () method the configuration ends:
etMyEdit = (EditText) findViewById (R.id.editText);
etMyEdit.setOnTouchListener(new View.OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
v.getParent().requestDisallowInterceptTouchEvent(true);
switch (event.getAction() & MotionEvent.ACTION_MASK){
case MotionEvent.ACTION_UP:
v.getParent().requestDisallowInterceptTouchEvent(false);
return false;
}
return false;
}
});
Hope it helps someone!
I have a list view whose item is a linearlayout which has a button as it's child view. I want the ontouchLIstener of the linearlayout to work. I don't want to use onInterceptTouchEvent. Is there a way a I can pass on the touch form the button to the parent listview. I tried this
- returning true from the button's onTouchListener
private View.OnTouchListener buttonListener = new View.OnTouchListener() {
#Override
public boolean onTouch(View view, MotionEvent motionEvent) {
Log.i(TAG, "Button On Touch");
return true;
}
};
But this does not work. It does not pass on the touch event to the linearlayout's onTouchListener.
There must be someway it should work.
The chain of events for a View group works like this - The dispatchEvent is called. Then dispatchEvent calls onInterceptTouchEvent. if it returns false, then the touch events are passed on to the children of the ViewGroup. If any of the children consume the event (in this case the button consumes the event) i.e if they return true then the motionevent is not passed on to other methods. Since the button is clickable it returns true in this case. If the onInterceptTouchEvent method returns true then the child views are not given the motion event and instead that ViewGroup's onTouchListener or onTouch method are called. Hence to pass on the touch event to the parent's (View Group) onTouchListener make sure to return true in the onInterceptTouchEvent method of the Parent (ViewGroup). You don't have to override onDispatchTouchEvent()
#Override
public boolean onInterceptTouchEvent(MotionEvent motionEvent) {
Log.i(TAG,"PARENT.onInterceptTouchEvent");
return true;
}
For more details about how the touch navigation is done, please see this stack post
Set your button clickable property to false, using:
button.setClickable(false);
Then in onTouch of button:
return false;
Note: This behavior is specific to button (and any other view that has clickable property set to true) that even if you return false from onTouch it will not propagate event to the parent and onClick method of the button will be called anyway.
EDIT: Another way is extending ListView class and overriding onInterceptTouch:
public class CustomListView extends ListView {
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// here you can listen for button touch events
}
}
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.
today I got a problem with touch event handling on android custom views.In this case i have created parent view call weekview and chiled call weekdayview.i want implement touch event like singleTap,LongPress in child view only and when i swipe on parent or child i wanna scroll parent view.when i implement touch event in both view it dose not work.
can anyone help me on this.It's really helpful to me.
Thank you
class ChildView extends View {
public void setGestureDetector (GestureDetector g)
{
gesture = g;
}
#Override
public boolean onTouchEvent (....)
{
return gesture.onTouchEvent (....); // touch event will dispatch to gesture
}
}
class ParentView extends View implements GestureDetector.OnGestureListener {
gesture = new GestureDetector (this);
child = new ChildView (...);
child.setGestureDetector (gesture);
#Override
public boolean onTouchEvent (..)
{
// handle your parent touch event here
}
public boolean onDown (...)
{
return true;
}
public boolean fling (...)
{
// here to handle child view fling
}
}
This is peso-code (not real android java) to show you the concept to use GestureDetector, you can deal with all events from your child view in your parent View. As I tested on my android phone, onTouchEvent in ChildView didn't recognize ACTION_UP very well, so even you swipe your Child View, sometimes fling will not work (it depends on ACTION_UP).
So if you want to write more accurate swipe on your Child View, better write your owen Gesture Detect class, and in your ChildView, you can do this -
float oldX;
float distanceX;
public boolean onTouchEvent (MotionEvent event...)
{
if (event.getAction == MotionEvent.ACTION_DOWN) {
// handle down
oldX = event.getX ();
}
if (event.getAction == MotionEvent.ACTION_MOVE {
// handle move
distanceX = event.getX() - oldX; // more accurate
gesture.onSwipe (distanceX); // your own gesture class
}
}
Set your swipe velocity (to detect user's intention to swipe) and override the onTouchEvent() in your child view.
Here, call super.onTouchEvent() and return, which calls your parent view. Handle the events in the parent view.
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()