ImageView scaling TOP_CROP
Asked Answered
S

11

94

I have an ImageView which is displaying a png that has a bigger aspect ratio than that of the device (vertically speaking - meaning its longer). I want to display this while maintaining aspect ratio, matching the width of the parent, and pinning the imageview to the top of the screen.

The problem i have with using CENTER_CROP as the scale type is that it will (understandable) center the scaled image instead of aligning the top edge to the top edge f the image view.

The problem with FIT_START is that the image will fit the screen height and not fill the width.

I have solved this problem by using a custom ImageView and overriding onDraw(Canvas) and handeling this manually using the canvas; the problem with this approach is that 1) I'm worried there may be a simpler solution, 2) I am getting a VM mem exception when calling super(AttributeSet) in the constructor when trying to set a src img of 330kb when the heap has 3 mb free (with a heap size of 6 mb) and cant work out why.

Any ideas / suggestions / solutions are much welcome :)

Thanks

p.s. i thought that a solution may be to use a matrix scale type and do it myself, but that seems to to be the same or more work than my current solution!

Shepard answered 13/6, 2011 at 11:56 Comment(2)
Did you try with CENTER_CROP and set the adjustViewBounds property as true with the ImageView?Meade
Yes i have tried that thx, no success im afraid as it will expand the view until it width its parent, which will not be larger than the screen, and then center the image on the screen with the excess height / 2 poking off the top and bottomShepard
S
87

Ok, I have a working solution. The prompt from Darko made me look again at the ImageView class (thanks) and have applied the transformation using a Matrix (as i originally suspected but did not have success on my first attempt!). In my custom imageView class I call setScaleType(ScaleType.MATRIX) after super() in the constructor, and have the following method.

    @Override
    protected boolean setFrame(int l, int t, int r, int b)
    {
        Matrix matrix = getImageMatrix(); 
        float scaleFactor = getWidth()/(float)getDrawable().getIntrinsicWidth();    
        matrix.setScale(scaleFactor, scaleFactor, 0, 0);
        setImageMatrix(matrix);
        return super.setFrame(l, t, r, b);
    }

I have placed int in the setFrame() method as in ImageView the call to configureBounds() is within this method, which is where all the scaling and matrix stuff takes place, so seems logical to me (say if you disagree)

Below is the super.setFrame() method from the AOSP (Android Open Source Project)

    @Override
    protected boolean setFrame(int l, int t, int r, int b) {
        boolean changed = super.setFrame(l, t, r, b);
        mHaveFrame = true;
        configureBounds();
        return changed;
    }

Find the full class src here

Shepard answered 13/6, 2011 at 16:47 Comment(12)
Thanks for the code, @doridori! It worked ok! I just don't understand why did you repeat the "setFrame" method in your explanation... I used just the first one with success (and completely ignored the second xD)Outfoot
@Outfoot the second one is the one from android sources.Compeer
After fighting with this via xml layout for two hours, this worked. I wish I could give you more upboats.Currish
I had to call super() before the body otherwise the image wouldn't display without a repaintBlacktail
Thanks, this work for me but how to get the similar effect for Bottom_Crop effect? i.e, keeping the bottom of the image in focus and scale it based on the width and height of imageviewHaplo
Like @Blacktail I had to call super() at the start of setFrame() in order for this to work properly for me when used in a PageViewer. Otherwise the images would disappear as I scrolled through the pagesBabbitt
In my custom ImageView subclass, I found that getWidth() was returning zero when called from within setFrame(). I replaced it with getMeasuredWidth(), and so far that works.Broomcorn
I'm trying to make an END_CROP scaling ImageView. Nothing so far. Any hints?Fahey
@VitorHugoSchwaab you have to use thms like matrix.postTranslate(..)Wot
can't use background?only use src?Salpingitis
what should I do if i want to RIGHT_CROP?Bookshelf
Wow, THANK YOU! Side note to anyone that is loading images in from a cdn on the fly and trying to use this. You will likely run into a NPE in the setFrame() call at getDrawable() because the image might not be set when that method is called. To remedy this, I used the logic of this method as a helper method and set a regular ImageView to scaleType="matrix" and then called the helper method only when the image has officially loaded in. Cheers!Markhor
H
45

Here is my code for centering it at the bottom.

BTW in Dori's Code is a little bug: Since the super.frame() is called at the very end, the getWidth() method might return the wrong value.

If you want to center it at the top simply remove the postTranslate line and you're done.

