Parent view doesn't get longpress event - android

I am trying to write a launcher-like app which can add Widgets to its screen.
I am using Leonardo Fischer's tutorial (http://leonardofischer.com/hosting-android-widgets-my-appwidgethost-tutorial/) which is great.
In order to remove a widget, the user is supposed to longpress the Widget and that's where I am running into some trouble; some Widgets (WhatsApp Messagelist, Evernote List, for instance) allow you to scroll them. For some reason, if you scroll, Android fires a LongClick event which wrongfully removes the widget...
My code:
(creates the widget and set LongClickListener)
public void createWidget(Intent data) {
Bundle extras = data.getExtras();
int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, -1);
AppWidgetProviderInfo appWidgetInfo = mAppWidgetManager.getAppWidgetInfo(appWidgetId);
final LauncherAppWidgetHostView hostView = (LauncherAppWidgetHostView) mAppWidgetHost.createView(this, appWidgetId, appWidgetInfo);
hostView.setAppWidget(appWidgetId, appWidgetInfo);
// relative layout
//RelativeLayout.LayoutParams lp = new RelativeLayout()
//mainlayout.addView(hostView, lp);
mainlayout.addView(hostView);
// [COMMENTED OUT] hostView.setOnLongClickListener(new AppWidgetLongClickListener(hostView));
}
UPDATE
Countless hours later, I think I partially understood what's happening, but I still can't get the correct behaviour.
According to http://balpha.de/2013/07/android-development-what-i-wish-i-had-known-earlier/ , you need to implement an onInterceptTouchEvent in the parent container (mainlayout in my case) to intercept and treat events before they reach the children (widgets in my case).
So I googled up the following code and tried to adapt to my needs:
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// Consume any touch events for ourselves after longpress is triggered
//Log.i(TAG,"OnIntercept: "+ev.toString());
if (mHasPerformedLongPress) {
Log.i(TAG,"Longpress OK!: "+ev.toString());
mHasPerformedLongPress = false;
return true;
}
// Watch for longpress events at this level to make sure
// users can always pick up this widget
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN: {
postCheckForLongClick();
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mHasPerformedLongPress = false;
if (mPendingCheckForLongPress != null) {
removeCallbacks(mPendingCheckForLongPress);
}
break;
}
// Otherwise continue letting touch events fall through to children
return false;
}
class CheckForLongPress implements Runnable {
private int mOriginalWindowAttachCount;
public void run() {
Log.i(TAG,"Inside RUN");
if (getParent()!= null) {
Log.i(TAG,"getParent:"+getParent().toString());
}
if ((getParent() != null) && hasWindowFocus()
&& (mOriginalWindowAttachCount == getWindowAttachCount())
&& !mHasPerformedLongPress) {
if (performLongClick()) { // <-- DOESN'T WORK :(
mHasPerformedLongPress = true;
}
}
}
public void rememberWindowAttachCount() {
mOriginalWindowAttachCount = getWindowAttachCount();
}
}
private void postCheckForLongClick() {
mHasPerformedLongPress = false;
if (mPendingCheckForLongPress == null) {
mPendingCheckForLongPress = new CheckForLongPress();
}
mPendingCheckForLongPress.rememberWindowAttachCount();
postDelayed(mPendingCheckForLongPress, ViewConfiguration.getLongPressTimeout());
}
#Override
public void cancelLongPress() {
super.cancelLongPress();
mHasPerformedLongPress = false;
if (mPendingCheckForLongPress != null) {
removeCallbacks(mPendingCheckForLongPress);
}
}
The above code does intercept touch events when I click a widget, but its logic seems aimed at intercepting (and direct for further treatment) longclicks to widgets. What I actually need is to intercept a longclick inside the parent view.
The trick seems to lie at if (performLongClick()), which, as far as I could get, fires a LongClick event to the widget...
... so I guess my question now is how to track a longclick inside the parent view.
Sorry for the long (and seemingly basic) question on handling Android UI events, but from what I googled this seems a very convoluted topic..

So it's done...! I am not sure whether this is an elegant solution, but it works.
onInterceptTouchEvent allows a parent view to act on events before they are sent to the final sender. Please note the it won't fire if you touch the actual view. So if you have a Layout with some "blank space" and some elements, onInterceptTouchEvent won't fire if you touch the layout's "blank space" (you will need the layout's onTouchEvent in this case).
Because we can essentially only track ACTION_UP, ACTION_MOVE and ACTION_DOWN events, we need to time the duration of a ACTION_DOWN / ACTION_UPpair of events to decide whether this is a longclick or not, so what I did follows:
public class time_counter {
private long begin_time;
private long end_time;
public time_counter(long i, long f) {
this.begin_time = i;
this.end_time = f;
}
public long getDuration() {
return (this.end_time - this.begin_time);
}
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// Consume any touch events for ourselves after longpress is triggered
// Watch for longpress events at this level to make sure
// users can always pick up this widget
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN: {
cnt = new time_counter(ev.getEventTime(), (long)0);
break;
}
case MotionEvent.ACTION_MOVE: {
if (cnt != null) {
cnt.end_time = ev.getEventTime();
}
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (cnt != null) {
cnt.end_time = ev.getEventTime();
}
Log.i(TAG, "DURATION: " + cnt.getDuration());
if (cnt.getDuration() > ViewConfiguration.getLongPressTimeout()) {
Log.i(TAG, "it's a longpress: " + this.toString());
if (processClick) {
processClick = false;
this.doRemoveWidget();
}
cancelLongPress();
return true;
}
break;
}
// Otherwise continue letting touch events fall through to children
return false;
}
Whenever Android sends an ACTION_DOWN event, the code start to track its duration using a simple "time counter" object. The counter's end timestamp is continually updated throughout ACTION_MOVE events and when Android sends an ACTION_UP or ACTION_CANCEL, the code checks for the final duration. If it's over ViewConfiguration.getLongPressTimeout() (default = 500ms), it triggers the action.
Notice that in my case I needed a boolean variable to prevent multiple event firing, since I wanted to use a LongClick to remove a widget. A second accidental firing, which would almost always happen, would trigger a null pointer exception since the widget had already been removed.
I tested it with several widgets (big, small, configurable, with and without scrolling views, etc, etc, etc.) and I haven't found a glitch.
Again, not sure if this an elegant or "android-wise" solution, but it solved my problem.
Hope this helps!
REFERENCE:
If you need an excellent article on touch events, please check http://balpha.de/2013/07/android-development-what-i-wish-i-had-known-earlier/. It gave me the correct "frame of mind" to address my problem.

If you really are seeing an event for starting a scroll being followed by an event for a long click, you could deal with that by setting a flag in your event handling class that tracks when scroll begins and ends, and choose to ignore the long click if a scroll is in progress.

Related

Swapping between Listeners in Android

I'm having problems dealing with a button and its two listeners.
My objective is swapping two listeners of a button using it.
It's not the behaviour I need. I need to release the button, and then click it again in a different way (with a different listener).
So.. I "onTouch" this button, and when I release my finger, I need to swap its "onTouch" listener to an "onClick()" one.
Now, I tried to accomplish my goal doing the following:
final View.OnClickListener play_listener = new View.OnClickListener() {
#Override
public void onClick(View v) { Utility.playRecording(mediaPlayer); } };
final View.OnTouchListener rec_listener = new View.OnTouchListener() {
#Override
public boolean onTouch(View view, MotionEvent event) {
if (Utility.checkPermission(view.getContext())) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Utility.startRecording(recorder, output_formats, currentFormat, file_exts, timer);
break;
case MotionEvent.ACTION_UP:
Utility.stopRecording(recorder, timer);
//disabling my onTouch Listener
recplay_button.setOnTouchListener(null);
//Setting a new listener for the same button
recplay_button.setOnClickListener(play_listener);
//Changing its color.
recplay_button.setBackground(getDrawable(R.drawable.coloranimreverse));
break;
}
} else {
Utility.requestPermission(view.getContext());
} return false; }};
So, the swapping works but I can't get the reason why after setting the onClickListener it also execute it, playing the sound I set in the other listener. Does the MotionEvent.ACTION_UP counts as a click?
Do you know how can I get through this? What I need is just not execute the onClick() listener in the same moment that I set it in the OnTouch() listener.
Thank you all.
Your OnClickListener is firing on ACTION_UP because you're unconditionally returning false from onTouch(). Returning false there tells the View that you've not consumed the event, and that it should handle it, as well. In this case, it means that the View will perform its click handling, and now that it's got an OnClickListener set, that gets called. (In fact, you could've set the OnClickListener from the start, and would've achieved the same behavior.)
Returning true in the ACTION_UP case will signal that you're consuming that event there, so the View won't end up calling its OnClickListener. This might be sufficient for your use case, however, it also means that the View won't perform any of the other state changes it would normally do for ACTION_UP; e.g., changing its Drawables to their not pressed state.
Rather than juggling listeners, and trying to decide which events to consume, and which to pass on, it might be preferable to handle everything in the OnTouchListener, track the current state in some sort of flag variable, and again return false unconditionally in onTouch(). In this way, we're simply "inserting" the desired behavior, and allowing the View to continue handling events and state as it normally would.
For example:
private boolean recordState = true;
final View.OnTouchListener rec_listener = new View.OnTouchListener() {
#Override
public boolean onTouch(View view, MotionEvent event) {
if (Utility.checkPermission(view.getContext())) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (recordState) {
Utility.startRecording(recorder, output_formats, currentFormat, file_exts, timer);
}
break;
case MotionEvent.ACTION_UP:
if (recordState) {
recordState = false;
Utility.stopRecording(recorder, timer);
recplay_button.setImageDrawable(getResources().getDrawable(R.drawable.coloranimreverse));
}
else {
Utility.playRecording(mediaPlayer);
}
}
} else {
Utility.requestPermission(view.getContext());
}
return false;
}
};

Unable to detect ACTION_MOVE or ACTION_UP touch events

I am having trouble detecting the MotionEvent.ACTION_MOVE & MotionEvent.ACTION_UP events. I am intending to have a button when you press and hold, it automatically decreases an associated value, however I have not gotten to that point in coding because I cannot detect the UP event.
for (int position = 0; position < mListItems.size(); position++) {
LayoutInflater inflater = getLayoutInflater();
PilotSkillRow row = (PilotSkillRow) inflater.inflate(R.layout.pilotskillrow, mSkillListView, false);
Button skillminus = (Button) row.findViewById(R.id.skillminus);
skillminus.setOnTouchListener(new SkillButtonTouchListener(position, false));
....
}
I understand returning true on the on touch events means that the later ontouch events such as ACTION_MOVE and ACTION_UP should fire.
private class SkillButtonTouchListener implements OnTouchListener {
public SkillButtonTouchListener(int pos, boolean plus) {
...
}
public boolean onTouch(View view, MotionEvent motionevent) {
int action = motionevent.getActionMasked();
switch (action)
{
case MotionEvent.ACTION_MOVE:
Log.e("a", "MOVE EVENT");
....
return true;
case MotionEvent.ACTION_UP:
Log.e("a", "UP EVENT");
....
break;
case MotionEvent.ACTION_DOWN:
Log.e("a", "DOWN EVENT");
....
return true;
default:
break;
}
return false;
}
}
However, when I run the code, the DOWN even it displayed, but the MOVE and UP EVENTS simply are never displayed. Could it be related to the fact I have an inflated layout?
Anyone have any ideas what I am doing wrong?
Update: If I use the android debugger to connect to the button to check what is happening, the UP event fires when I step through after pressing the button. Probably because there is another process consuming the UP event?
Update 2: The problem is that the textview inside an inflated layout refuses to update. When I perform an invalidate event on the adapter, it will stop the currently processing touch events (no errors). I have another textview which I am able to manually update without the need of the adapter and this does not cause any problems. So it seems to be a problem specific towards RelativeLayout. I have a workaround to only invalidate the data during the ACTION_UP event but I'd rather update the textview on the fly.

