Android - ImageView底部裁剪而不是中心裁剪

29

我尝试将ImageView定位,使图像的底部始终固定在视图的底部,无论ImageView的高度如何。 然而,所有缩放类型似乎都不适合我想做的事情。 CenterCrop接近,但我不希望图像居中。 类似于CSS处理绝对定位的方式。

原因是,我需要动画显示ImageView的高度,但似乎像是“揭示”图片上部分的方法。 我认为找出这种裁剪图像和动画ImageView高度的方法是最容易做到这一点的方法,但如果有人知道更好的方法,我很愿意指向正确的方向。

任何帮助都会受到赞赏。

6个回答

48

Jpoliachik的回答很棒,让我想要将它推广到支持上下和左右两个方向,并且变量化。现在,要进行向上剪裁,只需调用setCropOffset(0,0),向下剪裁setCropOffset(0,1),左侧剪裁也是setCropOffset(0,0),右侧剪裁则是setCropOffset(1,0)。如果您想以某个维度上图像的一部分偏移视口,可以调用例如setCropOffset(0, 0.25f)将其向下移动非可视空间的25%,而0.5f则居中。干杯!

/**
 * {@link android.widget.ImageView} that supports directional cropping in both vertical and
 * horizontal directions instead of being restricted to center-crop. Automatically sets {@link
 * android.widget.ImageView.ScaleType} to MATRIX and defaults to center-crop.
 */
public class CropImageView extends android.support.v7.widget.AppCompatImageView {
    private static final float DEFAULT_HORIZONTAL_OFFSET = 0.5f;
    private static final float DEFAULT_VERTICAL_OFFSET = 0.5f;

    private float mHorizontalOffsetPercent = DEFAULT_HORIZONTAL_OFFSET;
    private float mVerticalOffsetPercent = DEFAULT_VERTICAL_OFFSET;

    public CropImageView(Context context) {
        this(context, null);
    }

    public CropImageView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CropImageView(Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setScaleType(ScaleType.MATRIX);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        applyCropOffset();
    }

    /**
     * Sets the crop box offset by the specified percentage values. For example, a center-crop would
     * be (0.5, 0.5), a top-left crop would be (0, 0), and a bottom-center crop would be (0.5, 1)
     */
    public void setCropOffset(float horizontalOffsetPercent, float verticalOffsetPercent) {
        if (mHorizontalOffsetPercent < 0
                || mVerticalOffsetPercent < 0
                || mHorizontalOffsetPercent > 1
                || mVerticalOffsetPercent > 1) {
            throw new IllegalArgumentException("Offset values must be a float between 0.0 and 1.0");
        }

        mHorizontalOffsetPercent = horizontalOffsetPercent;
        mVerticalOffsetPercent = verticalOffsetPercent;
        applyCropOffset();
    }

    private void applyCropOffset() {
        Matrix matrix = getImageMatrix();

        float scale;
        int viewWidth = getWidth() - getPaddingLeft() - getPaddingRight();
        int viewHeight = getHeight() - getPaddingTop() - getPaddingBottom();
        int drawableWidth = 0, drawableHeight = 0;
        // Allow for setting the drawable later in code by guarding ourselves here.
        if (getDrawable() != null) {
            drawableWidth = getDrawable().getIntrinsicWidth();
            drawableHeight = getDrawable().getIntrinsicHeight();
        }

        // Get the scale.
        if (drawableWidth * viewHeight > drawableHeight * viewWidth) {
            // Drawable is flatter than view. Scale it to fill the view height.
            // A Top/Bottom crop here should be identical in this case.
            scale = (float) viewHeight / (float) drawableHeight;
        } else {
            // Drawable is taller than view. Scale it to fill the view width.
            // Left/Right crop here should be identical in this case.
            scale = (float) viewWidth / (float) drawableWidth;
        }

        float viewToDrawableWidth = viewWidth / scale;
        float viewToDrawableHeight = viewHeight / scale;
        float xOffset = mHorizontalOffsetPercent * (drawableWidth - viewToDrawableWidth);
        float yOffset = mVerticalOffsetPercent * (drawableHeight - viewToDrawableHeight);

        // Define the rect from which to take the image portion.
        RectF drawableRect =
                new RectF(
                        xOffset,
                        yOffset,
                        xOffset + viewToDrawableWidth,
                        yOffset + viewToDrawableHeight);
        RectF viewRect = new RectF(0, 0, viewWidth, viewHeight);
        matrix.setRectToRect(drawableRect, viewRect, Matrix.ScaleToFit.FILL);

        setImageMatrix(matrix);
    }
}