The nice thing is that with this code you can move it anywhere you want. (right, center => no problem ;)

    public class CenterBottomImageView extends ImageView {
    
        public CenterBottomImageView(Context context) {
            super(context);
            setup();
        }
        
        public CenterBottomImageView(Context context, AttributeSet attrs) {
            super(context, attrs);
            setup();
        }
    
        public CenterBottomImageView(Context context, AttributeSet attrs,
                int defStyle) {
            super(context, attrs, defStyle);
            setup();
        }
        
        private void setup() {
            setScaleType(ScaleType.MATRIX);
        }
    
        @Override
        protected boolean setFrame(int frameLeft, int frameTop, int frameRight, int frameBottom) {
            if (getDrawable() == null) {
                return super.setFrame(frameLeft, frameTop, frameRight, frameBottom);
            }
            float frameWidth = frameRight - frameLeft;
            float frameHeight = frameBottom - frameTop;
            
            float originalImageWidth = (float)getDrawable().getIntrinsicWidth();
            float originalImageHeight = (float)getDrawable().getIntrinsicHeight();
            
            float usedScaleFactor = 1;
            
            if((frameWidth > originalImageWidth) || (frameHeight > originalImageHeight)) {
                // If frame is bigger than image
                // => Crop it, keep aspect ratio and position it at the bottom and center horizontally
                
                float fitHorizontallyScaleFactor = frameWidth/originalImageWidth;
                float fitVerticallyScaleFactor = frameHeight/originalImageHeight;
                
                usedScaleFactor = Math.max(fitHorizontallyScaleFactor, fitVerticallyScaleFactor);
            }
            
            float newImageWidth = originalImageWidth * usedScaleFactor;
            float newImageHeight = originalImageHeight * usedScaleFactor;
            
            Matrix matrix = getImageMatrix();
            matrix.setScale(usedScaleFactor, usedScaleFactor, 0, 0); // Replaces the old matrix completly
//comment matrix.postTranslate if you want crop from TOP
            matrix.postTranslate((frameWidth - newImageWidth) /2, frameHeight - newImageHeight);
            setImageMatrix(matrix);
            return super.setFrame(frameLeft, frameTop, frameRight, frameBottom);
        }
    
    }

Beginner tip: If it plain doesn't work, you likely have to extends androidx.appcompat.widget.AppCompatImageView rather than ImageView

Holmun answered 22/7, 2012 at 19:17 Comment(6)
Great, thanks for pointing out the bug. I have not touched this code for a while so this may be a stupid suggestion but to fix the setWidth bug you point out could one not just use (r-l) instead?Shepard
Surely the line if((frameWidth > originalImageWidth) || (frameHeight > originalImageHeight)) should be reversed? In other words, shouldn't you be testing whether the image is bigger than the frame? I suggest replacing it with if((originalImageWidth > frameWidth ) || (originalImageHeight > frameHeight ))Pommard
I've picked width from parent using ((View) getParent()).getWidth() since the ImageView has MATCH_PARENTBlacktail
@Jay it works great. Im making matrix computations in onLayout() method, which gets called in on rotation also, where setFrame doesn't.Hardigg
Thanks for this, works perfectly, and also thanks for giving a way to quickly change the bottom crop to top crop by commenting 1 line of codeEmanative
How to make this work for cases where there's a need for bottom aligned images?Adnopoz
A
41

You don't need to write a Custom Image View for getting the TOP_CROP functionality. You just need to modify the matrix of the ImageView.

  1. Set the scaleType to matrix for the ImageView:

    <ImageView
          android:id="@+id/imageView"
          android:contentDescription="Image"
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:src="@drawable/image"
          android:scaleType="matrix"/>
    
  2. Set a custom matrix for the ImageView:

    final ImageView imageView = (ImageView) findViewById(R.id.imageView);
    final Matrix matrix = imageView.getImageMatrix();
    final float imageWidth = imageView.getDrawable().getIntrinsicWidth();
    final int screenWidth = getResources().getDisplayMetrics().widthPixels;
    final float scaleRatio = screenWidth / imageWidth;
    matrix.postScale(scaleRatio, scaleRatio);
    imageView.setImageMatrix(matrix);
    

Doing this will give you the TOP_CROP functionality.

Ashlieashlin answered 27/6, 2016 at 8:40 Comment(2)
This worked for me. However, I have to check scaleRation if it is < 1 I then just change the scaleType to centerCrop otherwise I will see blank space on edges.Eyeful
How to make this work for cases where there's a need for bottom aligned images?Adnopoz
A
26

This example works with images that is loaded after creation of object + some optimization. I added some comments in code that explain what's going on.

Remember to call:

imageView.setScaleType(ImageView.ScaleType.MATRIX);

or

android:scaleType="matrix"

Java source:

import com.appunite.imageview.OverlayImageView;

public class TopAlignedImageView extends ImageView {
    private Matrix mMatrix;
    private boolean mHasFrame;

    @SuppressWarnings("UnusedDeclaration")
    public TopAlignedImageView(Context context) {
        this(context, null, 0);
    }

    @SuppressWarnings("UnusedDeclaration")
    public TopAlignedImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    @SuppressWarnings("UnusedDeclaration")
    public TopAlignedImageView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        mHasFrame = false;
        mMatrix = new Matrix();
        // we have to use own matrix because:
        // ImageView.setImageMatrix(Matrix matrix) will not call
        // configureBounds(); invalidate(); because we will operate on ImageView object
    }

    @Override
    protected boolean setFrame(int l, int t, int r, int b)
    {
        boolean changed = super.setFrame(l, t, r, b);
        if (changed) {
            mHasFrame = true;
            // we do not want to call this method if nothing changed
            setupScaleMatrix(r-l, b-t);
        }
        return changed;
    }

    private void setupScaleMatrix(int width, int height) {
        if (!mHasFrame) {
            // we have to ensure that we already have frame
            // called and have width and height
            return;
        }
        final Drawable drawable = getDrawable();
        if (drawable == null) {
            // we have to check if drawable is null because
            // when not initialized at startup drawable we can
            // rise NullPointerException
            return;
        }
        Matrix matrix = mMatrix;
        final int intrinsicWidth = drawable.getIntrinsicWidth();
        final int intrinsicHeight = drawable.getIntrinsicHeight();

        float factorWidth = width/(float) intrinsicWidth;
        float factorHeight = height/(float) intrinsicHeight;
        float factor = Math.max(factorHeight, factorWidth);

        // there magic happen and can be adjusted to current
        // needs
        matrix.setTranslate(-intrinsicWidth/2.0f, 0);
        matrix.postScale(factor, factor, 0, 0);
        matrix.postTranslate(width/2.0f, 0);
        setImageMatrix(matrix);
    }

    @Override
    public void setImageDrawable(Drawable drawable) {
        super.setImageDrawable(drawable);
        // We have to recalculate image after chaning image
        setupScaleMatrix(getWidth(), getHeight());
    }

    @Override
    public void setImageResource(int resId) {
        super.setImageResource(resId);
        // We have to recalculate image after chaning image
        setupScaleMatrix(getWidth(), getHeight());
    }

    @Override
    public void setImageURI(Uri uri) {
        super.setImageURI(uri);
        // We have to recalculate image after chaning image
        setupScaleMatrix(getWidth(), getHeight());
    }

    // We do not have to overide setImageBitmap because it calls 
    // setImageDrawable method

}
Alitta answered 14/8, 2013 at 12:58 Comment(1)
How to make this work for cases where there's a need for bottom aligned images?Adnopoz
P
13

Based on Dori I'm using a solution which either scales the image based on the width or height of the image to always fill the surrounding container. This allows scaling an image to fill the whole available space using the top left point of the image rather than the center as origin (CENTER_CROP):

@Override
protected boolean setFrame(int l, int t, int r, int b)
{

    Matrix matrix = getImageMatrix(); 
    float scaleFactor, scaleFactorWidth, scaleFactorHeight;
    scaleFactorWidth = (float)width/(float)getDrawable().getIntrinsicWidth();
    scaleFactorHeight = (float)height/(float)getDrawable().getIntrinsicHeight();    

    if(scaleFactorHeight > scaleFactorWidth) {
        scaleFactor = scaleFactorHeight;
    } else {
        scaleFactor = scaleFactorWidth;
    }

    matrix.setScale(scaleFactor, scaleFactor, 0, 0);
    setImageMatrix(matrix);

    return super.setFrame(l, t, r, b);
}

I hope this helps - works like a treat in my project.

Pathe answered 11/2, 2013 at 15:46 Comment(1)
This is the better solution... And add: float width = r - l; float height = b - t;Warrior
S
10

None of these solutions worked for me, because I wanted a class that supported an arbitrary crop from either the horizontal or vertical direction, and I wanted it to allow me to change the crop dynamically. I also needed Picasso compatibility, and Picasso sets image drawables lazily.

