Jittery behaviour when scrolling within bounds - android

After a long hiatus, I've come back to a project I was working on last year which involves trying to scroll around a grid. I achieve this by handling the ACTION_MOVE case in the onTouchEvent of a custom class extending Canvas, and it all works very smoothly.
My problem is that I only want the user to be able to scroll the canvas so far, and after some boundaries have been reached to stop the scrolling. So I tried this:
case MotionEvent.ACTION_MOVE:
pointerIndex = event.findPointerIndex(activePointerId);
pointer.set(event.getX(pointerIndex),event.getY(pointerIndex));
//compute how far the finger slid
float dx = pointer.x - start.x;
float dy = pointer.y - start.y;
//adjust displacement values so that we don't go beyond the bounds
//determined by the (transformed) corners
matrix.mapPoints(transformedCorners,originalCorners);
//horizontal distance between left corners
float maxHorDisp = originalCorners[0] - transformedCorners[0];
//horizontal distance between right corners
float minHorDisp = originalCorners[2] - transformedCorners[2];
//vertical distance between top corners
float maxVertDisp = originalCorners[1] - transformedCorners[1];
//vertical distance between bottom corners
float minVertDisp = originalCorners[3] - transformedCorners[3];
dx = Math.max(minHorDisp,Math.min(maxHorDisp,dx);
dy = Math.max(minVertDisp, Math.min(maxVertDisp,dy);
//Only move if finger slides far away enough from the starting point
distance = Math.sqrt(dx*dx + dy*dy);
if((mode == DRAG) && (distance > 25)){
dspl.set(dx, dy);
matrix.set(savedMatrix);
matrix.postTranslate(dspl.x, dspl.y);
invalidate();
}
Here, originalCorners is just a float array keeping track of the corners of the screen before any transformations have occurred and is initialized like this:
viewWidth = w;
viewHeight = h;
originalCorners[0] = 0;
originalCorners[1] = 0;
originalCorners[2] = w;
originalCorners[3] = h;
and transformedCorners is another float array keeping track of where the corners are after any translations or zooming has occurred. It is always updated as follows:
matrix.mapPoints(transformedCorners,originalCorners);
This sort of achieves my goal of stopping scrolling past the boundaries, but the grid starts to jitter like crazy when sliding too far in any direction (before the boundaries are reached). You are initially stopped too early, but if you keep sliding in the same direction you can eventually get the edge of the grid to coincide with the edge of the screen.
I've tried a few things (like adding a little buffer value to xMax, yMax and subtracting it from xMin,yMin) but nothing has worked.
So how do I get rid of the jitter and also make it so you can get the edge of the grid to coincide with the edge of the screen with one smooth swipe?
Thanks very much!

Related

Android - How to spin the image

I have an image and its in imageView. I want to add a functionally that allows users to spin the image if they try to spin it to the right side.For example if user tries to spin it to the left side, it doesnt spin.
This is my spinwheel.
I tried to use GestureDetector and onFling event but it wasn't enough for me to detect if user tries to spin it to the right side or left side. How can i do this ?
Edit:
var normalVectorX = e2?.x!! - 515
var normalVectorY = e2?.y!! - 515
var tangentVectorX = -normalVectorY
var tangentVectorY = normalVectorX
var tangentVectorLength = (Math.sqrt(Math.pow((515 - e2?.y!!).toDouble(), 2.0)) + Math.pow((e2?.x!! - 515).toDouble(), 2.0))
var unitTangentX = tangentVectorX / tangentVectorLength
var unitTangentY = tangentVectorY / tangentVectorLength
var scalarProjection = (velocityX * unitTangentX) + (velocityY * unitTangentY)
if (scalarProjection > 0) // Right Side
spinWheel((spinCountX * 360).toFloat() + 360 - (mPrizeIndex * 60) , 12000)
This is the code implementation based on the pseudo code of answer.
515 = center of the wheel.
onFling gives you the MotionEvents of the corresponding fling gesture. You can then call getX(int) and getY(int) to get the coordinates.
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
float x = e2.getX(0); // Assuming you only care about the first finger
float y = e2.getY(0);
if (x < imageView.getWidth() / 2) {
// fling is on the left side
} else {
// fling is on the right side
}
}
Side note: I think the code above will behave unnaturally if the fling is horizontal. If you want your spin gesture to be more realistic, I think you will be better off calculating the tangential component.
Edit: Here's how you would calculate the tangential component.
You would want to calculate the scalar projection of the velocity vector on the unit tangent of the circle at the point where the touch event is.
Pseudo code:
velocityVector = (velocityX, velocityY)
locationOfTouch = (e2.getX(0), e2.getY(0))
centerOfWheel = (166, 155) # Measured from your image
normalVector = locationOfTouch - centerOfWheel
= (locationOfTouch.x - centerOfWheel.x, locationOfTouch.y - centerOfWheel.y)
To get the clockwise tangent vector, we take the normalVector, swap the X and Y components, and negate the X (from 2D Euclidean vector rotations).
tangentVector = (-normalVector.y, normalVector.x)
unitTangent = tangentVector / tangentVector.length()
Finally, you take the dot product to get the scalar projection:
scalarProjection = velocityVector.dotProduct(unitTangent)
= (velocityVector.x * unitTangent.x) + (velocityVector.y * unitTangent.y)
If scalarProjection is positive, that means the fling gesture is in the clockwise direction. If it is negative, that means the gesture is in anti-clockwise direction.
As a bonus, since we used the unit tangent, the resulting scalar projection also represents how fast the fling is in the clockwise direction, so you can change the spin animation to be faster or slower, or set a threshold such that a small velocity in that direction doesn't trigger the wheel to spin.

How to limit where a camera renders to onscreen in LibGDX

I have a camera set up in LibGDX which draws what is a HUD-like layer of buttons and status. In the middle of this, I want a block of text, which could be longer than the screen, so I want to be able to pan around it using gestures.
I was thinking, the way to do this would be to define a second camera, with an large viewport, i.e.:
textCamera = new OrthographicCamera(Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
textCamera.setToOrtho(true, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
And then apply it to my batch before writing out the bulk of the text. However, how can I restrict it such that this textCamera will only ever draw contents to the screen between, say, (0, 100) -> (600, 800). In this case the screen width, just for example, is 600 wide, and the height is maybe 1000 so I want to leave a gap at the top and bottom.
So, basically I want a big viewport, write all text out to viewport, be able to view it at 1:1 scale, but also be able to pan around the text. Just like you do when you pan up and down a website while surfing on an Android.
I think you want a scissor stack? This lets you define a rectangular sub-region of the display to render to, and only pixels inside that rectangle will be rendered.
Scissors API
One of the answers to this question has an example of using the scissor stack:
https://gamedev.stackexchange.com/questions/67024/how-do-i-crop-a-cameras-viewport
The libgdx scissorstack wiki page is pretty weak, but shows how to use it with a SpriteBatch.
You should create second stage for a HUD and then add to it ActorGestureListener with defined pan method. You can easily control its camera position by checking if the position is not bigger/lower than some value in the method.
Stage hudStage; //create it with even the same viewport as stage and add to it all hud's actors
...
hudStage.addListener(aListener);
...
final float MAX_X = 100, MIN_X = -100, MAX_Y = 100, MIN_Y = -100;
ActorGestureListener aListener = new ActorGestureListener()
{
#Override
public void pan(InputEvent event, float x, float y, float deltaX, float deltaY)
{
//if you want to move slower you can divide deltaX and deltaY by some value like:
//deltaX /= 5f;
if( stage.getCamera().position.x + deltaX < MAX_X && stage.getCamera().position.x + deltaX > MIN_X )
{
stage.getCamera().position.x += deltaX;
}
if( stage.getCamera().position.y + deltaY < MAX_Y && stage.getCamera().position.y + deltaY > MIN_Y )
{
stage.getCamera().position.y += deltaY;
}
}
};

Android accelerometer values return back to zero, causing problems

here's my code:
float x;
#Override
public void onSensorChanged(SensorEvent event) {
if(event.values[0] > .2){
x = -1*(Math.round(event.values[0]));
centerx = centerx +(x*10);
}
else if(event.values[0] < -.2){
x = -1*Math.round(event.values[0]);
centerx = centerx +(x*10);
}
mCustomDrawableView.invalidate();
y = Math.round(event.values[1]);
if(event.values[2] > .4){
z = event.values[2];
}
}
I'm using the accelerometer to move a vertical line back and forth. centerx is the top and bottom point of the vertical line. I'm trying to make it so if I move my device to the right, the line will move to the left and look like it's still in the same position, as if it were in the physical world (think of augmented reality).
I'm using the values given out of the SensorEvent to do some arithmetic to move center line. The only problem is that the accelerometer values will go up when I move the advice to something like 5, but then decrease to 4 to 3 to 2 to 1 to 0 as I stop moving. So, the line just kind of bounces to the side and then returns to the original center. I'm looking for a way to get rid of this "bounce," and have the line stay in it's new position. Any ideas?

How to keep a dot drawn on canvas at a fixed point on screen, even when the canvas is pinch zoomed? - Android

I have a background image as a drawable in my custom view. This drawable may be pinch zoomed or moved.
Currently I need a green dot that is drawn on the image to be stationary relative to the screen. That is, it should be always at the same position with the pin as shown below. (Of course, the pin is simply an ImageView and does NOT move at all!)
I have successfully made it stationary relative to the screen, when the map behind is moved as follows in my custom view, MapView:
#Override
public boolean onTouchEvent(MotionEvent ev) {
// Let the ScaleGestureDetector inspect all events.
mScaleDetector.onTouchEvent(ev);
final int action = ev.getAction();
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN: {
final float x = ev.getX();
final float y = ev.getY();
mLastTouchX = x;
mLastTouchY = y;
mActivePointerId = ev.getPointerId(0);
break;
}
case MotionEvent.ACTION_MOVE: { // triggered as long as finger movers
final int pointerIndex = ev.findPointerIndex(mActivePointerId);
final float x = ev.getX(pointerIndex);
final float y = ev.getY(pointerIndex);
// Only move if the ScaleGestureDetector isn't processing a gesture.
if (!mScaleDetector.isInProgress()) {
final float dx = x - mLastTouchX;
final float dy = y - mLastTouchY;
mPosX += dx;
mPosY += dy;
// update the starting point if the 'Start' button is not yet pressed
// to ensure the screen center (i.e. the pin) is always the starting point
if (!isStarted) {
Constant.setInitialX(Constant.INITIAL_X - dx);
Constant.setInitialY(Constant.INITIAL_Y - dy);
if ((historyXSeries.size() > 0) && (historyYSeries.size() > 0)) {
// new initial starting point
historyXSeries.set(0, Constant.INITIAL_X);
historyYSeries.set(0, Constant.INITIAL_Y);
}
}
invalidate();
}
mLastTouchX = x;
mLastTouchY = y;
break;
}
By doing that above, my green dot stays there, when the background image is moved.
But I have problems in trying to make it stay there, when the background image is zoomed.
Essentially, I don't really understand how canvas.scale(mScaleFactor, mScaleFactor) works, and therefore I cannot move the green dot accordingly like what I have done in the simple moving case.
I think something should be added in the scale listener handler below, could anybody help me fill that part?
private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
#Override
public boolean onScale(ScaleGestureDetector detector) {
mScaleFactor *= detector.getScaleFactor();
// Don't let the object get too small or too large.
mScaleFactor = Math.max(1f, Math.min(mScaleFactor, 10.0f)); // 1 ~ 10
// HOW TO MOVE THE GREEN DOT HERE??
invalidate();
return true;
}
Or please at least explain how canvas.scale(mScaleFactor, mScaleFactor) works, and how may I move the green dot accordingly?
Keep in mind that the canvas is thought to scale everything according to the scale factor, so while going against the zoom is possible, it is probably not the best approach. However, if this is what you're looking for, I will help you as best as I can.
I am assuming the following:
Scale factor is relative to the current zoom (old zoom is always scale factor 1). If this is not the case, then you should observe the zoom values after scaling roughly 200% two times and seeing if the resulting scale factor is 4 or 3 (exponential or linear). You can achieve the results below by normalizing the scale factor to 2 for a zoom factor of 200%, for example. You'll have to remember the old scale factor in order to do so.
No rotation is performed
If this is the case then following can be said for a marker with respect to the zoom center.
For every horizonal pixel x away from the zoom center after zoom, its original position could be calculated to be: zoom_center_x + *x* / scale_factor (or alternatively zoom_center_x + (marker_x - zoom_center_x) / scale_factor). In other words, if zoom center is (50, 0) and the marker is (100, 0) with a scale factor of 2, then the x position of the marker prior to the zoom was 50 + (100 - 50) / 2 or 75. Obviously, if the marker is in the same position of the zoom center, then the x position will be the same as the zoom center. Similarly, if the scale is 1, then the x position for the marker will be the same as it is now.
The same can be applied to the y axis.
While I can't know exactly how to set the position of your marker, I would expect the code to look something like:
Point zoomCenter = detector.getZoomCenter();
// Set marker variable here
marker.setX(Math.round(zoomCenter.getX() + ((double)(marker.getX() - zoomCenter.getX())) / mScaleFactor));
marker.setY(Math.round(zoomCenter.getY() + ((double)(marker.getY() - zoomCenter.getY())) / mScaleFactor));
I hope that helps.

ACTION_DOWN behaving like ACTION_MOVE

I'm creating a simple OpenGL 'app' to rotate a triangle. I wish, on the first touch, to save the angle the touch position corresponds to. Then, on motion, rotate the shape by the angle corresponding to current position minus angle of first touch.
It was my understanding that the first step should be done in MotionEvent.ACTION_DOWN, and the second in MotionEvent.ACTION_MOVE. However, it seems as if ACTION_DOWN is being called during the motion. That is, the below code causes the shape to rotate as a finger is dragged (and I understood that it would rotate only to the position of the initial touch):
private double mTheta;
#Override
public boolean onTouchEvent(MotionEvent e) {
super.onTouchEvent(e);
float x = e.getX();
float y = e.getY();
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN:
x -= getWidth() / 2;
y -= getHeight() / 2;
mTheta = Math.atan2(-x,-y) * 180.0f / Math.PI;
GL20Renderer.mAngle = (float) mTheta;
requestRender();
}
return true;
}
Is my code wrong, or is this some weird behaviour of the emulator? (I don't currently have access to an android device.)
(Addendum: I original attempted to implement the above fully, with a MotionEvent.ACTION_MOVE case for calculating the new angle and rendering. The ACTION_DOWN case was only saving the starting offset angle. This didn't work, in that the shape didn't rotate - because the offset angle was being re-calculated during movement - which is how I ended up at this point.)
It might have been that you forgot to put a break statement in your switch/case. So once ACTION_MOVE is done, ACTION_DOWN follows immediately after
Needed to be using getActionMasked() rather than getAction(). See comment from WarrenFaith.

Categories

Resources