So I have an ImageView using a Matrix to scale the Bitmap I'm displaying. I can double-tap to zoom to full-size, and my ScaleAnimation handles animating the zoom-in, it all works fine.
Now I want to double-tap again to zoom out, but when I animate this with ScaleAnimation, the ImageView does not draw the newly exposed areas of the image (as the current viewport shrinks), instead you see the portion of visible image shrinking in. I have tried using ViewGroup.setClipChildren(false), but this only leaves the last-drawn artifacts from the previous frame - leading to an trippy telescoping effect, but not quite what I was after.
I know there are many zoom-related questions, but none cover my situation - specifically animating the zoom-out operation. I do have the mechanics working - ie aside from the zoom-out animation, double-tapping to zoom in and out works fine.
Any suggestions?
In the end I decided to stop using the Animation classes offered by Android, because the ScaleAnimation applies a scale to the ImageView as a whole which then combines with the scale of the ImageView's image Matrix, making it complicated to work with (aside from the clipping issues I was having).
Since all I really need is to animate the changes made to the ImageView's Matrix, I implemented the OnDoubleTapListener (at the end of this post - I leave it as an "exercise to the reader" to add the missing fields and methods - I use a few PointF and Matrix fields to avoid excess garbage creation). Basically the animation itself is implemented by using View.post to keep posting a Runnable that incrementally changes the ImageView's image Matrix:
public boolean onDoubleTap(MotionEvent e) {
final float x = e.getX();
final float y = e.getY();
matrix.reset();
matrix.set(imageView.getImageMatrix());
matrix.getValues(matrixValues);
matrix.invert(inverseMatrix);
doubleTapImagePoint[0] = x;
doubleTapImagePoint[1] = y;
inverseMatrix.mapPoints(doubleTapImagePoint);
final float scale = matrixValues[Matrix.MSCALE_X];
final float targetScale = scale < 1.0f ? 1.0f : calculateFitToScreenScale();
final float finalX;
final float finalY;
// assumption: if targetScale is less than 1, we're zooming out to fit the screen
if (targetScale < 1.0f) {
// scaling the image to fit the screen, we want the resulting image to be centred. We need to take
// into account the shift that is applied to zoom on the tapped point, easiest way is to reuse
// the transformation matrix.
RectF imageBounds = new RectF(imageView.getDrawable().getBounds());
// set up matrix for target
matrix.reset();
matrix.postTranslate(-doubleTapImagePoint[0], -doubleTapImagePoint[1]);
matrix.postScale(targetScale, targetScale);
matrix.mapRect(imageBounds);
finalX = ((imageView.getWidth() - imageBounds.width()) / 2.0f) - imageBounds.left;
finalY = ((imageView.getHeight() - imageBounds.height()) / 2.0f) - imageBounds.top;
}
// else zoom around the double-tap point
else {
finalX = x;
finalY = y;
}
final Interpolator interpolator = new AccelerateDecelerateInterpolator();
final long startTime = System.currentTimeMillis();
final long duration = 800;
imageView.post(new Runnable() {
#Override
public void run() {
float t = (float) (System.currentTimeMillis() - startTime) / duration;
t = t > 1.0f ? 1.0f : t;
float interpolatedRatio = interpolator.getInterpolation(t);
float tempScale = scale + interpolatedRatio * (targetScale - scale);
float tempX = x + interpolatedRatio * (finalX - x);
float tempY = y + interpolatedRatio * (finalY - y);
matrix.reset();
// translate initialPoint to 0,0 before applying zoom
matrix.postTranslate(-doubleTapImagePoint[0], -doubleTapImagePoint[1]);
// zoom
matrix.postScale(tempScale, tempScale);
// translate back to equivalent point
matrix.postTranslate(tempX, tempY);
imageView.setImageMatrix(matrix);
if (t < 1f) {
imageView.post(this);
}
}
});
return false;
}
Related
So, I have a custom BoardView class extends View. I implemented drawing board, lines and drawing "O" drawable when user press on the cell.
But, I could not implement following problems correctly:
1. Zoom BoardView when user doing pinch(multi touch).
2. Scroll BoardView to left, right, top, bottom if BoardView bigger than BoardView initial width or height.
3. Find right cell coordinate when user pressed on the cell after zooming or scrolling.
This is my first game project, please help me if anybody know how to solve this problem.
I tried but did not work properly. BoardView width equal width screen width and BoardView height equal to BoardView width. It is square board view.
I give 200 bounty to implementing this problem.
Here is my project, everyone can download and edit: https://drive.google.com/file/d/0BxNIUTd_m1x8cUQ2NGpSMDBuVVE/view?usp=sharing
Github: https://github.com/boyfox/GameTicTacToe
BoardView code: http://pastie.org/10109253 or http://pastebin.com/TRU8Ybds
I found solution self, but need improving code, you can answer to my question with your solution!
Could you please move your code to github? It would be much easier to download, edit and propose changes.
If you're looking for a generic implementation of two finger zoom/rotate, take a look at my game (https://github.com/ZieIony/Gravity). The most interesting part is the GamePanel view and the dispatchTouchEvent method:
private PointF prevPos = new PointF(), prevPos2 = new PointF();
float scale = 1;
final float MIN_SCALE = 0.2f, MAX_SCALE = 2.0f;
float rotation = 0;
Matrix matrix = new Matrix();
private float prevDist;
public boolean dispatchTouchEvent(android.view.MotionEvent event) {
if (event.getPointerCount() == 2) {
float d = dist(event.getX(0), event.getY(0), event.getX(1),
event.getY(1));
float pivotX = (event.getX(0) + event.getX(1)) / 2;
float pivotY = (event.getY(0) + event.getY(1)) / 2;
float prevPivotX = (prevPos.x + prevPos2.x) / 2;
float prevPivotY = (prevPos.y + prevPos2.y) / 2;
if (event.getAction() == MotionEvent.ACTION_MOVE) {
float newScale = scale * d / prevDist;
newScale = Math.max(MIN_SCALE,
Math.min(newScale, MAX_SCALE));
float scaleFactor = newScale / scale;
scale = newScale;
matrix.postScale(scaleFactor, scaleFactor, pivotX, pivotY);
float prevAngle = (float) Math.atan2(
prevPos.x - prevPos2.x, prevPos.y - prevPos2.y);
float angle = (float) Math.atan2(
event.getX(0) - event.getX(1), event.getY(0)
- event.getY(1));
rotation += prevAngle - angle;
matrix.postRotate(
(float) ((prevAngle - angle) * 180.0f / Math.PI),
pivotX, pivotY);
matrix.postTranslate(-prevPivotX + pivotX, -prevPivotY
+ pivotY);
}
prevPos.x = event.getX(0);
prevPos.y = event.getY(0);
prevPos2.x = event.getX(1);
prevPos2.y = event.getY(1);
prevDist = d;
}
return true;
}
This method produces a transformation matrix, which you should use for drawing.
protected void dispatchDraw(Canvas canvas) {
canvas.save();
canvas.setMatrix(matrix);
// do your drawing here
canvas.restore();
}
Have a look at my answer here. If the answer seems satisfactory have a look at my github code. I think it is not difficult to implement your third point
"3. Find right cell coordinate when user pressed on the cell after zooming or scrolling.",
because the scaleFactor is available in MyView.java and scroll offsets could be obtained by getScrollX() and getScrollY(), by just doing simple math.
I am trying to implement zooming on a canvas which should focus on a pivot point. Zooming works fine, but afterwards the user should be able to select elements on the canvas. The problem is, that my translation values seem to be incorrect, because they have a different offset, than the ones where I don't zoom to the pivot point (zoom without pivot point and dragging works fine).
I used some code from this example.
The relevant code is:
class DragView extends View {
private static float MIN_ZOOM = 0.2f;
private static float MAX_ZOOM = 2f;
// These constants specify the mode that we're in
private static int NONE = 0;
private int mode = NONE;
private static int DRAG = 1;
private static int ZOOM = 2;
public ArrayList<ProcessElement> elements;
// Visualization
private boolean checkDisplay = false;
private float displayWidth;
private float displayHeight;
// These two variables keep track of the X and Y coordinate of the finger when it first
// touches the screen
private float startX = 0f;
private float startY = 0f;
// These two variables keep track of the amount we need to translate the canvas along the X
//and the Y coordinate
// Also the offset from initial 0,0
private float translateX = 0f;
private float translateY = 0f;
private float lastGestureX = 0;
private float lastGestureY = 0;
private float scaleFactor = 1.f;
private ScaleGestureDetector detector;
...
private void sharedConstructor() {
elements = new ArrayList<ProcessElement>();
flowElements = new ArrayList<ProcessFlow>();
detector = new ScaleGestureDetector(getContext(), new ScaleListener());
}
/**
* checked once to get the measured screen height/width
* #param hasWindowFocus
*/
#Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
if (!checkDisplay) {
displayHeight = getMeasuredHeight();
displayWidth = getMeasuredWidth();
checkDisplay = true;
}
}
#Override
public boolean onTouchEvent(MotionEvent event) {
ProcessBaseElement lastElement = null;
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
mode = DRAG;
// Check if an Element has been touched.
// Need to use the absolute Position that's why we take the offset into consideration
touchedElement = isElementTouched(((translateX * -1) + event.getX()) / scaleFactor, (translateY * -1 + event.getY()) / scaleFactor);
//We assign the current X and Y coordinate of the finger to startX and startY minus the previously translated
//amount for each coordinates This works even when we are translating the first time because the initial
//values for these two variables is zero.
startX = event.getX() - translateX;
startY = event.getY() - translateY;
}
// if an element has been touched -> no need to take offset into consideration, because there's no dragging possible
else {
startX = event.getX();
startY = event.getY();
}
break;
case MotionEvent.ACTION_MOVE:
if (mode != ZOOM) {
if (touchedElement == null) {
translateX = event.getX() - startX;
translateY = event.getY() - startY;
} else {
startX = event.getX();
startY = event.getY();
}
}
if(detector.isInProgress()) {
lastGestureX = detector.getFocusX();
lastGestureY = detector.getFocusY();
}
break;
case MotionEvent.ACTION_UP:
mode = NONE;
break;
case MotionEvent.ACTION_POINTER_DOWN:
mode = ZOOM;
break;
case MotionEvent.ACTION_POINTER_UP:
break;
}
detector.onTouchEvent(event);
invalidate();
return true;
}
private ProcessBaseElement isElementTouched(float x, float y) {
for (int i = elements.size() - 1; i >= 0; i--) {
if (elements.get(i).isTouched(x, y))
return elements.get(i);
}
return null;
}
#Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.save();
if(detector.isInProgress()) {
canvas.scale(scaleFactor,scaleFactor,detector.getFocusX(),detector.getFocusY());
} else
canvas.scale(scaleFactor, scaleFactor,lastGestureX,lastGestureY); // zoom
// canvas.scale(scaleFactor,scaleFactor);
//We need to divide by the scale factor here, otherwise we end up with excessive panning based on our zoom level
//because the translation amount also gets scaled according to how much we've zoomed into the canvas.
canvas.translate(translateX / scaleFactor, translateY / scaleFactor);
drawContent(canvas);
canvas.restore();
}
/**
* scales the canvas
*/
private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
#Override
public boolean onScale(ScaleGestureDetector detector) {
scaleFactor *= detector.getScaleFactor();
scaleFactor = Math.max(MIN_ZOOM, Math.min(scaleFactor, MAX_ZOOM));
return true;
}
}
}
Elements are saved with their absolute position on the canvas (with dragging in mind). I suspect that I don't take the new offset from the pivot point to translateX and translateY in consideration, but I can't figure out where and how I should do this.
Any help would be appreciated.
Okay, so you're basically trying to figure out where a certain screen X/Y coordinate corresponds to, after the view has been scaled (s) around a certain pivot point {Px, Py}.
So, let's try to break it down.
For the sake of argument, lets assume that Px & Py = 0, and that s = 2. This means the view was zoomed by a factor of 2, around the top left corner of the view.
In this case, the screen coordinate {0, 0} corresponds to {0, 0} in the view, because that point is the only point which hasn't changed. Generally speaking, if the screen coordinate is equal to the pivot point, then there is no change.
What happens if the user clicks on some other point, lets say {2, 3}? In this case, what was once {2, 3} has now moved by a factor of 2 from the pivot point (which is {0, 0}), and so the corresponding position is {4, 6}.
All this is easy when the pivot point is {0, 0}, but what happens when it's not?
Well, lets look at another case - the pivot point is now the bottom right corner of the view (Width = w, Height = h - {w, h}). Again, if the user clicks at the same position, then the corresponding position is also {w, h}, but lets say the user clicks on some other position, for example {w - 2, h - 3}? The same logic occurs here: The translated position is {w - 4, h - 6}.
To generalize, what we're trying to do is convert the screen coordinates to the translated coordinate. We need to perform the same action on this X/Y coordinate we received that we performed on every pixel in the zoomed view.
Step 1 - we'd like to translate the X/Y position according to the pivot point:
X = X - Px
Y = Y - Py
Step 2 - Then we scale X & Y:
X = X * s
Y = Y * s
Step 3 - Then we translate back:
X = X + Px
Y = Y + Py
If we apply this to the last example I gave (I will only demonstrate for X):
Original value: X = w - 2, Px = w
Step 1: X <-- X - Px = w - 2 - w = -2
Step 2: X <-- X * s = -2 * 2 = -4
Step 3: X <-- X + Px = -4 + w = w - 4
Once you apply this to any X/Y you receive which is relevant prior to the zoom, the point will be translated so that it is relative to the zoomed state.
Hope this helps.
I'm developing an app where a lot of views can be rotated - it's something like a map of physical objects. I have to detect when 2 objects (all objects are rectangles/squares) are overlapping and if a user has performed a single/double/long tap on an object. For this reason I need to know the drawing bounds of a view.
Let's look at the example image bellow - the green rectangle is rotated 45 degrees. I need to get the coordinates of the 4 corners of the green rectangle. If I use view.getHitRect() it returns the bounding box (marked in red) of the view, which is of no use to me.
Do you know how could I get the coordinates of the edges of a view?
The only solution I could think of is to subclass a View, manually store the initial coordinates of the corners and calculate their new values on every modification to the view - translation, scale and rotation but I was wondering if there is a better method.
P.S. The app should be working on Android 2.3 but 4.0+ solutions are also welcomed.
Thanks to pskink I explored again the Matrix.mapPoints method and managed to get the proper coordinates of the corners of the rectangle.
If you are running on Android 3.0+ you can easily get the view's matrix by calling myView.getMatrix() and map the points of interest. I had to use 0,0 for the upper left corner and getWidth(),getHeight() for the bottom right corner and map these coordinates to the matrix. After that add view's X and Y values to get the real values of the corners.
Something like:
float points[] = new float[2];
points[0] = myView.getWidth();
points[1] = myView.getHeight();
myView.getViewMatrix().mapPoints(points);
Paint p = new Paint();
p.setColor(Color.RED);
//offset the point and draw it on the screen
canvas.drawCircle(center.getX() + points[0], center.getY() + points[1], 5f, p);
If you have to support lower versions of Android you can use NineOldAndroids. Then I've copied and modified one of its internal methods to get the view's matrix:
public Matrix getViewMatrix()
{
Matrix m = new Matrix();
Camera mCamera = new Camera();
final float w = this.getWidth();
final float h = this.getHeight();
final float pX = ViewHelper.getPivotX(this);
final float pY = ViewHelper.getPivotY(this);
final float rX = ViewHelper.getRotationX(this);;
final float rY = ViewHelper.getRotationY(this);
final float rZ = ViewHelper.getRotation(this);
if ((rX != 0) || (rY != 0) || (rZ != 0))
{
final Camera camera = mCamera;
camera.save();
camera.rotateX(rX);
camera.rotateY(rY);
camera.rotateZ(-rZ);
camera.getMatrix(m);
camera.restore();
m.preTranslate(-pX, -pY);
m.postTranslate(pX, pY);
}
final float sX = ViewHelper.getScaleX(this);
final float sY = ViewHelper.getScaleY(this);;
if ((sX != 1.0f) || (sY != 1.0f)) {
m.postScale(sX, sY);
final float sPX = -(pX / w) * ((sX * w) - w);
final float sPY = -(pY / h) * ((sY * h) - h);
m.postTranslate(sPX, sPY);
}
m.postTranslate(ViewHelper.getTranslationX(this), ViewHelper.getTranslationY(this));
return m;
}
I've put this method in an overloaded class of a view (in my case - extending TextView). From there on it's the same as in Android 3.0+ but instead of calling myView.getMatrix() you call myView.getViewMatrix().
I'm using a custom ImageView to display a rather large bitmap. The bitmap display is being handled by a matrix that transforms it to what the user sees. I'm trying to implement a "double-tap-to-zoom" but I can't quite get it right. I'm trying to have the image zoom on the point where the user touched with this point ending up in the center of the screen at the end.
I worked through a lot of the matrix math and transformations and basically the following transformation is what I need to do
float dx = centerX - focusX;
float dy = centerY - focusY;
Matrix m = new Matrix( baseMatrix );
m.postTranslate( -focusX, -focusY );
m.postScale( scale, scale );
m.postTranslate( focusX + dx, focusY + dy );
Which if I was just swapping the matrices would be fine but I need to animate from the baseMatrix to this new one. Is there a way I can interpolate between these two matrices?
I tried to interpolate the scale and translation separately but that didn't work out well for me (quite possible that I did it wrong and it is the correct way to go). The way I am currently interpolating just for the scale is below. I've tried adding a translation interpolation in the handler as well and it just didn't work out
mHandler.post( new Runnable() {
#Override
public void run() {
mZooming = true;
long now = System.currentTimeMillis();
float currentMs = Math.min( durationMs, now - startTime );
float newScale = (float) mEasing.easeInOut( currentMs, 0, deltaScale, durationMs );
zoomTo( oldScale + newScale, destX, destY );
if ( currentMs < durationMs ) {
mHandler.post( this );
} else {
onZoomAnimationCompleted( getScale() );
scrollBy( dx, dy, durationMs )
}
}
});
Has anyone done something like this before? Am I approaching it completely wrong?
Thanks in advance
I m woring on an android opengl 1.1 2d game with a top view on a vehicule and a camera zoom relative to the vehicule speed. When the speed increases the camera zoom out to offer the player a best road visibility.
I have litte trouble finding the exact way to detect if a sprite is visible or not regarding his position and the current camera zoom.
Important precision, all of my game's objects are on the same z coord. I use 3d just for camera effect. (that's why I do not need frustrum complicated calculations)
here is a sample of my GLSurfaceView.Renderer class
public static float fov_degrees = 45f;
public static float fov_radians = fov_degrees / 180 * (float) Math.PI;
public static float aspect; //1.15572 on my device
public static float camZ; //927 on my device
#Override
public void onSurfaceChanged(GL10 gl, int x, int y) {
aspect = (float) x / (float) y;
camZ = y / 2 / (float) Math.tan(fov_radians / 2);
Camera.MINIDECAL = y / 4; // minimum cam zoom out (192 on my device)
if (x == 0) { // Prevent A Divide By Zero By
x = 1; // Making Height Equal One
}
gl.glViewport(0, 0, x, y); // Reset The Current Viewport
gl.glMatrixMode(GL10.GL_PROJECTION); // Select The Projection Matrix
gl.glLoadIdentity(); // Reset The Projection Matrix
// Calculate The Aspect Ratio Of The Window
GLU.gluPerspective(gl, fov_degrees, aspect , camZ / 10, camZ * 10);
GLU.gluLookAt(gl, 0, 0, camZ, 0, 0, 0, 0, 1, 0); // move camera back
gl.glMatrixMode(GL10.GL_MODELVIEW); // Select The Modelview Matrix
gl.glLoadIdentity(); // Reset The Modelview Matrix
when I draw any camera relative object I use this translation method :
gl.glTranslatef(position.x - camera.centerPosition.x , position.y -camera.centerPosition.y , - camera.zDecal);
Eveything is displayed fine, the problem comes from my physic thread when he checks if an object is visible or not:
public static boolean isElementVisible(Element element) {
xDelta = (float) ((camera.zDecal + GameRenderer.camZ) * GameRenderer.aspect * Math.atan(GameRenderer.fov_radians));
yDelta = (float) ((camera.zDecal + GameRenderer.camZ)* Math.atan(GameRenderer.fov_radians));
//(xDelta and yDelta are in reallity updated only ones a frame or when camera zoom change)
Camera camera = ObjectRegistry.INSTANCE.camera;
float xMin = camera.centerPosition.x - xDelta/2;
float xMax = camera.centerPosition.x + xDelta/2;
float yMin = camera.centerPosition.y - yDelta/2;
float yMax = camera.centerPosition.y + yDelta/2;
//xMin and yMin are supposed to be the lower bounds x and y of the visible plan
// same for yMax and xMax
// then i just check that my sprite is visible on this rectangle.
Vector2 phD = element.getDimToTestIfVisibleOnScreen();
int sizeXd2 = (int) phD.x / 2;
int sizeYd2 = (int) phD.y / 2;
return (element.position.x + sizeXd2 > xMin)
&& (element.position.x - sizeXd2 < xMax)
&& (element.position.y - sizeYd2 < yMax)
&& (element.position.y + sizeYd2 > yMin);
}
Unfortunately the object were disapearing too soon and appearing to late so i manuelly added some zoom out on the camera for test purpose.
I did some manual test and found that by adding approx 260 to the camera z index while calculating xDelta and yDelta it, was good.
So the line is now :
xDelta = (float) ((camera.zDecal + GameRenderer.camZ + 260) * GameRenderer.aspect * Math.atan(GameRenderer.fov_radians));
yDelta = (float) ((camera.zDecal + GameRenderer.camZ + 260)* Math.atan(GameRenderer.fov_radians));
Because it's a hack and the magic number may not work on every device I would like to understand what i missed. I guess there is something in that "260" magic number that comes from the fov or ration width/height and that could be set as a formula parameter for pixel perfect detection.
Any guess ?
My guess is that you should be using Math.tan(GameRenderer.fov_radians) instead of Math.atan(GameRenderer.fov_radians).
Reasoning:
If you used a camera with 90 degree fov, then xDelta and yDelta should be infinitely large, right? Since the camera would have to view the entire infinite plane.
tan(pi/2) is infinite (and negative infinity). atan(pi/2) is merely 1.00388...
tan(pi/4) is 1, compared to atan(pi/4) of 0.66577...