如何设置矩阵动画以“裁剪”图像?

时间:2013-08-19 01:13:14

标签: android graphics matrix transformation objectanimator

我正在尝试使用图像查看器,当用户点击图像时,图像被“裁剪掉”并显示完整的图像。

例如,在下面的屏幕截图中,用户最初只能看到小狗的一部分。但是在用户点击图像后,整个小狗都被揭露了。第一个图像后面的图像显示了动画的结果。

screenshot

最初,ImageView在X和Y中缩放到50%。当用户点击图像时,ImageView缩放回100%,并重新计算ImageView矩阵。

我尝试了各种方法来计算矩阵。但是我似乎无法找到适用于所有类型的裁剪和图像的裁剪:裁剪景观到肖像,裁剪景观到横向,裁剪纵向到纵向和裁剪纵向到横向。这甚至可能吗?

这是我现在的代码。我正在尝试找到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);
}

}

编辑:值得一提的是,线性缩放矩阵是行不通的。 ImageView线性缩放(0.5到1)。但是如果我在动画期间线性缩放矩阵,则在动画期间视图会变窄。最终结果看起来不错,但在动画过程中图像看起来很丑陋。

4 个答案:

答案 0 :(得分:7)

我意识到你(以及其他答案)一直试图使用矩阵操作来解决这个问题,但我想提出一种不同的方法,它具有你问题中提到的相同的视觉效果。

为什么不用裁剪来定义这个可见区域,而不是使用矩阵来操作图像的可见区域(视图)?这是一个相当简单的问题需要解决:我们需要做的就是定义可见矩形并简单地忽略任何超出其边界的内容。如果我们然后为这些边界设置动画,则视觉效果就像裁剪边界向上和向下缩放一样。

幸运的是,Canvas支持剪切各种clip*()方法以帮助我们在这里。动画剪辑边界很简单,可以使用与您自己的代码片段类似的方式完成。

如果你把所有东西放在常规ImageView的简单扩展中(为了封装),你会得到一些看起来像这样的东西:

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();
    }

}

真正的“魔法”是被覆盖的onDraw()方法的附加行,其中给定的Canvas被裁剪为由mClipRect定义的矩形区域。所有其他代码和方法主要用于帮助计算剪辑边界,确定裁剪是否合理以及动画。

现在将Activity中使用的内容简化为以下内容:

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();
            }
        });
    }
}

布局文件将指向我们的自定义ClippingImageView,如下所示:

<mh.so.img.ClippingImageView
        android:id="@+id/img"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/dog" />

让您了解视觉过渡:

enter image description here

答案 1 :(得分:3)

这可能会有所帮助...... Android image view matrix scale + translate

似乎需要进行两项操作:

  1. 将容器从0.5升级到1.0
  2. 将内部图像从2.0缩放到1.0
  3. 我认为这就像

    一样简单
    float zoom = 1.0F + value;
    float scale = 1.0F / zoom;
    img.setScaleX(scale);
    img.setScaleY(scale);
    ...
    matrix.postScale(zoom, zoom);
    

    要从图像中心缩放/缩放,您可能需要执行此类操作(可能需要更换“ - ”顺序)......

    matrix.postTranslate(translateCropX, translateCropY);
    matrix.postScale(zoom, zoom);
    matrix.postTranslate(-translateCropX, -translateCropY);
    

    我不确定这是否会动画(Why does calling setScaleX during pinch zoom gesture cause flicker?)。也许这会有所帮助... http://developer.android.com/training/animation/zoom.html

    结果是内部图像始终保持相同的分辨率。

    但是你也提到了肖像/风景差异,所以这会变得有点复杂。在这种情况下,我发现最困难的事情是要在所有情况下牢固地定义你想要发生的事情。

    我猜你是在一个小缩略图网格之后,所有大小相同(这意味着它们都具有相同的宽高比)。您希望在不同宽高比和大小的缩略图容器中显示图像,但放大。当您选择图像时,边框会扩展(与其他图像重叠?)。存在约束,所示图像必须保持相同的分辨率。您还提到选择完整图像后必须可见,缩略图大小的两倍。最后一点提出了另一个问题 - 缩略图高度,宽度或两者的两倍(如果图像宽高比不匹配,在这种情况下裁剪或缩放)。可能出现的另一个问题是图像分辨率不足时的情况。

    希望android能为你做很多事...... How to scale an Image in ImageView to keep the aspect ratio

