I have a ViewPager that can contain two types of views. One of the view types has buttons, one of the view types does not. The buttonless view type does have to trap touches in onTouchEvent in order to pan and zoom an image. But I want to let clicks bubble to the ViewPager (that has a clickListener attached to it)
I have to return true in the view's onTouchEvent ACTION_DOWN or else it won't see future events.
I can't use onInterceptTouchEvent() in the ViewPager to capture clicks because one of the views does have buttons and those buttons need their clicks
So how can my view trap swipes, and let clicks bubble up?
proper way is probably to intercept only the events you want to intercept, by returning true in onInterceptTouchEvent only when needed.
but if you want to go with the dark side, there's a dirty alternative, that probably will lure you and you'll probably regret later: intercept all the events, then if needed pass them down.
e.g. have inside your views something like
public boolean canInterceptTouch(MotionEvent ev) {
// return true if you are interested in this touch event, e.g. it falls into
// a clickable area
}
and something that handle the touch event like
public void interceptTouch(MotionEvent ev) {
// here you react to the event
}
inside your viewpager you have
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;
}
thien the view pager will handle the events in a dirty way like this:
#Override
public boolean onTouchEvent(MotionEvent ev) {
if (yourView.canInterceptTouch(ev))) {
yourView.interceptTouch(ev);
return true;
} else {
return doSomethingElse();
}
}
note: although I've done something similar, with decent result, I do NOT reccomend it as a solution unless you've very simple logic in the handling of the motion events, otherwise it becomes a mess. Your future self will be happier if you spend some time now to do it properly.
EDIT: code has not been tested, just to give you an idea of what you need. Sorry for any typo.
Try using this ViewGroup.onInterceptTouchEvent(MotionEvent).This allows a ViewGroup to watch events as they are dispatched to child Views`
Related
I have a parent, custom RelativeLayout which inflates two ImageViews: one content image, and a close button in the top right corner.
The issue: I cannot have both the gestures/scaling of the content image functioning AND the close button functioning.
The content image is a custom ImageView subclass (a modification of https://github.com/MikeOrtiz/TouchImageView), which has it's own OnTouchListener to allow for pinch-zooming, and responding to usual gestures.
This OnTouchListener contains the code:
private class PrivateOnTouchListener implements OnTouchListener {
#Override
public boolean onTouch(View v, MotionEvent event) {
mScaleDetector.onTouchEvent(event);
mGestureDetector.onTouchEvent(event);
//....
return true;
Now, if that return is true, the scaling/gestures for the content image works, but the close button onClick is never called, whereas if it's false then the scaling/getures don't work and the onClick can be called.
I don't understand why a return function called AFTER the event is passed to the gesture detector affects (?consumes the event) whether or not the gesture detector works.
Is there a simple way of ensuring the functionality of both child ImageViews, where that return function is false but both detectors still work?
What I've tried:
Ensuring all methods in the gestureDetector return false, so that the event isn't consumed. (The scaleDetector isn't custom, so I haven't done the same there; if you think that's where the problem is let me know)
An onInterceptTouch method in the parent RelativeLayout, but I'm not sure if I implemented that correctly
(Reading around event handling to understand how it works)
I am writing an very simple application with following scenario:
1) Screen A have 3 button to move on other screen's.
2) Now if I hold one button(say Button 1) and perform rapid click on other button then it launch multiple instance of other screen. Which I think should not be happened. How can prevent this.
3) and it's more weird. After move on other screen if I don't release Button 1 which was on Screen A then it still allow to perform click for rest of two button of screen A even I can see second screen.
Here it's clear launch second screen but still first screen button event working.
Any idea how can avoid such scenario.
How you are going to disable other buttons while having 1 enabled, that's an algorhytmic problem. You can try creating a boolean or control variable in your activity (and then pass the final reference of the activity to wherever you need it), or in a static context. But to answer the title of the question - you can "Cancel Touch Event" either by adding an OnTouchListener, or if you're extending class Button, you can override onTouchEvent(MotionEvent ev) method.
Using OnTouchListener will disable any previously defined touch-event behavior. You can call the actual click event from the inside by calling performClick method from your button.
//in order to use button inside OnTouchEvent, its reference must be final
//if it's not, create a new final reference to your button, like this:
final finalButton = button;
button.setOnTouchListener(new OnTouchListener() {
#Override
public boolean onTouchEvent(MotionEvent ev) {
// ... additional code if necessary
if(canBeClicked) {
finalButton.performClick();
return true;
}
else return false;
}
}
Overriding onTouchEvent in a class extending Button should look something like this.
#Override
public boolean onTouchEvent(MotionEvent ev) {
// ... additional code if necessary
//here we don't really need to call performClick(), although API recommends it
//we just send the touch event to the super-class and let it handle the situation.
if(activity.canBeClicked) return super.onTouchEvent(ev);
else return false;
}
One solution that I found is to disable click listener in onPause() and enable it in onResume() . Can we have better support for this?
I'm developing an Android app with GoogleMap and I need to implement a SlidingPanelLayout in this Activity.
I want to open the SlidingPanelLayout only when a certain button is clicked, because if I drag the finger
on the GoogleMap the SlindingPanelLayout appears. So if I fix the open SlidingPanelLayout only when the button is clicked, the problem will be solved.
Are there any way to block the SlidingPanelLayout opening when I drag on the map, or something like this?
If you really want the panel to open only through a button press, you should create a class that extends SlidingPaneLayout and override the onInterceptTouchEvent() method.
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(!isOpen()){
//The map panel is being shown. We don't want the SlidingPaneLayout to handle the MotionEvent.
return false;
}
else{
//The other panel is being shown... Let the SlidingPaneLayout handle the MotionEvent as normal.
return super.onInterceptTouchEvent(ev);
}
}
Remember to use the custom SlidingPaneLayout class in your code or layout and not the regular one. Also, you should, obviously, place a button that will call the openPane() method of your custom class somewhere.
====
THE APPROACH BELOW IS NOT THE ANSWER TO THE QUESTION, BUT AN ALTERNATIVE SOLUTION
Now, if you want to let the user to freely use the GoogleMap object and let the SlidingPaneLayout open if a drag event occur in a certain region of the screen/map, you can use this approach:
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//Get the user touch event position in dp units
float xTouchPosDp = ev.getX()/getResources().getDisplayMetrics().density;
if(!isOpen()){
if(xTouchPosDp < 30){
//If the panel is closed (map pane being entirely shown)
//and the touch event occur on the first 30 horizontal dp's
//Let the SlidingPaneLayout onTouchEvent() method handle the
//motion event alone (the GoogleMap object won't receive the event
//and depending on the movement, the panel will open)
return true;
}else{
//Now, if the panel is closed, but the touch event occur
//on the rest of the screen, let the GoogleMap object handle
//the motion event.
return false;
}
}
else{
//If the panel is opened, let the SlidingPaneLayout handle the
//motion event normally.
return super.onInterceptTouchEvent(ev);
}
}
Again, remember to use your custom SlidingPaneLayout class in your code/layout.
The problem with this solution is that if both panels are opened (they together fit the entire screen), you won't be able to move the map laterally.
Is there a clever way to "do something" every time a user clicks or taps on the screen? I'd like to know which view they tapped on, and do it without having to replace all my standard views with custom views.
Basically, I want to add click tracking in my app to help with analytics. We already have page tracking (by using a shared superclass for every activity), but in some cases page tracking isn't enough and we actually want to track clicks.
How can I execute a function every time the user clicks on a View on the screen?
In each of your Activities, just add this at the end of onCreate (this is especially easy if all your Activities are subclasses of a custom Activity, because then you only need to write this once):
getWindow().getDecorView().findViewById(android.R.id.content).setOnTouchListener(new View.OnTouchListener() {
public boolean onTouch(View v, MotionEvent e) {
//handle your code here
return false;//don't absorb the touch.
}
});
Edit
The above code does not allow easy recognition of which Views are touched. The below code will. Note that this may break other onTouchListeners if the View in question already has registered an onTouch event. If not, this will work great. It uses the droidQuery library to select all views in the hierarchy and set uses code like above to handle touches without absorbing the event. The best place for this would be at the end of onCreate, in every Activity:
First, select the highest level of the architecture you want. If your layout's root view has an id, that would be the best thing:
$.with(this, R.id.root_id)
If not, either add one, or select the topmost view (above your layout). Note that for analytics this may provide some results you do not need:
$.with(this).selectAll()
Now, on the end of this selection, append the following:
.each(new Function() {
#Override
public void invoke($ d, Object... args) {
d.view(0).setOnTouchListener(new View.OnTouchListener() {
public boolean onTouch(View v, MotionEvent e) {
//do something with the clicked view, such as:
handleTouch(v, e);
return false;//don't absorb the touch.
}
})
}
});
Then have a new method:
private void handleTouch(View v, MotionEvent e) {
//handle the view touch.
}
I have a project using a ViewPager which works correctly:
I can page from view to view by flicking left and right.
On one page of the ViewPager, I have a custom view and I am trying to add below it another view (LinearLayout) that is touch sensitive and that will not "page" the ViewPager. Like this:
<LinearLayout >
<myView />
<LinearLayout> // The touch sensitive region
</LinearLayout>
</LinearLayout>
After my view has been inflated and added to the ViewPager, I call setOnTouchListener() to attach a listener to the internal LinearLayout. The listener returns "true".
Unfortunateley when I move horizontally in that view, I move to another page.
The events I get are:
ACTION_DOWN, ACTION_MOVE (n times) and finally ACTION_CANCEL. Although I stayed within the view.
It seems that the event has gone up the view hierarchy and processed by the ViewPager.
Is there a logical explanation to this behaviour ?
I have found a workaround by intercepting events at the Activity level, overriding dispatchTouchEvent().
In there, I call super.dispatchTouchEvent() for processing all events unless the user is touching my touch sensitive view. I test if the user is on the correct page and at the right location. In that case I process the event myself.
#Override
public boolean dispatchTouchEvent (MotionEvent ev) {
if ( pager.getCurrentPosition()!=0 ) { // Test ViewPager page
super.dispatchTouchEvent(ev);
}
else {
int[] xy = new int[2];
findViewById(R.id.myTouchControl).getLocationOnScreen(xy);
int topOfView = xy[1];
if ( ev.getY() > topOfView ) {
// Custom processing
...
} else {
// Standard processing
super.dispatchTouchEvent(ev);
}
}
return false;
}
No need to override dispatchTouchEvent(). Instead, in the ACTION_DOWN event, use View.getParent().requestDisallowInterceptTouchEvent(true).
requestDisallowInterceptTouchEvent(true) requests the parent view (ViewPager) not to intercept this specific touch event. It will clear this flag after the MotionEvent ends, so you must call it in ACTION_DOWN.