Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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);
}
}
@arriolac
Copy link
Author

arriolac commented Mar 11, 2015

@gbenson-nflx updated the gist from the snippet you shared. Thanks!

@zelin
Copy link

zelin commented Apr 8, 2015

Great snippet ^^

@adriancosma
Copy link

adriancosma commented May 2, 2015

I would like to have the imageview crop the top center part of the image, now seems that is top left. How can I change the code to obtain a crop centered and not left aligned? Thanks!

@briandilley
Copy link

briandilley commented Jun 24, 2015

to center it:

    private void recomputeImgMatrix() {
        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);
    }

@Bapho
Copy link

Bapho commented Aug 11, 2015

be aware that the drawable can be null in recomputeImgMatrix().

@omidMirrajei
Copy link

omidMirrajei commented Aug 11, 2015

hi:
i import this class to my project ...
how to use for another class and use for image view ?

@omidMirrajei
Copy link

omidMirrajei commented Aug 12, 2015

pls any tell me

@spartacus777
Copy link

spartacus777 commented Sep 7, 2015

Worked perfectly in conjunction with UIL (universal image loader)

@cesards
Copy link

cesards commented Oct 20, 2015

This snippet has a bug in devices with API < 16

I recommend use my lib. It`s already fixed:

here

@Koitharu
Copy link

Koitharu commented Oct 23, 2015

Thanks! But it's will be better for using in xml with additional constructors:

    public TopCropImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
        setScaleType(ScaleType.MATRIX);
    }

    public TopCropImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setScaleType(ScaleType.MATRIX);
    }

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

@MrVilkaman
Copy link

MrVilkaman commented Feb 2, 2016

to left side add matrix.postTranslate(viewWidth - drawableWidth*scale,0); after matrix.setScale(scale, scale);

@naeimrezaeian
Copy link

naeimrezaeian commented Apr 9, 2016

when i use this class compile show this error
android.view.InflateException: Binary XML file

@jrh4016
Copy link

jrh4016 commented Apr 14, 2016

I'd love to get an explanation on how to use this in detail.

I've created a new file with this name and copied over the code. I then changed my ImageView to a TopCropImageView in XML, but I'm getting a litany of errors
Custom view TopCropImageView is not using the 2- or 3-argument View constructors; XML attributes will not work
java.lang.NullPointerException at line 35
Unable to start activity... android.view.InflateException: Binary XML file line #62: Binary XML file line #62: Error inflating class

@Firsto
Copy link

Firsto commented Apr 18, 2016

add this code

@barakhadar
Copy link

barakhadar commented Apr 19, 2016

First, great stuff! It works great! But, there's a but.. What I'm looking for is exactly the opposite...
How do I modify the code so that I'll have the same effect, but with the bottom of my image rather that it's top (as it works by default) ?

@barakhadar
Copy link

barakhadar commented Apr 19, 2016

Never mind... Thanks to @MrVilkaman answer, I found the way.. Thanks dude!

So what you'll do to accomplish that, simply add at the bottom of the class, right after matrix.setScale(scale, scale);, this line : matrix.postTranslate( 0, viewHeight - drawableHeight * scale);.

Hope it will help someone else too..
Cheers!

@arriolac
Copy link
Author

arriolac commented Jul 26, 2016

It's been a while since I've updated this class which explains why there's some issues/warnings caused by newer APIs. I'll be posting an update to this code soon.

@arriolac
Copy link
Author

arriolac commented Jul 27, 2016

Updated gist to now work when inflated in XML and making sure that the drawable is not null before using it.

@seyoung-hyun
Copy link

seyoung-hyun commented Feb 12, 2018

@arriolac
Thanks your code :)
Can I use your code in my commercial app?
Please let me know how I can use the code for commercial distribution.

@klemenzarn
Copy link

klemenzarn commented Apr 11, 2018

Thanks for this.

@lannyf77
Copy link

lannyf77 commented Jun 30, 2018

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

Hitesh309 commented Jan 10, 2019

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

Dishant624 commented Jan 10, 2019

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

@Hitesh309
Copy link

Hitesh309 commented Jan 10, 2019

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

AndreasMattsson commented Sep 27, 2021

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>

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