簡體   English   中英

Android - ImageView bottomCrop 而不是 centerCrop

[英]Android - ImageView bottomCrop instead of centerCrop

我試圖定位一個 ImageView,這樣無論 ImageView 的高度有多小,圖像的底部總是固定在視圖的底部。 但是,似乎沒有一種比例類型適合我正在嘗試做的事情。 CenterCrop 很接近,但我不希望圖像居中。 類似於 CSS 處理絕對定位的方式。

原因是,我需要為 ImageView 的高度設置動畫,但讓它看起來好像在“揭示”圖像的上部。 我認為找出這種裁剪圖像和動畫 ImageView 高度的方法是最簡單的方法,但如果有人知道更好的方法,我很樂意指出正確的方向。

任何幫助表示贊賞。

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

我最終繼承了 ImageView 並創建了一種啟用“BottomCrop”類型圖像縮放的方法。

我通過根據視圖高度計算比例和預期圖像高度,將圖像分配給正確大小的 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);
}        

}

我使用了@Jpoliachik 代碼並且效果很好,我做了一些調整,因為有時getWidthgetHeight返回0 - getMeasuredWidthgetMeasuredHeight解決了這個問題。

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

根據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 的可讀性

我們必須添加一個新的OffsetImageView樣式到我們的attrs.xml

<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" />

此解決方案工作正常。 一點點改進將使自定義視圖從 .xml 自定義為 topCrop 或 bottomCrop。 這是 gitHub 上的完整解決方案: ScalableImageView

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

您是否嘗試過 Imageview 的 Scaletype FIT_END這是顯示圖像結尾的最佳可用選項。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM