Skip to content

Instantly share code, notes, and snippets.

@arriolac
Last active October 1, 2024 10:40
Show Gist options
  • Save arriolac/3843346 to your computer and use it in GitHub Desktop.
Save arriolac/3843346 to your computer and use it in GitHub Desktop.
Custom Android ImageView for top-crop scaling of the contained drawable.
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.AttributeSet;
import android.widget.ImageView;
/**
* Created by chris on 7/27/16.
*/
public class TopCropImageView extends ImageView {
public TopCropImageView(Context context) {
super(context);
init();
}
public TopCropImageView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public TopCropImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public TopCropImageView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
recomputeImgMatrix();
}
@Override
protected boolean setFrame(int l, int t, int r, int b) {
recomputeImgMatrix();
return super.setFrame(l, t, r, b);
}
private void init() {
setScaleType(ScaleType.MATRIX);
}
private void recomputeImgMatrix() {
final Drawable drawable = getDrawable();
if (drawable == null) {
return;
}
final Matrix matrix = getImageMatrix();
float scale;
final int viewWidth = getWidth() - getPaddingLeft() - getPaddingRight();
final int viewHeight = getHeight() - getPaddingTop() - getPaddingBottom();
final int drawableWidth = drawable.getIntrinsicWidth();
final int drawableHeight = drawable.getIntrinsicHeight();
if (drawableWidth * viewHeight > drawableHeight * viewWidth) {
scale = (float) viewHeight / (float) drawableHeight;
} else {
scale = (float) viewWidth / (float) drawableWidth;
}
matrix.setScale(scale, scale);
setImageMatrix(matrix);
}
}
@klemenzarn
Copy link

Thanks for this.

@lannyf77
Copy link

final Matrix matrix = getImageMatrix();

any concern about the doc says "Do not change this matrix in place but make a copy.":

/** Returns the view's optional matrix. This is applied to the
view's drawable when it is drawn. If there is no matrix,
this method will return an identity matrix.
Do not change this matrix in place but make a copy.
If you want a different matrix applied to the drawable,
be sure to call setImageMatrix().
*/
public Matrix getImageMatrix() {
if (mDrawMatrix == null) {
return new Matrix(Matrix.IDENTITY_MATRIX);
}
return mDrawMatrix;
}

@Hitesh309
Copy link

Image not loading after swipe to refresh layout in glide library.
Please give some feedback.
`public class TopCropImageView extends android.support.v7.widget.AppCompatImageView {

public TopCropImageView(Context context) {
    super(context);
    init();
}

public TopCropImageView(Context context, AttributeSet attrs) {
    super(context, attrs);
    init();
}

public TopCropImageView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init();
}

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public TopCropImageView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr);
    init();
}

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom);
    recomputeImgMatrix();
}

@Override
protected boolean setFrame(int l, int t, int r, int b) {
    recomputeImgMatrix();
    return super.setFrame(l, t, r, b);
}


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

private void recomputeImgMatrix() {

    Drawable drawable = getDrawable();
    if (drawable != null) {

        final Matrix matrix = getImageMatrix();

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

        if (drawableWidth * viewHeight > drawableHeight * viewWidth) {
            scale = (float) viewHeight / (float) drawableHeight;
        } else {
            scale = (float) viewWidth / (float) drawableWidth;
        }
        matrix.setScale(scale, scale);

        if ((drawableWidth * scale) > viewWidth) {
            float tr = -(((drawableWidth * scale) - viewWidth) / 2);
            matrix.postTranslate(tr, 0);
        }
        setImageMatrix(matrix);
    }
}

}
`

@Dishant624
Copy link

please add this method on code
@Override protected void onDraw(Canvas canvas) { recomputeImgMatrix(); super.onDraw(canvas); }

@Hitesh309
Copy link

Thanks @Dishant624 for your code
You save my day :)

@MichalDanielDobrzanski
Copy link

MichalDanielDobrzanski commented Jul 2, 2019

@Dishant624 - could you elaborate why? It would be much more costly in terms of performance.
Also this was enough for me:

@Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        recomputeImgMatrix();
    }

Hence no need for overriding setFrame method. Also, setFrame is supported ONLY for two View subclasses: https://stackoverflow.com/questions/4751963/android-why-cant-i-override-setframe-from-view

In Kotlin, I got succeeded with MotionLayout with:

class BottomCenterImageView : AppCompatImageView {

    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)

    constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

    init {
        scaleType = ScaleType.MATRIX
    }

    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        super.onLayout(changed, left, top, right, bottom)
        recomputeImageMatrix()
    }

    private fun recomputeImageMatrix() {
        val drawable = drawable ?: return
        val viewWidth = width - paddingLeft - paddingRight
        val viewHeight = height - paddingTop - paddingBottom
        val drawableWidth = drawable.intrinsicWidth
        val drawableHeight = drawable.intrinsicHeight
        imageMatrix = imageMatrix.apply {
            setTranslate(
                Math.round((viewWidth - drawableWidth) * 0.5f).toFloat(),
                Math.round((viewHeight - drawableHeight).toFloat()).toFloat()
            )
        }
    }
}

@fm-eb
Copy link

fm-eb commented Jan 22, 2021

Here is a Kolin version:

import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatImageView

class TopCropImageView : AppCompatImageView {

    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

    constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle)
    
    init {
        scaleType = ScaleType.MATRIX
    }
    
    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        super.onLayout(changed, left, top, right, bottom)
        recomputeImgMatrix()
    }

    override fun setFrame(l: Int, t: Int, r: Int, b: Int): Boolean {
        recomputeImgMatrix()
        return super.setFrame(l, t, r, b)
    }

    private fun recomputeImgMatrix() {
        val matrix = imageMatrix
        
        val viewWidth = width - paddingLeft - paddingRight
        val viewHeight = height - paddingTop - paddingBottom
        val drawableWidth = drawable.intrinsicWidth
        val drawableHeight = drawable.intrinsicHeight
        
        val scale = if (drawableWidth * viewHeight > drawableHeight * viewWidth) {
            viewHeight.toFloat() / drawableHeight.toFloat()
        } else {
            viewWidth.toFloat() / drawableWidth.toFloat()
        }
        
        matrix.setScale(scale, scale)
        imageMatrix = matrix
    }
}

@AndreasMattsson
Copy link

And here a version adapted to allow you to set the alignment in the range (0.0, 0.0) = top left to (1.0, 1.0) = (bottom right) either via code or XML attributes:

open class AlignmentCropImageView : AppCompatImageView {

    constructor(context: Context) : super(context) {
        initAttrs(context, null, 0)
    }

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
        initAttrs(context, attrs, 0)
    }

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr
    ) {
        initAttrs(context, attrs, defStyleAttr)
    }

    open var alignmentX = 0.5f
    open var alignmentY = 0.5f

    private fun initAttrs(context: Context, attrs: AttributeSet?, defStyleAttr: Int) {
        scaleType = ScaleType.MATRIX
        context.obtainStyledAttributes(
            attrs,
            R.styleable.AlignmentCropImageView,
            defStyleAttr,
            0
        ).apply {
            alignmentX = getFloat(R.styleable.AlignmentCropImageView_alignmentX, alignmentX)
            alignmentY = getFloat(R.styleable.AlignmentCropImageView_alignmentX, alignmentY)
        }.recycle()
    }

    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        super.onLayout(changed, left, top, right, bottom)
        recomputeImgMatrix()
    }

    override fun setFrame(l: Int, t: Int, r: Int, b: Int): Boolean {
        recomputeImgMatrix()
        return super.setFrame(l, t, r, b)
    }

    private fun recomputeImgMatrix() {
        val matrix = imageMatrix

        val viewWidth = width - paddingLeft - paddingRight
        val viewHeight = height - paddingTop - paddingBottom
        val drawableWidth = drawable?.intrinsicWidth ?: 0
        val drawableHeight = drawable?.intrinsicHeight ?: 0

        val scale = if (drawableWidth * viewHeight > drawableHeight * viewWidth) {
            viewHeight.toFloat() / drawableHeight.toFloat()
        } else {
            viewWidth.toFloat() / drawableWidth.toFloat()
        }
        matrix.setScale(scale, scale)
        matrix.postTranslate(
            (viewWidth - drawableWidth * scale) * alignmentX,
            (viewHeight - drawableHeight * scale) * alignmentY
        )
        imageMatrix = matrix
    }
}

attrs.xml:

<resources>
    <declare-styleable name="AlignmentCropImageView">
        <attr name="alignmentX" format="float" />
        <attr name="alignmentY" format="float" />
    </declare-styleable>
</resources>

@AndroidDeveloperLB
Copy link

AndroidDeveloperLB commented Oct 1, 2024

This is for the top.
What if I want to make sure a specific point/rectangle is shown while fitting&cropping, keeping the aspect ratio?
How can I do it?
Currently the only similar thing that officially exists is center-crop, but it's only to the center. What if the most important part in the image is at the bottom, instead? Or 10% (or 10px) from the bottom, etc... ? Or if there is a specific region in the bitmap that's most important?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment