Android / kindle drag drop freezing occasionally - android

I'm not sure if this is a device issue or an issue with my code but I created a simple draggable sortable list using a relative layout and image views. I have very specific reasons for doing it this way and that is not my question.
The problem I'm having is occasionally it will totally freeze my app. The item will get stuck in the dragging state. If I lift my finger the shadow (dragging) object is still on the screen and if I touch the screen it will move to that spot. This will go on for about a minute and then I will get an error saying the app is non responsive with the option to kill it or wait. The only useful bit in the logcat is as follows:
12-09 14:23:13.157: W/WindowManager(16415): Drag is in progress but there is no drag window handle.
Then when the app times out I get this as an error
12-09 14:59:09.782: E/ActivityManager(16415): ANR in com.appname.appname (com.appname.appname/.MainActivity)
12-09 14:59:09.782: E/ActivityManager(16415): Reason: keyDispatchingTimedOut
I googled this error message and the only info was someone with no drag listener and another person saying it was the device touch sensor not keeping up.
Ideally, I'd love to fix this error and prevent it from happening in the first place. It does seem to happen mostly if I drag quickly, but I can't very well ask my users not to drag quickly... right?
Alternatively, is there a way that I could detect that dragging has frozen the app and interrupt the drag. Like set a timer on the touch listener and if there are no drag_location messages within like a second or two interrupt the drag? The timer stuff I know how to do, but I don't know how I would force the drag to stop while it's frozen. Any ideas?
Here's the code:
setup
//happens once when the app loads
RelativeLayout trackList = (RelativeLayout) findViewById(R.id.nsTrackList1);
trackList.setOnDragListener(new MyTrackDragListener(this));
//happens in a loop for each "track" (image view)
trackButton = new ImageView(this);
trackButton.setImageDrawable(nsPackages[trackId].banner[bannerSizeSelector]);
trackButton.setOnTouchListener(new MyTrackTouchListener());
On touch
public class MyTrackTouchListener implements OnTouchListener {
boolean isDragging=false;
float prevX, prevY;
public boolean onTouch(View view, MotionEvent motionEvent) {
if(motionEvent.getPointerCount() < 2 && !isDragging) return false;
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
isDragging=false;
prevX=0;
prevY=0;
return true;
} else if (motionEvent.getAction() == MotionEvent.ACTION_MOVE) {
if(isDragging) return true;
boolean wasFirst = (prevX == 0 || prevY == 0);
float theTotalDiff = Math.abs(prevX - motionEvent.getX()) + Math.abs(prevY - motionEvent.getY());
prevX=motionEvent.getX();
prevY=motionEvent.getY();
if(wasFirst) return true;
if(theTotalDiff <3) return true;
ClipData data = ClipData.newPlainText("", "");
DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(view);
view.startDrag(data, shadowBuilder, view, 0);
int thisViewId = view.getId();
//hide view
view.setVisibility(View.GONE);
isDragging=true;
return true;
} else if (motionEvent.getAction() == MotionEvent.ACTION_UP || motionEvent.getAction() == MotionEvent.ACTION_CANCEL) {
isDragging=false;
return true;
}else {
Integer thisAction = motionEvent.getAction();
Log.d("looper","Motion action: "+thisAction.toString());
return false;
}
}
}
On Drag
class MyTrackDragListener implements OnDragListener {
public static boolean isDragging=false;
private MainActivity parent;
public MyTrackDragListener(MainActivity myAct){
parent=myAct;
}
#Override
public boolean onDrag(View v, DragEvent event) {
int action = event.getAction();
switch (event.getAction()) {
case DragEvent.ACTION_DRAG_STARTED:
isDragging=true;
// do nothing
return true;
case DragEvent.ACTION_DROP:
View view = (View) event.getLocalState();
parent.doDropSort(view,(int) event.getY());
return true;
case DragEvent.ACTION_DRAG_ENDED:
if(isDragging && event.getResult()==false){
View view2 = (View) event.getLocalState();
parent.doDropSort(view2,(int) event.getY(),true);
return true;
}
isDragging=false;
break;
case DragEvent.ACTION_DRAG_LOCATION:
parent.doDragHover((int) event.getY());
return true;
default:
Log.d("looper","drag other... "+String.valueOf(event.getAction()));
}
return false;
}
}
A few things I already tried
Removing the drag listener entirely
Always return true from onDrag
Always return false from onDrag
Basically every combination of return true/false in drag and touch
Removing the 2 finger and Action_Move part and triggering drag on Action_down instead
Same results. Drag and drop works perfectly about 70% of the time and then suddenly does the freezing behavior described above. Sometimes it's on the very first drag sometimes it's after several. I've noticed on consistent pattern except possibly drag speed. It seems to USUALLY happen when I'm dragging quickly, but drag direction or where I drag to doesn't seem to matter.