My implementation is adapted directly from ImageView.java in the AOSP. To use it, declare like so in XML:

    <com.yourapp.PercentageCropImageView
        android:id="@+id/view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="matrix"/>

From source, if you wish to have a top crop, call:

imageView.setCropYCenterOffsetPct(0f);

If you wish to have a bottom crop, call:

imageView.setCropYCenterOffsetPct(1.0f);

If you wish to have a crop 1/3 of the way down, call:

imageView.setCropYCenterOffsetPct(0.33f);

Furthermore, if you elect to use another crop method, like fit_center, you may do so and none of this custom logic will be triggered. (Other implementations ONLY let you use their cropping methods).

Lastly, I added a method, redraw(), so if you elect to change your crop method/scaleType dynamically in code, you can force the view to redraw. For example:

fullsizeImageView.setScaleType(ScaleType.FIT_CENTER);
fullsizeImageView.redraw();

To go back to your custom top-center-third crop, call:

fullsizeImageView.setScaleType(ScaleType.MATRIX);
fullsizeImageView.redraw();

Here is the class:

/* 
 * Adapted from ImageView code at: 
 * http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.4.4_r1/android/widget/ImageView.java
 */
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.widget.ImageView;

public class PercentageCropImageView extends ImageView{

    private Float mCropYCenterOffsetPct;
    private Float mCropXCenterOffsetPct;

    public PercentageCropImageView(Context context) {
        super(context);
    }

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

    public PercentageCropImageView(Context context, AttributeSet attrs,
            int defStyle) {
        super(context, attrs, defStyle);
    }

    public float getCropYCenterOffsetPct() {
        return mCropYCenterOffsetPct;
    }

    public void setCropYCenterOffsetPct(float cropYCenterOffsetPct) {
        if (cropYCenterOffsetPct > 1.0) {
            throw new IllegalArgumentException("Value too large: Must be <= 1.0");
        }
        this.mCropYCenterOffsetPct = cropYCenterOffsetPct;
    }

    public float getCropXCenterOffsetPct() {
        return mCropXCenterOffsetPct;
    }

    public void setCropXCenterOffsetPct(float cropXCenterOffsetPct) {
        if (cropXCenterOffsetPct > 1.0) {
            throw new IllegalArgumentException("Value too large: Must be <= 1.0");
        }
        this.mCropXCenterOffsetPct = cropXCenterOffsetPct;
    }

    private void myConfigureBounds() {
        if (this.getScaleType() == ScaleType.MATRIX) {
            /*
             * Taken from Android's ImageView.java implementation:
             * 
             * Excerpt from their source:
    } else if (ScaleType.CENTER_CROP == mScaleType) {
       mDrawMatrix = mMatrix;

       float scale;
       float dx = 0, dy = 0;

       if (dwidth * vheight > vwidth * dheight) {
           scale = (float) vheight / (float) dheight; 
           dx = (vwidth - dwidth * scale) * 0.5f;
       } else {
           scale = (float) vwidth / (float) dwidth;
           dy = (vheight - dheight * scale) * 0.5f;
       }

       mDrawMatrix.setScale(scale, scale);
       mDrawMatrix.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f));
    }
             */

            Drawable d = this.getDrawable();
            if (d != null) {
                int dwidth = d.getIntrinsicWidth();
                int dheight = d.getIntrinsicHeight();

                Matrix m = new Matrix();

                int vwidth = getWidth() - this.getPaddingLeft() - this.getPaddingRight();
                int vheight = getHeight() - this.getPaddingTop() - this.getPaddingBottom();

                float scale;
                float dx = 0, dy = 0;

                if (dwidth * vheight > vwidth * dheight) {
                    float cropXCenterOffsetPct = mCropXCenterOffsetPct != null ? 
                            mCropXCenterOffsetPct.floatValue() : 0.5f;
                    scale = (float) vheight / (float) dheight;
                    dx = (vwidth - dwidth * scale) * cropXCenterOffsetPct;
                } else {
                    float cropYCenterOffsetPct = mCropYCenterOffsetPct != null ? 
                            mCropYCenterOffsetPct.floatValue() : 0f;

                    scale = (float) vwidth / (float) dwidth;
                    dy = (vheight - dheight * scale) * cropYCenterOffsetPct;
                }

                m.setScale(scale, scale);
                m.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f));

                this.setImageMatrix(m);
            }
        }
    }

    // These 3 methods call configureBounds in ImageView.java class, which
    // adjusts the matrix in a call to center_crop (android's built-in 
    // scaling and centering crop method). We also want to trigger
    // in the same place, but using our own matrix, which is then set
    // directly at line 588 of ImageView.java and then copied over
    // as the draw matrix at line 942 of ImageVeiw.java
    @Override
    protected boolean setFrame(int l, int t, int r, int b) {
        boolean changed = super.setFrame(l, t, r, b);
        this.myConfigureBounds();
        return changed;
    }
    @Override
    public void setImageDrawable(Drawable d) {          
        super.setImageDrawable(d);
        this.myConfigureBounds();
    }
    @Override
    public void setImageResource(int resId) {           
        super.setImageResource(resId);
        this.myConfigureBounds();
    }

    public void redraw() {
        Drawable d = this.getDrawable();

        if (d != null) {
            // Force toggle to recalculate our bounds
            this.setImageDrawable(null);
            this.setImageDrawable(d);
        }
    }
}
Stringhalt answered 14/3, 2015 at 23:3 Comment(1)
If one will copy and paste this code and let Android Studio to automatically convert it to Kotlin, it will work perfectly in 2023.Thorpe
A
5

