Issue with Android's onTouchEvent() - android

I am creating my first Android app using this guide as a reference. Currently, I have a red button on my canvas and when the user clicks the button a boolean (green) will be set to true in order for the button's bitmap to a green button.
That part of the application works, however it works regardless where the user clicks on the canvas. I only want the boolean to be changed when the user clicks on the button's bitmap. Here is what I currently have in my code:
The onTouchEvent() method
#Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
button.handleActionDown((int)event.getX(), (int)event.getY());
if (button.isTouched()) {
green = true;
}
} if (event.getAction() == MotionEvent.ACTION_MOVE) {
} if (event.getAction() == MotionEvent.ACTION_UP) {
if (button.isTouched()) {
green = false;
button.setTouched(false);
}
}
return true;
}
The handleActionDown() Method
public void handleActionDown(int eventX, int eventY) {
if (eventX >= (x - bitmap.getWidth() / 2) && (eventX <= (x + bitmap.getWidth()/2))) {
if (eventY >= (y - bitmap.getHeight() / 2) && (eventY <= (y + bitmap.getHeight()/2))) {
setTouched(true);
} else {
setTouched(false);
}
} else {
setTouched(false);
}
}
Can anybody see what I am missing in order for the ACTION_DOWN event to make it so it only triggers when the bitmap's bitmap is touched?
Regards

Avoid using onTouchEvent with buttons, it is better to use onClick because the OS detects whether or not a specific button is clicked instead of you having to calculate the position of a button
First, set the onClickListener
btn.setOnClickListener(this);
This assumes the current class implements View.OnClickListener, so if you don't implement it you either have to use a different class or create an anonymous inner class.
Then, in the onClick method, make sure the ID matches and add whatever action you want to do inside an if-statement (or alternatively a switch statement)
#Override
public void onClick(View v) {
if(v.getId() == R.id.btn1){
//do whatever you want on press
}
}

Related

Android: Only make certain part of custom view be clickable

I have a custom view, assume it looks like this:
I would like for my custom view to respond to the onClicks, however the catch is that I would like it to respond to the clicks ONLY on the red portion/circle. Not the whole view.
Is it possible to make to make the text above and the grey portion not clickable?
Thank you.
In a custom view, you handle clicks by overriding the onTouchEvent method of android's View class. First check that the location the user has clicked is within the circle. Then normally you would give some feedback on the MotionEvent.ACTION_DOWN event to let user know they have clicked, such as highlight the circle. Then on MotionEvent.ACTION_UP you can call your onClick method.
#Override
public boolean onTouchEvent(MotionEvent event) {
boolean isTouchInCircle = checkTouchInCircle(event.getX(), event.getY());
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (isTouchInCircle) {
circleColor = highlightColor;
invalidate();
}
break;
case MotionEvent.ACTION_MOVE:
if (isTouchInCircle) {
circleColor = highlightColor;
} else {
circleColor = normalColor
}
invalidate();
break;
case MotionEvent.ACTION_UP:
if (isTouchInCircle) {
onClickCircle();
}
break;
}
return true;
}
// Circle click zone approximated as a square
private boolean checkTouchInCircle(float touchX, float touchY) {
if (touchX < circleCenterX + circleRadius
&& touchX > circleCenterX - circleRadius
&& touchY < circleCenterY + circleRadius
&& touchY > circleCenterY - circleRadius) {
return true;
} else {
return false;
}
}
Unfortunately the answer Carson posted was not exactly what I was looking for as my example was only a simple one, with the reality sometimes it being a lot more difficult and checking the touch locations would be convoluted (imagine multiple views/shapes within the custom view being the click locations).
What I did was in the custom view do a find view by id on the elements of the custom view. Then I did setOnClickListener(this) on each element that I would like to be clicked able rather than on the whole view itself, so mCircle.setOnClickListener(this); and mInput.setOnClickListener(this);, then did implements View.OnClickListener for the custom view to handle the actions.

SeekBar.setProgress won't update the view