YAY! I Figured it out. Here's the answer incase it helps someone in the future.
I finally narrowed it down to only the top list item (don't know how I missed that) and then started commenting out lines until it didn't break which narrowed it down to this line:
view.setVisibility(View.GONE);
The way my list was set up each item used the Layout Property BELOW in a relative layout (except the top item). Once I removed the top item from the view the 2nd to top item was below an item which was visibility GONE so it didn't know what to do. The solution was to set the parameter of the next item in the view. Luckily I manually set the view IDs of my track buttons (there are other views that scroll too.. labels and such) so they are sequential. So my fix was simply finding the item with id+1 and making it become the top item by setting BELOW to zero like this...
// the top item in my list is ALWAYS view id 1
if(thisViewId == 1){
ImageView nextTrackButton = (ImageView) trackList.findViewById(thisViewId+1);
LayoutParams ntLP = (LayoutParams) nextTrackButton.getLayoutParams();
ntLP.addRule(RelativeLayout.BELOW,0);
nextTrackButton.setLayoutParams(ntLP);
}
//hide view
view.setVisibility(View.GONE);

Related

onTouch: detect ACTION_UP without blocking other touch events

I have a custom view which acts as a button. I am drawing all the canvas myself. Now I'm making an outline when ACTION_DOWN and remove it after ACTION_UP or ACTION_CANCEL.
#Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e("test", "ACTION_DOWN");
break;
case MotionEvent.ACTION_UP:
Log.e("test", "ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.e("test", "ACTION_CANCEL");
break;
}
return true;
}
This can work for me, except now it is blocking another gesture behind this view which is detecting ACTION_MOVE (scroll left).
If I return false, then it is working fine but now ACTION_UP is not called.
I want to call ACTION_UP if finger is lifted, but pass events down otherwise.
Have you tried overriding dispatchTouchEvent?
https://developer.android.com/reference/android/view/View.html#dispatchTouchEvent(android.view.MotionEvent)
UPDATE:
So touch events are a bit of a beast. The rundown of it is this...
They bubble up at first from your root container in your Activity. This is done by calling dispatchTouchEvent and then onInterceptTouchEvent assuming intercepting wasn't blocked by a child view.
If no view intercepts the event, it will bubble to the leaf node (such as a button) where onTouch is called. If the node doesn't handle it (returns true) its parent gets a chance and so on.
This means that you can use dispatchTouchEvent or onInterceptTouchEvent to spy on touch events without changing the behavior. Unless you're actually going to intercept the event I suggest using dispatchTouchEvent as it's guaranteed to run whereas intercepting may be blocked (example: DrawerLayout will intercept touch events near the edge in order to open the drawer).
So the final result is:
public class MyView extends Button {
#Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
Log.e("test", "ACTION_DOWN");
break;
case MotionEvent.ACTION_UP:
Log.e("test", "ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.e("test", "ACTION_CANCEL");
break;
}
return super.dispatchTouchEvent(event);
}
}
UPDATE:
Sorry, so I've been under the impression for some reason (mostly my poor reading) that we were dealing with the parent. Here's what I would do...
Go ahead and implement onTouch and return true to consume all the events. This means that any touch events that start on your view will be eaten up. What we'll do then is translate the point to the parent's coordinate space and manually pass the touch event up, it'll look like this inside your custom view...
private boolean passingTouchEventToParent = true;
final private Rect hitRect = Rect();
#Override
public boolean onTouch(MotionEvent event) {
// Handle your custom logic here
final ViewParent viewParent = getParent();
if (passingTouchEventToParent &&
viewParent != null &&
viewParent instanceof View) {
// Gets this view's hit rectangle in the parent's space
getHitRect(hitRect);
event.offsetLocation((float) hitRect.left, (float) hitRect.top);
passingTouchEventToParent = viewParent.onTouchEvent(event);
}
if (event.getActionMasked() == MotionEvent.ACTION_UP) {
// Motion event finished, reset passingTouchEventToParent
passingTouchEventToParent = true;
}
return true;
}

Motion event 'MotionEvent.ACTION_MOVE' not registered on touch

I'm trying to create a dragable view and to change drop target view's alpha depending on distance from dragged view to drop target view. For this I need a way to continually check current position of dragged view. I was trying to implement it using OnTouchListener.
Code in MainActivity's method onCreate:
myTextView.setOnTouchListener(new MyTouchListener());
Code for class MyTouchListener:
class MyTouchListener implements View.OnTouchListener {
#Override
public boolean onTouch(View view, MotionEvent motionEvent) {
Log.i("i", "Touched");
switch (motionEvent.getAction()) {
case MotionEvent.ACTION_DOWN: {
ClipData clipData = ClipData.newPlainText("", "");
View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(view);
view.startDrag(clipData, shadowBuilder, view, 0);
view.setVisibility(View.INVISIBLE);
break;
}
case MotionEvent.ACTION_MOVE: {
Log.i("i", "this one is never called");
break;
}
}
return true;
}
}
After I start dragging my textView, log outputs "Touched" two times and nothing else. Can anybody tell me why case MotionEvent.ACTION_MOVE is never called?
If this is not the way to implement what I'm trying to do, could you guide me to more proper way?
It has something to do with
view.startDrag(clipData, shadowBuilder, view, 0);
If you comment this line out then ACTION_MOVE events will work as they should. In my opinion this startDrag method somehow eliminates any listening to the original view. While dragging it gives a signal to every other visible view on the screen that there is an object being dragged so other objects could listen to dragging events and nothing else besides that.
UPDATE:
I've actually just found the same question with closely the same answer here:
onTouch() ACTION_MOVE case is not called while dragging

