onProgressChanged being run even when there is no progress change - java

I have an android app with two seekbars, and I've set them up so that the app doesn't respond unless both of them are pressed. My problem is that when they are both pressed down, the method onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) starts continuously running in an infinite loop. This happens even though I am holding my fingers completely still on the bars (which means that the progress is not changing on either of the bars, and therefore the onProgressChanged method shouldn't be being called). Does anyone have any insights as to why this could be happening?
Here is some relevant code:
void setupLRSpeedBars(final int UIID, final Bool bar1, final Bool bar2) {
SeekBar sb = (SeekBar) findViewById(UIID);
sb.setProgress(9);
sb.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (bar1.isTrue()) {
progress -= 9;
transmit((UIID == R.id.leftSpeedBar ? "L" : "R") +
(progress >= 0 ? "+" : "") +
Integer.toString(progress));
}
}
public void onStartTrackingTouch(SeekBar seekBar) {
bar2.set(true);
}
public void onStopTrackingTouch(SeekBar seekBar) {
bar2.set(false);
seekBar.setProgress(9);
//transmit((UIID == R.id.leftSpeedBar ? "L" : "R") + "+0");
transmit("s");
}
});
}
Also note that I've used a custom Bool class instead of a primitive boolean, in order for me to get around the restriction that all variables outside of the OnSeekBarChangeListener be marked final. (I need to send information between the onSeekBarChangeListeners in order to work out whether the user is holding their fingers down on both seekbars at the same time)
I have a hunch that this might be the cause of my problems, but I don't see how.
class Bool {
private boolean bool;
public Bool () {}
public Bool(boolean bool) { this.bool = bool; }
public void set (boolean bool) { this.bool = bool; }
public boolean get () { return bool; }
public boolean isTrue () { return bool; }
}
EDIT:
I'll also note that I'm using a custom, vertical seekbar. Here is the code:
package android.widget;
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.MotionEvent;
public class VerticalSeekBar extends SeekBar {
private OnSeekBarChangeListener myListener;
public VerticalSeekBar(Context context) {
super(context);
}
public VerticalSeekBar(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public VerticalSeekBar(Context context, AttributeSet attrs) {
super(context, attrs);
}
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(h, w, oldh, oldw);
}
#Override
protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(heightMeasureSpec, widthMeasureSpec);
setMeasuredDimension(getMeasuredHeight(), getMeasuredWidth());
}
#Override
public void setOnSeekBarChangeListener(OnSeekBarChangeListener mListener){
this.myListener = mListener;
}
#Override
public synchronized void setProgress(int progress){
super.setProgress(progress);
onSizeChanged(getWidth(), getHeight(), 0, 0);
}
protected void onDraw(Canvas c) {
c.rotate(-90);
c.translate(-getHeight(), 0);
super.onDraw(c);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
if (!isEnabled()) {
return false;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if(myListener!=null)
myListener.onStartTrackingTouch(this);
break;
case MotionEvent.ACTION_MOVE:
setProgress(getMax() - (int) (getMax() * event.getY() / getHeight()));
onSizeChanged(getWidth(), getHeight(), 0, 0);
myListener.onProgressChanged(this, getMax() - (int) (getMax() * event.getY() / getHeight()), true);
break;
case MotionEvent.ACTION_UP:
myListener.onStopTrackingTouch(this);
break;
case MotionEvent.ACTION_CANCEL:
break;
}
return true;
}
}