7
我进行了更改,使得调整大小的代码从onSizeChanged()方法中调用,而不是从setFrame()方法中调用,因为在调整大小后setFrame()方法将不会被调用,导致图片无法正确对齐。看起来效果不错。谢谢。 - Carlos Fonseca
1
正如Carlos Fonseca所说,最好将调整大小的代码放在onSizeChanged()中,特别是如果您在视图分页器中使用此CropImageView,否则您将有一些图像根本不显示。 - Quentin G.

32

我最终通过创建ImageView的子类并实现“底部裁剪”类型的图像缩放功能来解决问题。

我通过计算缩放比例和期望的图片高度(基于视图高度)将图像分配给正确尺寸的RectF。

public class BottomCropImage extends ImageView {

public BottomCropImage(Context context) {
    super(context);
    setup();
}

public BottomCropImage(Context context, AttributeSet attrs) {
    super(context, attrs);
    setup();
}

public BottomCropImage(Context context, AttributeSet attrs,
        int defStyle) {
    super(context, attrs, defStyle);
    setup();
}

private void setup() {
    setScaleType(ScaleType.MATRIX);
}

@Override
protected boolean setFrame(int l, int t, int r, int b) {
    Matrix matrix = getImageMatrix();

    float scale;
    int viewWidth = getWidth() - getPaddingLeft() - getPaddingRight();
    int viewHeight = getHeight() - getPaddingTop() - getPaddingBottom();
    int drawableWidth = getDrawable().getIntrinsicWidth();
    int drawableHeight = getDrawable().getIntrinsicHeight();

    //Get the scale 
    if (drawableWidth * viewHeight > drawableHeight * viewWidth) {
        scale = (float) viewHeight / (float) drawableHeight;
    } else {
        scale = (float) viewWidth / (float) drawableWidth;
    }

    //Define the rect to take image portion from
    RectF drawableRect = new RectF(0, drawableHeight - (viewHeight / scale), drawableWidth, drawableHeight);
    RectF viewRect = new RectF(0, 0, viewWidth, viewHeight);
    matrix.setRectToRect(drawableRect, viewRect, Matrix.ScaleToFit.FILL);


    setImageMatrix(matrix);

    return super.setFrame(l, t, r, b);
}        

}

1
如何将其转换为TopCropImage? - Thamilan S
1
@Mani 我最终两个都做了,我觉得这会很有用!Github链接 - Jpoliachik
我已经自己完成了,因为我需要在昨天就完成它,无论如何还是谢谢你,我做的和你一样。 - Thamilan S

16

我使用了@Jpoliachik的代码,效果不错,但是我做了一些调整,因为有时候getWidthgetHeight会返回0getMeasuredWidthgetMeasuredHeight解决了这个问题。

@Override
protected boolean setFrame(int l, int t, int r, int b) {
   if (getDrawable() == null)
       return super.setFrame(l, t, r, b);

   Matrix matrix = getImageMatrix();

   float scale;
   int viewWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
   int viewHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
   int drawableWidth = getDrawable().getIntrinsicWidth();
   int drawableHeight = getDrawable().getIntrinsicHeight();
   //Get the scale
   if (drawableWidth * viewHeight > drawableHeight * viewWidth) {
       scale = (float) viewHeight / (float) drawableHeight;
   } else {
       scale = (float) viewWidth / (float) drawableWidth;
   }

   //Define the rect to take image portion from
   RectF drawableRect = new RectF(0, drawableHeight - (viewHeight / scale), drawableWidth, drawableHeight);
   RectF viewRect = new RectF(0, 0, viewWidth, viewHeight);
   matrix.setRectToRect(drawableRect, viewRect, Matrix.ScaleToFit.FILL);

   setImageMatrix(matrix);

   return super.setFrame(l, t, r, b);
}

