Image out of bounds after transformation on view

2020-07-18 07:03发布

问题:

I'm having a problem with displaying my image.

I have an Image I want to display full screen. So I have this Imageview with match_parent and 20dp padding.

It looks good but when I apply rotation on it, it seems that the bounds of the view doesn't change and the image can get clipped out of the screen ! Totally don't want that to happen! How do I rescale the image so that the image also fits in the ImageView when its 90 degrees rotated.

This is my XML WITH rotation in it.

EDIT:

How to fix the bounds of the Image so the Text is aligned just above the image?

回答1:

The rotation is not taken into account when measuring the view and calculating the scale ratio. A possible solution is to do it yourself :

public class RotatedImageView extends ImageView {

    ...
    constructors
    ...


    private double mRotatedWidth;
    private double mRotatedHeight;

    private boolean update() {
        Drawable d = getDrawable();

        if (d == null) {
            return false;
        }

        int drawableWidth = d.getIntrinsicWidth();
        int drawableHeight = d.getIntrinsicHeight();

        if (drawableWidth <= 0 || drawableHeight <= 0) {
            return false;
        }

        double rotationRad = getRotation() / 180 * Math.PI;

        // calculate intrinsic rotated size
        // see diagram

        mRotatedWidth = (Math.abs(Math.sin(rotationRad)) * drawableHeight
                + Math.abs(Math.cos(rotationRad)) * drawableWidth);
        mRotatedHeight = (Math.abs(Math.cos(rotationRad)) * drawableHeight
                + Math.abs(Math.sin(rotationRad)) * drawableWidth);

        return true;
    }


    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        if (update()) {
            double ratio = mRotatedWidth / mRotatedHeight;

            int wMax = Math.min(getDefaultSize(Integer.MAX_VALUE, widthMeasureSpec), getMaxWidth());
            int hMax = Math.min(getDefaultSize(Integer.MAX_VALUE, heightMeasureSpec), getMaxHeight());

            int w = (int) Math.min(wMax, hMax * ratio);
            int h = (int) Math.min(hMax, wMax / ratio);

            setMeasuredDimension(w, h);
        } else {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }

    }

    private final float[] values = new float[9];

    protected void onDraw(Canvas canvas) {

        if (update()) {
            int availableWidth = getMeasuredWidth();
            int availableHeight = getMeasuredHeight();

            float scale = (float) Math.min(availableWidth / mRotatedWidth, availableHeight / mRotatedHeight);

            getImageMatrix().getValues(values);

            setScaleX(scale / values[Matrix.MSCALE_X]);
            setScaleY(scale / values[Matrix.MSCALE_Y]);
        }

        super.onDraw(canvas);
    }

    @Override
    public void setRotation(float rotation) {
        super.setRotation(rotation);
        requestLayout();
    }
}

adjustViewBounds must be true :

<com.mypackage.RotatedImageView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_margin="20dp"
    android:adjustViewBounds="true"
    android:rotation="90"
    android:maxWidth="100dp"
    android:maxHeight="100dp"
    android:scaleType="fitCenter"
    android:src="@drawable/test" />

A nice explanation of the calculation, courtesy of Cheticamp :

UPDATE: Now trying to adjust the bounds. There is no difference between wrap_content and match_parent (both grow as much as possible, based on the image aspect). You should instead use maxWidth and / or maxHeight, or put it in a LinearLayout with a 0 size and a weight.

It is also not animatable, adjusting bounds while animating requires a layout pass for each frame, which is very inefficient. See the other answer for a version usable with View.animate()



回答2:

Another version of the RotatedImageView which rotation can be animated with a ViewPropertyAnimator. The idea is the same, but the scaling is done in onDraw() instead of onMeasure(), so it does not need a layout pass each time. In order to make the animation work, I had to hijack the update listener. If you want to use your own listener, don't forget to invalidate() the view in onAnimationUpdate().

public class RotatedImageView2 extends ImageView {

    ...
    constructors
    ...

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int beforeWidth = getMeasuredWidth();
        int beforeHeight = getMeasuredHeight();
        int max = Math.max(beforeWidth, beforeHeight);

        // try to grow
        setMeasuredDimension(getDefaultSize(max, widthMeasureSpec), getDefaultSize(max, heightMeasureSpec));
    }


    private final float[] values = new float[9];

    @Override
    protected void onDraw(Canvas canvas) {

        Drawable d = getDrawable();

        if (d == null) {
            return;
        }

        int drawableWidth = d.getIntrinsicWidth();
        int drawableHeight = d.getIntrinsicHeight();

        if (drawableWidth <= 0 || drawableHeight <= 0) {
            return;
        }

        double rotationRad = getRotation() / 180 * Math.PI;

        double rotatedWidth = (Math.abs(Math.sin(rotationRad)) * drawableHeight
                + Math.abs(Math.cos(rotationRad)) * drawableWidth);
        double rotatedHeight = (Math.abs(Math.cos(rotationRad)) * drawableHeight
                + Math.abs(Math.sin(rotationRad)) * drawableWidth);

        int availableWidth = getMeasuredWidth();
        int availableHeight = getMeasuredHeight();

        float scale = (float) Math.min(availableWidth / rotatedWidth, availableHeight / rotatedHeight);

        getImageMatrix().getValues(values);

        setScaleX(scale / values[Matrix.MSCALE_X]);
        setScaleY(scale / values[Matrix.MSCALE_Y]);

        super.onDraw(canvas);
    }

    @Override
    public void setRotation(float rotation) {
        super.setRotation(rotation);
        // force redraw
        invalidate();
    }

    @Override
    public ViewPropertyAnimator animate() {
        // force redraw on each frame
        // (a ViewPropertyAnimator does not use setRotation())
        return super.animate().setUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                invalidate();
            }
        });
    }
}

Use example :

<com.mypackage.RotatedImageView2
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_margin="20dp"
    android:adjustViewBounds="true"
    android:rotation="90"
    android:scaleType="fitCenter"
    android:src="@drawable/test" />


回答3:

according to a research leading to this topic i wonder if @Sarge Borsch answer could work in your case.

Try setting

android:scaleType="centerInside"
android:adjustViewBounds="true"

If centerInside is not correct because you want display in center, maybe try to position the imageview instead of the image inside.

Another suggestion: your imageview is set on "wrap_content" and i don't know exactly the order of everything but maybe the problem comes because it rotates after calculating dimensions (because of wrap_content). I think it is a possibility because the screenshoot you put shows that the image is not even fitting the width. TL;DR : try to fix the imageview size (padding on activity + match_parent) instead of wrap content, in combination of "adjustViewBounds".



回答4:

The attribute android:rotation refers to the View that is the ImageView, not its contents.

If you want to rotate the contents, either set a new BItmap as content, or override onDraw() and rotate the canvas