Canvas Pinch-Zoom to Point Within Bounds - android

I've been stuck on this problem for eight hours, so I figured it was time to get some help.
Before I begin my problem, I'll just let it be known that I've been through this site and Google, and none of the answers I've found have helped. (This is one, another, and another.)
Here's the deal: I have a class that extends SurfaceView (let's call it MySurface) and overrides many methods in it. Normally, it draws several squares and text boxes, which is all fine. As soon as a user starts touching, it converts to a Bitmap, then draws each frame that until the user releases.
Here's the rub: I want to implement such a functionality that the user can place two fingers on the screen, pinch to zoom, and also pan around (but ONLY with two fingers down).
I found a few implementations of pinch-to-zoom and adapted them to my Canvas object in MySurface via the following:
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.save();
canvas.scale(mScaleVector.z, mScaleVector.z); // This is the scale factor as seen below
canvas.translate(mScaleVector.x, mScaleVector.y); // These are offset values from 0,0, both working fine
// Start draw code
// ...
// End draw code
canvas.restore();
}
private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
#Override
public boolean onScale(ScaleGestureDetector detector) {
float factor = detector.getScaleFactor();
if (Math.abs(factor - 1.0f) >= 0.0075f) {
mScaleVector.z *= factor;
mScaleVector.z = Math.max(MIN_ZOOM, Math.min(mScaleVector.z, MAX_ZOOM));
}
// ...
invalidate();
return true;
}
}
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction() & MotionEvent.ACTION_MASK;
int pointerIndex = (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
if (event.getPointerCount() == 2) {
if (action == MotionEvent.ACTION_POINTER_DOWN && pointerIndex == 1) {
// The various pivot coordinate codes would belong here
}
}
detector.onTouchEvent(event); // Calls the Scale Gesture Detector
return true;
}
While both elements work fine--the scrolling back and forth and the pinch-to-zoom--there is one large problem. The pinch-to-zoom, when used, zooms into the point 0,0, instead of zooming into the finger point.
I've tried a lot of ways to fix this:
Using canvas.scale(mScaleVector.z, mScaleVector.z, mScaleVector.x, mScaleVector.y);; obviously, this produces unwanted results as the mScaleVector x and y values are 0-offsets.
Managing a "pivot" coordinate that uses the same offset as the translate() method, but this produces either the same 0,0 issue, or jumping around when the view is touched.
Numerous other things... I've done a lot with the aforementioned pivot coordinate, trying to base its location on the user's first touch, and moving it relative to that touch each successive gesture.
Additionally, this canvas must be bounded, so the user cannot scroll forever. However, when I use the .scale(sx, sy, px, py) method, it pushes things beyond any bounds I set in .translate().
I'm... pretty much open to anything at this point. I know this functionality can be added, as it is seen in the Android 4.0 gallery (when viewing a single image). I've tried to track down the source code that handles this, to no avail.

Here is the code I use to implement pinch zoom in an ImageView using ScaleGestureDetector. With little or no modification you should be able to use it too, since you can use transformation marices too, to draw on a Canvas.
#Override
public boolean onScale(ScaleGestureDetector detector) {
float mScaleFactor = (float) Math.min(
Math.max(.8f, detector.getScaleFactor()), 1.2);
float origScale = saveScale;
saveScale *= mScaleFactor;
if (saveScale > maxScale) {
saveScale = maxScale;
mScaleFactor = maxScale / origScale;
} else if (saveScale < minScale) {
saveScale = minScale;
mScaleFactor = minScale / origScale;
}
right = width * saveScale - width
- (2 * redundantXSpace * saveScale);
bottom = height * saveScale - height
- (2 * redundantYSpace * saveScale);
if (origWidth * saveScale <= width
|| origHeight * saveScale <= height) {
matrix.postScale(mScaleFactor, mScaleFactor, width / 2, height / 2);
if (mScaleFactor < 1) {
matrix.getValues(m);
float x = m[Matrix.MTRANS_X];
float y = m[Matrix.MTRANS_Y];
if (mScaleFactor < 1) {
if (Math.round(origWidth * saveScale) < width) {
if (y < -bottom)
matrix.postTranslate(0, -(y + bottom));
else if (y > 0)
matrix.postTranslate(0, -y);
} else {
if (x < -right)
matrix.postTranslate(-(x + right), 0);
else if (x > 0)
matrix.postTranslate(-x, 0);
}
}
}
} else {
matrix.postScale(mScaleFactor, mScaleFactor, detector.getFocusX(), detector.getFocusY());
matrix.getValues(m);
float x = m[Matrix.MTRANS_X];
float y = m[Matrix.MTRANS_Y];
if (mScaleFactor < 1) {
if (x < -right)
matrix.postTranslate(-(x + right), 0);
else if (x > 0)
matrix.postTranslate(-x, 0);
if (y < -bottom)
matrix.postTranslate(0, -(y + bottom));
else if (y > 0)
matrix.postTranslate(0, -y);
}
}
return true;
}
In my case, I computed the neccesary values in the onMeasure() method of the View, you might want to do this somewhere else in your SurfaceView
width = MeasureSpec.getSize(widthMeasureSpec); // Change this according to your screen size
height = MeasureSpec.getSize(heightMeasureSpec); // Change this according to your screen size
// Fit to screen.
float scale;
float scaleX = (float) width / (float) bmWidth;
float scaleY = (float) height / (float) bmHeight;
scale = Math.min(scaleX, scaleY);
matrix.setScale(scale, scale);
setImageMatrix(matrix);
saveScale = 1f;
scaleMappingRatio = saveScale / scale;
// Center the image
redundantYSpace = (float) height - (scale * (float) bmHeight);
redundantXSpace = (float) width - (scale * (float) bmWidth);
redundantYSpace /= (float) 2;
redundantXSpace /= (float) 2;
matrix.postTranslate(redundantXSpace, redundantYSpace);
origWidth = width - 2 * redundantXSpace;
origHeight = height - 2 * redundantYSpace;
right = width * saveScale - width - (2 * redundantXSpace * saveScale);
bottom = height * saveScale - height - (2 * redundantYSpace * saveScale);
setImageMatrix(matrix);
A little explanation:
saveScale is the current scale ratio of the Bitmap
mScaleFactor is the factor you have to multiply your scale ratio with.
maxScale and minScale can be constant values.
width and height are the dimensions of the screen.
redundantXSpace and redundantYSpace are the empty between the image borders and screen borders since the image is centered when in it is smaller then the screen
origHeight and origWidth are the sizes of the bitmap
matrix is the current transformation matrix used to draw the bitmap
The trick is, that when I first scaled and centered the image on initialization, I picked that scale ratio to be 1 and with scaleMappingRatio I mapped the actual scale values of the image relative to that.

protected override void OnDraw(Canvas canvas)
{
canvas.Save();
int x = (int)(canvas.Width * mScaleFactor - canvas.Width) / 2;
int y = (int)(canvas.Height * mScaleFactor - canvas.Height) / 2;
if (mPosX > (canvas.Width + x - 50))
{
mPosX = canvas.Width + x - 50;
}
if (mPosX < (50 - canvas.Width - x))
{
mPosX = 50 - canvas.Width - x;
}
if (mPosY > (canvas.Height + y - 50))
{
mPosY = canvas.Height + y - 50;
}
if (mPosY < (50 - canvas.Height - y))
{
mPosY = 50 - canvas.Height - y;
}
canvas.Translate(mPosX, mPosY);
canvas.Scale(mScaleFactor, mScaleFactor, canvas.Width/2, canvas.Height/2);
base.OnDraw(canvas);
canvas.Restore();
}
This is monodroid code, but can be easily converted to JAVA.
You may choose whatever values for your bound (here is 50 pixels margin)

Related

TextureView TranslationX & Y not expected behaviour on API 23