1
这解决了我在横屏和竖屏之间切换时遇到的问题,谢谢。 - Gabe
1
Jpoliachik代码存在一个问题,当软键盘调整ImageView的大小时(当包含的Activity具有android:windowSoftInputMode="adjustResize"时),会出现问题。您的代码解决了这个问题,做得很好!谢谢 - Bartek Lipinski

7

根据 qix的回答,我做了一些改进:

  1. 创建了自定义的 XML 属性。您不需要调用setCropOffset()。相反,您可以将app:verticalCropOffsetapp:horizontalCropOffset 添加到您的 XML 布局中(接受分数和浮点数)。
  2. 添加了app:offsetScaleType 属性来控制图片如何缩放:
    • crop: 与原始答案相同的行为,即缩放图像使得图像的两个维度都等于或大于视图的对应维度; 然后应用app:horizontalCropOffsetapp:verticalCropOffset
    • fitInside: 缩放图像使得图像的两个维度都等于或小于视图的对应维度; 然后应用app:horizontalFitOffsetapp:verticalFitOffset
    • fitX: 缩放图像使得其 X 维度等于视图的 X 维度。Y 维度缩放以保持比例。如果图像的 Y 维度大于视图的维度,则应用app:verticalCropOffset,否则应用app:verticalFitOffset
    • fitY: 缩放图像使得其 Y 维度等于视图的 Y 维度。X 维度缩放以保持比例。如果图像的 X 维度大于视图的维度,则应用app:horizontalCropOffset,否则应用app:horizontalFitOffset
  3. 将代码转换为 Kotlin
  4. 进行了一些小的重构,以提高 Kotlin 的可读性

我们需要在 attrs.xml 中添加一个新的 OffsetImageView 样式:

<declare-styleable name="OffsetImageView">
    <attr name="horizontalFitOffset" format="float|fraction" />
    <attr name="verticalFitOffset" format="float|fraction" />
    <attr name="horizontalCropOffset" format="float|fraction" />
    <attr name="verticalCropOffset" format="float|fraction" />
    <attr name="offsetScaleType" format="enum">
        <enum name="crop" value="0"/>
        <enum name="fitInside" value="1"/>
        <enum name="fitX" value="2"/>
        <enum name="fitY" value="3"/>
    </attr>
</declare-styleable>

OffsetImageView 代码(添加你自己的包并导入你的模块的 R 文件):

import android.content.Context
import android.content.res.TypedArray
import android.graphics.Matrix
import android.graphics.RectF
import android.util.AttributeSet
import androidx.annotation.AttrRes
import androidx.annotation.StyleableRes
import androidx.appcompat.widget.AppCompatImageView


/**
 * [android.widget.ImageView] that supports directional cropping in both vertical and
 * horizontal directions instead of being restricted to center-crop. Automatically sets [ ] to MATRIX and defaults to center-crop.
 *
 * XML attributes (for offsets either a float or a fraction is allowed in values, e. g. 50% or 0.5):
 * - app:verticalCropOffset
 * - app:horizontalCropOffset
 * - app:verticalFitOffset
 * - app:horizontalFitOffset
 * - app:offsetScaleType
 *
 * The `app:offsetScaleType` accepts one of the enum values:
 * - crop: the same behavior as in the original answer, i. e. the image is scaled so that both dimensions of the image will be equal to or larger than the corresponding dimension of the view; `app:horizontalCropOffset` and `app:verticalCropOffset` are then applied
 * - fitInside: image is scaled so that both dimensions of the image will be equal to or less than the corresponding dimension of the view; `app:horizontalFitOffset` and `app:verticalFitOffset` are then applied
 * - fitX: image is scaled so that its X dimension is equal to the view's X dimension. Y dimension is scaled so that the ratio is preserved. If image's Y dimension is larger than view's dimension, `app:verticalCropOffset` is applied, otherwise `app:verticalFitOffset` is applied
 * - fitY: image is scaled so that its Y dimension is equal to the view's Y dimension. X dimension is scaled so that the ratio is preserved. If image's X dimension is larger than view's dimension, `app:horizontalCropOffset` is applied, otherwise `app:horizontalFitOffset` is applied
 */
