Disable DrawerLayout's scrim touch gesture - android

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.

Related

"Nephew" views block pinch gesture

I have a hierachy of custom views build as following:
Custom View A has a few View B's as children
Custom View B is like a button and needs to react to click/tap
Custom View C has a pinch gesture on it and is a sibling of A, and is under A in the view hierarchy
When pinching only on the Custom View A, everything goes well, as A does not catch any motion event and the view under it, C, receives the events and makes the pinch gesture (zoom in/out) correctly.
When tapping the Custom View B, the view is clicked and it is all good.
The issue is, when trying to pinch and one finger starts on View B, it catches its events and those are not propagated under it and no pinching ever happens on the Custom View C.
I would like to have View B react to tap only and pass all other events (pinch) to the view underneath itself, in this case Custom View C.
How can I achieve that scenario?
Found the solution!
We try to find a TapConfirmed event (using a gestureDetector to find it), and we dispatch the event directly to the sibling to pass the event:
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if (conditionToIntercept) {
return true;
}
return false;
}
#Override
public boolean onTouchEvent(MotionEvent event) {
if(conditionToIntercept{
if (!gestureDetectorCompat.onTouchEvent(event)) {
siblingView.dispatchTouchEvent(event);
return true;
}
}
return false;
}

Android Navigation Drawer Doesn't Pass onTouchEvent to Activity

I have an Activity which uses the Android NavigationDrawer.
When using only fragments (as usual), everything works perfect.
But now I want to use this drawer on other activities of my app, and for some of them,
I don't want the main view to be a fragment.
Question
The problem is, the onTouchEvent() of the activity itself (and the onItemClickedListener() of a child ListView for that matter) isn't called, because the drawer consumes it.
Of course, I want it to be called:)
Needless to say, I would hope the answer will be simple (even a XML one), and hopefully not by extending the Drawer class (unless that's what it takes of course).
More Info
The Activity's main layout is very simple, basically a ListView and the DrawerLayout on top of it (below in XML).
The Drawer has one fragment as it's childView (for fragment navigation) and of course, the ListView for the Drawer Items.
I've seen many questions regarding (not exactly) similar issues, and the frequent answer was to use onInterceptTouch(), requestDisallowInterceptTouchEvent() on the DrawerLayout, and on the Parent view (Activity's main content) and even onTouchEvent() (with False returned) on the ListView of the Drawer.
Nothing seems to do the trick.
I read this link
and it does seem like using Intercept methods somewhere could be the answer. But how?
Please let me know if you need any code. But it's a very basic code/layout for this matter.
Thanks!
Apparently the answer is somewhat easy, although it does make you extend the DrawerLayout and do some thinking, and maybe will result in some strange results (using the LAST
example, I haven't seen any, yet).
Anyway, related questions which looking backwards can help understanding the issue (will explain about the first one later on):
1. DrawerLayout prevents call of MainActivity.onTouchEvent()
2. How can I requestDisallowTouchEvents on Android DrawerLayout
3. Set drag margin for Android Navigation Drawer
Answer
First, please note that I put lots of examples here. If you just want the best one (for me), jump to the last one.
Secondly, if someone has enough reputation, please comment on the first link's question and put a link to this answer (it can help that guy).
Example 1
Well, basically, just extend Android's DrawerLayout and replace onTouchEvent() to this:
#Override
public boolean onTouchEvent(MotionEvent arg0) {
super.onTouchEvent(arg0);
return false;
}
This solution will do anything except that it won't open the Drawer on slides, only menu clicks and the like. Besides, it forwards clicks so when the Drawer is open
for instance, touching outside of it will NOT close it, but click on whatever is behind (e.g. a ListView). Le'ts try harder...
Example 2
Now, let's catch the open OR visible cases, to return true (and consume the action at the Drawer).
#Override
public boolean onTouchEvent(MotionEvent arg0) {
super.onTouchEvent(arg0);
if(isDrawerOpen(findViewById(R.id.list_slidermenu)) ||
isDrawerVisible(findViewById(R.id.list_slidermenu))){
return true;
}
return false;
}
This solution is better, as it prevents clicks on behind the Drawer when the drawer is open or even visible (slide starts...). But touch-sliding it still doesn't work.
Example 3
Ok, so let's just split cases. Touches (MotionEvent.ACTION_DOWN) inside the Drawer's margin (area that Google desided to slide Drawer when touched at)
will result in returning True to consume the action, and others will forward the event (return False).
#Override
public boolean onTouchEvent(MotionEvent arg0) {
super.onTouchEvent(arg0);
float edge = 30;//that's for a left drawer obviously. Use <parentWidth - 30> for the right one.
View mDrawerListView = findViewById(R.id.drawer_listview);
if(isDrawerOpen(mDrawerListView) ||
isDrawerVisible(mDrawerListView)){
return true;
} else if(arg0.getAction() == MotionEvent.ACTION_DOWN && arg0.getX() > edge){
return false;
}
return true;
}
Note that I used 30dp. That's what I found to be the margin (although in one of the links it is said to be 20....).
Well, the next example would of course be deciding what is, exactly, that edge (see in code above) value is, according to Android. We don't want to
use a number that could change or whatever.
New Question
So now that first link should come handy. It "hacks" the Drawer code to get that Drawer edge/megin number. BUT, it didn't work for me, as those exact Field names could not be found.
I run mDrawerLayout.getClass().getField() which returns all the fields, but without any luck finding what we want. Anyone?
Last Example - Full Code
Ok, looking on example number 3, after understanding what exactly I did, we can make it faster by extending the onFinishInflate() method and save it as a global variable
for this CustomDrawerLayout for later use. We can also put that first 'if' inside the second one to save some more work. OK here goes:
View mDrawerListView;
...
#Override
protected void onFinishInflate() {
super.onFinishInflate();
mDrawerListView = findViewById(R.id.drawer_listview);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
if(event.getX() > 30 && event.getAction() == MotionEvent.ACTION_DOWN){
if(isDrawerOpen(mDrawerListView) || isDrawerVisible(mDrawerListView)){
return true;
} else{
return false;
}
}
return true;
}
That's it for now! Hope it'll helps someone in the future beside myself, hehe....
While working on the same problem I was inspired by guy_m's answer and boiled down his proposals to the following solution.
Again it amounts to extending DrawerLayout and overriding onInterceptTouchEvent(). The logic is simple:
Whenever the touch event occurs off the drawer view (the slideable part), we return false. Then our DrawerLayout is out of the game when it comes to handling the event -- the event is handled by whatever view we put into the DrawerLayout at the respective position.
On the other hand, when the event occurs inside the drawer view, we delegate to super.onInterceptTouchEvent() to decide what to do with the event. That way the drawer will slide in and out as before on touch gestures happening on itself.
The following code sample is for a DrawerLayout whose drawer view is located on the right (android:gravity="right"). It should be obvious how to modify it to cover also the case of a left-placed drawer.
public class CustomDrawerLayout extends DrawerLayout
{
#Override
public boolean onInterceptTouchEvent( MotionEvent event )
{
final View drawerView = getChildAt( 1 );
final ViewConfiguration config = ViewConfiguration.get( getContext() );
// Calculate the area on the right border of the screen on which
// the DrawerLayout should *always* intercept touch events.
// In case the drawer is closed, we still want the DrawerLayout
// to respond to touch/drag gestures there and reopen the drawer!
final int rightBoundary = getWidth() - 2 * config.getScaledTouchSlop();
// If the drawer is opened and the event happened
// on its surface, or if the event happened on the
// right border of the layout, then we let DrawerLayout
// decide if it wants to intercept (and properly handle)
// the event.
// Otherwise we disallow DrawerLayout to intercept (return false),
// thereby letting its child views handle the event.
return ( isDrawerOpen( drawerView ) && drawerView.getLeft() <= event.getX()
|| rightBoundary <= event.getX() )
&& super.onInterceptTouchEvent( event );
}
}
With these answers, i still had some trouble. I could get the motionEvent back to the activity but I lost the onClick listener answer by fragment or everything on the screen. So I found another way to have everything work ( get answer when override OntouchEvent from activity, and answer to onClick Listener )
Extend DrawerLayout and Override this methode :
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(super.onInterceptTouchEvent(ev)) return true;
else {
Activity activity = AppContext.getCurrentActivity();
return activity.onTouchEvent(ev);
}
}
if the drawer want the motion event, let it handle it. And if not, pass the event to activity yourself. (AppContext.getCurrentActivity is something from you with current activity, you can for instance attach activity as weakreference to the drawerLayout OnCreate)
The good thing with this way, you don't care about the edge and don't care if start or end. And you don't care also if it is open or close. Everything work fine.
I have a solution:
Set OnTouchListener on the screen layout (the first childview of DrawerLayout, normally) and transmit the TouchEvent to a custom GestureDetector.
So, you can do your own things in it. One more important thing: if you want to override onSingleTapUp() or something else, you should return true in onDown() to make sure that you can get the rest MotionEvent to make onSingleTapUp() work.
private class MyGestureListener implements GestureDetector.OnGestureListener{
#Override
public boolean onDown(MotionEvent e) {
return true;
}
#Override
public void onShowPress(MotionEvent e) {
}
#Override
public boolean onSingleTapUp(MotionEvent e) {
// do your own things
return true;
}
#Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
return false;
}
#Override
public void onLongPress(MotionEvent e) {
}
#Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return false;
}
}
and set it :
mGestureDetector=new GestureDetector(this, new MyGestureListener());
layout_content.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
return mGestureDetector.onTouchEvent(event);
}
});
To add on to guy_m 's answer, here is my implementation for a drawer that opens from the right, includes constructors so that it is viewable in the layout editor and also takes into account when a user swipes from past the edge point:
public class CustomDrawerLayout extends DrawerLayout {
View mDrawerListView;
float edge;
int holddown = 0;
static final String TAG = CustomDrawerLayout.class.getSimpleName();
public CustomDrawerLayout(#NonNull Context context) {
super(context);
setscreendimensionvals(context);
}
public CustomDrawerLayout(#NonNull Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
setscreendimensionvals(context);
}
public CustomDrawerLayout(#NonNull Context context, #Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setscreendimensionvals(context);
}
private void setscreendimensionvals(Context context){
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
/*((Activity) context).getWindowManager()
.getDefaultDisplay()
.getMetrics(displayMetrics); */
int width = displayMetrics.widthPixels;
float density = displayMetrics.density;
edge = width - (30 * density); // 30 is the edge of the screen where the navigation drawer comes out
Log.d(TAG,"edge: " + edge);
Log.d(TAG,"width: " + width);
}
#Override
protected void onFinishInflate() {
super.onFinishInflate();
mDrawerListView = findViewById(R.id.drawerconstraint_overworld);
}
#Override
public boolean onTouchEvent(MotionEvent event){
super.onTouchEvent(event); // need to add action up and a local variable to detect when lifted finger
//Log.d(TAG,"point: " + event.getX());
if(event.getX() >= edge && (event.getAction() == MotionEvent.ACTION_DOWN || event.getAction() == MotionEvent.ACTION_MOVE)){
holddown = 1;
//Log.d(TAG,"hold down");
}
if(event.getAction() == MotionEvent.ACTION_UP){
holddown = 0;
//Log.d(TAG,"hold up");
}
if(holddown == 1){
return true;
}else{
if(isDrawerOpen(mDrawerListView) || isDrawerVisible(mDrawerListView)){
return true;
} else{
return false;
}
}
}
}
For anyone who might have the unfortunate luck of encountering as persistent an issue as this one, I will add onto the others' answers with my own problem case and solution in the hopes that fewer souls will face this nightmare of a headscratcher.
Due notice is that my explanation will most likely work for any swipeable view whose parent is DrawerLayout (e.g. this solution only works for views that are children of DrawerLayout), but I will regale my experience and my toils for the purpose of clarity.
In my case, I needed to have a MaterialCalendarView (3rd-party CalendarView on steroids) in a DrawerLayout with a NavigationView to the right (i.e. with "android:gravity"="end"). It wasn't long after implementing the view hierarchy that I realised there had existed a conflict between the swipe events of my NavigationView and MaterialCalendarView.
In essence, what occured was that whenever I began swiping the MaterialCalendarView to the right in order to swipe back to the next month, I wound up triggering the DrawerLayout's touch event interceptor and closing said DrawerLayout instead of swiping to the previous month.
So, the solution should be easy, shouldn't it? Set a onTouchListeneron the MaterialCalendarView, call requestDisallowInterceptTouchEvent(), and call it a day—akin to this in the view-hosting Activity:
calendar.setOnTouchListener { _, motionEvent ->
when(motionEvent.action) {
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
drawerLayout.requestDisallowInterceptTouchEvent(true)
}
}
true
}
...and you should be all set?
Well, the fact that I'm answering here is enough to infer that wasn't the case and that my onTouchListener wasn't, like the rest, being triggered.
After extensively scouring this thread and trying to follow everyone's advice, I came at a point wherein none of the solutions offered were helpful for someone who'd simply wanted to "exclude" a view from being detected by the DrawerLayout's touch event interceptor. Some ideas entirely paralysed my touch event infrastructure, while others simply gave me more of the same behaviour. I had hit a roadblock and I didn't know what to do.
Then, an epiphany.
I realised that due to my inexperience with writing custom views I'd missed the glaringly obvious: what I needed to do was simply find out where the MaterialCalendarView was, get its coordinates, and see if any touch events are inside in order to call the proper implementation (be it the Activity or default DrawerLayout one)! And, of course, since in the former, the onTouchListener disables interception of touch events by the DrawerLayout, that meant only the MaterialCalendarView could handle the swipes! It was so simple!
And fast-forward to learning about MotionEvents, reading up on what the heck a Rect was, and a muddy in-between of crashes, I finally wrote the custom DrawerLayout which responded to my swipes on the MaterialCalendarView only with the Activity implementation and ignored the ones outside, opting for the DrawerLayout touch interceptor:
class EventCalendarDrawerLayout : DrawerLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)
constructor(context: Context, attrs: AttributeSet) : this(context, attrs, 0)
lateinit var calendar: MaterialCalendarView
lateinit var drawer: View
override fun onFinishInflate() {
super.onFinishInflate()
drawer = getChildAt(1)
calendar = findViewById(R.id.event_calendar)
}
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
val rect = Rect()
calendar.getGlobalVisibleRect(rect) // get the calendar rect positions
// respond to proper motions and forward events contained inside the calendar's rect only
if((event.action == MotionEvent.ACTION_MOVE ||
event.action == MotionEvent.ACTION_DOWN) && rect.contains(event.x.roundToInt(), event.y.roundToInt())) {
return (context as Activity).onTouchEvent(event)
}
// otherwise return the default intercept touch event response
return super.onInterceptTouchEvent(event)
}
}
It's not rocket science, truly, but it is something I deemed worth showing, as it was new and unexpected for me (and undoubtedly many others yet to venture here). Nevertheless, I believe that this implementation may work to ignore as many views embedded inside DrawerLayouts as one could possibly wish for.

ScrollView with children view, how to intercept scroll conditionally

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.

How to accept onSingleTapUp() on inner view while accept onFling() on outer view?

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.

OnTouchEvent not working on child views

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()

Categories

Resources