如何通过动画矩阵来“裁剪”图像?

8
我正在尝试创建一个图像查看器,当用户点击图像时,图像会被“裁剪”并显示完整的图像。
例如,在下面的屏幕截图中,用户最初只能看到小狗的部分。但在用户单击图像后,整个小狗就会显现出来。第一张图像后面逐渐消失的图像显示了动画的结果。
最初,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)。但是如果我在动画期间线性缩放矩阵,则视图在动画期间会被挤压。最终结果看起来很好,但是在动画过程中图像看起来很丑陋。


图片会变大吗(像图片一样),还是只是缩小以显示整个图片,同时保持相同的边界? - Phil
@phil ImageView的缩放比例从0.5到1。它保持相同的布局边界。 - Thierry Roy
4个回答

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


3

这可能会有所帮助...

Android图像视图矩阵缩放+平移

似乎需要两个操作:

  1. 将容器从0.5缩放到1.0
  2. 将内部图像从2.0缩放到1.0

我认为这很简单。

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

我不确定这是否会有动画效果(为什么在捏合缩放手势期间调用setScaleX会导致闪烁?)。也许这可以帮助... http://developer.android.com/training/animation/zoom.html 结果是内部图像始终保持相同的分辨率。
然而,您还提到了纵向/横向差异,因此情况变得更加复杂。我发现在这种情况下最困难的事情是明确定义在所有情况下要发生的事情。
我猜你需要一个小缩略图的网格,它们都是相同大小的(这意味着它们都有相同的宽高比)。你想在缩略图容器中显示具有不同宽高比和大小的图像,但是要放大。当你选择一张图片时,边框会扩展(重叠其他图片?)。有一个限制,即显示的图像必须保持相同的分辨率。你还提到,在选择图像后,完整的图像必须可见,并且是缩略图大小的两倍。这最后一点引发了另一个问题-两倍于缩略图的高度、宽度或两者都是(在这种情况下,如果图像宽高比不匹配,是否裁剪或缩放)。可能出现的另一个问题是图像分辨率不足的情况。
希望Android可以为你做很多事情... 如何在ImageView中缩放图像以保持纵横比

我已经编辑了我的问题。你的推理很好,但会在动画期间导致“挤压”图像。但是感谢你指出其他StackOverflow答案。我会研究它们的。 :) - Thierry Roy
似乎奇怪的是,一个统一的比例尺会压缩图像。这对我来说表明Android不希望经常更新矩阵(或者不希望在更新img比例尺的同时更新矩阵)。如果您只是单独动画矩阵或容器会发生什么?也许更直接的方法是手动绘制到画布上(这肯定会给您更明确地控制正在发生的事情和可能更好的性能)? - jozxyqk
Android动画效果完美。这实际上是一个数学问题。我正在将ImageView按1:1比例缩放(例如,ImageView在X和Y轴上缩放为0.5到1),但图像矩阵具有不同的比例(例如,横向图像的比例为3:1)。我找不到适用于每种情况的矩阵公式。 - Thierry Roy
这段代码大致的意思是... float aspectRatio = translateCropX/translateCropY; matrix.postScale(aspectRatio * zoom, zoom); 在动画期间图像被压缩,但在动画之前和之后没有这种情况,这表明这种方法行不通。也许需要像这样做... float zoomX = 1.0F + value * aspectRatio;(只是猜测) - jozxyqk

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

2

您可以拥有一个超出父视图边界的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动画(droidQuery中包含了NineOldAndroids,所以你只需要将droidquery.jar添加到你的libs目录中,就能够完成前面讨论的所有操作。使用droidQueryimport 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.
        }


    }
});

我不想在每个ImageView周围包含一个FrameLayout,因为这会影响性能。但是你的答案确实可以正确显示动画。 - Thierry Roy
@Thierry-DimitriRoyпјҢдҪ еҸҜд»ҘеңЁдҪ зҡ„AndroidManifest.xmlдёӯж·»еҠ ApplicationеұһжҖ§android:hardwareAccelerated="true"пјҢиҝҷеә”иҜҘеҸҜд»Ҙи§ЈеҶідҪ зҡ„жҖ§иғҪй—®йўҳгҖӮ - Phil
@Thierry-DimitriRoy - 另外,我认为其他不使用父视图的解决方案实际上会对性能造成更大的损害,因为它们将包括原始的“位图”操作或高级图形。 - Phil
使用父级进行动画会降低性能,因为它需要布局传递。通过缩放位图,在动画过程中在GPU上完成操作。 - Thierry Roy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接