Maybe go into the source code for the image view on android and see how it draws the center crop etc.. and maybe copy some of that code into your methods. i don't really know for a better solution than doing this. i have experience manually resizing and cropping the bitmap (search for bitmap transformations) which reduces its actual size but it still creates a bit of an overhead in the process.

Airburst answered 13/6, 2011 at 12:25 Comment(0)
C
5
public class ImageViewTopCrop extends ImageView {
public ImageViewTopCrop(Context context) {
    super(context);
    setScaleType(ScaleType.MATRIX);
}

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

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

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

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

private void computMatrix() {
    Matrix matrix = getImageMatrix();
    float scaleFactor = getWidth() / (float) getDrawable().getIntrinsicWidth();
    matrix.setScale(scaleFactor, scaleFactor, 0, 0);
    setImageMatrix(matrix);
}

}

Catabasis answered 30/12, 2014 at 8:48 Comment(3)
computMatrix:you can do any matrix here.Catabasis
onLayout saves me a lot! Thanks! I've encountered a problem where it computes the matrix but doesn't display the image immediately and adding onLayout on the code solves my problem.Bookshelf
This is another good answer here, even in and for 2023. One can let Android Studio to convert this code to Kotlin and it will work just fine. The only change needed would be to extend AppCompatImageView instead of normal ImageView.Thorpe
W
1

If you are using Fresco (SimpleDraweeView) you can easily do it with:

 PointF focusPoint = new PointF(0.5f, 0f);
 imageDraweeView.getHierarchy().setActualImageFocusPoint(focusPoint);

This one would be for a top crop.

More info at Reference Link

Win answered 31/8, 2017 at 9:24 Comment(0)
Y
0

There are 2 problems with the solutions here:

  • They do not render in the Android Studio layout editor (so you can preview on various screen sizes and aspect ratios)
  • It only scales by width, so depending on the aspect ratios of the device and the image, you can end up with an empty strip on the bottom

This small modification fixes the problem (place code in onDraw, and check width and height scale factors):

@Override
protected void onDraw(Canvas canvas) {

    Matrix matrix = getImageMatrix();

    float scaleFactorWidth = getWidth() / (float) getDrawable().getIntrinsicWidth();
    float scaleFactorHeight = getHeight() / (float) getDrawable().getIntrinsicHeight();

    float scaleFactor = (scaleFactorWidth > scaleFactorHeight) ? scaleFactorWidth : scaleFactorHeight;

    matrix.setScale(scaleFactor, scaleFactor, 0, 0);
    setImageMatrix(matrix);

    super.onDraw(canvas);
}
Yettayetti answered 7/12, 2016 at 23:5 Comment(0)
P
-1

Simplest Solution: Clip the image

 @Override
    public void draw(Canvas canvas) {
        if(getWidth() > 0){
            int clipHeight = 250;
            canvas.clipRect(0,clipHeight,getWidth(),getHeight());
         }
        super.draw(canvas);
    }
Pablo answered 5/12, 2015 at 6:31 Comment(1)
This will not scale the image up if it is smaller than the view, which is why the other solutions are not as simple.Yettayetti

© 2022 - 2024 — McMap. All rights reserved.