Currently I am making a camera player with a textureview to render my camera. Because the preview can have any dimension I have created some custom code to alter the textureview when OnSurfaceTextureUpdated is called:
void updateTextureMatrix(int width, int height) {
Display display = WindowManager.DefaultDisplay;
var isPortrait = (display.Rotation == SurfaceOrientation.Rotation0 || display.Rotation == SurfaceOrientation.Rotation180);
int previewWidth = orgPreviewWidth;
int previewHeight = orgPreviewHeight;
if(isPortrait) {
previewWidth = orgPreviewHeight;
previewHeight = orgPreviewWidth;
}
// determine which part to crop
float widthRatio = (float)width / previewWidth;
float heightRatio = (float)height / previewHeight;
float scaleX;
float scaleY;
if(widthRatio > heightRatio) {
// height must be cropped
scaleX = 1;
scaleY = widthRatio * ((float)previewHeight / height);
} else {
// width must be cropped
scaleX = heightRatio * ((float)previewWidth / width);
scaleY = 1;
}
Android.Graphics.Matrix matrix = new Android.Graphics.Matrix();
matrix.SetScale(scaleX, scaleY);
_textureView.SetTransform(matrix);
float scaledWidth = width * scaleX;
float scaledHeight = height * scaleY;
float dx = (width - scaledWidth) * 0.5f;
float dy = (height - scaledHeight) * 0.5f;
_textureView.TranslationX = dx;
_textureView.TranslationY = dy;
}
The scaling & calculation of dx & dy work perfectly fine on older android devices but the devices I have at my disposal with API level 23 throw unexpected behaviour.
The galaxy S3 displays it correctly:
But on the S7:
The phone cuts off a lot of the image, despite positioning it correctly. This makes me believe the bottom part is not being rendered where on older devices it is. Can anyone confirm this and point me in the correct position to fix this issue?
After long testing I figured out the issue was due to the SetTransform method. I was setting my scale using the matrix but this somehow rendered my texture & ignored the TranslationX & TranslationY. Removing the matrix & replacing it by
float scaledWidth = width * scaleX;
float scaledHeight = height * scaleY;
float dx = (width - scaledWidth) * 0.5f;
float dy = (height - scaledHeight) * 0.5f;
_textureView.ScaleX = scaleX;
_textureView.ScaleY = scaleY;
_textureView.TranslationX = dx;
_textureView.TranslationY = dy;
Fixed my issue of rendering wrongly on certain android devices.

Align image on top in Custom ImageView

