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