class OffsetImageView(context: Context, attrs: AttributeSet?, @AttrRes defStyleAttr: Int) : AppCompatImageView(context, attrs, defStyleAttr) {
    companion object {
        private const val DEFAULT_HORIZONTAL_OFFSET = 0.5f
        private const val DEFAULT_VERTICAL_OFFSET = 0.5f
    }

    enum class OffsetScaleType(val code: Int) {
        CROP(0), FIT_INSIDE(1), FIT_X(2), FIT_Y(3)
    }

    private var mHorizontalCropOffsetPercent = DEFAULT_HORIZONTAL_OFFSET
    private var mHorizontalFitOffsetPercent = DEFAULT_HORIZONTAL_OFFSET
    private var mVerticalCropOffsetPercent = DEFAULT_VERTICAL_OFFSET
    private var mVerticalFitOffsetPercent = DEFAULT_VERTICAL_OFFSET
    private var mOffsetScaleType = OffsetScaleType.CROP

    init {
        scaleType = ScaleType.MATRIX
        if (attrs != null) {
            val a = context.obtainStyledAttributes(attrs, R.styleable.OffsetImageView, defStyleAttr, 0)

            readAttrFloatValueIfSet(a, R.styleable.OffsetImageView_verticalCropOffset)?.let {
                mVerticalCropOffsetPercent = it
            }
            readAttrFloatValueIfSet(a, R.styleable.OffsetImageView_horizontalCropOffset)?.let {
                mHorizontalCropOffsetPercent = it
            }
            readAttrFloatValueIfSet(a, R.styleable.OffsetImageView_verticalFitOffset)?.let {
                mVerticalFitOffsetPercent = it
            }
            readAttrFloatValueIfSet(a, R.styleable.OffsetImageView_horizontalFitOffset)?.let {
                mHorizontalFitOffsetPercent = it
            }
            with (a) {
                if (hasValue(R.styleable.OffsetImageView_offsetScaleType)) {
                    val code = getInt(R.styleable.OffsetImageView_offsetScaleType, -1)
                    if (code != -1) {
                        OffsetScaleType.values().find {
                            it.code == code
                        }?.let {
                            mOffsetScaleType = it
                        }
                    }
                }
            }

            a.recycle()
        }
    }