Looking for a listener that differentiates between up and down events

I apologise for the somewhat noobish question.
I've tried googling it, searching this site and the android developer site, yet didn't manage to find an answer.
I'm looking for a listener that differentiates between up and down events. Currently I'm using OnTouchListener, but it gets triggered very fast, causing a noticable lag, even when I immediately return after an event that is not Down and Up.
I've also tried OnClickListener, but it seems to only get triggered when you lift your finger up, rather than have seperate events for down click and up click like I need.
Would appreciate some help,
Thanks!
YourBtn.setOnTouchListener(new View.OnTouchListener(){
#Override
public boolean onTouch(View v, MotionEvent event){
if(event.getAction() == MotionEvent.ACTION_UP){
//do stuff
}
else if(event.getAction() == MotionEvent.ACTION_DOWN){
//do stuff
}
else if(event.getAction() == MotionEvent.ACTION_CANCEL){
//do stuff
}
return true; //return false can be used it depends
}
});
P.S: it can be: a button, a textview, an imageview or any UI element
I strongly recommend to always add ACTION_CANCEL to avoid any misbehaviours or crashes
See this:
https://developer.android.com/training/graphics/opengl/touch.html
Override the following method on your activity, then you can treat each case, when the user touch the screen, and when he take of his finger.
#Override
public boolean onTouchEvent(MotionEvent e) {
// MotionEvent reports input details from the touch screen
// and other input controls. In this case, you are only
// interested in events where the touch position changed.
float x = e.getX();
float y = e.getY();
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN:
// do your stuff
break;
case MotionEvent.ACTION_UP:
// do your stuff
break;
}
return true;
}
You should look at GestureDetector to simplify your touch event handling.

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.

Overriding onTouchEvent competing with ScrollView

From a simplistic overview I have a custom View that contains some bitmaps the user can drag around and resize.
The way I do this is fairly standard as in I override onTouchEvent in my CustomView and check if the user is touching within an image, etc.
My problem comes when I want to place this CustomView in a ScrollView. This works, but the ScrollView and the CustomView seem to compete for MotionEvents, i.e. when I try to drag an image it either moves sluggishly or the view scrolls.
I'm thinking I may have to extend a ScrollView so I can override onInterceptTouchEvent and let it know if the user is within the bounds of an image not to try and scroll. But then because the ScrollView is higher up in the hierarchy how would I get access to the CustomView's current state?
Is there a better way?
Normally Android uses a long press to begin a drag in cases like these since it helps disambiguate when the user intends to drag an item vs. scroll the item's container. But if you have an unambiguous signal when the user begins dragging an item, try getParent().requestDisallowInterceptTouchEvent(true) from the custom view when you know the user is beginning a drag. (Docs for this method here.) This will prevent the ScrollView from intercepting touch events until the end of the current gesture.
None of the solutions found worked "out of the box" for me, probably because my custom view extends View, not ViewGroup, and thus I can't implement onInterceptTouchEvent.
Also calling getParent().requestDisallowInterceptTouchEvent(true) was throwing NPE, or doing nothing at all.
Finally this is how I solved the problem:
Inside your custom onTouchEvent call requestDisallow... when your view will take care of the event. For example:
#Override
public boolean onTouchEvent(MotionEvent event) {
Point pt = new Point( (int)event.getX(), (int)event.getY() );
if (event.getAction() == MotionEvent.ACTION_DOWN) {
if (/*this is an interesting event my View will handle*/) {
// here is the fix! now without NPE
if (getParent() != null) {
getParent().requestDisallowInterceptTouchEvent(true);
}
clicked_on_image = true;
}
} else if (event.getAction() == MotionEvent.ACTION_MOVE) {
if (clicked_on_image) {
//do stuff, drag the image or whatever
}
} else if (event.getAction() == MotionEvent.ACTION_UP) {
clicked_on_image = false;
}
return true;
}
Now my custom view works fine, handling some events and letting scrollView catch the ones we don't care about. Found the solution here: http://android-devblog.blogspot.com.es/2011/01/scrolling-inside-scrollview.html
Hope it helps.
There is an Android event called MotionEvent.ACTION_CANCEL (value = 3). All I do is override my custom control's onTouchEvent method and capture this value. If I detect this condition then I respond accordingly.
Here is some code:
#Override
public boolean onTouchEvent(MotionEvent event) {
if(isTouchable) {
int maskedAction = event.getActionMasked();
if (maskedAction == MotionEvent.ACTION_DOWN) {
this.setTextColor(resources.getColor(R.color.octane_orange));
initialClick = event.getX();
} else if (maskedAction == MotionEvent.ACTION_UP) {
this.setTextColor(defaultTextColor);
endingClick = event.getX();
checkIfSwipeOrClick(initialClick, endingClick, range);
} else if(maskedAction == MotionEvent.ACTION_CANCEL)
this.setTextColor(defaultTextColor);
}
return true;
}

Categories

Resources