The “magic move” effect of an image is common in many apps. This effect is basically the animation of an image as it changes from a thumbnail to a full screen image.

Here for the sake of intuition, we first directly on the final effect:

This effect seems simple, but it’s not that easy to implement. Why? We can briefly analyze this animation effect.

In general, preview images and full-screen images are not in the same Activity, so we need to pass in the location and size of the original thumbnail image before starting a new Activity. This is actually easy to do using extras.

The next step is to translate the position and size, which we usually do with Translate and scale, but this doesn’t work. If you look closely at the image above, notice what happens around the panda’s ears during the animation. Instead of doing equal scaling, ImageView dynamically adjusts how the content is clipped based on the size.

As we all know, ImageView’s scaleType property dictates how the image should be scaled. For thumbnail images, we use centerCrop for aesthetic purposes, so that the image fills the entire ImageView, while for larger images, to see the entire image completely, We always use the fitCenter. (The differences between different Scale types will not be described in this article, please make clear in advance)

Analysis of theImageViewThe realization of zooming and cropping pictures

To implement a Scale Type animation, we first need to figure out how it is implemented in the ImageView. An important method is involved here: ImageView#configureBounds(), which is called when the ImageView size changes. In this method, the ImageView adjusts the bounds and mDrawMatrix of the drawable. This matrix is very important, and our animation effects will depend on it to a great extent.

Simply put, the matrix is used during drawing as long as the ImageView scaleType is not FIT_XY (which is implemented by drawable Bounds).

The two methods used here are CENTER_CROP and FIT_CENTER respectively. Let’s look at how the matrix is configured under these two methods.

The first is CENTER_CROP:

. } else if (ScaleType.CENTER_CROP == mScaleType) { mDrawMatrix = mMatrix; float scale; float dx = 0, dy = 0; if (dwidth * vheight > vwidth * dheight) { scale = (float) vheight / (float) dheight; Dx = (vwidth - dwidth * scale) * 0.5f; } else { scale = (float) vwidth / (float) dwidth; Dy = (vheight - dheight * scale) * 0.5f; } mDrawMatrix.setScale(scale, scale); mDrawMatrix.postTranslate(Math.round(dx), Math.round(dy)); }...Copy the code

dwidth * vheight > vwidth * dheight

Can be reduced to:

dwidth / vwidth > dheight / vheight

The logic is, if the width is significantly wider, make the scaled image the same height as the ImageView, so that the horizontal content can be panted out, and vice versa…

Then FIT_CENTER:

} else {
    mTempSrc.set(0, 0, dwidth, dheight);
    mTempDst.set(0, 0, vwidth, vheight);

    mDrawMatrix = mMatrix;
    mDrawMatrix.setRectToRect(mTempSrc, mTempDst, scaleTypeToScaleToFit(mScaleType));
}Copy the code

This one is relatively simple, directly using the Matrix method.

These matrices will eventually be assigned to the mDrawMatrix, which will then be applied to the Canvas during onDraw to achieve the transformation. Also using Find Lead you can see that the Matrix is obtained by a public method called getImageMatrix. At this point the matrix analysis is complete, we just need to get the starting and ending matrix, and then Animator can implement the animation.

Use custom properties and types in the Animator

In this case, we need to animate the matrix, which involves Property and TypeEvaluator. ImageView does not have a Property for imageMatrix, so we need to implement a Property for imageMatrix. It is very simple to implement:

private final static Property<ImageView, Matrix> IMAGE_MATRIX = new Property<ImageView, Matrix>(Matrix.class, "imageMatrix") { @Override public void set(ImageView object, Matrix value) { object.setImageMatrix(value); } @Override public Matrix get(ImageView object) { return object.getImageMatrix(); }};Copy the code

However, since the value type we want to animate is Matrix, the Animator cannot evaluate this type by default. Note the difference between an interpolator and an estimator, where you give a starting value, an end value and a progress, and then calculate what the current progress is. The Animator can evaluate simple numeric types directly, but for other Object derived types, we need to provide a method for evaluating them by implementing the TypeEvaluator interface.

The matrix valuation is also very simple, and we know that we can use the following formula for the valuation of floating point numbers:

V = S + (E - S) * P
Copy the code

Where V is the value to be calculated, S, E, and P are the starting value, end value, and progress respectively. For the matrix, directly into the actual matrix is equivalent to the addition and number multiplication operations, not involving complex matrix calculation, we only need to carry out the above calculation of the 9 elements of the matrix.

Take a quick look at the implementation:

private static class MatrixEvaluator implements TypeEvaluator<Matrix> { private float[] mTmpStartValues = new float[9]; private float[] mTmpEndValues = new float[9]; private Matrix mTmpMatrix = new Matrix(); @Override public Matrix evaluate(float fraction, Matrix startValue, Matrix endValue) { startValue.getValues(mTmpStartValues); endValue.getValues(mTmpEndValues); for (int i = 0; i < 9; i++) { float diff = mTmpEndValues[i] - mTmpStartValues[i]; mTmpEndValues[i] = mTmpStartValues[i] + (fraction * diff); } mTmpMatrix.setValues(mTmpEndValues); return mTmpMatrix; }}Copy the code

For performance purposes, we cache a matrix inside the estimator and return the results each time stored in this matrix.

It’s not over yet…

At this point, we’ve been able to transform the matrix, but not enough. The ImageView’s position and size haven’t changed yet. One of the biggest differences between Android and iOS regarding the position and size of a View is that iOS can very easily set UIView’s frame and bounds (corresponding to the property of CALayer, which is the actual representation of UIView). In Android, the position and size of a View are usually set by the parent container calling the View# Layout method on onLayout. But we can still call the Layout method at any time to resize and position the View.

Why can’t you set LayoutParams? The same effect can be achieved, but this will cause the parent container to re-execute Layout Passes, requiring measures and layouts to be re-executed. If the view hierarchy is complex, the performance overhead can be significant, especially if the action is performed at 60fps. The Layout method, on the other hand, sets the position and size “without alerting” the parent container (hardware-accelerated only affects RenderNode, which contains descriptions of View properties).

For convenience, we call the position and size of the View bounds. Another point to note about the bounds change is the reference frame.

For general apps, the view hierarchy of thumbnails is usually deep (for example, the view hierarchy may be under the list item Layout under RecyclerView). Now the View’s bounds origin reference frame is different from the larger View’s bounds origin reference frame.

What I do here is put the two bounds into the same reference frame (use View#getLocationInWindow to position them relative to Window), and then compare the difference in the origin of the two bounds, Use this difference to offset the bounds of the larger picture, and you get the position of the thumbnail in the larger picture reference frame. Let’s just post the code:

int[] thumbnailOrigin = new int[2];
int[] fullOrigin = new int[2];
mThumbnailImageView.getLocationInWindow(thumbnailOrigin);
mFullImageView.getLocationInWindow(fullOrigin);

int thumbnailLeft = mFullImageView.getLeft() + (thumbnailOrigin[0] - fullOrigin[0]);
int thumbnailTop = mFullImageView.getTop() + (thumbnailOrigin[1] - fullOrigin[1]);

Rect thumbnailBounds = new Rect(thumbnailLeft, thumbnailTop,
        thumbnailLeft + mThumbnailImageView.getWidth(),
        thumbnailTop + mThumbnailImageView.getHeight());
Rect fullBounds = new Rect(mFullImageView.getLeft(), mFullImageView.getTop(),
        mFullImageView.getRight(), mFullImageView.getBottom());Copy the code

For the sake of demonstration, both ImageViews are in the same Activity. In real life, the data needs to be delivered via the Intent extras.

Since the bounds property doesn’t exist, the Animator doesn’t evaluate Rect by default, so you can just implement the interface yourself.

conclusion

Up to this point, we’ve used two animators altogether, one to transform bounds and the other to transform matrix. Because ImageView is scaled and clipped by the matrix, this effect cannot be achieved by simply changing bounds or Transform.

In addition, if you want to use PhotoView is also no problem, because it is also through the matrix implementation, this article only provides an idea, the specific implementation depends on your needs.

The core code for the image at the beginning of this article can be found in ImageTransitionActivity

To promote information

If you are interested in my Android source analysis series, you can click on star and I will continue to update the articles from time to time.