Disclaimer: I have already tried following similar threads (namely How can I use OnClickListener in parent View and onTouchEvent in Child View? and How can i get both OnClick and OnTouch Listeners), yet my code still fails to work.
I have a (parent) activity which contains a (child) view. Activity has all the network code, while view contains a bitmap displaying game state. View executes the game moves and draws them, while activity sends and receives the moves.
As such, I need to listen for player's actions on the bitmap, handle them there and immediately after it is handled in the view, I need to send the made move to the server. So the best way I've found to handle all that so far is by using onClickListener along with onTouchListener. But I've spent 4 hours and can't get this to work, so any help would be appreciated.
My parent onClick:
gameView.setOnClickListener(new View.OnClickListener(){
#Override
public void onClick(View v){
//gameView.performClick();
sendMove(theGame.getLastMove());
}
});
My child view:
#Override
public boolean onTouchEvent(MotionEvent event)
{
if(canIMove) {
float x = event.getX();
float y = event.getY();
int recWidth = this.getWidth() / 7;
int currentWidth = 0;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (x > currentWidth && x < currentWidth + recWidth)
{
//Bitmap touched
theGame.Move();
ReloadGameBoard();
}
return true;
}
}
return false;
}
It matters not whether I try to performClick() in onClick or in onTouch, it doesn't also matter whether I return true or false in onTouch, it doesn't matter whether I put it in ACTION_DOWN nor ACTION_UP, the result is always the same: child View onTouch gets executed and parent onClick does not.
And please, if you decide to downvote this, at least tell me why.
Related
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'm using a SemiClosedSlidingDrawer (http://pastebin.com/FtVyrcEb) and i've added on content part some buttons on the top of slider which are always visibles.
The problems is that they are clickable (or click event is dispatched) only when slider is fully opened... When slider is "semi-opened" click event not seems dispached to button... I have inspected with debugger into onInterceptTouchEvent() and in both cases (opened/semi-collapsed) the following code
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if (mLocked) {
return false;
}
final int action = event.getAction();
float x = event.getX();
float y = event.getY();
final Rect frame = mFrame;
final View handle = mHandle;
handle.getHitRect(frame);
//FOLLOWING THE CRITICAL CODE
if (!mTracking && !frame.contains((int) x, (int) y)) {
return false;
}
return false but only when slider is opened event was dispached...
It checks if a (x,y) relative to the click are contained in a rectangle created starting from the HandleButton view of sliding drawer...
final Rect frame = mFrame;
final View handle = mHandle;
handle.getHitRect(frame);
and this is obviously false because i'm clicking on a button contained inside the content part of slidingdrawer and that's ok...
As i said above the problem is that in semi-collapsed state, buttons contained in content part are not receiving the event...
Have you any idea how can i solve this issue?
Can be some state of slidingdrawer that avoid to click childs when collapsed?
Thanks in advance...
Right, I think I've figured out a way to do this.
First you need to modify onInterceptTouchEvent() to return true whenever the user presses the visible content during the semi-opened state. So, for instance, if your SemiClosedSlidingDrawer view is located at the very bottom of the screen, you can use a simple detection algorithm, like this:
public boolean onInterceptTouchEvent(MotionEvent event) {
...
handle.getHitRect(frame);
// NEW: Check if the user pressed on the "semi-open" content (below the handle):
if(!mTracking && (y >= frame.bottom) && action == MotionEvent.ACTION_DOWN) {
return true;
}
if (!mTracking && !frame.contains((int) x, (int) y)) {
...
}
Now the touch events during the user's interaction with the semi-opened content will be dispatched to onTouchEvent(). Now we just need to intercept these events and "manually" redirect them to the right view (note that we also need to offset the coordinates for the child view):
public boolean onTouchEvent(MotionEvent event) {
...
if (mTracking) {
...
}
else
{
// NEW: Dispatch events to the "semi-open" view:
final Rect frame = mFrame;
final View handle = mHandle;
handle.getHitRect(frame);
float x = event.getX();
float y = event.getY() - frame.bottom;
MotionEvent newEvent = MotionEvent.obtain(event);
newEvent.setLocation(x, y);
return mContent.dispatchTouchEvent(newEvent);
}
return mTracking || mAnimating || super.onTouchEvent(event);
}
It's a bit of a messy implementation, but I think the basic concept is right. Let me know how it works for you!
I would like to change view height when user move finger down on screen.
new OnTouchListener() {
float lastY = Float.MIN_VALUE;
#Override
public boolean onTouch(View v, MotionEvent event) {
if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_MOVE) {
if (lastY != Float.MIN_VALUE) {
final float dist = event.getRawY() - lastY;
myView.post(new Runnable() {
#Override
public void run() {
ViewGroup.LayoutParams lp = myView.getLayoutParams();
lp.height += dist;
myView.setLayoutParams(lp);
}
});
view.invalidate();
}
lastY = event.getRawY();
}
if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) {
lastY = Float.MIN_VALUE;
}
return true;
}
}
But view height changes only when user stops moving finger!
How to change view height immediately when ACTION_MOVE occurred?
First, you don't need to call invalidate() or requestLayout() because setLayoutParams() already does that so calling it manually again will only cause unnecessary overhead.
Second, lose the mView.post() call: onTouch() is executed on the main (aka UI) thread anyway. Just do the layoutparams stuff in the onTouch() body along with everything else.
Third, your mView.post() call is what's probably causing all the trouble because View.post() effectively posts the job to UI thread's Handler, which queues jobs until the time the UI thread is free to execute them, one by one. And in your case it's too busy running your onTouch() method on every event, so everything you post gets executed when you stop delivering touch events - i.e. stop dragging.
Finally, here's a good reference on the subject that I used when I had to implement a resizeable view, it's also very likely to cover all the functionality you're trying to implement by hand: k9mail's own SplitView class on GitHub. Note how they just save the coordinates in ACTION_DOWN block and then just measure the height from there instead of calling getHeight() and incrementing it with last coordinate delta. This allows for one less operation + handling ACTION_DOWN and having the mDragging boolean flag will allow you to drop the obscure lastY = Float.MIN_VALUE; thing.
Instead of view.invalidate() you could use view.requestLayout().
This wont be super fast though, as it will cause a lot of layout events. It's hard to give more advice without seeing your layout and knowing what kind of effect you are aiming for.
I have a view in a linearlayout. When the view is longpressed the view will be removed from this linearlayout and placed, on the same position of the screen, to a relative layout. On this way a can move the view over the screen with my finger.
it almost works:
i got the longpress event working (remove the view and place the view to the relativelayout). after that i add an ontoucheventlistener so my view stays with my finger, but only for a second. the last time the touchevent is fired i got "MotionEvent.ACTION_CANCEL". When i remove my finger and place my finger again to the view i can go feature with my movement, then it will keep until i remove my finger.
I think that my problem is that the view it removed for a short moment, at that time i get a "MotionEvent.ACTION_CANCEL", however, there are still some unhandled events, they will be fired first. Thats why i got for about 1 second still ontouchevents. (this is just a thought).
someone a idee how i can keep the ontouchevent, or let the ontouchevent fired without replacing my finger?
Edited
My thought is not correct. When i do a longpress the view stays with my finger, however i lost the view as soon as i move about 50 to 100 pixels to any direction.
Edited 2
the longpress code of the view inside the linearlayout
view.setOnLongClickListener(new OnLongClickListener() {
public boolean onLongClick(View v) {
_linearLayout.removeView(v);
moveView(v);
return true;
}
});
moveView will be called by the longpress
private void moveView(View v) {
_relativeLayout.addView(v);
v.setOnTouchListener(new OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
switch(event.getAction()) {
case MotionEvent.ACTION_MOVE:
int x = (int) event.getRawX();
int y = (int) event.getRawY();
v.layout(x, y, x + v.getWidth(), y + v.getHeight());
break;
case MotionEvent.ACTION_UP:
_relativeLayout.removeView(v);
v = null;
break;
case MotionEvent.ACTION_CANCEL:
//it comes here when i move my finger more then 100 pixels
break;
}
return true;
}
});
}
of corse, this is the relevant part of the code and not the original code
Have a look at the Experience - Android Drag and Drop List post. There is also a source code that does something very similar to what are you trying to get. Eric, the author of that post, uses a separate temporary ImageView to hold a dragged view image, taken with getDrawingCache(). He also hides the original list item by setVisibility(View.INVISIBLE).
In the DragNDrop project (link above) you can replace drag(0,y) calls by drag(x,y) in DragNDrop.java to see a dragging in all directions.
Just throwing some possibilities in because I can't see the code but MotionEvent.ACTION_CANCEL could be being called as the parent layout is switched and the view is getting redrawn.
You could try overriding the onTouchEvent and at the point the ACTION_CANCEL event is sent try get hold of an equivalent view in the new layout and operate on that but I still think the better idea is to redesign so it all takes place in the same layout.
My class extends View and I need to get continuous touch events on it.
If I use:
public boolean onTouchEvent(MotionEvent me) {
if(me.getAction()==MotionEvent.ACTION_DOWN) {
myAction();
}
return true;
}
... the touch event is captured once.
What if I need to get continuous touches without moving the finger?
Please, tell me I don't need to use threads or timers. My app is already too much heavy.
Thanks.
Use if(me.getAction() == MotionEvent.ACTION_MOVE). It's impossible to keep a finger 100% completely still on the screen so Action_Move will get called every time the finger moves, even if it's only a pixel or two.
You could also listen for me.getAction() == MotionEvent.ACTION_UP - until that happens, the user must still have their finger on the screen.
You need to set this properties for the element
android:focusable="true"
android:clickable="true"
if not, just produce the down action.
Her is the simple code snippet which shows that how you can handle the continues touch event. When you touch the device and hold the touch and move your finder, the Touch Move action performed.
#Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
if(isTsunami){
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// Write your code to perform an action on down
break;
case MotionEvent.ACTION_MOVE:
// Write your code to perform an action on contineus touch move
break;
case MotionEvent.ACTION_UP:
// Write your code to perform an action on touch up
break;
}
}
return true;
}
Try this. It works to me:
public static OnTouchListener loadContainerOnTouchListener() {
OnTouchListener listener = new OnTouchListener(){
#Override
public boolean onTouch(View v, MotionEvent event) {
LinearLayout layout = (LinearLayout)v;
for(int i =0; i< layout.getChildCount(); i++)
{
View view = layout.getChildAt(i);
Rect outRect = new Rect(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
if(outRect.contains((int)event.getX(), (int)event.getY()))
{
Log.d(this.getClass().getName(), String.format("Over view.id[%d]", view.getId()));
}
}
}
Remember: the listener you´ll set must be a container layout (Grid, Relative, Linear).
LinearLayout layout = findViewById(R.id.yourlayoutid);
layout.setOnTouchListener(HelperClass.loadContainerOnTouchListener());
This might help,
requestDisallowInterceptTouchEvent(true);
on the parent view, like this -
#Override
public boolean onTouch(View view, MotionEvent motionEvent) {
view.getParent().requestDisallowInterceptTouchEvent(true);
switch(motionEvent.getAction()){
}
return false;
}
I was making a game with a custom view used as a thumb control. . . here is what I did
float x = 0, y = 0;
#Override
public boolean onTouchEvent(MotionEvent event) {
x = event.getX();
y = event.getY();
// handle touch events with
switch( event.getActionMasked() ) {
case MotionEvent.ACTION_DOWN :
if(cont)
{
// remove any previous callbacks
removeCallbacks(contin);
// post new runnable
postDelayed(contin, 10);
}
invalidate();
return true;
case MotionEvent.ACTION_MOVE :
if(!cont && thumbing != null)
{
// do non-continuous operations here
}
invalidate();
return true;
case MotionEvent.ACTION_UP :
// set runnable condition to false
x = 0;
// remove the callbacks to the thread
removeCallbacks(contin);
invalidate();
return true;
default :
return super.onTouchEvent(event);
}
}
public boolean cont = false;
// sets input to continuous
public void set_continuous(boolean b) { cont = b; }
public Runnable contin = new Runnable()
{
#Override
public void run() {
if(x != 0)
{
// do continuous operations here
postDelayed(this, 10);
}
}
};
A quick note however, make sure in your main activity that is calling this view removes the callbacks manually via the onPause method as follows
#Override
protected void onPause() {
if(left.cont) left.removeCallbacks(left.contin);
if(right.cont) right.removeCallbacks(left.contin);
super.onPause();
}
That way if you pause and come back touch events aren't being handled twice and the view is free from it's thread's overhead.
** tested on Samsung Galaxy S3 with hardware acceleration on **
All these answer are partially correct but they do not resolve in the right way the problem.
First of all, for everyone out there that decide to track when the event is ACTION_MOVE. Well that works only guess when? When user move his finger, so could if you decide to implement a custom thumb control is okay but for a normal custom button that's not the case.
Second, using a flag inside ACTION_DOWN and check it in ACTION_UP seems the logic way to do it, but as Clusterfux find out if you implement a while(!up_flag) logic you get stuck into troubles ;)
So the proper way to do it is mentioned here:
Continuous "Action_DOWN" in Android
Just keep in mind that if the logic you're going to write during the continuous press has to modify the UI in some way, you have to do it from the main thread in all the other cases it's better use another thread.
You can use the below code snippet as a reference in which I used the background to detect if the screen is held or not...
Main_Layout.setOnTouchListener(new View.OnTouchListener() {
#SuppressLint("ResourceAsColor")
#Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
Main_Layout.setBackgroundColor(R.color.green);
event.setAction(MotionEvent.ACTION_DOWN);
break;
default:
Main_Layout.setBackgroundColor(R.color.blue);
break;
}
return false;
}
});