我正在尝试定位 ImageView 以便图像的底部始终固定在视图的底部,无论 ImageView 的高度有多小。但是,似乎没有一种比例类型适合我想要做的事情。CenterCrop 很接近,但我不希望图像居中。类似于 CSS 处理绝对定位的方式。
原因是,我需要为 ImageView 的高度设置动画,但让它看起来好像是“显示”了图像的上部。我认为弄清楚这种裁剪图像和动画 ImageView 高度的方法是最简单的方法,但如果有人知道更好的方法,我希望被指出正确的方向。
任何帮助表示赞赏。
Jpoliachik 的回答很酷,让我想将它概括为支持上/下和左/右,数量可变。:) 现在顶部裁剪,只需调用setCropOffset(0,0)
, bottom crop setCropOffset(0,1)
, left crop 也是setCropOffset(0,0)
, right crop setCropOffset(1,0)
。如果您想在一维中将视口偏移图像的一部分,您可以调用 egsetCropOffset(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 代码,它运行良好,我做了一些调整,因为有时会返回 - 并解决了getWidth
问题。getHeight
0
getMeasuredWidth
getMeasuredHeight
@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的回答,我做了一些改进:
setCropOffset()
。相反,您可以只添加app:verticalCropOffset
和app:horizontalCropOffset
到您的 XML 布局(接受分数和浮点数)。app:offsetScaleType
了控制图像缩放方式的属性:
crop
:与原始答案中的行为相同,即图像被缩放以使图像的两个尺寸都等于或大于视图的相应尺寸;app:horizontalCropOffset
然后app:verticalCropOffset
被应用fitInside
:图像被缩放,使得图像的两个维度都等于或小于视图的相应维度;app:horizontalFitOffset
然后app:verticalFitOffset
被应用fitX
:图像被缩放,使其 X 尺寸等于视图的 X 尺寸。Y 维度被缩放,以便保持比率。如果图像的 Y 尺寸大于视图的尺寸,app:verticalCropOffset
则应用,否则app:verticalFitOffset
应用fitY
:图像被缩放,使其 Y 维度等于视图的 Y 维度。X 维度被缩放,以便保持比率。如果图像的 X 尺寸大于视图的尺寸,app:horizontalCropOffset
则应用,否则app:horizontalFitOffset
应用我们必须添加一个新的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" />
此解决方案工作正常。稍作改进将使 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)
}
您是否尝试过 Imageview 的 Scaletype FIT_END?这是显示图像结尾的最佳可用选项。