In my Android game I have implemented a custom code in the onTouchEvent method of a (custom) SurfaceView to emulate a ScrollView. I already tried an actual ScrollView, but due to performance and lack of customizability I preferred to override onTouchEvent myself.
It works perfectly, but I cannot properly emulate the inertia effect typical of scrollviews.
What I did right now is this:
int beginRawY = 0;
int beginScrollValue = 0;
Integer firstPointerId = null;
[...]
private boolean onTouchEvent(MotionEvent event) {
int index = event.getActionIndex();
int action = event.getActionMasked();
int pointerId = event.getPointerId(index);
int rawY = (int) event.getRawY();
// This is meant to deal with multitouch: scroll only if I'm using "the same finger".
boolean rightPointer = firstPointerId != null && firstPointerId == pointerId;
switch (action) {
case MotionEvent.ACTION_DOWN:
initVelocityTracker(event);
if(firstPointerId == null) {
// Save initial scrollValue and touch position
beginRawY = rawY;
beginScrollValue = scrollValue;
firstPointerId = pointerId;
return true;
}
case MotionEvent.ACTION_MOVE:
if(rightPointer) {
updateVelocity(event);
// Updates scroll value to new scroll value
scrollValue = beginScrollValue -rawY + beginRawY;
return true;
}
case MotionEvent.ACTION_UP:
if(rightPointer) {
//Start the Dissipator runnable, passing to it the speed of the player's touch (number of pixels in 30 milliseconds)
dissipator.setVelocity(updateVelocity(event).pixelY());
dissipator.run();
firstPointerId = null;
return true;
}
}
return false;
}
private void initVelocityTracker(MotionEvent event) {
if(mVelocityTracker == null) {
// Retrieve a new VelocityTracker object to watch the velocity of a motion.
mVelocityTracker = VelocityTracker.obtain();
}
else {
// Reset the velocity tracker back to its initial state.
mVelocityTracker.clear();
}
// Add a user's movement to the tracker.
mVelocityTracker.addMovement(event);
}
private PixelDot updateVelocity(MotionEvent event) {
int pointerId = event.getPointerId(event.getActionIndex());
mVelocityTracker.addMovement(event);
mVelocityTracker.computeCurrentVelocity(30);
return new PixelDot(
VelocityTrackerCompat.getXVelocity(mVelocityTracker, pointerId),
VelocityTrackerCompat.getYVelocity(mVelocityTracker, pointerId));
}
[...]
private class DissipatorRunnable implements Runnable {
private float velocity = 0;
public void setVelocity(float velocity) {
this.velocity = velocity;
}
#Override
public void run() {
// Simply linear descreasing of the scroll speed
float vValue = velocity;
if(velocity > 0) {
while (vValue > 0 && scrollValue > 0) {
scrollValue = scrollValue - vValue;
vValue -= 5;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} else {
while (vValue < 0 && scrollValue < height) {
scrollValue = scrollValue - vValue;
vValue += 5;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
Code is simplier than it looks:
I use a ScrollValue to represent "how much the player scrolled". It is the number of pixels.
On Touch Down a save th eposition of the finger and the actual scrollvalue. I initialize a VelocityTracker too.
On Touch Move I update the velocity tracker and the scrollvalue.
On Touch Up I start a custom Runnable called Dissipator:
Every tot milliseconds I reduce (or increment) scrollvalue by the last tracked velocity, and then reduce the velocity value.
This kinda works, but the effect doesn't look like a scrollview with proper inertia, inertia is instead silly and too strong.
What should I do to emulate standard scrollviews inertia?
Here is the code I am using:
// Handling Touch Events
#Override
public boolean onTouch(View view, MotionEvent motionEvent) {
float onOffBitmapWidth = this.onOffBitmap.getWidth();
if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
if (this.touchMove) {
if (togglePositionX > this.onOffBitmap.getWidth() / 4.0f) {
togglePositionX = this.onOffBitmap.getWidth() / 2.0f - this.toggleBitmap.getWidth()/4.0f;
} else if (togglePositionX <= this.onOffBitmap.getWidth() / 4.0f) {
togglePositionX = 0.0f;
}
this.invalidate();
this.touchMove = false;
return true;
} else {
return false;
}
} else if (motionEvent.getAction() == MotionEvent.ACTION_CANCEL) {
this.touchMove = false;
this.invalidate();
} else if (motionEvent.getAction() == MotionEvent.ACTION_MOVE) {
this.touchMove = true;
float currentX = motionEvent.getX();
if (currentX > 0.0f && currentX < (this.onOffBitmap.getWidth() / 2.0f - this.toggleBitmap.getWidth()/4.0f)) {
togglePositionX = currentX;
} else if (currentX >= (this.onOffBitmap.getWidth() / 2.0f - this.toggleBitmap.getWidth()/4.0f)) {
togglePositionX = this.onOffBitmap.getWidth() / 2.0f - this.toggleBitmap.getWidth()/4.0f;
} else if (currentX <= 0.0f) {
togglePositionX = 0.0f;
}
this.invalidate();
return true;
}
return true;
}
#Override
public void onClick(View v) {
if (togglePositionX == 0.0f) {
togglePositionX = this.onOffBitmap.getWidth() / 2.0f - this.toggleBitmap.getWidth()/4.0f;
} else {
togglePositionX = 0.0f;
}
this.invalidate();
}
I am using onClick event for single tap event. The problem is that ACTION_MOVE is called even if I only tap the screen. I even do it in a funny way (with the very tip of my finger).
I end up using an array list containing the history of touch positions that the user performs on the view + a flag to detect wether it's a real ACTION_MOVE or not. Here is the code that I implemented inside if (motionEvent.getAction() == MotionEvent.ACTION_MOVE)
float currentX = motionEvent.getX();
this.userTouchMoveArray.add(currentX);
if (this.userTouchMoveArray.size() == 1) {
touchIsMoved = false;
} else {
float oldestX = userTouchMoveArray.get(0);
if (Math.abs(currentX - oldestX) > 2.0f) {
touchIsMoved = true;
} else {
touchIsMoved = false;
}
}
Working like a charm. (You can define your own tolerance, here I am using 2 px)
I'm trying to make a touch event that won't be activated until the finger has moved a few units from the initial position.
So far I've set up my onTouch method like this:
private XYEvents xyEvent = new XYEvents();
public boolean motionTracker(MotionEvent event, int n)
{
int note = n;
switch(event.getAction())
{
case MotionEvent.ACTION_DOWN:
xyEvent.setInitial(event);
playNote(note);
break;
case MotionEvent.ACTION_MOVE:
byte data1;
byte data2;
//I figured I should input a condition to check if the finger has moved a few units before it should start doing stuff like so:
if (xyEvent.getXThreshold(event))
{
int xMod = xyEvent.eventActions(event)[0];
data1 = (byte) (xMod & 0x7f);
data2 = (byte) ((xMod & 0x7f00) >> 8);
xModulation((int)data1, (int)data2);
}
break;
}
This method is the one I'm having problems with:
private float initialX, initialY;
private int xValue;
boolean getXThreshold(MotionEvent event)
{
float deltaX = event.getX();
float threshold = 10;
float condition = (deltaX - initialX);
if(condition <= threshold || condition >= -threshold )
return false;
else
return true;
}
the getXThreshold method seems to do what it's supposed to in another method that looks like this:
public int[] eventActions(MotionEvent event)
{
int value = xValue;
int xNull = 8192;
if(!getXThreshold(event))
xValue = xNull;
if(getXThreshold(event))
xValue = xHandleMove(event, true);
return value;
}
Any suggestions?
/M
It seems that this argument:
if(condition <= threshold || condition >= -threshold )
return false;
else
return true;
Needed to be flipped around, otherwise it always returned false for some reason.
Now it looks like this and works great.
boolean getXThreshold(MotionEvent event)
{
float deltaX = event.getX();
float threshold = 10;
float condition = (deltaX - initialX);
return condition >= threshold || condition <= -threshold;
}
Have a great week!
/M
I have the following code:
public boolean onTouchEvent(MotionEvent event) {
fingerSize = event.getSize();
fingerPosX = event.getX();
fingerPosY = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
start = true;
break;
}
}
return true;
}
I use the three variables fingerSize, fingerPosX and fingerPosY to draw a circle:
canvas.drawCircle(fingerPosX, fingerPosY, fingerSize
* fingersizeCorrection, paint);
So basically the circle follows my finger as i move through the screen with my finger. The problem is that the movement of the circle as it tries to follow my finger is very choppy.
How this be improved to be more fluint?
this all happens in a SurfaceView, i also added a fps limiter like this:
int FRAMES_PER_SECOND = 60;
int SKIP_TICKS = 1000/FRAMES_PER_SECOND;
long next_game_tick = timer.getElapsedTimeMil();
int sleep_time = 0;
next_game_tick += SKIP_TICKS;
sleep_time = (int) (next_game_tick-timer.getElapsedTimeMil());
if(sleep_time >= 0){
try {
Thread.sleep(sleep_time);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
else{
}
I have a custom View with bitmaps on it that the user can drag about.
I want to make it so when they long click one of them I can pop up a context menu with options such as reset position etc.
In the custom View I add my OnLongClickListener:
this.setOnLongClickListener(new View.OnLongClickListener() {
#Override
public boolean onLongClick(View v) {
// show context menu..
return true;
}
});
And override onTouchEvent to look something like this:
public boolean onTouchEvent(MotionEvent event) {
handleDrag(event);
super.onTouchEvent(event);
return true;
}
The handleDrag function finds what object is been pressed, and handles updating it's position.
My problem is that when I start to drag an image the OnLongClickListener fires also. I'm not sure the best way around this.
I've tried adding a threshold to handleDrag to return false if user touches down but doesn't attempt to drag, but I'm finding it still difficult to get the correct handler fired.
Can anyone suggest a way to skip the OnLongClickListener while dragging?
I think I have this solved through tweaking my threshold approach.
First, I changed my onTouchEvent to look like this:
public boolean onTouchEvent(MotionEvent event) {
mMultiTouchController.handleDrag(event);
return super.onTouchEvent(event);
}
They now both fire, so I then changed my OnLongClickListener to the following:
this.setOnLongClickListener(new View.OnLongClickListener() {
#Override
public boolean onLongClick(View v) {
if (!mMultiTouchController.has_moved) {
// Pop menu and done...
return false;
}
return true;
}
});
(mMultiTouchController is the class containing all my gesture detection code).
The key here is within this class, I added the bool 'has_moved'. When I go to start a drag I then compute the delta:
float diffX = Math.abs(mCurrPtX - mPrevPt.getX());
float diffY = Math.abs(mCurrPtY - mPrevPt.getY());
if (diffX < threshold && diffY < threshold) {
has_moved = false;
return;
}
Now when the onLongClick fires I know whether to take action or not.
The final piece was to set:
setHapticFeedbackEnabled(false);
in my View so that the user doesn't get a vibrate every time the longClick fires but no action is taken. I plan to do the vibration manually as a next step.
This seems to be ok so far, hope that helps anyone who has come across a similar situation as this one.
I would stop using the onLongClickListener and just implement your own, which is pretty easy to do. Then you have the control you need to keep them from interfering with each other.
The following code implements the following gestures: drag, tap, double tap, long click, and pinch.
static final short NONE = 0;
static final short DRAG = 1;
static final short ZOOM = 2;
static final short TAP = 3;
static final short DOUBLE_TAP = 4;
static final short POST_GESTURE = 5;
short mode = NONE;
static final float MIN_PINCH_DISTANCE = 30f;
static final float MIN_DRAG_DISTANCE = 5f;
static final float DOUBLE_TAP_MAX_DISTANCE = 30f;
static final long MAX_DOUBLE_TAP_MS = 1000;
static final long LONG_PRESS_THRESHOLD_MS = 2000;
public class Vector2d {
public float x;
public float y;
public Vector2d() {
x = 0f;
y = 0f;
}
public void set(float newX, float newY) {
x = newX;
y = newY;
}
public Vector2d avgVector(Vector2d remote) {
Vector2d mid = new Vector2d();
mid.set((remote.x + x)/2, (remote.y + y)/2);
return mid;
}
public float length() {
return (float) Math.sqrt(x * x + y * y);
}
public float distance(Vector2d remote) {
float deltaX = remote.x - x;
float deltaY = remote.y - y;
return (float) Math.sqrt(deltaX * deltaX + deltaY * deltaY);
}
}
private Vector2d finger1 = new Vector2d();
private Vector2d finger2 = new Vector2d();
private Vector2d pinchStartDistance = new Vector2d();
private Vector2d pinchMidPoint;
private Vector2d fingerStartPoint = new Vector2d();
private long gestureStartTime;
private Marker selectedMarker;
#Override
public boolean onTouch(View v, MotionEvent event) {
// Dump touch event to log
dumpEvent(event);
// Handle touch events here...
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
finger1.set(event.getX(), event.getY());
if (mode == TAP) {
if (finger1.distance(fingerStartPoint) < DOUBLE_TAP_MAX_DISTANCE) {
mode = DOUBLE_TAP;
} else {
mode = NONE;
gestureStartTime = SystemClock.uptimeMillis();
}
} else {
gestureStartTime = SystemClock.uptimeMillis();
}
fingerStartPoint.set(event.getX(), event.getY());
break;
case MotionEvent.ACTION_POINTER_DOWN:
finger2.set(event.getX(1), event.getY(1));
pinchStartDistance.set(Math.abs(finger1.x - finger2.x), Math.abs(finger1.y - finger2.y));
Log.d(TAG, String.format("pinch start distance = %f, %f", pinchStartDistance.x, pinchStartDistance.y));
if (pinchStartDistance.length() > MIN_PINCH_DISTANCE) {
if (pinchStartDistance.x < MIN_PINCH_DISTANCE) {
pinchStartDistance.x = MIN_PINCH_DISTANCE;
}
if (pinchStartDistance.y < MIN_PINCH_DISTANCE) {
pinchStartDistance.y = MIN_PINCH_DISTANCE;
}
pinchMidPoint = finger1.avgVector(finger2);
mode = ZOOM;
Log.d(TAG, "mode=ZOOM" );
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
if (mode == ZOOM) {
Vector2d pinchEndDistance = new Vector2d();
pinchEndDistance.set(Math.abs(finger1.x - finger2.x), Math.abs(finger1.y - finger2.y));
if (pinchEndDistance.x < MIN_PINCH_DISTANCE) {
pinchEndDistance.x = MIN_PINCH_DISTANCE;
}
if (pinchEndDistance.y < MIN_PINCH_DISTANCE) {
pinchEndDistance.y = MIN_PINCH_DISTANCE;
}
Log.d(TAG, String.format("pinch end distance = %f, %f", pinchEndDistance.x, pinchEndDistance.y));
zoom(pinchMidPoint, pinchStartDistance.x/pinchEndDistance.x, pinchStartDistance.y/pinchEndDistance.y);
// Set mode to "POST_GESTURE" so that when the other finger lifts the handler won't think it was a
// tap or something.
mode = POST_GESTURE;
} else if (mode == NONE) {
// The finger wasn't moved enough for it to be considered a "drag", so it is either a tap
// or a "long press", depending on how long it was down.
if ((SystemClock.uptimeMillis() - gestureStartTime) < LONG_PRESS_THRESHOLD_MS) {
Log.d(TAG, "mode=TAP");
mode = TAP;
selectedMarker = checkForMarker(finger1);
if (selectedMarker != null) {
Log.d(TAG, "Selected marker, mode=NONE");
mode = NONE;
((Activity) parent).showDialog(ResultsActivity.DIALOG_MARKER_ID);
}
}
else {
Log.d(TAG, "mode=LONG_PRESS");
addMarker(finger1);
requestRender();
}
} else if (mode == DOUBLE_TAP && (SystemClock.uptimeMillis() - gestureStartTime) < MAX_DOUBLE_TAP_MS) {
// The finger was again not moved enough for it to be considered a "drag", so it is
// a double-tap. Change the center point and zoom in.
Log.d(TAG, "mode=DOUBLE_TAP");
zoom(fingerStartPoint, 0.5f, 0.5f);
mode = NONE;
} else {
mode = NONE;
Log.d(TAG, "mode=NONE" );
}
break;
case MotionEvent.ACTION_MOVE:
if (mode == NONE || mode == TAP || mode == DOUBLE_TAP) {
finger1.set(event.getX(), event.getY());
if (finger1.distance(fingerStartPoint) > MIN_DRAG_DISTANCE) {
Log.d(TAG, "mode=DRAG" );
mode = DRAG;
scroll(fingerStartPoint.x - finger1.x, fingerStartPoint.y - finger1.y);
}
}
else if (mode == DRAG) {
scroll(finger1.x - event.getX(), finger1.y - event.getY());
finger1.set(event.getX(), event.getY());
}
else if (mode == ZOOM) {
for (int i=0; i<event.getPointerCount(); i++) {
if (event.getPointerId(i) == 0) {
finger1.set(event.getX(i), event.getY(i));
}
else if (event.getPointerId(i) == 1) {
finger2.set(event.getX(i), event.getY(i));
}
else {
Log.w(TAG, String.format("Unknown motion event pointer id: %d", event.getPointerId(i)));
}
}
}
break;
}
return true;
}
/** Show an event in the LogCat view, for debugging */
private void dumpEvent(MotionEvent event) {
String names[] = { "DOWN" , "UP" , "MOVE" , "CANCEL" , "OUTSIDE" ,
"POINTER_DOWN" , "POINTER_UP" , "7?" , "8?" , "9?" };
StringBuilder sb = new StringBuilder();
int action = event.getAction();
int actionCode = action & MotionEvent.ACTION_MASK;
sb.append("event ACTION_" ).append(names[actionCode]);
if (actionCode == MotionEvent.ACTION_POINTER_DOWN
|| actionCode == MotionEvent.ACTION_POINTER_UP) {
sb.append("(pid " ).append(
action >> MotionEvent.ACTION_POINTER_ID_SHIFT);
sb.append(")" );
}
sb.append("[" );
for (int i = 0; i < event.getPointerCount(); i++) {
sb.append("#" ).append(i);
sb.append("(pid " ).append(event.getPointerId(i));
sb.append(")=" ).append((int) event.getX(i));
sb.append("," ).append((int) event.getY(i));
if (i + 1 < event.getPointerCount())
sb.append(";" );
}
sb.append("]" );
Log.d(TAG, sb.toString());
}
//This code is to handle the gestures detection
final Handler handler = new Handler();
private Runnable mLongPressRunnable;
detector = new GestureDetector(this, new MyGestureDectector());
view.setOnTouchListener(new OnTouchListener() {
#SuppressLint("ClickableViewAccessibility")
#SuppressWarnings("deprecation")
#Override
public boolean onTouch(View v, MotionEvent event) {
detector.onTouchEvent(event);
if (event.getAction() == MotionEvent.ACTION_DOWN) {
handler.postDelayed(mLongPressRunnable, 1000);
}
if ((event.getAction() == MotionEvent.ACTION_MOVE)
|| (event.getAction() == MotionEvent.ACTION_UP)) {
handler.removeCallbacks(mLongPressRunnable);
}
}
return true;
}
});
mLongPressRunnable = new Runnable() {
public void run() {
Toast.makeText(MainActivity.this, "long", Toast.LENGTH_SHORT)
.show();
}
};
class MyGestureDectector implements GestureDetector.OnDoubleTapListener,
OnGestureListener {
//Implement all the methods
}