I am trying to apply a ScaleAnimation to a View, but I have a somewhat nontraditional requirement: I would like to scale only a very specific region of the View.
Using the existing ScaleAnimation class I can easily uniformly scale a view. I can also set a pivot point about which to scale a view. Below is an example of that:
This is straightforward. But I wish to achieve the following, which results from scaling only a particular region of a view (or in this case a small horizontal rectangle in the middle of the smiley):
I dug around the source code for ScaleAnimation, and the following function seems to be responsible for scaling:
protected void applyTransformation(float interpolatedTime, Transformation t) {
float sx = 1.0f;
float sy = 1.0f;
if (mFromX != 1.0f || mToX != 1.0f) {
sx = mFromX + ((mToX - mFromX) * interpolatedTime);
}
if (mFromY != 1.0f || mToY != 1.0f) {
sy = mFromY + ((mToY - mFromY) * interpolatedTime);
}
if (mPivotX == 0 && mPivotY == 0) {
t.getMatrix().setScale(sx, sy);
} else {
t.getMatrix().setScale(sx, sy, mPivotX, mPivotY);
}
}
Since this function is simply applying a scale operation to a matrix, I was thinking there is some sort of matrix operation that I could use to write a custom scale animation, but my linear algebra know-how is lacking. The solution to this may be far simpler as it seems as though others would run into this issue, but I haven't been able to find any solutions. Thanks in advance.
A matrix transforms the entire bitmap. You can not scale regions with a single matrix.
Related
i think this question has been asked quite often, but I couldn`t find an appropriate solution for my implementation. I built an custom imageview with an onScaleListener and an onGestureListener that scales and pans the containing image. The scaling is done with a matrix scaling. The function looks like that:
#Override
public boolean onScale(ScaleGestureDetector scaleGestureDetector) {
scaleFactor *= scaleGestureDetector.getScaleFactor();
scaleFactor = Math.max(initScale, Math.min(scaleFactor, initScale + 3.0f));
matrix = getImageMatrix();
matrix.setScale(scaleFactor, scaleFactor);
matrix.getValues(values);
matrix.postTranslate(-values[Matrix.MTRANS_X] + Math.max(0, centerX - centerImageX),
-values[Matrix.MTRANS_Y] + Math.max(0, centerY - centerImageY));
setImageMatrix(matrix);
}
postTranslate() is needed to center the image if needed. To finish this I need to scroll to (scrollTo(x,y)) the position where the focus of the scaling gesture stays in the same position on the screen. At the end it should look like scaling in a webview.
Can anybody help me with this?
When I use:
float scrollPosX = ((scrollwidth) * ((getScrollX() + touchX) / imagewidth));
float scrollPosY = ((scrollheight) * ((getScrollY() + touchY) / imageheight));
it will work for the first scaling, but when scaling in a scaled image it will scroll to the relative position. I think it is all related to the fact that I only get the touch position with getScrollX() and getScrollY() [it`s difficult to explain]
Now I found a solution that works for me. In my case I have to use the scaling step instead of the scaling factor.
scrollTo((int) (getScrollX() - x + (scaleGestureDetector.getScaleFactor() * x) ),
(int) (getScrollY() - y + (scaleGestureDetector.getScaleFactor() * y) ));
with x and y as the touch position on the scaled image.
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 am trying to zoom in my canvas when the user pinches the screen In IOS.
I am translating my code from Android(which works), here's a snift:
focusX = gestureDetector.getFocusX();
focusY = gestureDetector.getFocusY();
enter code herecanvas.scale(mScaleFactor,mScaleFactor,focusX,focusY);
my translated IOS code doesn't give the same results:
- (void)onScale:(UIPinchGestureRecognizer *)gesture
{
if (gesture.state == UIGestureRecognizerStateBegan) {
CGPoint endPoint = [gesture locationInView:self];
focusX = endPoint.x;
focusY = endPoint.y;
}
}
CGContextTranslateCTM(UIGraphicsGetCurrentContext(), focusX, focusY);
CGContextScaleCTM(UIGraphicsGetCurrentContext(), mScaleFactor, mScaleFactor)
Why?
I found the problem, CGContextTranslateCTM is redundant. Also I had another problem , the scale factor of the recognizer in Android is relative whereas in IOS it's absolute.
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;
}