I am trying to make an Android application with two seekbars that can be controlled at the same time (multi-touch). The full source code can be found here
The app is a robot controller, each seekbar controls a motor so to move forward you slide both bars up and so on (speed is controlled by how far you push it up or down) where 50 is the stop state, 100 is the maximum speed forward and 0 is maximum speed backward.
What I am trying to implement is that after you let go of the seekbar it will go back to the stop state (progress 50), however, even though the value is actually set to 50 (using setProgress) but the view won't update to reflect that.
I tried using .invalidate(), .requestLayout() and some old solutions here and here but couldn't get it to work.
Here are parts of the code: (for full code check the github link above)
public class MainActivity extends ActionBarActivity {
...
...
public static void ResetSeekBar(View v) {
if ( v.getId() == R.id.sb1) {
Log.d("before", "Progress = " + Integer.toString(sb1.getProgress()));
sb1.setProgress(50);
Log.d("after", "Progress = " + Integer.toString(sb1.getProgress()));
sb1.invalidate();
//sb1.requestLayout();
} else if ( v.getId() == R.id.sb2) {
sb2.setProgress(50);
}
}
}
public class MySeekBar extends SeekBar {
public MySeekBar(Context context, AttributeSet attrs) {
super(context, attrs);
}
public boolean onTouchEvent(final MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
Log.d("tag", "UP");
MainActivity.ResetSeekBar(this);
} else if (event.getAction() == MotionEvent.ACTION_MOVE) {
Log.d("tag", Integer.toString(getProgress()));
MainActivity.UpdateText(this, Integer.toString(getProgress()));
} else if (event.getAction() == MotionEvent.ACTION_DOWN) {
Log.d("tag", "DOWN ");
MainActivity.UpdateText(this, Integer.toString(getProgress()));
}
return super.onTouchEvent(event);
}
}
Using a regular button onClick works and do update the view but not after MotionEvent up. So what am I doing wrong?
Also how can I make it reset to 50 but with a fast transition instead of suddenly jumping to the middle?
Of course the solution comes after you ask poeple! anyways, it is much simpler than I thought.
super.onTouchEvent(event) should be called before processing the event, i think I was trying to change it before it actually processed the change, so it was overriding what i did! (not sure just guessing based on the behaviour that I saw)
and here is the function after the fix:
public boolean onTouchEvent(final MotionEvent event) {
super.onTouchEvent(event);
if (event.getAction() == MotionEvent.ACTION_UP) {
Log.d("tag", "UP");
MainActivity.ResetSeekBar(this);
} else if (event.getAction() == MotionEvent.ACTION_MOVE) {
Log.d("tag", Integer.toString(getProgress()));
MainActivity.UpdateText(this, Integer.toString(getProgress()));
} else if (event.getAction() == MotionEvent.ACTION_DOWN) {
Log.d("tag", "DOWN ");
MainActivity.UpdateText(this, Integer.toString(getProgress()));
}
return true;
}

android ClickableSpan intercepts the click event