答案 2 :(得分:2)

您可以让View的孩子ViewGroup超过父母的范围。因此,举例来说,让我们说上面图像的清晰部分是父视图,而ImageView是它的子视图,那么我们只需要放大或缩小图像。为此,请从具有特定边界的FrameLayout开始。例如,在XML

<FrameLayout
    android:id="@+id/image_parent"
    android:layout_width="200dip"
    android:layout_height="200dip" />

或以编程方式:

FrameLayout parent = new FrameLayout(this);
DisplayMetrics dm = getResources().getDisplayMetrics();
//specify 1/5 screen height and 1/5 screen width
ViewGroup.LayoutParams params = new FrameLayout.LayoutParams(dm.widthPixles/5, dm.heightPixles/5);
parent.setLayoutParams(params);

//get a reference to your layout, then add this view:
myViewGroup.addView(parent);

接下来,添加ImageView孩子。您可以在XML

中声明它
<FrameLayout
    android:id="@+id/image_parent"
    android:layout_width="200dip"
    android:layout_height="200dip" >
    <ImageView 
        android:id="@+id/image"
        android:layout_width="200dip"
        android:layout_height="200dip"
        android:scaleType="fitXY" />
</FrameLayout>

并使用

检索它
ImageView image = (ImageView) findViewById(R.id.image);

或者,您可以通过编程方式创建它:

ImageView image = new ImageView(this);
image.setScaleType(ScaleType.FIT_XY);
parent.addView(image);

无论哪种方式,你都需要操纵它。要将ImageView边界设置为超过其父节点,可以执行以下操作:

//set the bounds to twice the size of the parent
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(parent.getWidth()*2, parent.getHeight()*2);
image.setLayoutParams(params);

//Then set the origin (top left corner). For API 11+, you can use setX() or setY().
//To support older APIs, its simple to use NineOldAndroids (http://nineoldandroids.com)
//This is how to use NineOldAndroids:
final float x = (float) (parent.getWidth()-params.width)/2;
final float y = (float) (parent.getHeight()-params.height)/2;
ViewHelper.setX(image, x);
ViewHelper.setY(image, y);

最后,您可以使用droidQuery库轻松处理View动画(NineOldAndroids中包含droidQuery,因此您只需添加droidquery.jar到你的libs目录,并能够做到目前为止所讨论的所有事情。使用 droidQuery import self.philbrown.droidQuery.*。自动导入可能搞砸了),你可以处理点击,以及动画,如下:

$.with(image).click(new Function() {
    @Override
    public void invoke($ droidQuery, Object... params) {

        //scale down
        if (image.getWidth() != parent.getWidth()) {
            droidQuery.animate("{ width: " + parent.getWidth() + ", " +
                                 "height: " + parent.getHeight() + ", " +
                                 "x: " + Float.valueOf(ViewHelper.getX(parent)) + ", " +//needs to include a decimal in the string so droidQuery knows its a float.
                                 "y: " + Float.valueOf(ViewHelper.getY(parent)) + //needs to include a decimal in the string so droidQuery knows its a float.
                               "}",
                               400,//or some other millisecond value for duration
                               $.Easing.LINEAR,//specify the interpolator
                               $.noop());//don't do anything when complete.
        }
        //scale up
        else {
            droidQuery.animate("{ width: " + parent.getWidth()*2 + ", " +
                                 "height: " + parent.getHeight()*2 + ", " +
                                 "x: " + x + ", " +//needs to include a decimal in the string so droidQuery knows its a float.
                                 "y: " + y + //needs to include a decimal in the string so droidQuery knows its a float.
                               "}",
                               400,//or some other millisecond value for duration
                               $.Easing.LINEAR,//specify the interpolator
                               $.noop());//don't do anything when complete.
        }


    }
});

答案 3 :(得分:2)

试试这个

public void setImageCrop(float value) {
    Matrix matrix = new Matrix();
    matrix.postScale(Math.max(1, 2 - (1 * value)), Math.max(1, 2 - (1 * value)));
    matrix.postTranslate(Math.min(0, translateCropX - (translateCropX * value)), Math.min(0, translateCropY - (translateCropY * value)));
    img.setImageMatrix(matrix);
}