I have read a few questions regarding this topic on SO but haven't really found a solid answer to it.
I have a framelayout that I stack multiple custom views on, however the onTouch event only works with the top view. (the custom views are all the same view with the same onTouch event, just multiple of them)
FrameLayout
customView[2] <--- this is the last view added and the only one that receives the event
customView[1]
customView[0]
I'm testing it on Android 2.2 and am wondering if there is any way for the other views below to know where the touch happened?
EDIT (Adding some code)
I'm adding some code to hopefully help explain where I'm running into issues. At first I just automatically had the onTouchEvent return true. This made it so that the last view (in my case customerView[2]) would be the only one generating a value.
However, once I added the method to set the onTouchEvent to return true or false, now the only view returning a generated value is customView[0].
I hope this clears up what I am asking. I'm rather new to this and I appreciate you taking the time to explain it (and of course I appreciate your patience).
Also, I realize that my TextView's don't update with the value on each touchEvent, I'm working on fixing that.
My Activity:
public class MyActivity extend Activity {
CustomView[] customView;
TextView[] textView;
int numViews 3;
//FrameLayout and Params created
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
for(int i = 0; i < numViews; i++) {
customView[i] = new CustomView(this, i);
//Allows the onTouch to be handled by all Views - View[0] is the bottom view
if(i == 0) {
customView[i].setTouchBool(true); //set view's onTouch to return true
} else {
customView[i].setTouchBool(false); //set view's onTouch to return false
}
//Set TextView to display the number generated by the CustomView
textView[i].setText(Double.toString(customView[i].getGeneratedNumber()));
//Add views to main layout
frame.addView(textView[i]);
frame.addView(customView[i]);
}
}
}
My View:
public class CustomView extends View {
boolean onTouchHandler = true;
int xVal = 0, yVal = 0;
int index;
double generatedNum = 0;
public CustomView(Context context) {
this(context, 0);
this.index = 0;
}
public CustomView(Context context, int index) {
super(context);
this.index = index;
}
#Override
public boolean onTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
switch(action) {
case MotionEvent.ACTION_DOWN: {
//do logic
}
case MotionEvent.ACTION_MOVE: {
//do logic
}
case MotionEvent.ACTION_UP: {
xVal = (int) ev.getX();
yVal = (int) ev.getY();
generateNumber(xVal, yVal, index);
break;
}
}
return onTouchHandler;
}
private void generateNumber(int x, int y, int index) {
if(index == 0) {
generatedNum = (x / 2) * (y / 2) + 64;
} else {
generatedNum = (x / 2) * (y / 2) + (index * 128);
}
}
public double getGeneratedNumber() {
return generatedNum;
}
public boolean setTouchBool(boolean b) {
this.onTouchHandler = b;
}
}
Android will cascade down the views calling onTouchEvent on each one until it receives a true from one of them. If you want a touch event to be handled by all of them, then return false until it reaches the last one.
EDIT:
Ok. If I understand correctly, you have a single top view containing a bunch of child views one layer deep. My original answer was assuming that you had three custom views that were on top of each other in the ViewGroup's hierarchy (View3 is a child of View2. View2 is a child of View1. View1 is a child of ParentView). You want the user's touch event on the parent view to get sent to all of it's children.
If that's the case, AFAIK, there is no view in Android's API that allows that. So, you'll have to make a custom view that does it.
OK, I haven't tested this, so please tell me if it works and if it's what you're trying. Create a custom class that extends whatever object frame is, then override the onTouch method like so.
#Override
public boolean onTouchEvent(MotionEvent ev) {
for(int i = 0; i < this.getChildCount(); i++){
this.getChildAt(i).dispatchTouchEvent(ev);
}
return true;
}
Now, keep the same logic that your custom views have, except they should all return false because your parent view will not receive the onTouch event unless they do as stated in my previous answer
note: with this implementation, the child view that the user actually touches will fire twice because the logic will go
fire child touch event -> return false -> fire parent touch event -> fire child touch event again
I know this question is very old, but I had the same problem and solved it by creating my own Layout to determine which child is actually touched.
I therefore iterate over the children of my custom layout and check if the user actually clicked on the view. The collision detection is handled in the custom view's onTouch() method. (Collision detection is done by intersecting a Region() with the event's x,y coordinates. For me this was convennient because I drew the custom view with a Path())
Here is a kotlin code snippet from my custom layout for better understanding:
class CustomLayout(context: Context, attrs: AttributeSet) :
RelativeLayout(context, attrs){
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
if(ev.action != MotionEvent.ACTION_UP){
return true
}
//Iterate over child view and search for the right child that should handle this touch event
for (i in childCount - 1 downTo 0) {
val child = getChildAt(i)
if (!viewTouched(child, ev)) {
continue
}
//Do something
Timber.d("Touched view: ${child.id}")
}
return true
}
private fun viewTouched(child: View, ev: MotionEvent) : Boolean {
child as OnTouchListener
//onTouch() does the collision detection
return child.onTouch(child, ev)
}
Related
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.
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 use a SlidingDrawer as my main layout. Inside the content area I have a Fragment (which contains a ListView) When the activity first loads everything is great, the listview scrolls correctly.
When I start a different activity and then come back, the first scroll motion I try is intercepted by the SlidindDrawer, and either opens or closes it. As soon as you stop the scroll and pick up your finger, the ListView is again able to scroll.
I would like the ListView to be able to scroll when the activity resumes. And just generally be able to control whether the SlidingDrawer is the one getting focus.
UPDATE:
I have narrowed the issue down a little bit. I have extended the SLidingDrawer to allow for click on buttons in the handle with the following code.
Override
public boolean onInterceptTouchEvent(MotionEvent event) {
super.onInterceptTouchEvent(event);
if (mHandleLayout != null) {
int clickX = (int) (event.getX() - mHandleLayout.getLeft());
int clickY = (int) (event.getY() - mHandleLayout.getTop());
if (isAnyClickableChildHit(mHandleLayout, clickX, clickY))
return false;
}
return super.onInterceptTouchEvent(event);
}
private boolean isAnyClickableChildHit(ViewGroup viewGroup, int clickX, int clickY) {
for (int i = 0; i < viewGroup.getChildCount(); i++) {
View childView = viewGroup.getChildAt(i);
if (TAG_CLICK_INTERCEPTED.equals(childView.getTag())) {
childView.getHitRect(mHitRect);
if (mHitRect.contains(clickX, clickY))
return true;
}
if (childView instanceof ViewGroup && isAnyClickableChildHit((ViewGroup) childView, clickX, clickY))
return true;
}
return false;
}
If I comment out the onInterceptTouchEvent function, everything seems to work normally.
I noticed that you are calling super.onInterceptTouchEvent(event) twice. Why?
That could be the reason for the issue.
I'm having a hard time trying to figure this out. I have a gridview of 8 buttons. At the moment I'm using an onItemClickListener to trigger the buttons actions, however this produces two problems for me.
1) The buttons action happens after the button has been unpressed.
2) Two buttons cannot the pressed at the same time, you must release the first button.
As I have learnt, an onTouchListener should resolve my first issue, though I'm not sure how to determine which button has been pressed. My code for the onItemClickListener is as follows
gridview.setOnItemClickListener(new OnItemClickListener() {
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
Toast.makeText(Activity.this, "" + position, Toast.LENGTH_SHORT).show();
}
});
Now with the above, I know exactly which button has been pushed. I believe the code for implementing as an onTouchListener is as follows
gridview.setOnTouchListener(new OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
return false;
}
}) {
How am I supposed to determine which button has been pressed using MotionEvent? Before I was getting passed 'position' and it made this fairly easy. I also need to account for if two or more buttons have been pushed simultaneously/without letting another one go.
Does anyone know how to achieve this?
Having hit this very issue recently and coming across this post in my quest for help, I wanted to add two things from what I did which seem to have worked:
1) I added the onTouchListener to the object in the adapter rather than the activity or gridview.
2) In the OnTouchListener, I looked for MotionEvent.ACTION_DOWN (first finger touch) and MotionEvent.ACTION_POINTER_DOWN (subsequent finger touches), this way I can get multitouches and process them immediately without waiting for the user to lift their finger(s).
Note that I called it ImageAdapter, even though I've added a TextView to each as that way I can use the TextView background for the image, but add invisible text to the TextView so it works with Talkback):
public class ImageAdapter extends BaseAdapter {
private Context mContext;
public ImageAdapter(Context c) {
mContext = c;
}
public int getCount() {
return numCols * numRows;
}
public Object getItem(int position) {
return this;
}
public long getItemId(int position) {
return position;
}
// create a new TextView for each item referenced by the Adapter
public View getView(int position, View convertView, ViewGroup parent) {
TextView textView;
if (convertView == null) { // if it's not recycled, initialize some attributes
textView = new TextView(mContext);
} else {
textView = (TextView) convertView;
}
// place any other initial setup needed for the TextView here
// here's our onTouchListener
textView.setOnTouchListener(new View.OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
boolean returnValue;
int thePosition = v.getId();
// MotionEvent.ACTION_DOWN gets the first touch
// MotionEvent.ACTION_POINTER_DOWN gets any subsequent touches (if you place a second finger on the screen)
// Between these I can get touches as soon as they happen, including multitouch support, without needing to wait until the user lifts their finger.
if ((event.getAction() == MotionEvent.ACTION_DOWN) || (event.getAction() == MotionEvent.ACTION_POINTER_DOWN)) {
TextView textView;
if (v == null) { // if it's not recycled, initialize some attributes
textView = new TextView(mContext);
} else {
textView = (TextView) v;
}
// Do any processing based on the touch - I call a function and pass the position (number of cell, 1..n) and textview so can make changes to it as needed
ScreenTapped(thePosition, textView);
// I used a returnValue
returnValue = true;
} else returnValue = false;
return returnValue;
});
return textView;
} // getView
} //imageadapter
I am infact trying to figure the same thing out. I got as far as figuring out which gridcell has been clicked using the following code
public boolean onTouch(View v, MotionEvent me) {
float currentXPosition = me.getX();
float currentYPosition = me.getY();
int position = gridView.pointToPosition((int) currentXPosition, (int) currentYPosition);
Position gives you the number on the gridView, and you can supposedly retrieve that particular item as following
gridView.getItemAtPosition(position)
But that is where I am stuck. My gridView has Textview items in it, and I am having trouble converting the item to a textview and then performing operations on it.
Hope this helps!
When using gridView the philosophy is:
the grid view implements the onTouchLister
when touch happens onTouchLister gathers the coordinates (a lot :) )
for all ACTION_MOVE events
when the touch event is MOVE_UP, calculate the real positions under
the coordinates and return the item in the grid
So the solution would be:
In your activity where you have findViewById(some_grid_view)
//Register handler for the onTouch event of gridView in your activity
gridView.setOnTouchListener(new MyActivityOnTouchListener(this));
NOTE: my onTouch listener is implemented in another class (MyActivityOnTouchListener) instead of inside the activity
...then in the MyActivityOnTouchListener class you implement the onTouch method:
public class CalendarActivityOnTouchListener implements View.OnTouchListener {
private MyActivity myActivityContext;
private GridView mGridView;
private HashSet<Point> movementCoordinates = new HashSet<Point>;
//Constructor
public MyActivityOnTouchListener (MyActivity context){
this.myActivityContext= context;
mGridView= myActivityContext.getGridView(); //assign touched gridView into a local variable
}
#Override
public boolean onTouch(View v, MotionEvent event) {
/*
* NOTE:
* ACTION_MOVE fires events until you release it
* ACTION_UP once you release it fires it
*/
//while touching the grid a bunch of ACTION_MOVE events are dispatched
if (event.getAction() == MotionEvent.ACTION_MOVE) {
//gather all coordinates touched (in a set to avoid duplicates)
movementCoordinates.add(new Point((int)event.getX(), (int)event.getY()));
return true;
}
//Finally the finger is lifted
if (event.getAction() == MotionEvent.ACTION_UP) {
//convert all movementCoordinates gathered in the previous block into real grid positions
int position;
for(Point p : movementCoordinates){
Log.d("Luka", p.x +" / "+p.y);
position = calendarGridView.pointToPosition(p.x, p.y);
//...Do whatever with the position
}
}
}
}
Be careful about the pointToPosition() method because in some cases it can return -1 instead of the position behind the coordinates. For example, if you have a margin between items in the grid those coordinates cannot return a position, hence the -1
hope it helps...
I have a set of images. I am displaying it on the screen as a grid.
So based upon selecting each image i want to do actions.
That I have done. But one more requirement is that when we move our hand through these images then also I have to perform the same actions. That is, I will have to track on which image I have touched right now and perform the actions. How will I implement it? Does anyone have any idea? Please respond..
Try onTouch() event From View.OnTouchListener. This is called when the user performs an action qualified as a touch event, including a press, a release, or any movement gesture on the screen (within the bounds of the item).
Hope this helps.
You can set listeners to your images, i.e.
imgView.setOnTouchListener(...)
imgView.setOnFocusChangeListener(...)
or
imgView.setOnClickListener()
and then perform the action in these listeners.
If you use setOnFocusChangeListener, then you should be able to handle all cases regardless in which way you selected the image, via touch or trackball.
I got it done.
In the below code colorGV is my grid name.
Add listener to it.
colorGV.setOnTouchListener(new OnTouchClick(context));
And define onTouchClick as:
private class OnTouchClick implements OnTouchListener {
Context context;
OnTouchClick(Context context) {
this.context = context;
}
public boolean onTouch(View v, MotionEvent event) {
try {
if (event.getAction() == MotionEvent.ACTION_MOVE) {
int x = (int) event.getX();
int y = (int) event.getY();
int position = -1;
for (int i = 0; i < colorGV.getChildCount(); i++) {
Rect ButtonRect = new Rect();
colorGV.getChildAt(i).getHitRect(ButtonRect);
if (ButtonRect.contains(x, y)) {
position = i;
break;
}
}
if (position >= 0 && prevPos != position) {
System.out.println("Position changed :" + position);
return true;
}
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
}