I have a TextView in a Layout. It's so simple.
I put a OnClickListener in the layout and some part of the TextView is set to be ClickableSpan.
I want the ClickableSpan to do something in the onClick function when it's clicked and
when the other part of the TextView is clicked, it has to do something in the onClick functions of the OnClickListener of the layout.
Here's my code.
RelativeLayout l = (RelativeLayout)findViewById(R.id.contentLayout);
l.setOnClickListener(new OnClickListener(){
#Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "whole layout", Toast.LENGTH_SHORT).show();
}
});
TextView textView = (TextView)findViewById(R.id.t1);
textView.setMovementMethod(LinkMovementMethod.getInstance());
SpannableString spannableString = new SpannableString(textView.getText().toString());
ClickableSpan span = new ClickableSpan() {
#Override
public void onClick(View widget) {
Toast.makeText(MainActivity.this, "just word", Toast.LENGTH_SHORT).show();
}
};
spannableString.setSpan(span, 0, 5, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
textView.setText(spannableString);
I've also run into this problem, and thanks to the source code #KMDev mentioned, I've came up with a much cleaner approach.
First, since I'm only having a TextView that is to be made partially clickable, in fact I don't need most of the functionalities LinkMovementMethod (and its super class ScrollingMovementMethod) which adds ability to handle key press, scrolling, etc.
Instead, create a custom MovementMethod that uses the OnTouch() code from LinkMovementMethod:
ClickableMovementMethod.java
package com.example.yourapplication;
import android.text.Layout;
import android.text.Selection;
import android.text.Spannable;
import android.text.method.BaseMovementMethod;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.view.MotionEvent;
import android.widget.TextView;
/**
* A movement method that traverses links in the text buffer and fires clicks. Unlike
* {#link LinkMovementMethod}, this will not consume touch events outside {#link ClickableSpan}s.
*/
public class ClickableMovementMethod extends BaseMovementMethod {
private static ClickableMovementMethod sInstance;
public static ClickableMovementMethod getInstance() {
if (sInstance == null) {
sInstance = new ClickableMovementMethod();
}
return sInstance;
}
#Override
public boolean canSelectArbitrarily() {
return false;
}
#Override
public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
int action = event.getActionMasked();
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
int x = (int) event.getX();
int y = (int) event.getY();
x -= widget.getTotalPaddingLeft();
y -= widget.getTotalPaddingTop();
x += widget.getScrollX();
y += widget.getScrollY();
Layout layout = widget.getLayout();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
ClickableSpan[] link = buffer.getSpans(off, off, ClickableSpan.class);
if (link.length > 0) {
if (action == MotionEvent.ACTION_UP) {
link[0].onClick(widget);
} else {
Selection.setSelection(buffer, buffer.getSpanStart(link[0]),
buffer.getSpanEnd(link[0]));
}
return true;
} else {
Selection.removeSelection(buffer);
}
}
return false;
}
#Override
public void initialize(TextView widget, Spannable text) {
Selection.removeSelection(text);
}
}
Then using this ClickableMovementMethod, touch event will not be consumed by movement method any more. However, TextView.setMovementMethod() which calls TextView.fixFocusableAndClickableSettings() will set clickable, long-clickable and focusable to true which will make View.onTouchEvent() consume the touch event. To fix for this, simply reset the three attributes.
So the final utility method, to accompany the ClickableMovementMethod, is here:
public static void setTextViewLinkClickable(TextView textView) {
textView.setMovementMethod(ClickableMovementMethod.getInstance());
// Reset for TextView.fixFocusableAndClickableSettings(). We don't want View.onTouchEvent()
// to consume touch events.
textView.setClickable(false);
textView.setLongClickable(false);
}
This works like a charm for me.
Click events on ClickableSpans are fired, and click outside them are passed throught to parent layout listener.
Note that if your are making your TextView selectable, I haven't tested for that case, and maybe you need to dig into the source yourself :P
The first answer to your question is that you aren't setting a click listener on your TextView which is consuming click events as user2558882 points out. After you set a click listener on your TextView, you'll see that areas outside the ClickableSpans's touch area will work as expected. However, you'll then find that when you click on one of your ClickableSpans, the TextView's onClick callback will be fired as well. That leads us to a difficult issue if having both fire is an issue for you. user2558882's reply can't guarantee that your ClickableSpan's onClick callback will be fired before your TextView's. Here's some solutions from a similar thread that are better implemented and an explanation from the source. The accepted answer that thread should work on most devices, but the comments for that answer mention certain devices having issues. It looks like some devices with custom carrier/manufacturer UIs are to blame, but that's speculation.
So why can't you guarantee onClick callback order? If you take a look at the source for TextView (Android 4.3), you'll notice that in the onTouchEvent method, boolean superResult = super.onTouchEvent(event); (super is View) is called before handled |= mMovement.onTouchEvent(this, (Spannable) mText, event); which is the call to your movement method which then calls your ClickableSpan's onClick. Taking a look at super's (View) onTouchEvent(..), you'll notice:
// Use a Runnable and post this rather than
// performClick directly. This lets other visual
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) { // <---- In the case that this won't post,
performClick(); // it'll fallback to calling it directly
}
performClick() calls the click listener set, which in this case is our TextView's click listener. What this means, is that you won't know in what order your onClick callbacks are going to fire. What you DO know, is that your ClickableSpan and TextView click listeners WILL be called. The solution on the thread I mentioned previously, helps ensure the order so you can use flags.
If ensuring compatibility with a lot of devices is a priority, you are best served by taking a second look at your layout to see if you can avoid being stuck in this situation. There are usually lots of layout options to skirt cases like this.
Edit for comment answer:
When your TextView executes onTouchEvent, it calls your LinkMovementMethod's onTouchEvent so that it can handle calls to your various ClickableSpan's onClick methods. Your LinkMovementMethod does the following in its onTouchEvent:
#Override
public boolean onTouchEvent(TextView widget, Spannable buffer,
MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACTION_UP ||
action == MotionEvent.ACTION_DOWN) {
int x = (int) event.getX();
int y = (int) event.getY();
x -= widget.getTotalPaddingLeft();
y -= widget.getTotalPaddingTop();
x += widget.getScrollX();
y += widget.getScrollY();
Layout layout = widget.getLayout();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
ClickableSpan[] link = buffer.getSpans(off, off, ClickableSpan.class);
if (link.length != 0) {
if (action == MotionEvent.ACTION_UP) {
link[0].onClick(widget);
} else if (action == MotionEvent.ACTION_DOWN) {
Selection.setSelection(buffer,
buffer.getSpanStart(link[0]),
buffer.getSpanEnd(link[0]));
}
return true;
} else {
Selection.removeSelection(buffer);
}
}
return super.onTouchEvent(widget, buffer, event);
}
You'll notice that it takes the MotionEvent, gets the action (ACTION_UP: lifting finger, ACTION_DOWN: pressing down finger), the x and y coordinates of where the touch originated and then finds which line number and offset (position in the text) the touch hit. Finally, if there are ClickableSpans that encompass that point, they are retrieved and their onClick methods are called. Since we want to pass on any touches to your parent layout, you could either call your layouts onTouchEvent if you want it to do everything it does when touched, or you could call it's click listener if that implements your needed functionality. Here's where do to that:
if (link.length != 0) {
if (action == MotionEvent.ACTION_UP) {
link[0].onClick(widget);
} else if (action == MotionEvent.ACTION_DOWN) {
Selection.setSelection(buffer,
buffer.getSpanStart(link[0]),
buffer.getSpanEnd(link[0]));
}
return true;
} else {
Selection.removeSelection(buffer);
// Your call to your layout's onTouchEvent or it's
//onClick listener depending on your needs
}
}
So to review, you'll create a new class that extends LinkMovementMethod, override it's onTouchEvent method, copy and paste this source with your calls in the correct position where I commented, ensure you're setting your TextView's movement method to this new subclass and you should be set.
Edited again for possible side effect avoidance
Take a look at ScrollingMovementMethod's source (LinkMovementMethod's parent) and you'll see that it's a delegate method which calls a static method return Touch.onTouchEvent(widget, buffer, event); This means that you can just add that as your last line in the method and avoid calling super's (LinkMovementMethod's) onTouchEvent implementation which would duplicate what you're pasting in and other events can fall through as expected.
Here is an easy solution and it worked for me
You can achieve this using a work around in getSelectionStart() and getSelectionEnd() functions of the Textview class,
tv.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
ClassroomLog.log(TAG, "Textview Click listener ");
if (tv.getSelectionStart() == -1 && tv.getSelectionEnd() == -1) {
//This condition will satisfy only when it is not an autolinked text
//Fired only when you touch the part of the text that is not hyperlinked
}
}
});
Declare a global boolean variable:
boolean wordClicked = false;
Declare and initialize l as final:
final RelativeLayout l = (RelativeLayout)findViewById(R.id.contentLayout);
Add an OnClickListener to textView:
textView.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
if (!wordClicked) {
// Let the click be handled by `l's` OnClickListener
l.performClick();
}
}
});
Change span to:
ClickableSpan span = new ClickableSpan() {
#Override
public void onClick(View widget) {
wordClicked = true;
Toast.makeText(Trial.this, "just word", Toast.LENGTH_SHORT).show();
// A 100 millisecond delay to let the click event propagate to `textView's`
// OnClickListener and to let the check `if (!wordClicked)` fail
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
wordClicked = false;
}
}, 100L);
}
};
Edit:
Keeping user KMDev's answer in view, the following code will meet your specifications. We create two spans: one for the specified length: spannableString.setSpan(.., 0, 5, ..); and the other with the remainder: spannableString.setSpan(.., 6, spannableString.legth(), ..);. The second ClickableSpan(span2) performs a click on the RelativeLayout. Moreover, by overriding updateDrawState(TextPaint), we are able to give the second span a non-distinctive (non-styled) look. Whereas, first span has a link color and is underlined.
final RelativeLayout l = (RelativeLayout)findViewById(R.id.contentLayout);
l.setOnClickListener(new OnClickListener(){
#Override
public void onClick(View v) {
Toast.makeText(Trial.this, "whole layout", Toast.LENGTH_SHORT).show();
}
});
TextView textView = (TextView)findViewById(R.id.t1);
textView.setMovementMethod(LinkMovementMethod.getInstance());
textView.setHighlightColor(Color.TRANSPARENT);
SpannableString spannableString = new SpannableString(textView.getText().toString());
ClickableSpan span = new ClickableSpan() {
#Override
public void onClick(View widget) {
Toast.makeText(Trial.this, "just word", Toast.LENGTH_SHORT).show();
}
};
spannableString.setSpan(span, 0, 5, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
ClickableSpan span2 = new ClickableSpan() {
#Override
public void onClick(View widget) {
l.performClick();
}
#Override
public void updateDrawState(TextPaint tp) {
tp.bgColor = getResources().getColor(android.R.color.transparent);
tp.setUnderlineText(false);
}
};
spannableString.setSpan(span2, 6, spannableString.length(),
Spannable.SPAN_INCLUSIVE_INCLUSIVE);
textView.setText(spannableString);
Special thanks to user KMDev for noticing the issues with my original answer. There's no need for performing a (faulty) check using boolean variable(s), and setting an OnclickListener for the TextView is not required.
The easiest and fastest way to implement ClickableSpan is:
new SmartClickableSpan
.Builder(this)
.regularText("I agree to all ")
.clickableText(new ClickableOptions().setText("Terms of Use").setOnClick(new
ClickableSpan() {
#Override
public void onClick(#NonNull View view) {
// Your Code..
}
}))
.into(myTextView);
Adding regular Clickable Spans in Android requires calculating sizes for each clickable text, and when it comes to adding a lot of clickable and regular words or sentences in a TextView is becomes a mess.. By using SmartClickableSpan, you'll be able to add whatever amount of clickable words or sentences without any worries of calculating length of each text on every update on it.
SmartClickableSpan Github:
https://github.com/HseinNd98/SmartClickableSpan

Capturing touch events in an android list item and prevent scrolling

I'm trying a wild idea here by putting a custom control within the items of a certain list view. The control is only "activated" if the user touches down on a certain trigger point and then they can "drag around."
My question is, what can I do in onTouchEvent(...) to prevent the listview from receiving the event and scrolling. Right now I can touch and get ahold of the control, but if I move my finger too much up or down the listview takes over and starts scrolling, then my view doesn't even receive a ACTION_UP event.
Here is my current onTouchEvent code:
if (e.getAction() == MotionEvent.ACTION_DOWN) {
Log.d("SwipeView", "onTouchEvent - ACTION_DOWN" + e.getX() + " " + e.getY());
int midX = (int)(this.getWidth() / 2);
int midY = (int)(this.getHeight() / 2);
if (Math.abs(e.getX() - midX) < 100 &&
Math.abs(e.getY() - midY) < 100) {
Log.d("SwipeView", "HEY");
setDragActive(true);
}
this.invalidate();
return true;
} else if (e.getAction() == MotionEvent.ACTION_MOVE) {
_current[0] = e.getX();
_current[1] = e.getY();
this.invalidate();
return true;
} else if (e.getAction() == MotionEvent.ACTION_UP) {
_current[0] = 0;
_current[1] = 0;
setDragActive(false);
this.invalidate();
return true;
}
I'm sure it has something to do with the event hierarchy in some fashion.
This might not be exactly what you're looking for, but it's possible to implement capture capabilities in your activity. add
private View capturedView;
public void setCapturedView(View view) { this.capturedView = view); }
#Override
public boolean dispatchTouchEvent(MotionEvent event) {
return (this.capturedView != null) ?
this.capturedView.dispatchTouchEvent(event) :
super.dispatchTouchEvent(event);
}
to your activity, then simply pass your view on ACTION_DOWNand null on ACTION_UP. it's not exactly pretty, but it works. i'm sure there's a proper way to do this though.
finally learned the correct way to do this: requestDisallowInterceptTouchEvent

scrolling within grid view

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;
}
}

Categories

Resources