    constructor(context: Context) : this(context, null)
    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        applyOffset()
    }

    private fun readAttrFloatValueIfSet(typedArray: TypedArray, @StyleableRes index: Int): Float? {
        try {
            with(typedArray) {
                if (!hasValue(index)) return null
                var value = getFloat(index, -1f)
                if (value >= 0) return value

                value = getFraction(index, 1, 1, -1f)
                if (value >= 0) return value

                return null
            }
        } catch (e: RuntimeException) {
            e.printStackTrace()
            return null
        }
    }

    /**
     * Sets the crop box offset by the specified percentage values. For example, a center-crop would
     * be (0.5, 0.5), a top-left crop would be (0, 0), and a bottom-center crop would be (0.5, 1)
     */
    fun setOffsets(horizontalCropOffsetPercent: Float,
                   verticalCropOffsetPercent: Float,
                   horizontalFitOffsetPercent: Float,
                   verticalFitOffsetPercent: Float,
                   scaleType: OffsetScaleType) {
        require(!(mHorizontalCropOffsetPercent < 0
                || mVerticalCropOffsetPercent < 0
                || mHorizontalFitOffsetPercent < 0
                || mVerticalFitOffsetPercent < 0
                || mHorizontalCropOffsetPercent > 1
                || mVerticalCropOffsetPercent > 1
                || mHorizontalFitOffsetPercent > 1
                || mVerticalFitOffsetPercent > 1)) { "Offset values must be a float between 0.0 and 1.0" }
        mHorizontalCropOffsetPercent = horizontalCropOffsetPercent
        mVerticalCropOffsetPercent = verticalCropOffsetPercent
        mHorizontalFitOffsetPercent = horizontalFitOffsetPercent
        mVerticalFitOffsetPercent = verticalFitOffsetPercent
        mOffsetScaleType = scaleType
        applyOffset()
    }

    private fun applyOffset() {
        val matrix: Matrix = imageMatrix
        val scale: Float
        val viewWidth: Int = width - paddingLeft - paddingRight
        val viewHeight: Int = height - paddingTop - paddingBottom
        val drawable = drawable
        val drawableWidth: Int
        val drawableHeight: Int

        if (drawable == null) {
            drawableWidth = 0
            drawableHeight = 0
        } else {
            // Allow for setting the drawable later in code by guarding ourselves here.
            drawableWidth = drawable.intrinsicWidth
            drawableHeight = drawable.intrinsicHeight
        }

        val scaleHeight = when (mOffsetScaleType) {
            OffsetScaleType.CROP -> drawableWidth * viewHeight > drawableHeight * viewWidth // If drawable is flatter than view, scale it to fill the view height.
            OffsetScaleType.FIT_INSIDE -> drawableWidth * viewHeight < drawableHeight * viewWidth // If drawable is is taller than view, scale according to height to fit inside.
            OffsetScaleType.FIT_X -> false // User wants to fit X axis -> scale according to width
            OffsetScaleType.FIT_Y -> true // User wants to fit Y axis -> scale according to height
        }
        // Get the scale.
        scale = if (scaleHeight) {
            viewHeight.toFloat() / drawableHeight.toFloat()
        } else {
            viewWidth.toFloat() / drawableWidth.toFloat()
        }
        val viewToDrawableWidth = viewWidth / scale
        val viewToDrawableHeight = viewHeight / scale

        if (drawableWidth >= viewToDrawableWidth && drawableHeight >= viewToDrawableHeight) {
            val xOffset = mHorizontalCropOffsetPercent * (drawableWidth - viewToDrawableWidth)
            val yOffset = mVerticalCropOffsetPercent * (drawableHeight - viewToDrawableHeight)

            // Define the rect from which to take the image portion.
                val drawableRect = RectF(
                        xOffset,
                        yOffset,
                        xOffset + viewToDrawableWidth,
                        yOffset + viewToDrawableHeight)
                val viewRect = RectF(0f, 0f, viewWidth.toFloat(), viewHeight.toFloat())
                matrix.setRectToRect(drawableRect, viewRect, Matrix.ScaleToFit.FILL)
        } else {
            val xOffset = mHorizontalFitOffsetPercent * (viewToDrawableWidth - drawableWidth) * scale
            val yOffset = mVerticalFitOffsetPercent * (viewToDrawableHeight - drawableHeight) * scale

            val drawableRect = RectF(
                    0f,
                    0f,
                    drawableWidth.toFloat(),
                    drawableHeight.toFloat())
            val viewRect = RectF(xOffset, yOffset, xOffset + drawableWidth * scale, yOffset + drawableHeight * scale)
            matrix.setRectToRect(drawableRect, viewRect, Matrix.ScaleToFit.FILL)
        }
        imageMatrix = matrix
    }
}

在您的布局中使用如下:

<your.package.OffsetImageView
    android:id="@+id/image"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@drawable/image"
    app:verticalFitOffset="0.3"
    app:horizontalFitOffset="70%"
    app:offsetScaleType="fitInside" />

1

这个解决方案运行良好。 稍作改进即可使CustomView从.xml到topCrop或bottomCrop可定制。 以下是完整的gitHub解决方案:ScalableImageView

val drawableRect = when (matrixType) {
    FIT_BOTTOM -> RectF(0f, drawableHeight - offset, drawableWidth, drawableHeight)
    FIT_TOP -> RectF(0f, 0f, drawableWidth, offset)
}

-4

你试过使用ImageView的ScaleType FIT_END 吗?这是显示图片末尾的最佳选项。


2
FIT_END不会裁剪图像。它将图像适配到可用的框架中。看起来没有任何可用的内置比例类型符合我的要求,但我需要子类化ImageView。 - Jpoliachik
没错,FIT_END不会裁剪图像。但这是显示图像末尾的唯一可用选项。 - prijupaul

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