I think that the problem is in the onTouchEvent implementation of your VerticalSeekBar because you are processing every MotionEvent.ACTION_MOVE received.
From the documentation:
A new onTouchEvent() is triggered with an ACTION_MOVE event whenever the current touch contact position, pressure, or size changes. As described in Detecting Common Gestures, all of these events are recorded in the MotionEvent parameter of onTouchEvent().
Because finger-based touch isn't always the most precise form of interaction, detecting touch events is often based more on movement than on simple contact. To help apps distinguish between movement-based gestures (such as a swipe) and non-movement gestures (such as a single tap), Android includes the notion of "touch slop." Touch slop refers to the distance in pixels a user's touch can wander before the gesture is interpreted as a movement-based gesture. For more discussion of this topic, see Managing Touch Events in a ViewGroup.
That is, you think that your fingers are completely still but your seek bars are receiving ACTION_MOVE events.
In your case, the "touch slop" approximation is now a good idea because the calculated touch slop is huge for your purposes, as touch slop is defined as:
"Touch slop" refers to the distance in pixels a user's touch can wander before the gesture is interpreted as scrolling. Touch slop is typically used to prevent accidental scrolling when the user is performing some other touch operation, such as touching on-screen elements.
To solve your problem you can calculate the distance between the last managed position and the current one to trigger your onProgressChanged:
private static final float MOVE_PRECISION = 5; // You may want to tune this parameter
private float lastY;
// ...
#Override
public boolean onTouchEvent(MotionEvent event) {
if (!isEnabled()) {
return false;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastY = event.getY();
if (myListener != null)
myListener.onStartTrackingTouch(this);
break;
case MotionEvent.ACTION_MOVE:
if (calculateDistanceY(event) > MOVE_PRECISION) {
setProgress(getMax() - (int) (getMax() * event.getY() / getHeight()));
onSizeChanged(getWidth(), getHeight(), 0, 0);
myListener.onProgressChanged(this, getMax() - (int) (getMax() * event.getY() / getHeight()), true);
lastY = event.getY();
}
break;
case MotionEvent.ACTION_UP:
myListener.onStopTrackingTouch(this);
break;
case MotionEvent.ACTION_CANCEL:
break;
}
return true;
}
private float calculateDistanceY (MotionEvent event) {
return Math.abs(event.getY() - lastY);
}

Related

How to distinct between a tap on the right/ left side of the display?

I thought this is a pretty relevant and common question, but I couldnt find an answer.
At the moment I have this method:
public boolean onTouchEvent (MotionEvent evt){
if (evt.getAction() == MotionEvent.ACTION_DOWN) {
do stuff ...
}
}
So if the user taps on the screen (wherever) the code is executed. Now I want the distinction between the right side of the display and the left side (left side means --> go back).
You can do this many ways. Here is one of them:
Attach onTouch listener to the view, which stretches to its edges. (For example your RelativeLayout which holds rest of views)
private View.OnTouchListener onTouchListener = new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
float halfOfAScreen = mainLayout.getMaxWidth() / 2;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
float fingerPosition = event.getX();
if(fingerPosition < halfOfAScreen) {
onBackPressed();
}
return true;
default:
return false;
}
}
};
Refer to this post on how to get touch position.
It seems in your case you will use
int x = (int)event.getX();
int y = (int)event.getY();
and work within the bounds of your layout that you want the app to react to.

Detecting a SwipeGesture on a ViewGroup and all its child views

I have a RelativeLayout that contains many child views with various touch events. I want to get notified when the user swipes anywhere on the parent RelativeLayout so I can update some UI while still letting the child views handle their own touch/drag events. What is the standard way of accomplishing this for Android?
I was thinking that I could put an overlay over all the views and have it detect swipe gestures and if it wasn't a swipe I could pass the touch event on to other views in the hierarchy. It doesn't seem like Android supports that sort of touch detection and once one view decides to see if a event is a certain gesture no other views will be able to see the events.
A swipe gesture consists of three touch events: ACTION_DOWN, ACTION_MOVE and ACTION_UP. You need to record all three events and then see if it was a swipe or not. If it was not a swipe then we would need to pass those events to other child views to see if it meets their criteria for the gesture they are looking for. If it is a swipe we would want to block the events from being sent to the child view. Just not sure if this is actually possible.
Update
Using the ideas of the users in the answers section I was able to write a layout that met my specification. This RelativeLayout just handles right and left swipes but could be added to to handle more directions. OnSwipeListener is just an interface with two methods void swipedLeft() and void swipedRight().
public class SwipeRelativeLayout extends RelativeLayout {
public OnSwipeListener mSwipeListener = null;
private static final int SWIPE_DISTANCE_THRESHOLD = 100;
private float mStartX = 0;
private float mStartY = 0;
private float mEndX = 0;
private float mEndY = 0;
public SwipeRelativeLayout(Context context) {
super(context);
}
public SwipeRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SwipeRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public SwipeRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean handled = onTouchEvent(event);
if (event.getAction() == MotionEvent.ACTION_UP) return handled;
return false;
}
#Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
mStartX = event.getRawX();
mStartY = event.getRawY();
break;
}
case MotionEvent.ACTION_MOVE: {
float distanceX = event.getRawX() - mStartX;
float distanceY = event.getRawY() - mStartY;
if (Math.abs(distanceX) > Math.abs(distanceY) && Math.abs(distanceX) > SWIPE_DISTANCE_THRESHOLD) {
if (distanceX > 0) {
if (mSwipeListener != null) mSwipeListener.swipedRight();
} else {
if (mSwipeListener != null) mSwipeListener.swipedLeft();
}
return true;
}
return false;
}
}
return true;
}
}
When a touch event occurs it is first passed to the parent layout view and passed on to child view via onInterceptTouchEvent returning true or false. You want to override and intercept the touch events on the parent RelativeLayout and determine if you have seen a swipe gesture or not. If you have seen a swipe you want to return that you have handled it. In this case ACTION_UP is the end of a possible swipe and if your onTouchEvent handled the event then you can return true and the views below it will not get the finishing event and thus ignore their gestures.
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean handled = onTouchEvent(event);
if (event.getAction() == MotionEvent.ACTION_UP) return handled;
return false;
}