setVisibilty() wont work first time

I have a layout called a controller in it i have a couple buttons and such
the problem is in my onTouch function i want to show it on one click and hide it on another. Now this works after 2 touches. The first touch is supposed to show the controller while the next is supposed to make it disappear. The first and second touches do nothing but on the third touch it works. Here are the related functions for this
public boolean onTouchEvent(MotionEvent event)
{
int eventx = event.getAction();
switch(eventx)
{
case MotionEvent.ACTION_DOWN:
if(isLifted)
{
if(!isVisible)
{
Log.i("onTouch", "called showPuse menu");
isVisible = true;
isPaused = true;
showPauseMenu();
}
else if(isVisible)
{
hidePauseMenu();
isVisible= false;
}
isLifted = false;
}
break;
case MotionEvent.ACTION_UP:
if(!isLifted)
{
isLifted = true;
//Log.i("onTouchEvent", "Lifted");
}
}
return false;
}
/***************************************************
* Shows All Views needed to be shown
* Also pauses video and audio
*
* *************************************************/
private void showPauseMenu()
{
Log.i("showPauseMenu", "called");
playButton.setVisibility(View.VISIBLE);
Log.i("showPauseMenu", "plaButton visible");
bottomButtons.setVisibility(View.VISIBLE);
Log.i("showPauseMenu", "bottom Menu showed");
playButton.invalidate();
bottomButtons.invalidate();
pauseMedia();
}
/************************************************
* Hides Views that are part of Pause Menu
* Also starts video and audio back again
*/
private void hidePauseMenu() {
playButton.setVisibility(View.GONE);
bottomButtons.setVisibility(View.GONE);
playMedia();
}
Can anyone say what the problem might be? I've been looking at this code for a couple of days now and cant see what it might be.
A few pointers about this code:
The isLifted variable presumably starts out false, and on the first touch event it causes nothing to happen on the down event. When the user lifts his/her finger the variable is set to true so the second event can actually be processed. This means the first touch will never have any visible effect.
You're using an isVisible boolean instead of just checking the visibility of the components themselves. This makes it very easy to get them out of sync.
Without the full class it's hard to tell, but I'd investigate both these points.
While designing the xml make the widget android:visibility="gone". During the program check the state if its hidden onclick set View.VISIBLE and if visible on second touchView.GONE.
I think this will work. Try it once.

Android: can't see ACTION_MOVE/UP in onTouchEvent of a RelativeLayout

I registered a listener to a RelativeLayout, see below. I'd like to add some custom event handling,
mOnTouchListener = new OnTouchListener() {
#Override
public boolean onTouch(View view, MotionEvent motionEvent) {
final int action = motionEvent.getAction();
boolean ret = false;
switch (action) {
case MotionEvent.ACTION_DOWN:
ret = doSth(motionEvent);
break;
case MotionEvent.ACTION_MOVE:
ret = doSth(motionEvent);
break;
case MotionEvent.ACTION_UP:
ret = doSth(motionEvent);
break;
}
return ret; // returning false got me none of MOVE/UP events right here
}
};
However, I can't get any MOVE/UP events unless returned true.
Another try, I registered same listener to a CheckBox, everything went quite well.
Is there difference between ViewGroup and Widget? Design purpose?
"However, I can't get any MOVE/UP events unless returned true."
You've answered your own question. If you don't return true indicating that you care about and are handling the event, the system will not send you the subsequent events, because you've told the system you don't care about it. If you need to monitor the entire life cycle of the touch event, you need to return true always.

How to get a continuous Touch Event?

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

Categories

Resources