I'm trying to position an ImageView so that the bottom of the image is always pinned to the bottom of the view, no matter how small the height of the ImageView is. However, none of the scale types seem to fit what I am trying to do. CenterCrop is close, but I don't want the image to be centered. Similar to how CSS would handle absolute positioning.
The reason is, I need to animate the height of the ImageView, but make it seem as though it is "revealing" the upper portion of the image. I assume figuring out this method of cropping the image and animating the ImageView height is the easiest way to do this, but if someone knows of a better way I'd love being pointed in the right direction.
Any help appreciated.
Jpoliachik's answer was cool enough to make me wanna generalize it to support both top/bottom and left/right, by a variable amount. :) Now to top crop, just call setCropOffset(0,0)
, bottom crop setCropOffset(0,1)
, left crop is also setCropOffset(0,0)
, and right crop setCropOffset(1,0)
. If you want to offset the viewport by some fraction of the image in one dimension, you can call e.g. setCropOffset(0, 0.25f)
to shift it down by 25% of the non-viewable space, while 0.5f would center it. Cheers!
/**
* {@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);
}
}
I ended up subclassing ImageView and creating a way to enable a 'BottomCrop' type image scaling.
I assigned the image to a RectF of the correct size by calculating the scale and expected image height based on the view height.
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);
}
}
I used @Jpoliachik code and it worked good, I made a couple of tweaks because sometimes getWidth
and getHeight
were returning 0
- getMeasuredWidth
and getMeasuredHeight
solved the problem.
@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);
}
This solution works fine.
A little improvement would make a CustomView customizable from .xml to topCrop or bottomCrop.
Here is the full solution on gitHub : ScalableImageView
val drawableRect = when (matrixType) {
FIT_BOTTOM -> RectF(0f, drawableHeight - offset, drawableWidth, drawableHeight)
FIT_TOP -> RectF(0f, 0f, drawableWidth, offset)
}
Did you try Imageview's Scaletype FIT_END Thts the best available option to show the end of a image.