android:scaleType="center" doesn't do what I think it should: how does dp vs px work?

76 views Asked by At

I am attempting to accomplish what must surely be the simplest thing imaginable. I have a background image for a menu. I wish to display as much of that image as fits on the screen, at one pixel per pixel, with no scaling.

As far as I can tell, this is exactly what scaleType="center" is supposed to accomplish. Unfortunately, no matter what I do, it refuses to do what I want, and instead only shows a small portion of the image.

To clarify with real numbers, I have a 720x1280px background image in the sw320dp drawables folder, being called by my sw320dp layout. For any screensize between 320x480px and 720x1280px, they should call that layout and display a portion of that background image. So, a 480x800px Nexus One should display a 480x800px portion of the image. To try and do this, I have:

<ImageView
android:id="@+id/menuBackground"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:scaleType="center"
app:srcCompat="@drawable/menubackground_w720" />

Unfortunately, it doesn't work. Instead, it just displays a very small portion of the image, roughly equal to 320x480px plus or minus a little bit for aspect ratio.

After much swearing and tearing my hair out, I have narrowed the possibilities down to three plausible theories:

  1. I'm doing something really, really, unbelievably simple wrong in a stupid way.
  2. I fundamentally don't understand how dp works, as well as scalable layouts.
  3. The world is a bizarre, irrational Lovecraftian nightmare in which nothing makes any sense and I am doomed to forever chase my own tail in hopeless circles of madness.

Assuming that the third option, even if true, is a "won't fix", I'd appreciate if someone could tell me whether it's option one or two, and link me to an appropriate up-to-date resource. Thanks for any assistance!

2

There are 2 answers

0
Nick On

Try using

android:scaleType="centerCrop"

I've been having same issue with imageviews and this solved my problem everytime.

0
Ben P. On

tl;dr

I wish to display as much of that image as fits on the screen, at one pixel per pixel, with no scaling.

Move your image into the res/drawable-nodpi/ directory.


After much swearing and tearing my hair out, I have narrowed the possibilities down to three plausible theories:

  1. I'm doing something really, really, unbelievably simple wrong in a stupid way.
  2. I fundamentally don't understand how dp works, as well as scalable layouts.
  3. The world is a bizarre, irrational Lovecraftian nightmare in which nothing makes any sense and I am doomed to forever chase my own tail in hopeless circles of madness.

I think #2 is probably the closest to the truth. That's okay; it's complicated.

Let's start by considering this image:

enter image description here

I've resized it here so that it doesn't take up a ton of space in my answer, but on my computer it is 1000x1000 pixels. Each black or white square is 100x100 pixels. That will make it really easy to reason about what's going on in our application.

Next, I make an app that's dead simple: it just displays a 200x200 dp ImageView, and nothing else.

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#eee">

    <ImageView
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:layout_gravity="center"
        android:scaleType="center"
        android:src="@drawable/checkerboard"/>

</FrameLayout>

I put my checkerboard.png into the res/drawable/ directory, and I run the app. My emulator is a Nexus 5X, which (according to this site) has a screen resolution of 1080x1920 pixels. This is what I see:

enter image description here

Huh. That leads to some questions:

Why is my 200 dp wide ImageView taking up half the screen?

The express purpose of the dp unit is to allow Views to be approximately the same physical size across different devices. One challenge here is that different devices can have different display densities; one phone might have a high-res display that fits 320 pixels into a physical inch while another might have a low-res display that only fits 120 pixels into a physical inch.

The baseline for Android is 160 dpi. At this density, 1 dp == 1 px. If your device has a higher density, 1 dp will equal more than 1 px. The Nexus 5X has a density of ~423 dpi, so it uses about 2.6 pixels for each 1 dp in my layouts. So, on this specific emulator, 200x200 dp means approximately 520x520 px.

But why is my .png only showing 200x200 pixels worth of image?

Another challenge in making things the same physical size on different devices is how to deal with images. We've provided the system only a single checkerboard.png file, but we know that our ImageView is going to have different pixel dimensions on different devices. That leaves two options:

The system could perform no scaling. If it did, then I'd see about 520x520 pixels worth of image on my Nexus 5X, but someone still running a Nexus One (252 dpi) would only see about 315x315 pixels worth of image. The overall ImageView would be about the same physical size, but the two of us would see wildly different image content.

Or, the system can scale the image up. Now it performs the same scaling we talked about for dp to px on the image data itself. Having done that, both the Nexus 5X and the Nexus One will see 200x200 pixels worth of image data. However, the scaling artifacts will be worse on the Nexus 5X, since the system has to scale the image up more to satisfy the higher display density.

What should I do to fix it?

That depends on what your end goal is. If you want to provide only a single .png file, and you want the system to not scale it up (i.e. you want 1px on any device, regardless of display density, to display 1px of your image), then you should move the .png to the res/drawable-nodpi/ directory.

https://developer.android.com/guide/topics/resources/providing-resources.html

nodpi: This can be used for bitmap resources that you don't want to be scaled to match the device density.

If we do this for our sample app, we see this when we run it:

enter image description here

Indeed, we see about 520x520 pixels worth of image.

If, on the other hand, you want every user to see the same physical image, but you want to avoid scaling artifacts, then you should provide multiple resolutions of your image file.

Consider what happens when you download the .png version of an icon from Google's Material Icon repo. Let's say we take ic_face.png. You get a directory structure that looks like this:

android/
    drawable-mdpi/
        ic_face_black_24dp.png (24x24 pixels)
    drawable-hdpi/
        ic_face_black_24dp.png (36x36 pixels)
    drawable-xhdpi/
        ic_face_black_24dp.png (48x48 pixels)
    drawable-xxhdpi/
        ic_face_black_24dp.png (72x72 pixels)
    drawable-xxxhdpi/
        ic_face_black_24dp.png (96x96 pixels)

What this is doing is giving different versions of the same image at different resolutions. Devices with a high display density will take the image that most closely matches their density, and then only scale it a very small amount. This reduces the scaling artifacts dramatically.

Further reading

https://developer.android.com/guide/topics/resources/providing-resources.html

https://developer.android.com/guide/practices/screens_support.html