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.
Related
I have an ordinary RecyclerView, and on top of it a transparent View that implements GestureListener, which basically have the same size of the RecyclerView.
The GestureListener will listen to scroll and fling gestures, and pass this MotionEvent to the RecyclerView underneath it.
I have already made the RecyclerView able to scroll and fling. However, I can't find a way to pass a click event down to the RecyclerView's items as well.
I already know that this is because ACTION_DOWN is consumed in the GestureListener. In fact, GestureListener has a onSingleTap() method for you to override, and this method was called whenever I perform a click.
According to this post, I tried to set an OnTouchListener to my itemView and listen to ACTION_UP events. However, the onTouch() method is never called.
Below is how I do it:
1. Create a callback in the transparent GestureListener
#Override
public boolean onSingleTapUp(MotionEvent e) {
if (scrollDetector == null) return false;
scrollDetector.onSingleTap(e);
return true;
}
Configure the callback in the activity, and pass the MotionEvent to the RecyclerView
#Override
public void onSingleTap(MotionEvent e) {
mRecyclerView.onTouchEvent(e);
}
Set OnTouchListener to the itemView in the adapter:
itemView.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
v.performClick();
return true;
}
return false;
}
});
Using debugger, I can see that mRecyclerView.onTouchEvent(e) was called; but the onTouch() of itemView was not called.
So... How should I correctly pass the MotionEvent to the itemView?
You may ask - "Why do you place a GestureListener on top of the RecyclerView?"
This is because I need to change the height of the RecyclerView when the RecyclerView is scrolled. However, if I do this using RecyclerView's addOnScrollListener, the value of dy will fluctuate between negative and positive values, because dy is affected by its height as well. And the fluctuation will also be reflected to the UI.
Therefore I need a scroll detector that does not change its height when scrolled, and just pass the scroll and fling values to RecyclerView by programmatically calling scrollBy() and fling().
You should known that recyclerview's event.If recyclerview can move when you scroll views,it will call onInterceptTouchEvent() and return true to intercept event.So you can't get the ACTION_MOVE event.Maybe you should rewrite the recyclerview's onInterceptTouchEvent() and return false. Then you can get all the event in your itemView's methods.
Stupid me. I should use dispatchTouchEvent(MotionEvent e) instead of onTouchEvent(MotionEvent e).
However, this is not enough.
Simply calling dispatchTouchEvent(e) using the MotionEvent from GestureListener is not working, because that e is an ACTION_UP event.
To simulate a click, you need both ACTION_DOWN and ACTION_UP.
And itemView does not need to set OnTouchListener since you have already simulate
Code:
#Override
public void onSingleTap(MotionEvent e) {
long downTime = SystemClock.uptimeMillis();
long upTime = downTime + 100;
MotionEvent downEvent = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN,
e.getX(), e.getY(), 0);
mRecyclerView.dispatchTouchEvent(downEvent);
MotionEvent upEvent = MotionEvent.obtain(upTime, upTime, MotionEvent.ACTION_UP,
e.getX(), e.getY(), 0);
mRecyclerView.dispatchTouchEvent(upEvent);
downEvent.recycle();
upEvent.recycle();
}
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 decided to post this question and answer in response to this comment to this question:
How to handle click in the child Views, and touch in the parent ViewGroups?
I will paste the comment here:
Suppose I want to override the touch events only for handling some of
the children, what can I do inside this function to have it working ?
I mean, for some children it would work as usual, and for some, the
parent-view will decide if they will get the touch events or not.
So the question is this: How do I prevent the parent onTouchEvent() from overriding some child elements' onTouchEvent(), while having it override those of other children?
The onTouchEvents() for nested view groups can be managed by the boolean onInterceptTouchEvent.
The default value for the OnInterceptTouchEvent is false.
The parent's onTouchEvent is received before the child's. If the OnInterceptTouchEvent returns false, it sends the motion event down the chain to the child's OnTouchEvent handler. If it returns true the parent's will handle the touch event.
However there may be instances when we want some child elements to manage OnTouchEvents and some to be managed by the parent view (or possibly the parent of the parent).
This can be managed in more than one way.
One way a child element can be protected from the parent's OnInterceptTouchEvent is by implementing the requestDisallowInterceptTouchEvent.
public void requestDisallowInterceptTouchEvent (boolean
disallowIntercept)
This prevents any of the parent views from managing the OnTouchEvent for this element, if the element has event handlers enabled.
If the OnInterceptTouchEvent is false, the child element's OnTouchEvent will be evaluated. If you have a methods within the child elements handling the various touch events, any related event handlers that are disabled will return the OnTouchEvent to the parent.
This answer:
https://stackoverflow.com/a/13540006/3956566 gives a good visualisation of how the propagation of touch events passes through:
parent -> child|parent -> child|parent -> child views.
Another way is returning varying values from the OnInterceptTouchEvent for the parent.
This example taken from Managing Touch Events in a ViewGroup and demonstrates how to intercept the child's OnTouchEvent when the user is scrolling.
4a.
#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.
*/
final int action = MotionEventCompat.getActionMasked(ev);
// Always handle the case of the touch gesture being complete.
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
// Release the scroll.
mIsScrolling = false;
return false; // Do not intercept touch event, let the child handle it
}
switch (action) {
case MotionEvent.ACTION_MOVE: {
if (mIsScrolling) {
// We're currently scrolling, so yes, intercept the
// touch event!
return true;
}
// If the user has dragged her finger horizontally more than
// the touch slop, start the scroll
// left as an exercise for the reader
final int xDiff = calculateDistanceX(ev);
// Touch slop should be calculated using ViewConfiguration
// constants.
if (xDiff > mTouchSlop) {
// Start scrolling!
mIsScrolling = true;
return true;
}
break;
}
...
}
// In general, we don't want to intercept touch events. They should be
// handled by the child view.
return false;
}
Edit: To answer comments.
This is some code from the same link showing how to create the parameters of the rectangle around your element:
4b.
// The hit rectangle for the ImageButton
myButton.getHitRect(delegateArea);
// Extend the touch area of the ImageButton beyond its bounds
// on the right and bottom.
delegateArea.right += 100;
delegateArea.bottom += 100;
// Instantiate a TouchDelegate.
// "delegateArea" is the bounds in local coordinates of
// the containing view to be mapped to the delegate view.
// "myButton" is the child view that should receive motion
// events.
TouchDelegate touchDelegate = new TouchDelegate(delegateArea, myButton);
// Sets the TouchDelegate on the parent view, such that touches
// within the touch delegate bounds are routed to the child.
if (View.class.isInstance(myButton.getParent())) {
((View) myButton.getParent()).setTouchDelegate(touchDelegate);
}
Lets revamp the issue.
You happen to have a ViewGroup with a bunch of children. You want to intercept the touch event for everything withing this ViewGroup with a minor exception of some children.
I have been looking for an answer for the same question for quite a while. Did not manage to find anything reasonable and thus came up on my own with the following solution.
The following code snippet provides an overview of the ViewGroup's relevant code that intercepts all touches with the exception of the ones coming from views that happen to have a special tag set (You should set it elsewhere in your code).
private static int NO_INTERCEPTION;
private boolean isWithinBounds(View view, MotionEvent ev) {
int xPoint = Math.round(ev.getRawX());
int yPoint = Math.round(ev.getRawY());
int[] l = new int[2];
view.getLocationOnScreen(l);
int x = l[0];
int y = l[1];
int w = view.getWidth();
int h = view.getHeight();
return !(xPoint < x || xPoint > x + w || yPoint < y || yPoint > y + h);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
for (int i=0; i<floatingMenuItems.getChildCount(); i++){
View child = floatingMenuItems.getChildAt(i);
if (child == null || child.getTag(NO_INTERCEPTION) == null) {
continue;
}
if(isWithinBounds(child, ev)){
return false;
}
}
return true;
}
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 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.