13

I would like my ImageView to scale in a particular fashion:

  • Scale so that the height of the image always fits the height of the ImageView
  • Crop any excess width

A picture speaks louder than a 1000 words, so here is a representation of how I want my ImageView to behave. Suppose it has a fixed height of say 100dp and suppose its width is match_parent.

enter image description here

Note that

  • on the phone layout, the image height is stretched, but the sides are cropped, akin to CROP_CENTER.
  • on the tablet layout, the image is also stretched to fit the ImageView height, behaving like FIT_CENTER

I suspect I need scaleType:matrix, but after that I'm lost. How can I make sure an image fits Y, but crops X?

Maarten
  • 6,894
  • 7
  • 55
  • 90

5 Answers5

27

In xml, use:

    android:scaleType="centerCrop"
    android:adjustViewBounds="true"

from & thanks to: https://stackoverflow.com/a/15600295/2162226

Community
  • 1
  • 1
Gene Bo
  • 11,284
  • 8
  • 90
  • 137
6

With a little help from my friends Carlos Robles and pskink, came up with the following custom ImageView:

public class FitYCropXImageView extends ImageView {
    boolean done = false;

    @SuppressWarnings("UnusedDeclaration")
    public FitYCropXImageView(Context context) {
        super(context);
        setScaleType(ScaleType.MATRIX);
    }

    @SuppressWarnings("UnusedDeclaration")
    public FitYCropXImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
        setScaleType(ScaleType.MATRIX);
    }

    @SuppressWarnings("UnusedDeclaration")
    public FitYCropXImageView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        setScaleType(ScaleType.MATRIX);
    }

    private final RectF drawableRect = new RectF(0, 0, 0,0);
    private final RectF viewRect = new RectF(0, 0, 0,0);
    private final Matrix m = new Matrix();
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (done) {
            return;//Already fixed drawable scale
        }
        final Drawable d = getDrawable();
        if (d == null) {
            return;//No drawable to correct for
        }
        int viewHeight = getMeasuredHeight();
        int viewWidth = getMeasuredWidth();
        int drawableWidth = d.getIntrinsicWidth();
        int drawableHeight = d.getIntrinsicHeight();
        drawableRect.set(0, 0, drawableWidth, drawableHeight);//Represents the original image
        //Compute the left and right bounds for the scaled image
        float viewHalfWidth = viewWidth / 2;
        float scale = (float) viewHeight / (float) drawableHeight;
        float scaledWidth = drawableWidth * scale;
        float scaledHalfWidth = scaledWidth / 2;
        viewRect.set(viewHalfWidth - scaledHalfWidth, 0, viewHalfWidth + scaledHalfWidth, viewHeight);

        m.setRectToRect(drawableRect, viewRect, Matrix.ScaleToFit.CENTER /* This constant doesn't matter? */);
        setImageMatrix(m);

        done = true;

        requestLayout();
    }
}
Maarten
  • 6,894
  • 7
  • 55
  • 90
1

If you use scaleType:matrix you will need to create your own Matrix and asign it to the view by means of setImageMatrix(Matrix) or manually modify the matrix at hen onMEasure method of a customImageView.

public class MyImageView extends ImageView   {

 boolean done=false;

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        if (done)
            return;

        final Drawable d = getDrawable();
        final int drawableW = d.getIntrinsicWidth();
        final int drawableH = d.getIntrinsicHeight();
        float ratio =  drawableW / drawableH;

        //int width = getMeasuredWidth();
        int height = getMeasuredHeight();

        float scale=height/drawableH;

          Matrix m = getImageMatrix();

          float[] f = new float[9];
          m.getValues(f);

          f[Matrix.MSCALE_X]=scale;
          f[Matrix.MSCALE_Y]=scale;

          m.setValues(f);  

        done = true;

        requestLayout();

    }

}
Carlos Robles
  • 10,828
  • 3
  • 41
  • 60
  • yeah, thats why you adjust the height in the onmeasure method. aslo you change the width to keep the ratio, and since the scaletype is center_crop, if the widht is too much, it will be cropped – Carlos Robles Jan 29 '14 at 09:03
  • if you give me a second i will explain how to do it. – Carlos Robles Jan 29 '14 at 09:19
  • finally i took the time and write a solution working with the matrix. i think it will work well in any case – Carlos Robles Jan 29 '14 at 09:55
  • Wow... Didn't expect to get so low-level. Thanks! – Maarten Jan 29 '14 at 11:04
  • But what if the drawable is not yet set at measure? – Maarten Jan 29 '14 at 11:05
  • until they build the scaletype:fitX or scaletype:fitY, im afraid we have to go low level! – Carlos Robles Jan 29 '14 at 11:06
  • 1
    you can check for that, after of `if (done)` and return if there isn't drawable. The system will keep calling onMeasure. Also you can migrate this code to your activity, and do all the maths after you assign the drawable. Of course the functions would be different, but the logic would be the same. – Carlos Robles Jan 29 '14 at 11:09
1
    LinearLayout ll = new LinearLayout(this);
    ll.setOrientation(LinearLayout.VERTICAL);
    LayoutParams params;

    final ImageView iv0 = new ImageView(this);
    //iv0.setBackgroundColor(0xffff0000);
    params = new LayoutParams(LayoutParams.MATCH_PARENT, 100);
    ll.addView(iv0, params);

    final ImageView iv1 = new ImageView(this);
    //iv1.setBackgroundColor(0xff00ff00);
    params = new LayoutParams(60, 100);
    ll.addView(iv1, params);

    setContentView(ll);

    Runnable action = new Runnable() {
        @Override
        public void run() {
            Drawable d = getResources().getDrawable(R.drawable.layer0);
            int dw = d.getIntrinsicWidth();
            int dh = d.getIntrinsicHeight();
            RectF src = new RectF(0, 0, dw, dh);

            ImageView[] iviews = {iv0, iv1};
            for (int i = 0; i < iviews.length; i++) {
                ImageView iv = iviews[i];
                iv.setImageDrawable(d);
                iv.setScaleType(ScaleType.MATRIX);

                float h = iv.getHeight();
                float w = iv.getWidth();
                float cx = w / 2;
                float scale = h / dh;
                float deltaw = dw * scale / 2;
                RectF dst = new RectF(cx - deltaw, 0, cx + deltaw, h);
                Matrix m = new Matrix();
                m.setRectToRect(src, dst, ScaleToFit.FILL);
                iv.setImageMatrix(m);
            }
        }
    };
    iv1.post(action);
pskink
  • 23,874
  • 6
  • 66
  • 77
  • Some word of explanation, maybe? What if the drawable is not yet set at the end of the `ImageView` message queue? – Maarten Jan 29 '14 at 11:07
  • what you mean by "if the drawable is not yet set" ? – pskink Jan 29 '14 at 11:09
  • You perform `iv.setImageDrawable(d);` in the `Runnable`. What if the drawable is fetched asynchronously from the internet and you have no guarantee about layout having happened when you get your hands on it, and conversely, having the drawable when layout occurs. – Maarten Jan 29 '14 at 11:15
  • i use Runnable only because i need to make sure that ImageViews are already layout and have non zero width & height, you can assign your image Drawable any time you want provided that ImageView is non zero sized (you can also do it by extending ImageView and overriding onSizeChanged) – pskink Jan 29 '14 at 11:21
0

If you want to display the center of the image, use:

android:scaleType="centerCrop"
android:adjustViewBounds="true"

If you want to show the edge of the image instead of the center, use:

android:scaleType="matrix"
android:adjustViewBounds="true"
pram
  • 1,484
  • 14
  • 17