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)。但是,如果我在动画期间线性缩放矩阵,则视图在动画期间是挤压的。最终结果看起来不错,但在动画过程中图像看起来很难看。

4

4 回答 4

7

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:

enter image description here

于 2013-08-28T23:50:43.877 回答
3

This might help... Android image view matrix scale + translate

There seems to be two operations needed:

  1. Scale the container from 0.5 to 1.0
  2. Zoom the inner image from 2.0 to 1.0

I assume this is as simple as

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

To scale/zoom from the center of the image you might need to do something like this (perhaps swapping the "-" order)...

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

I'm not certain if this will animate (Why does calling setScaleX during pinch zoom gesture cause flicker?). Maybe this will help... http://developer.android.com/training/animation/zoom.html

The result is the inner image remains the same resolution, always.

However you also mention portrait/landscape differences, so this gets a bit more complicated. I find the most difficult thing in cases such as this is to solidly define what you want to have happen in all cases.

I'm guessing you're after a grid of small thumbnails, all of the same size (which mean they all have the same aspect ratio). You want to display images within the thumbnail containers of different aspect ratios and size, but zoomed in. When you select an image, the borders expand (overlapping other images?). There's the constraint that the shown image must keep the same resolution. You also mention that the complete image must be visible after selecting it, and twice the thumbnail size. This last point raises another question - twice the thumbnails height, width or both (in which case do you crop or scale if the image aspect ratio doesn't match). Another issue that might crop up is the case when an image has insufficient resolution.

Hopefully android can do a lot of this for you... How to scale an Image in ImageView to keep the aspect ratio

于 2013-08-22T17:14:31.923 回答
2

您可以拥有超出父级范围的Viewa的子级。ViewGroup因此,例如,假设您上面图像的清晰部分是父视图,而ImageView它是子视图,那么我们只需要放大或缩小图像。为此,请从FrameLayout具有特定界限的 a 开始。例如在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目录并能够执行到目前为止讨论的所有内容。使用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.
        }


    }
});
于 2013-08-23T13:40:16.913 回答
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);
}
于 2013-08-28T13:07:45.197 回答