119

I want to download an image (of unknown size, but which is always roughly square) and display it so that it fills the screen horizontally, and stretches vertically to maintain the aspect ratio of the image, on any screen size. Here is my (non-working) code. It stretches the image horizontally, but not vertically, so it is squashed...

ImageView mainImageView = new ImageView(context);
    mainImageView.setImageBitmap(mainImage); //downloaded from server
    mainImageView.setScaleType(ScaleType.FIT_XY);
    //mainImageView.setAdjustViewBounds(true); 
    //with this line enabled, just scales image down
    addView(mainImageView,new LinearLayout.LayoutParams( 
            LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
fredley
  • 32,953
  • 42
  • 145
  • 236
  • do all Android devices have exactly the same width/height ratio? If not, it is simply impossible to scale an image to fit the whole width/height while preserving the original ratio... – NoozNooz42 Jun 07 '10 at 16:09
  • 4
    No, I do not want the image to fill the screen, just to scale to the screen width, I do not care how much of the screen the image takes up vertically, as long as the image is in the correct proportions. – fredley Jun 07 '10 at 16:10
  • 1
    Similar question, good answer: http://stackoverflow.com/questions/4677269/how-to-stretch-three-images-across-the-screen-preserving-aspect-ratio – Pēteris Caune Dec 27 '11 at 20:10
  • 1
    did 'adjustViewBounds' not work? – Darpan Jun 17 '15 at 11:12

17 Answers17

133

I accomplished this with a custom view. Set layout_width="fill_parent" and layout_height="wrap_content", and point it to the appropriate drawable:

public class Banner extends View {

  private final Drawable logo;

  public Banner(Context context) {
    super(context);
    logo = context.getResources().getDrawable(R.drawable.banner);
    setBackgroundDrawable(logo);
  }

  public Banner(Context context, AttributeSet attrs) {
    super(context, attrs);
    logo = context.getResources().getDrawable(R.drawable.banner);
    setBackgroundDrawable(logo);
  }

  public Banner(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    logo = context.getResources().getDrawable(R.drawable.banner);
    setBackgroundDrawable(logo);
  }

  @Override protected void onMeasure(int widthMeasureSpec,
      int heightMeasureSpec) {
    int width = MeasureSpec.getSize(widthMeasureSpec);
    int height = width * logo.getIntrinsicHeight() / logo.getIntrinsicWidth();
    setMeasuredDimension(width, height);
  }
}
Bob Lee
  • 1,962
  • 1
  • 12
  • 9
  • This is the most elegant and flexible solution i've found, good job! Thanks! – Sander Versluys Nov 04 '10 at 09:50
  • 12
    Thank you!! This should be the correct answer! :) I adjusted this for a little more flexibility in this post: http://stackoverflow.com/questions/4677269/how-to-stretch-three-images-across-the-screen-preserving-aspect-ratio There I use a ImageView so one can set the drawable in the XML or use it like normal ImageView in code to set the image. Works great! – Patrick Boos Jan 14 '11 at 05:51
  • 1
    Freakin genius! that worked like a champ! Thanks it solved my issue with an image banner across the top not right in all screen sizes! Thank you so much! – JPM Apr 13 '11 at 18:07
  • 1
    Watch out for logo being null (if you're downloading content from the Internet). Otherwise, this works great, follow Patrick's post for the XML snippet. – Artem Russakovskii Nov 15 '11 at 02:17
  • Just ran into 2 more problems in certain cases - getIntrinsicWidth() was returning 0 (division by 0), so you want to check for that. Also, once I put a check for whether logo is null, Android would crash if it is because I forgot to return super.onMeasure otherwise. – Artem Russakovskii Nov 16 '11 at 00:18
  • 46
    I can't believe how difficult it is to do things on Android that are trivially easy in iOS and Windows Phone. – mxcl Jan 26 '12 at 11:13
  • 1
    I've made some changes, but instead of posting them somewhere else, would it be possible to turn this answer into a community wiki? My changes are all things that handle the special cases mentioned here, as well as the ability to choose which side gets resized by way of the layout definition. – Steve Pomeroy Jul 11 '12 at 20:59
  • 1
    I generalized this class and made it extend ImageView instead. Great starting code though. – Dandre Allison Oct 08 '12 at 07:17
  • @DavidCaunt let me see if I can make it a Gist, I don't think I put it online yet. – Dandre Allison Dec 17 '12 at 17:57
  • So, it turns out I don't remember where I made the class, it must have been fairly trivial. I made this Gist: https://gist.github.com/4320447 I needed a slightly different meaning of Banner, where the height was fixed. I imagine setting height to wrap_content wouldn't work because that seems to trivial to be missed on this thread. The Gist gets the banner like the old G+ on iOS: http://venturebeat.files.wordpress.com/2012/05/google-plus-iphone.jpg?w=558&h=9999&crop=0 – Dandre Allison Dec 17 '12 at 18:19
  • Borrowing from , shouldn't you convert all sizes in `onMeasure()` to `float` temporarily for more precise division results, and afterwards cast the result back to `int`? – caw Sep 14 '13 at 01:12
  • I believe this solution is over complicated and not generalized if the image is from another source. See my solution below if you want to achieve this with RelativeLayout. – Warpzit Sep 30 '13 at 07:18
24

In the end, I generated the dimensions manually, which works great:

DisplayMetrics dm = new DisplayMetrics();
context.getWindowManager().getDefaultDisplay().getMetrics(dm);
int width = dm.widthPixels;
int height = width * mainImage.getHeight() / mainImage.getWidth(); //mainImage is the Bitmap I'm drawing
addView(mainImageView,new LinearLayout.LayoutParams( 
        width, height));
fredley
  • 32,953
  • 42
  • 145
  • 236
  • I have been looking for this kind of solution to fix my issue.With the help of this calculation, I got it successfully loading image without reducing aspect ratio. – Stephen Oct 13 '16 at 10:29
21

I just read the source code for ImageView and it is basically impossible without using the subclassing solutions in this thread. In ImageView.onMeasure we get to these lines:

        // Get the max possible width given our constraints
        widthSize = resolveAdjustedSize(w + pleft + pright, mMaxWidth, widthMeasureSpec);

        // Get the max possible height given our constraints
        heightSize = resolveAdjustedSize(h + ptop + pbottom, mMaxHeight, heightMeasureSpec);

Where h and w are the dimensions of the image, and p* is the padding.

And then:

private int resolveAdjustedSize(int desiredSize, int maxSize,
                               int measureSpec) {
    ...
    switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            /* Parent says we can be as big as we want. Just don't be larger
               than max size imposed on ourselves.
            */
            result = Math.min(desiredSize, maxSize);

So if you have a layout_height="wrap_content" it will set widthSize = w + pleft + pright, or in other words, the maximum width is equal to the image width.

This means that unless you set an exact size, images are NEVER enlarged. I consider this to be a bug, but good luck getting Google to take notice or fix it. Edit: Eating my own words, I submitted a bug report and they say it has been fixed in a future release!

Another solution

Here is another subclassed workaround, but you should (in theory, I haven't really tested it much!) be able to use it anywhere you ImageView. To use it set layout_width="match_parent", and layout_height="wrap_content". It is quite a lot more general than the accepted solution too. E.g. you can do fit-to-height as well as fit-to-width.

import android.content.Context;
import android.util.AttributeSet;
import android.widget.ImageView;

// This works around the issue described here: http://stackoverflow.com/a/12675430/265521
public class StretchyImageView extends ImageView
{

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

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

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
    {
        // Call super() so that resolveUri() is called.
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        // If there's no drawable we can just use the result from super.
        if (getDrawable() == null)
            return;

        final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);

        int w = getDrawable().getIntrinsicWidth();
        int h = getDrawable().getIntrinsicHeight();
        if (w <= 0)
            w = 1;
        if (h <= 0)
            h = 1;

        // Desired aspect ratio of the view's contents (not including padding)
        float desiredAspect = (float) w / (float) h;

        // We are allowed to change the view's width
        boolean resizeWidth = widthSpecMode != MeasureSpec.EXACTLY;

        // We are allowed to change the view's height
        boolean resizeHeight = heightSpecMode != MeasureSpec.EXACTLY;

        int pleft = getPaddingLeft();
        int pright = getPaddingRight();
        int ptop = getPaddingTop();
        int pbottom = getPaddingBottom();

        // Get the sizes that ImageView decided on.
        int widthSize = getMeasuredWidth();
        int heightSize = getMeasuredHeight();

        if (resizeWidth && !resizeHeight)
        {
            // Resize the width to the height, maintaining aspect ratio.
            int newWidth = (int) (desiredAspect * (heightSize - ptop - pbottom)) + pleft + pright;
            setMeasuredDimension(newWidth, heightSize);
        }
        else if (resizeHeight && !resizeWidth)
        {
            int newHeight = (int) ((widthSize - pleft - pright) / desiredAspect) + ptop + pbottom;
            setMeasuredDimension(widthSize, newHeight);
        }
    }
}
Timmmm
  • 88,195
  • 71
  • 364
  • 509
  • I submitted [a bug report](http://code.google.com/p/android/issues/detail?id=38056). Watch, as it is ignored! – Timmmm Oct 01 '12 at 15:01
  • 3
    Apparently I have to eat my words - they say it has been fixed in a future release! – Timmmm Oct 02 '12 at 10:53
  • Thank you very much Tim. This should be the final right one answer. I have searched for lot of time and nothing is better than your solution.! – tainy Jan 23 '15 at 08:09
  • what if I need to set the maxHeight when using the above StretchyImageView . I did not find any solution for that . – tainy Jan 28 '15 at 06:09
17

Setting adjustViewBounds to true and using a LinearLayout view group worked very well for me. No need to subclass or ask for device metrics:

//NOTE: "this" is a subclass of LinearLayout
ImageView splashImageView = new ImageView(context);
splashImageView.setImageResource(R.drawable.splash);
splashImageView.setAdjustViewBounds(true);
addView(splashImageView);
Matt Stoker
  • 350
  • 4
  • 7
  • 1
    ... or using ImageView XML property - android:adjustViewBounds="true" – Anatolii Shuba Mar 03 '15 at 12:05
  • Perfect! This was so easy and it stretches the image perfect. Way is this answer not the correct one? The answer with the CustomView did not work for me(some strange xml error) – user3800924 Mar 24 '16 at 19:31
14

I've been struggling with this problem in one form or another for AGES, thank you, Thank You, THANK YOU.... :)

I just wanted to point out that you can get a generalizable solution from what Bob Lee's done by just extending View and overriding onMeasure. That way you can use this with any drawable you want, and it won't break if there's no image:

    public class CardImageView extends View {
        public CardImageView(Context context, AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
        }

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

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

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            Drawable bg = getBackground();
            if (bg != null) {
                int width = MeasureSpec.getSize(widthMeasureSpec);
                int height = width * bg.getIntrinsicHeight() / bg.getIntrinsicWidth();
                setMeasuredDimension(width,height);
            }
            else {
                super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            }
        }
    }
duhrer
  • 151
  • 1
  • 5
  • 2
    Of all the many, many solutions to this problem, this is the first one which is just a drop-in solution, where the others always have drawback (like not being able to change the image at runtime without a code re-write). Thank you! – MacD Mar 29 '13 at 10:40
  • This is pure awesomeness - have been struggling with this for some time now and all the obvious solutions using the xml attributes never worked. With this I could just replace my ImageView with the extended view and everything worked as expected! – slott Apr 05 '13 at 06:56
  • This is absolutely the best solution to this problem. – erlando Oct 24 '13 at 13:57
  • That's a great answer. You can use this class in xml file without worries like so: . Great! – arniotaki Dec 16 '14 at 11:37
11

In some cases this magic formula beautifully solves the problem.

For anyone struggling with this coming from another platform, the "size and shape to fit" option is handled beautifully in Android, but it's hard to find.

You typically want this combination:

  • width match parent,
  • height wrap content,
  • adjustViewBounds turned ON (sic)
  • scale fitCenter
  • cropToPadding OFF (sic)

Then it's automatic and amazing.

If you're an iOS dev, it's utterly amazing how simply, in Android, you can do "totally dynamic cell heights" in a table view .. err, I mean ListView. Enjoy.

<com.parse.ParseImageView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:id="@+id/post_image"
    android:src="@drawable/icon_192"
    android:layout_margin="0dp"
    android:cropToPadding="false"
    android:adjustViewBounds="true"
    android:scaleType="fitCenter"
    android:background="#eff2eb"/>

enter image description here

Fattie
  • 27,874
  • 70
  • 431
  • 719
6

I have managed to achieve this using this XML code only. It might be the case that eclipse does not render the height to show it expanding to fit; however, when you actually run this on a device, it properly renders and provides the desired result. (well at least for me)

<FrameLayout
     android:layout_width="match_parent"
     android:layout_height="wrap_content">

     <ImageView
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:adjustViewBounds="true"
          android:scaleType="centerCrop"
          android:src="@drawable/whatever" />
</FrameLayout>
Snebhu
  • 1,077
  • 1
  • 12
  • 19
  • Sorry this does not work. It scales the width, but centerCrop does not maintain the aspect ratio. – ricosrealm Apr 14 '14 at 03:13
  • 1
    After many hours struggling with custom classes extending ImageView (as written in many answers and articles) and having an exception like "The following classes could not be found" I removed that code. Suddenly I noticed a problem in my ImageView was in background attribute! Changed it to `src` and the ImageView became proportional. – CoolMind Jul 12 '16 at 19:45
5

Everyone is doing this programmily so I thought this answer would fit perfectly here. This code worked for my in the xml. Im NOT thinking about ratio yet, but still wanted to place this answer if it would help anyone.

android:adjustViewBounds="true"

Cheers..

Sindri Þór
  • 2,887
  • 3
  • 26
  • 32
5

I did it with these values within a LinearLayout:

Scale type: fitStart
Layout gravity: fill_horizontal
Layout height: wrap_content
Layout weight: 1
Layout width: fill_parent
Rob W
  • 341,306
  • 83
  • 791
  • 678
helduel
  • 75
  • 1
  • 1
3

A very simple solution is to just use the features provided by RelativeLayout.

Here is the xml that makes it possible with standard Android Views:

<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fillViewport="true">

    <RelativeLayout 
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        >
        <LinearLayout
            android:id="@+id/button_container"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:layout_alignParentBottom="true"
            >
            <Button
                android:text="button"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"/>
            <Button
                android:text="button"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"/>
            <Button
                android:text="button"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"/>
        </LinearLayout>
        <ImageView 
            android:src="@drawable/cat"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:adjustViewBounds="true"
            android:scaleType="centerCrop"
            android:layout_above="@id/button_container"/>
    </RelativeLayout>
</ScrollView>

The trick is that you set the ImageView to fill the screen but it has to be above the other layouts. This way you achieve everything you need.

Warpzit
  • 27,966
  • 19
  • 103
  • 155
3

Its simple matter of setting adjustViewBounds="true" and scaleType="fitCenter" in the XML file for the ImageView!

<ImageView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:src="@drawable/image"

        android:adjustViewBounds="true"
        android:scaleType="fitCenter"
        />

Note: layout_width is set to match_parent

Kaushik NP
  • 6,733
  • 9
  • 31
  • 60
1

You are setting the ScaleType to ScaleType.FIT_XY. According to the javadocs, this will stretch the image to fit the whole area, changing the aspect ratio if necessary. That would explain the behavior you are seeing.

To get the behavior you want... FIT_CENTER, FIT_START, or FIT_END are close, but if the image is narrower than it is tall, it will not start to fill the width. You could look at how those are implemented though, and you should probably be able to figure out how to adjust it for your purpose.

Cheryl Simon
  • 46,552
  • 15
  • 93
  • 82
  • 3
    I had tried FIT_CENTER, but not the others. Unfortunately they don't work either, both with setAdjustViewBounds set to true and false. Is there anyway to get the pixel width of the screen of the device, as well as the width/height of the downloaded image and manually set the size? – fredley Jun 08 '10 at 10:42
1

Look there is a far easier solution to your problem:

ImageView imageView;

protected void onCreate(Bundle savedInstanceState){
    super.onCreate(savedInstanceState);
    setContentView(R.layout.your_layout);
    imageView =(ImageView)findViewById(R.id.your_imageView);
    Bitmap imageBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.your_image);
    Point screenSize = new Point();
    getWindowManager().getDefaultDisplay().getSize(screenSize);
    Bitmap temp = Bitmap.createBitmap(screenSize.x, screenSize.x, Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(temp);
    canvas.drawBitmap(imageBitmap,null, new Rect(0,0,screenSize.x,screenSize.x), null);
    imageView.setImageBitmap(temp);
}
1

You can use my StretchableImageView preserving the aspect ratio (by width or by height) depending on width and height of drawable:

import android.content.Context;
import android.util.AttributeSet;
import android.widget.ImageView;

public class StretchableImageView extends ImageView{

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

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

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if(getDrawable()!=null){
            if(getDrawable().getIntrinsicWidth()>=getDrawable().getIntrinsicHeight()){
                int width = MeasureSpec.getSize(widthMeasureSpec);
                int height = width * getDrawable().getIntrinsicHeight()
                            / getDrawable().getIntrinsicWidth();
                setMeasuredDimension(width, height);
            }else{
                int height = MeasureSpec.getSize(heightMeasureSpec);
                int width = height * getDrawable().getIntrinsicWidth()
                            / getDrawable().getIntrinsicHeight();
                setMeasuredDimension(width, height);
            }
        }
    }
}
valerybodak
  • 4,195
  • 2
  • 42
  • 53
1

ScaleType.CENTER_CROP will do what you want: stretch to full width, and scale the height accordingly. if the scaled height exceeds the screen limits, the image will be cropped.

P.Melch
  • 8,066
  • 43
  • 40
0

For me the android:scaleType="centerCrop" did not resolve my problem. It actually expanded the image way more. So I tried with android:scaleType="fitXY" and It worked excellent.

0

This working fine as per my requirement

<ImageView android:id="@+id/imgIssue" android:layout_width="fill_parent" android:layout_height="wrap_content" android:adjustViewBounds="true" android:scaleType="fitXY"/>

Anand Savjani
  • 2,484
  • 3
  • 26
  • 39