I have a bottom drawer - a view that is bottom aligned with the bottom of the screen and when dismissed animates itself down until it becomes invisible (in such state its top border is aligned with the bottom of the screen). I am trying to implement dismissing this view by vertical swipe down on it. To do it I attached a gesture detector in this method of the view:
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
return mGestureDetector.onTouchEvent(event);
}
I did it this way since this view contains a horizontal recycler view, so I do not want to intercept the child touches too early to make sure my view responds only to swipe down and does not steal horizontal touches from the recycler view (as described here: https://developer.android.com/training/gestures/viewgroup.html)
Now, my problem is that when I swipe on one area of this view, the fling recognition works, while when I swipe on another area ofthis view it does not work. In the first case I see in the debugger that onInterceptTouchEvent is called three times, the third call is recognized by the gesture recognizer as fling. In the second case I see that onInterceptTouchEvent is called only once.
It looks that some other view is stealing my touches, but I have no idea which one is that since my app is pretty complex and I am not the only author :). What is an easy way of debugging that in a general case? I would like to just know what view in the entire hierarchy of my activity consumed my event making onInterceptTouchEvent called only once - that would be a good start for the further investigation.
Thanks!
The way I do it is using emulator with android source code downloaded.
I put a breakpoint in View.dispatchTouchEvent (all places that returns true). This will show me which view is handling the event.
If this does not help, you can also place various breakpoint log messages.
I hope this will help you solve the problem :D
Related
OK, I have an app that needs access to basic swipe gestures. I've implemented a GestureDetector using code scraped from StackOverflow and then modified (to make it work at all). For it to work, it needs to steal Touch events.
Originally, I was detecting onScroll events and such and trying to pass them on to elements that needed to scroll, but this was really jumpy and never scrolled by the amount I wanted, so I started to simply pass the touch events on to the super-class after reading them. This caused really strange behavior and some elements never scrolled at all (no idea why), so I started passing events in a 3 way split ... some to the super-class, some to the gesture-detector, and some to the on-screen view that needed to scroll! Yeah, a mess, but everything is finally working smoothly. Except ...
If you touch the bottom of an EditText (it's full-screen except for the AppBar), then as the keyboard appears and scrolls the EditText up, the appbar scrolls off the screen (right under the statusbar icons ... looks weird because they overlay each other) and I can't make it scroll back. Also means I can't get to my Menu or the Cut/Copy icons. Hitting the menu key when it has scrolled off crashes the app.
Any idea how to stop the AppBar from scrolling off the screen?
#Override
public boolean dispatchTouchEvent(MotionEvent me) {
int top = dm.heightPixels - myEditText.getMeasuredHeight();
MotionEvent mod = MotionEvent.obtain(me.getDownTime(), me.getEventTime(),
me.getAction(), me.getX(), me.getY()-top, me.getMetaState());
if (me.getY() <= top)
return super.dispatchTouchEvent(me);
myEditText.onTouchEvent(mod);
return this.detector.onTouchEvent(me);
}
OK, this seems to fix it! In the activity manifest ...
android:windowSoftInputMode="adjustResize"
Bamn! No more problems!
Problem
While developing some kind of "Contextual actions" for ListView item on swipe gesture I have been a little bit confused about the way Android dispatches MotionEvents to ViewGroup's children.
According to documentation all the things start with ViewGroupd.dispatchTouchEvent(MotionEvent) method call. Then view group ask whether the particular motion event should be intercepted or we can dispatch it to our childrens. This works great with all kinds of scrolling so we can efficiently resolve conflicts between vertical and horizontal scrolling (for example ListView inside ViewPager).
However I can't successfully handle simple click event in list view this way.
Motion events dispatching trace looks like this
ListView.dispatchTouchEvent(DOWN)
ListView.onInterceptTouchEvent(DOWN) returns false;
QuickActionView.dispatchTouchEvent(DOWN)
QuickActionView.onTouchEvent(DOWN) return true because MotionEvent.ACTION_DOWN indicates about start of gesture and it possibly can be horizontal scroll and we should appear quick actions layout to the user in this case. If we return false from here on MotionEvent.ACTION_DOWN it prevents from other touch events to be dispatched to this child of ListView.
Due to step 4 QuickActionView.dispatchTouchEvent(DOWN) return true
Due to step 5 ListView.dispatchTouchEvent(DOWN) return true
As a result ListView remember touch target. So next message dispatched like this.
ListView.dispatchTouchEvent(UP)
ListView.onInterceptTouchEvent(UP) returns false;
QuickActionView.dispatchTouchEvent(UP)
QuickActionView.onTouchEvent(UP) return false because we now see that the gesture is over and it isn't scroll or fling or whatever.
Due to step 4 QuickActionView.dispatchTouchEvent(UP) return false
Due to step 5 ListView.dispatchTouchEvent(UP) return false
So there is no chance for ListView to handle item click because after step 5 with UP action the motion event doesn't directed to the ListView's onTouchEvent(MotionEvent) method.
I have also tried to "intercept" touch event with ACTION_UP inside ListView, but have noticed that according to Android documentation after interception only further motion events will be directed to the ViewGroup's onTouchEvent(MotionEvent) method, and the current one won't.
Question
How can I support both the ListView's native item click handling and ability to dispatch MotionEvents to ListItem view (it is horizontal scrollable and have image buttons)?
It is preferred to hear from someone experienced in such a problem solving or have deeper understanding about how it designed to work with specified conflict solving
At the moment I have two solutions to this problem
Divide QuickActionView (which is actually ListView item) into two sections:
____________________
| clickable | |
|________________|__|
^
|
scrollable
So that I can't scroll ListView item in case the initial ACTION_DOWN motion event starts from clickable area, but list view handles item clicking natively. And I can't click list item inside scrollable area, whereas I can scroll to appear quick actions if my gesture starts from scrollable area.
The second solution is rather hacky but avoids limitations of the first one
Inside ListView I can remember all the touch events via MotionEvent.obtain(MotionEvent) method. Then I wait for the ACTION_UP MotionEvent and ask the listItem view whether the gesture has been processed or not. If list item tells that the gesture has NOT been processed I then call private method ListView.cancelTouchTarget() and post all saved MotionEvents to the listView via super.dispatchTouchEvent(MotionEvent)
The problem is the the scrolling will intercept with touch event has set to the parent layout.
Can I keep the onTouch event with the scroll in ScrollView ?
This is a very tricky part. There is an overriden method from Activity which is: public boolean onTouchEvent(MotionEvent event)
This is the general method that interprets all the touch events from the whole screen. And you could say, "ok, I can implement this and I am good to go..". And here comes the difficult part on how android works.
As you know every View has its own onTouchEvent() method that you could implement in order to add some custom implementation. So which method will listen? The ScrollView or the Activity? It appears that these touch events go from the "inside" elements to the "outside" elements. I mean parent-child relations.
Another thing to take into account is that the onTouchEvent method returns a boolean. This boolean parameter determines whether the touch event should go one level up or it is handled by the current View. Meaning that if you have a CustomViewA that implements the onTouchEvent() and CustomViewB implementing its own touch event, and the A is a child in B then the touch event would go through A first and if it is not handled it would go to B.
So basically yes it could be done. It depends on what touch event you wanted to do.
So in our case, the ScrollView returns true when the touch events are a horizontal. The activity's touch event will be handled only if the ScrollView touch event is not handled by itself then you are fine. Otherwise you have to override and implement the on touch event of scroll view and in some cases you have to return false so as for the whole layout to implement it. Good luck with the last part. I started to implement a fling effect but came up with some difficulties so I have implemented a 2 finger move with scroll view in it and it works like a charm.
This is about a week of research and experimenting and it is an overview of what I came up with. if you find anything else please let me know. Hope it helped.
I use a view switcher to switch between views in a way that is similar to standard activities switching in a task. For example, the current view in the switcher might have a button that, when clicked, initiates the view switch: the current view, which is now obsolete, slides out, and is replaced by the new current view, which slides in.
The animated switch is fine only for one thing: I cannot find a correct way to tell the sliding out view to stop processing user events, like touch events. So what happens is, if done fast enough, and it doesn't have to be that fast, the user can click on the button that initiates the view switch more than once, which is bad. Once a click (or any user action) has initiated a view switch, I would like to invalidate and ignore all other user events on the sliding out view.
Is there a clean, standard way to do so? I've tried setEnabled(false) on the sliding out view, but it seems click listeners in child views are still handled afterward. The thing I want to avoid is looking for all event handlers and adding verification code that ensures that nothing is done if the view is actually sliding out.
I had a similar problem and want to share my solution. I override the Activity.dispatchTouchEvent method as noted here. In doing so I intercept all touch events before they are dispatched to the other views.
public boolean mDisplayBlocked = false;
#Override
public boolean dispatchTouchEvent(MotionEvent pEvent) {
if (!mDisplayBlocked) {
return super.dispatchTouchEvent(pEvent);
}
return mDisplayBlocked;
}
In your click listener, before you initiate a view switch, I believe you should be able to disable the listener.
I imagine other UI interactions will still be possible though. Perhaps you need a masterDisable method, which will disable all UI interactions on that View, which you can call before you make the view switch.
There is no method to tell a parent to disable all of it children's events. And also, calling setEnabled(false) on a parent does not disable it's children.
Probably the easiest way to do this is to call setEnabled(false) on the button.
May be, If you set the visibility to GONE for the child controls of the fading out view, you can check the same in the listener and handle events only for the visible views.
Well, it was painful, but I think I finally found the right way to achieve what I wanted, and you really have to find out how the Android API wants you to do it. So here is how I manage the child view that is sliding out:
childView.startAnimation(animation);
parentView.removeView(childView); // makes childView a "disappearing child" of parentView
That may seems strange, but when you do that, childView is still drawn in its parent, even though it is not "physically" there anymore (it doesn't receive user inputs anymore, which is the behavior I was after in the first place). Hence, childView becomes a "disappearing child" of parentView, a very special case implemented in ViewGroup. And when the animation ends (through AnimationListener), you must do:
parentView.clearDisappearingChildren();
otherwise parentView will still draw the pixels of not-animated-anymore childView.
Note that the first thing I tried was:
childView.startAnimation(animation);
And when the animation ended:
parentView.removeView(childView);
Even though that way of doing seemed natural to me, all hell started to break loose! I started getting NullPointerException deep in ViewGroup.dispatchDraw. All of this because AnimationListener.onAnimationEnd was called during the execution of ViewGroup.dispatchDraw, and changing the list of child views is a very bad idea at that point...
I'm pretty new to Android app development, and I've been playing around with swipe gestures using Android's SimpleOnGestureListener and a ViewFlipper. There are 3 children of the ViewFlipper, and each is a ScrollView. They're all dynamically populated when the Activity loads, and they don't change after that. The ScrollView is where the SimpleOnGestureListeners are attached.
Here's the layout I'm using:
+ViewFlipper
++ScrollView (x3, one for each page, each with the following:)
+++LinearLayout (vertical)
++++TextView
++++TableLayout (dynamically populated w/TableRows)
++++View
I extended the onFling method with the common tutorial code you can find anywhere online, and it works great--except when one of the ScrollViews doesn't contain enough content to scroll.
I've narrowed the problem down to touch detection by overriding and calling super on every one of the SimpleOnGestureListener's methods to add a print-to-log.
When I swipe on a page that scrolls, I get something full of "in onClick" "in onScroll" "in onFling" etc. On a page that's too short to scroll, I get "in onClick" "in onShowPress" "in onLongPress", and that's only if I'm touching the content within the too-short scrollview's children--if I touch elsewhere I get no events at all.
Ideas on what's wrong, or how to detect the swipe gesture no matter how big the ScrollView is?
EDIT: I've determined that when I run this on an Android 2.2 emulator, as opposed to the Android 2.1u1 DroidX emulator I've been using, it goes away. This is reproducible across multiple environments.
I have some more insight on this; it seems as though onInterceptTouchEvent is not called for every motion event when a scrollview is contained within a flipper (or a WorkspaceView).
In particular, the behavior I found while modifying another view class to fix this very same issue (it is not unique to flippers) was as follows--note that this is Android 2.1 only:
If the scrollview is long enough to scroll, the ACTION_DOWN motion event is caught by the ScrollView, and every subsequent ACTION_MOVE event goes through onInterceptTouchEvent of the flipper, where it is intercepted and handled appropriately. In android 2.2, this behavior happens regardless of the scroll length.
Back to 2.1: If the scrollview is not long enough to scroll, the ACTION_DOWN motion event is not caught by the scrollview, but instead comes back to the onTouchEvent of the flipper. All subsequent ACTION_MOVE events of the same gesture skip the onInterceptTouchEvent function and go straight to the onTouchEvent function!
The way I resolved this was to take the functionality I had in onTouchEvent for ACTION_MOVE events and refactor it into its own method. In this way, I can have onTouchEvent call onInterceptTouchEvent followed by that functionality if it detects that the event has previously gone unhandled.
case MotionEvent.ACTION_MOVE:
if (touchState == TOUCH_STATE_SCROLLING) {
handleScrollMove(ev);
} else {
// Log.d("workspace","caught a move touch event but not scrolling");
//NOTE: We will never hit this case in Android 2.2. This is to fix a 2.1 bug.
//We need to do the work of interceptTouchEvent here because we don't intercept the move
//on children who don't scroll.
Log.d("workspace","handling move from onTouch");
if(onInterceptTouchEvent(ev) && touchState == TOUCH_STATE_SCROLLING){
handleScrollMove(ev);
}
}
break;
This is from WorkspaceView.java (a modification of Android's Workspace.java, found at the andro-views project on google code, and now here: Horizontal "tab"ish scroll between views ). In the case that we receive a move event, and we are scrolling (which only happens if we have deliberately chosen to intercept it--ie, it's set in the intercept function, so we've been to the intercept function already) we perform the move behavior we desire. If we receive a move event here and we are not scrolling, then we send the event back through onIntercept, and then see if we're now set to scrolling. If so, we perform the action.
It's not elegant, but it works!
I needed to create a new class that extended ScrollView, and used this:
#Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
return gestureDetector.onTouchEvent(event);
}
#Override
public boolean dispatchTouchEvent(MotionEvent ev){
gestureDetector.onTouchEvent(ev);
super.dispatchTouchEvent(ev);
return true;
}
I have no idea why, but if I try to return anything but true in dispatchTouchEvent (the logical thing would have been to
return (gestureDetector.onTouchEvent(ev) || super.dispatchTouchEvent(ev));
if I understand properly), it doesn't work, and this does.
Try setting android:fillViewport="true" in your layout xml for each of the ScrollViews. That tells the ScrollView to be as large as the view it's contained in.
Had the same issue. You need to intercept the touch event on the children of the ScrollView when it's too short to have a scrollbar.