I am using ViewPager from TouchImageView to load images to my app. This is the TouchImageView.java which responsible for the loading of image.
float redundantXSpace = viewWidth - (scaleX * drawableWidth);
float redundantYSpace = viewHeight - (scaleY * drawableHeight);
matchViewWidth = viewWidth - redundantXSpace;
matchViewHeight = viewHeight - redundantYSpace;
if (!isZoomed() && !imageRenderedAtLeastOnce) {
//
// Stretch and center image to fit view
//
matrix.setScale(scaleX, scaleY);
matrix.postTranslate(redundantXSpace / 2, redundantYSpace / 2);
normalizedScale = 1;
} else {
//
// These values should never be 0 or we will set viewWidth and viewHeight
// to NaN in translateMatrixAfterRotate. To avoid this, call savePreviousImageValues
// to set them equal to the current values.
//
if (prevMatchViewWidth == 0 || prevMatchViewHeight == 0) {
savePreviousImageValues();
}
prevMatrix.getValues(m);
The code load my images on vertical center. What I want to achieve is to load the images on vertical top. If I change the matchViewHeight = viewHeight - redundantYSpace; to matchViewHeight = viewHeight;, the image will fill the entire height. What modification should I made to make to align on vertical top?
use
matrix.setScale(scaleX, scaleY);
matrix.postTranslate(redundantXSpace / 2, 0);
postTranslate - moved the image. You do not need to move along the y axis for align on top.
For align on bottom:
matrix.setScale(scaleX, scaleY);
matrix.postTranslate(redundantXSpace / 2, redundantYSpace );

glulookat "resets" after movement (with android)

here's my problem: I'm making a pool game in android and I want to make the camera rotate freely around the center of the table. The thing is that when I stop my movement it looks like the glulookat resets itself because I only see the same thing ver and over again. If somebody knows a way on how to solve this or another way to do what I watn to do i'd be REALLY appreciated
This is my renderer class:
public void onDrawFrame(GL10 gl) {
// Redraw background color
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
// Set GL_MODELVIEW transformation mode
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity(); // reset the matrix to its default state
// Screen position to angle conversion
theta = (float) ((360.0/screenHeight)*mMoveY*3.0); //3.0 rotations possible
phi = (float) ((360.0/screenWidth)*mMoveX*3.0);
// Spherical to Cartesian conversion.
// Degrees to radians conversion factor 0.0174532
eyeX = (float) (r * Math.sin(theta/**0.0174532*/) * Math.sin(phi/**0.0174532*/));
eyeY = (float) (r * Math.cos(theta/**0.0174532*/));
eyeZ = (float) (r * Math.sin(theta/**0.0174532*/) * Math.cos(phi/**0.0174532*/));
// Reduce theta slightly to obtain another point on the same longitude line on the sphere.
eyeXtemp = (float) (r * Math.sin(theta/**0.0174532*/-dt) * Math.sin(phi/**0.0174532*/));
eyeYtemp = (float) (r * Math.cos(theta/**0.0174532*/-dt));
eyeZtemp = (float) (r * Math.sin(theta/**0.0174532*/-dt) * Math.cos(phi/**0.0174532*/));
// Connect these two points to obtain the camera's up vector.
upX=eyeXtemp-eyeX;
upY=eyeYtemp-eyeY;
upZ=eyeZtemp-eyeZ;
// Set the view point
GLU.gluLookAt(gl, eyeX, eyeY, eyeZ, 0,0,0, upX, upY, upZ);
and here's my onTouch method in my activity class:
public boolean onTouchEvent(MotionEvent e) {
float x = e.getX();
float y = e.getY();
switch (e.getAction()) {
case MotionEvent.ACTION_MOVE:
float dx = x - mPreviousX;
float dy = y - mPreviousY;
// reverse direction of rotation above the mid-line
/*if (y > getHeight() / 2) {
dx = dx * -1 ;
}
// reverse direction of rotation to left of the mid-line
if (x < getWidth() / 2) {
dy = dy * -1 ;
}*/
mRenderer.mMoveX = dx * TOUCH_SCALE_FACTOR/100;
mRenderer.mMoveY = dy * TOUCH_SCALE_FACTOR/100;
requestRender();
}
mPreviousX = x;
mPreviousY = y;
return true;
}
When you do the math, you're using mMoveX and mMoveY. It looks like you should be adding those to other, more persistent x/y variables instead.
Something like:
mCamX += mMoveX;
mCamY += mMoveY;
theta = (float) ((360.0/screenHeight)*mCamY*3.0); //3.0 rotations possible
phi = (float) ((360.0/screenWidth)*mCamX*3.0);

Android: How to rotate an element by touching it?

I'm trying to rotate a drawable inside a view by touching it and moving the finger. I've come up with several solutions, but none of them feels natural on the device.
Here's my first approach: Depending on where the user touched the screen and in which direction the finger was moved, I'm changing the drawable's rotation, calculated more or less arbitrarily.
private void updateRotation(float x, float y, float oldX, float oldY) {
int width = getWidth();
int height = getHeight();
float centerX = width / 2;
float centerY = height / 2;
float xSpeed = rotationSpeed(x, oldX, width);
float ySpeed = rotationSpeed(y, oldY, height);
if ((y < centerY && x > oldX)
|| (y > centerY && x < oldX))
rotation += xSpeed;
else if ((y < centerY && x < oldX)
|| (y > centerY && x > oldX))
rotation -= xSpeed;
if ((x > centerX && y > oldY)
|| (x < centerX && y < oldY))
rotation += ySpeed;
else if ((x > centerX && y < oldY)
|| (x < centerX && y > oldY))
rotation -= ySpeed;
}
private static float rotationSpeed(float pos, float oldPos, float max) {
return (Math.abs(pos - oldPos) * 180) / max;
}
This approach had a few annoying side effects: Sometimes the drawable would rotate while the finger wasn't moving and the rotation was generally not as fast as the user's finger.
Hence I threw this code away and started with my second approach. I'm using trigonometry to calculate the actual rotation that would be equivalent to the finger movement:
private void updateRotation(float x, float y, float oldX, float oldY) {
float centerX = getWidth() / 2;
float centerY = getHeight() / 2;
float a = distance(centerX, centerY, x, y);
float b = distance(centerX, centerY, oldX, oldY);
float c = distance(x, y, oldX, oldY);
double r = Math.acos((Math.pow(a, 2) + Math.pow(b, 2) - Math.pow(c, 2))
/ (2 * a * b));
if ((oldY < centerY && x < oldX)
|| (oldY > centerY && x > oldX)
|| (oldX > centerX && y < oldY)
|| (oldX < centerX && y > oldY))
r *= -1;
rotation += (int) Math.toDegrees(r);
}
private float distance(float x1, float y1, float x2, float y2) {
return Math.abs(x1 - x2) + Math.abs(y1 - y2);
}
Although this does sound like the correct solution to me, it's not working well either. Finger movement is sometimes ignored - could the calculation be too expensive? Furthermore, the rotation is still a bit faster than the actual finger movement.
Ideally, if I start rotating the drawable touching a specific point, this very point should stay below the finger at all times. Can you tell me how to achieve this?
Edit:
Here's my adoption of Snailer's suggestion. I had to switch the arguments for the atan2 method to rotate into the right direction, now it works great:
private void updateRotation(float x, float y) {
double r = Math.atan2(x - getWidth() / 2, getHeight() / 2 - y);
rotation = (int) Math.toDegrees(r);
}
This can be done easily by getting the angle created by your finger and the center of the screen. Similar to what you have above in the second example. In your onTouchEvent send the getX()/getY() to this method:
private double getDegreesFromTouchEvent(float x, float y){
double delta_x = x - (Screen Width) /2;
double delta_y = (Screen Height) /2 - y;
double radians = Math.atan2(delta_y, delta_x);
return Math.toDegrees(radians);
}
Then when you draw() you can just rotate the Canvas according to results. Obviously, you'll want to use if (MotionEvent.getAction() == MotionEvent.ACTION_MOVE) to update the angle.

Correct transformations for image zooming

I'm writing an app for android (although I think this is a generic question) and I need to display a large image (in an ImageView) that can be scrolled and zoomed. I've managed to get scrolling to work by capturing touch events and performing matrix translations, and i'm now working on zooming.
If I simply apply a scale transformation to the image, it zooms in at the origin, which is the top left-hand corner of the screen. I would like to zoom in at the center of the screen.
From what i've read, this means I need a transformation to make the origin the center of the screen. I think what is required is something like the following- assume the center of the screen is (5, 5) for simplicity...
-Translate by (-5, -5)
-Scale by the zoom factor
-Translate by (+5, +5)*zoomfactor
Unfortunatly, this doesnt seem to work- the zoom seems to go anywhere BUT the center...can someone help me out here?
EDIT: This is the code that now works
Matrix zoommatrix = new Matrix();
float[] centerpoint = {targetimageview.getWidth()/2.0f, targetimageview.getHeight()/2.0f};
zoommatrix.postScale(zoomfactor, zoomfactor, centerpoint[0], centerpoint[1]);
zoommatrix.preConcat(targetimageview.getImageMatrix());
targetimageview.setImageMatrix(zoommatrix);
targetimageview.invalidate();
Check ImageViewTouchBase in the Android source code's Camera app; its "zoomTo" method does this:
protected void zoomTo(float scale, float centerX, float centerY) {
if (scale > mMaxZoom) {
scale = mMaxZoom;
}
float oldScale = getScale();
float deltaScale = scale / oldScale;
mSuppMatrix.postScale(deltaScale, deltaScale, centerX, centerY);
setImageMatrix(getImageViewMatrix());
center(true, true);
}
That center method is probably the bit you'll really care about:
protected void center(boolean horizontal, boolean vertical) {
if (mBitmapDisplayed.getBitmap() == null) {
return;
}
Matrix m = getImageViewMatrix();
RectF rect = new RectF(0, 0,
mBitmapDisplayed.getBitmap().getWidth(),
mBitmapDisplayed.getBitmap().getHeight());
m.mapRect(rect);
float height = rect.height();
float width = rect.width();
float deltaX = 0, deltaY = 0;
if (vertical) {
int viewHeight = getHeight();
if (height < viewHeight) {
deltaY = (viewHeight - height) / 2 - rect.top;
} else if (rect.top > 0) {
deltaY = -rect.top;
} else if (rect.bottom < viewHeight) {
deltaY = getHeight() - rect.bottom;
}
}
if (horizontal) {
int viewWidth = getWidth();
if (width < viewWidth) {
deltaX = (viewWidth - width) / 2 - rect.left;
} else if (rect.left > 0) {
deltaX = -rect.left;
} else if (rect.right < viewWidth) {
deltaX = viewWidth - rect.right;
}
}
postTranslate(deltaX, deltaY);
setImageMatrix(getImageViewMatrix());
}

Categories

Resources