Cannot get the invalidate statement to work

package com.game.crazyeights;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.view.MotionEvent;
import android.view.View;
import com.game.crazyeights.R;
public class TitleView extends View {
private Bitmap titleGraphic;
private Bitmap playButtonUp;
private Bitmap playButtonDown;
private boolean playButtonPressed;
private int screenH;
private int screenW;
public TitleView(Context context) {
super(context);
titleGraphic = BitmapFactory.decodeResource(getResources(), R.drawable.titlegraphic);
playButtonUp = BitmapFactory.decodeResource(getResources(), R.drawable.play_button_up);
playButtonDown = BitmapFactory.decodeResource(getResources(), R.drawable.play_button_down);
}
#Override
public void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
screenW = w;
screenH = h;
}
#Override
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(titleGraphic, (screenW-titleGraphic.getWidth())/2 , 0, null);
if (playButtonPressed) {
canvas.drawBitmap(playButtonDown, (screenW-playButtonUp.getWidth())/2, (int)(screenH*0.7), null);
} else {
canvas.drawBitmap(playButtonUp, (screenW-playButtonUp.getWidth())/2, (int)(screenH*0.7), null);
}
}
public boolean onTouchedEvent(MotionEvent event) {
int eventaction = event.getAction();
int X = (int)event.getX();
int Y = (int)event.getY();
switch (eventaction) {
case MotionEvent.ACTION_DOWN:
if (X > (screenW-playButtonUp.getWidth())/2 && X < (screenW-playButtonUp.getWidth())/2 + playButtonUp.getWidth() && Y > (int) (screenH*0.7) && Y < (int) (screenH*0.7) + playButtonUp.getHeight()) {
playButtonPressed = true;
}
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
playButtonPressed = false;
break;
}
invalidate();
return true;
}
}
Basically, I tried to put a invalidate at the end to redraw the screen to update the buttons state. What I mean by that is when the button is pressed down on it has a picture of it in a different state I wanna update to but, it doesn't do that when I use the invalidate. And when the user lets go of the button it updates back to the regular button state image..
If you didn't understand what I said above ill try to simplify it. I want the button to update its image when pressed down on, and to go back to its previous image when you stop pressing the button. I tried using invalidate but, it didn't work. Any idea here?
You should not place invalidate() at the end of the method. OnTouchedEvent() is fired every time you touch or move within that View.
Make sure that case within MotionEvent.ACTION_DOWN is called. I mean those boolean expressions are valid. Then call invalidate() at the end of that case.
Also I this this method is cumbersome and unnecessary. Instead you can use ImageView.
Good luck!

Changing properties of a view during drag

