I'm trying to do an image viewer that when the user clicks on the image, the image is "cropped-out" and reveal the complete image.
For example in the screenshot below the user only see the part of the puppy initially. But after the user has clicked the image, the whole puppy is revealed. The image faded behind the first shows what the results of the animation would be.
Initially the ImageView is scaled to 50% in X and Y. And when the user clicks on the image the ImageView is scaled back to 100%, and the ImageView matrix is recalculated.
I tried all sort of way to calculate the matrix. But I cannot seem to find one that works with all type of crop and image: cropped landscape to portrait, cropped landscape to landscape, cropped portrait to portrait and cropped portrait to landscape. Is this even possible?
Here's my code right now. I'm trying to find what to put in setImageCrop()
.
public class MainActivity extends Activity {
private ImageView img;
private float translateCropX;
private float translateCropY;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
img = (ImageView) findViewById(R.id.img);
Drawable drawable = img.getDrawable();
translateCropX = -drawable.getIntrinsicWidth() / 2F;
translateCropY = -drawable.getIntrinsicHeight() / 2F;
img.setScaleX(0.5F);
img.setScaleY(0.5F);
img.setScaleType(ScaleType.MATRIX);
Matrix matrix = new Matrix();
matrix.postScale(2F, 2F); //zoom in 2X
matrix.postTranslate(translateCropX, translateCropY); //translate to the center of the image
img.setImageMatrix(matrix);
img.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
final PropertyValuesHolder animScaleX = PropertyValuesHolder.ofFloat(View.SCALE_X, 1F);
final PropertyValuesHolder animScaleY = PropertyValuesHolder.ofFloat(View.SCALE_Y, 1F);
final ObjectAnimator objectAnim = ObjectAnimator.ofPropertyValuesHolder(img, animScaleX, animScaleY);
final PropertyValuesHolder animMatrixCrop = PropertyValuesHolder.ofFloat("imageCrop", 0F, 1F);
final ObjectAnimator cropAnim = ObjectAnimator.ofPropertyValuesHolder(MainActivity.this, animMatrixCrop);
final AnimatorSet animatorSet = new AnimatorSet();
animatorSet.play(objectAnim).with(cropAnim);
animatorSet.start();
}
});
}
public void setImageCrop(float value) {
// No idea how to calculate the matrix depending on the scale
Matrix matrix = new Matrix();
matrix.postScale(2F, 2F);
matrix.postTranslate(translateCropX, translateCropY);
img.setImageMatrix(matrix);
}
}
Edit: It's worth mentioning that scaling the matrix linearly will not do. The ImageView is scaled linearly (0.5 to 1). But if I scale the Matrix linearly during the animation, the view is squish during the animation. The end result looks fine, but the image looks ugly during the animation.
I realise you (and the other answers) have been trying to solve this problem using matrix manipulation, but I'd like to propose a different approach with the same visual effect as outlined in your question.
In stead of using a matrix to manipulate to visible area of the image(view), why not define this visible area in terms of clipping? This is a rather straightforward problem to solve: all we need to do is define the visible rectangle and simply disregard any content that falls outside of its bounds. If we then animate these bounds, the visual effect is as if the crop bounds scale up and down.
Luckily, a Canvas
supports clipping through a variety of clip*()
methods to help us out here. Animating the clipping bounds is easy and can be done in a similar fashion as in your own code snippet.
If you throw everything together in a simple extension of a regular ImageView
(for the sake of encapsulation), you would get something that looks this:
public class ClippingImageView extends ImageView {
private final Rect mClipRect = new Rect();
public ClippingImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initClip();
}
public ClippingImageView(Context context, AttributeSet attrs) {
super(context, attrs);
initClip();
}
public ClippingImageView(Context context) {
super(context);
initClip();
}
private void initClip() {
// post to message queue, so it gets run after measuring & layout
// sets initial crop area to half of the view's width & height
post(new Runnable() {
@Override public void run() {
setImageCrop(0.5f);
}
});
}
@Override protected void onDraw(Canvas canvas) {
// clip if needed and let super take care of the drawing
if (clip()) canvas.clipRect(mClipRect);
super.onDraw(canvas);
}
private boolean clip() {
// true if clip bounds have been set aren't equal to the view's bounds
return !mClipRect.isEmpty() && !clipEqualsBounds();
}
private boolean clipEqualsBounds() {
final int width = getWidth();
final int height = getHeight();
// whether the clip bounds are identical to this view's bounds (which effectively means no clip)
return mClipRect.width() == width && mClipRect.height() == height;
}
public void toggle() {
// toggle between [0...0.5] and [0.5...0]
final float[] values = clipEqualsBounds() ? new float[] { 0f, 0.5f } : new float[] { 0.5f, 0f };
ObjectAnimator.ofFloat(this, "imageCrop", values).start();
}
public void setImageCrop(float value) {
// nothing to do if there's no drawable set
final Drawable drawable = getDrawable();
if (drawable == null) return;
// nothing to do if no dimensions are known yet
final int width = getWidth();
final int height = getHeight();
if (width <= 0 || height <= 0) return;
// construct the clip bounds based on the supplied 'value' (which is assumed to be within the range [0...1])
final int clipWidth = (int) (value * width);
final int clipHeight = (int) (value * height);
final int left = clipWidth / 2;
final int top = clipHeight / 2;
final int right = width - left;
final int bottom = height - top;
// set clipping bounds
mClipRect.set(left, top, right, bottom);
// schedule a draw pass for the new clipping bounds to take effect visually
invalidate();
}
}
The real 'magic' is the added line to the overridden onDraw()
method, where the given Canvas
is clipped to a rectangular area defined by mClipRect
. All the other code and methods are mainly there to help out with calculating the clip bounds, determining whether clipping is sensible, and the animation.
Using it from an Activity
is now reduced to the following:
public class MainActivity extends Activity {
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.image_activity);
final ClippingImageView img = (ClippingImageView) findViewById(R.id.img);
img.setOnClickListener(new OnClickListener() {
@Override public void onClick(View v) {
img.toggle();
}
});
}
}
Where the layout file will point to our custom ClippingImageView
like so:
<mh.so.img.ClippingImageView
android:id="@+id/img"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/dog" />
To give you an idea of the visual transition:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With