I have this code in the ACTION_DOWN of my onTouch(View v) method as follows...
if (event.getAction() == MotionEvent.ACTION_DOWN) {
DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(view);
view.startDrag(null, shadowBuilder, view, 0);
myOnClickListener.onClick(view);
return true;
}
Now my view in this case is a ChessPiece. I have chessPiece.setBackground(R.color.black) and chessPiece.setImageBitmap(pawnBitmap).
Currently when I drag the chess piece, it drags the background color with it, which I dont want. So is there anyway to remove the background of the view when it's dragging so that the DragShadow is just of the bitmap?
You have to create your own dragshadow. This example helped me when I wanted to play with the dragshadow: http://lemonycode.blogspot.be/2012/11/using-custom-dragshadowbuidler-in.html
As you can see, there are two important methods:
public void onProvideShadowMetrics(Point shadowSize, Point touchPoint)
and:
public void onDrawShadow(Canvas canvas)
The first method is used to size the dragshadow and the second to actually draw your shadow. In your case, your chess piece. You could use
Bitmap bitmap = ((BitmapDrawable)image.getDrawable()).getBitmap();
to get the bitmap and alter it before drawing it on the canvas.
On OnDragListener()
private final class MyDragListener implements OnDragListener {
#Override
public boolean onDrag(View v, DragEvent event) {
switch (event.getAction()) {
case DragEvent.ACTION_DRAG_STARTED:
getResources().getDrawable(android.graphics.Color.TRANSPARENT);
break;
....
}
return true;
}
}
This should work. Good luck!

Drag an drop a button on android .. help

i'm trying to drag and drop a button the problem is that when i use getX() at motion event it works but the button starts to tremble . When i call the method getRawX() it does not tremble but it jumps at least 80px right before i start the drag and drop .
how can i managed that , i'll post my code here:
public class MyButton extends Button {
private final static int START_DRAGGING = 0;
private final static int STOP_DRAGGING = 1;
private int status;
private LinearLayout parentLayout;
public MyButton(Context context) {
super(context);
}
public MyButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
// Log.i("teste", "Button width: " + btWidth + ", Height : "+ btHeight);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
status = START_DRAGGING;
Log.i("teste", "Coordenada on ACTION_DOWN: " + (int) event.getRawX());
break;
case MotionEvent.ACTION_UP:
status = STOP_DRAGGING;
Log.i("teste", "Coordenada on ACTION_UP: " + (int) event.getRawX());
break;
case MotionEvent.ACTION_MOVE:
if(status == START_DRAGGING){
parentLayout.setPadding((int)event.getRawX(), 0,0,0);
parentLayout.invalidate();
Log.i("teste", "Coordenada on ACTION_MOVE: " + (int) event.getRawX());
}
break;
}
return true;
}
}
event.getX() returns touch coordinates relative to your view (the button), event.getRawX() returns touch coordinates relative to the display, so I would think the first way is the correct one, if you set the padding of the button, instead of the layout. But you'll still have the "jump" problem because you're supposed to touch the button, not its edge, and the first move will put the edge under your finger.
I would try using a GestureDetector, its OnGestureListener has an onScroll() method that gives you the scrolling distance (it does the job of remembering last position and giving a relative motion), so that you can add that value to the padding, that is, you drag 10px => you add 10px of padding.
code example:
private GestureDetector gd =
new GestureDetector(getContext(), new SimpleOnGestureListener() {
#Override
public boolean onScroll(MotionEvent e1, MotionEvent e2,
float distanceX, float distanceY) {
setPadding((int)(getPaddingLeft()+distanceX),0,0,0);
}
#Override
public boolean onDown(MotionEvent e) {
return true; // else no event will be handled
}
I would also add some checks to prevent negative or excessive paddings.
I have been working on touching and moving objects too. I started out making something move in a similar way: changing the top and left padding. Pretty quickly, as I added more moveable objects to the screen, things got a bit confusing. I would touch one spot on the screen and something else would start moving. What was going on was there were multiple overlapping views on the screen. Only the top view would receive the touch events.
I found a post that suggested taking a look at the Android Launcher code and seeing how they did drag and drop. I think their approach is really good. You do have to add a ViewGroup to hold your moveable objects, but that works out. The bounds of views match what you see on the screen and you end up with no surprises. Events go to the view you expect.
If it turns out you are going to have more than one moveable object, you might want to take a look at my blog post: "Moving Views In Android". More explanations about the Android Launcher and source code are there.

Categories

Resources