preface

How do you make your app “look the same” on devices with all the different screen resolutions on Android? You may also have the following doubts:

  • Differences and functions of PX, DP and SP
  • Differences and functions between MipMap and Drawable
  • What is the difference between mdPI hdPI xHDPI image resources
  • How to adapt image resources under different densities
  • How do devices with different resolutions fit widths
  • How is dPI determined

This article will answer the above questions one by one.

The basic unit

px

Pixels We see images on a screen made up of Pixels, which contain color information. The usual phone resolution: 1080 x 1920 refers to a phone that can display 1080 pixels wide and 1920 pixels high.

ppi

Pixels Per Inch Indicates the number of Pixels Per Inch. The more Pixels Per unit area, the sharper the image. Ppi is generally used to describe the screen precision of monitors, mobile phones and tablets.

dpi

Dots Per Inch Indicates the number of points Per Inch. Dpi is generally used to describe the precision of printing (books, magazines, telegrams)

dp/dip

Dip is the abbreviation for demod-independent Pixels, and can also be called DP more simply. The purpose of this unit is to screen out density differences between different devices, more on that later.

sp

Scalable Pixels is used to set fonts and is adaptable when the user changes the font size.

A simple example

Having clarified the basic concepts, we now start with an example to illustrate the differences and connections between the above units.

<? The XML version = "1.0" encoding = "utf-8"? > <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"  android:id="@+id/big" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <View android:layout_gravity="center" android:background="@color/green" android:layout_width="200px" android:layout_height="200px"> </View> </FrameLayout>Copy the code

In the layout file, there is A View that is 200px in length and width, with A resolution of 480(width)x800(height) for device A and 1080(width)x1920(height) for device B. The effect is as follows:


DisplayMetrics.java
    /**
     * The exact physical pixels per inch of the screen in the X dimension.
     */
    public float xdpi;
    /**
     * The exact physical pixels per inch of the screen in the Y dimension.
     */
    public float ydpi;
Copy the code

One way to get the DisplayMetrics object is:

DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); A device width: 480(px)/240(ppi)=2inch B device width: 1080(px)/420(ppi)=2.5inch it can be seen that there is little difference between device sizes of A and B. Ppi of device A =240 and PPI of device B =420. Obviously, device B can accommodate more pixels per unit length than device A>. Therefore, device B only needs A smaller size to display the same 200px, so the >view on device B looks much smaller than device A. The cause of the problem is known, but the display is unacceptable. We want views of the same size to “look the same” on different devices

We can’t determine the PPI of each device, calculate the actual number of pixels, and then dynamically set the size of the view, so the static layout size in the layout can’t dynamically change to adapt to. Of course there would be a unified place for us to switch, yes! Android already does that for us. And then dPI, DP.

Introduce DPI and DP

The Android system uses DPI to describe the density of the screen and DP to describe the relationship between density and pixels. Device A dpi=240 device B dpi=420 The answer is dp. Android stipulates that when DPI =160, 1dp=1px, when DPI =240, 1DP =1.5px, and so on, and gives a simple name to each RANGE of DPI for intuitive identification, such as 120< DPI <=160, called mdPI, 120< DPI <=240, called HDPI. Finally, the following rules are formed:

Ldpi (value <= 120 Dpi) MDPI (120 Dpi < value <= 160 DPI) HDPI (160 DPI < value <= 240 Dpi) XHDPI (240 Dpi < value <= 320 Xxhdpi (320 dpi < value <= 480 dpi) xxxhdpi (480 dpi < value <= 640 dpi)

Now that we know that DP can correspond to different PX on different DPI devices, which is equivalent to the intermediate conversion layer, we just need to set the length and width unit of view to the appropriate DP, there is no need to pay attention to the density difference between devices, and the system will help us complete the DP-PX conversion. Modify our previous example slightly to see if it works:

<? The XML version = "1.0" encoding = "utf-8"? > <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"  android:id="@+id/big" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <View android:layout_gravity="center" android:background="@color/green" android:layout_width="200dp" android:layout_height="200dp"> </View> </FrameLayout>Copy the code



[1]


Mipmap image resource file

From the dp mentioned above, we know that using DP when setting the view size and spacing can minimize the differences between device densities. You might ask, how does bitmap display fit different density devices?

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(bitmap.getWidth(), bitmap.getHeight());
    }

    private void init() {
        String path = Environment.getExternalStorageDirectory() + "/Download/photo1.jpg";
        bitmap = BitmapFactory.decodeFile(path);
        paint = new Paint();
        paint.setAntiAlias(true);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        Rect src = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
        RectF rectF = new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight());
        canvas.drawBitmap(bitmap, src, rectF, paint);
    }
Copy the code

A custom view loads an image from disk and displays it on a view whose size is determined by the size of the Bitmap. Taking the above devices A and B as examples, the results are shown as follows:



















Scale = 480/240 = 2 width = 172 * 2= 344 height = 172 * 2= 344 It can be understood that device B has A high density, and generally the higher the density, the more pixels are required per unit size. Suppose that 172*172 on device A occupies an area of 1inch, then more pixels are needed to fill the same area on device B. Therefore, the image resolution on device B should be larger (usually because ppi is the factor that really determines the number of pixels per unit size of device, and some devices have a larger DPI but a smaller PPI).

Photo1.jpg is now stored in hdPI and xxhdpi respectively, but in different sizes. When the program is running:

When device A finds that its density belongs to HDPI, it will directly look for the corresponding photo1.jpg under HDPI and display it. When device B finds that its density belongs to xxHDPI, it will directly look for the corresponding photo1.jpg under XXHDPI and display it

Take a look at the results:


A equipment 172 * 172 * 4 = 118336 ≈ 116K B equipment 344 * 344 * 4 = 473344 ≈ 462K note: When parsing a bitmap, the default is inPreferredConfig=ARGB_8888, which means four bytes per pixel

Note Displaying photo1.jpg on device B requires more memory. Hdpi and XXHDIPI are just listed above. Similarly, mdPI, XHDPI and XXXHDPI will be loaded from the corresponding MIPmap folder according to different device densities. This way, we don’t have to worry about bitmaps displaying on devices of different densities.

Image resource file loading

It seems that files of different sizes of the same set of resources are placed in each miPmap folder too much for APK size. Can we only put pictures of a certain density and rely on the system to adapt the rest? Now just keep the photo1.jpg image under HDPI and see how it works on devices A and B:






A device 172 * 172 * 4 = 118336 ≈ 116K B Device 301 * 301 * 4 = 362404 ≈ 354K

Compared with photo1.jpg separately placed in HDPI, xxHDPI and only placed in HDPI, it can be seen that the image occupies less memory on device B. Why is that? Next, look for the answer from the source code.

Construct the Bitmap

Bitmap bitmap = BitmapFactory.decodeResource(getContext().getResources(), R.mipmap.photo1);
Copy the code

Hdpi /photo1.jpg is also loaded on devices A and B, and the returned bitmap size is different.

public static Bitmap decodeResource(Resources res, int id, BitmapFactory.Options opts) { validate(opts); Bitmap bm = null; InputStream is = null; try { final TypedValue value = new TypedValue(); OpenRawResource (id, Value); density = res.openrawResource (id, Value); density = res.openrawResource (id, Value); bm = decodeResourceStream(res, value, is, null, opts); } catch (Exception e) { /* do nothing. If the exception happened on open, bm will be null. If it happened on close, bm is still valid. */ } finally { try { if (is ! = null) is.close(); } catch (IOException e) { // Ignore } } if (bm == null && opts ! = null && opts.inBitmap ! = null) { throw new IllegalArgumentException("Problem decoding into existing bitmap"); } return bm; }Copy the code
public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value, @Nullable InputStream is, @Nullable Rect pad, @Nullable BitmapFactory.Options opts) { validate(opts); if (opts == null) { opts = new BitmapFactory.Options(); } if (opts.inDensity == 0 && value ! = null) {final int density = value.density; if (density == TypedValue.DENSITY_DEFAULT) { opts.inDensity = DisplayMetrics.DENSITY_DEFAULT; } else if (density ! = TypedValue.DENSITY_NONE) { opts.inDensity = density; } } if (opts.inTargetDensity == 0 && res ! Opts.intargetdensity opts.inTargetDensity = res.getDisplayMetrics().denSitydpi; Return decodeStream(is, pad, opts);} decodeStream(is, pad, opts); }Copy the code

The key points mentioned above are density, namely the density of TypedValue and Options respectively. Take a look at TypedValue density:

    /**
     * If the Value came from a resource, this holds the corresponding pixel density.
     * */
    public int density;
Copy the code

Simple explanation: Indicates the density folder from which the resource is fetched. For example, photo1.jpg under HDPI of A and B, then density=240

Take a look at Options Density

* The pixel density to use for the bitmap.  This will always result
* in the returned bitmap having a density set for it

public int inDensity;

* The pixel density of the destination this bitmap will be drawn to.
* This is used in conjunction with {@link #inDensity} and
* {@link #inScaled} to determine if and how to scale the bitmap before
* returning it.

public int inTargetDensity;

Copy the code

InDensity indicates the folder of which density the resource comes from. The value is obtained from TypedValue. InTargetDensity indicates the device at which density the resource will be displayed. When constructing a Bitmap, multiple of Bitmap amplification abbreviation is determined based on inDensity and inTargetDensity.

NeedSize = (int)(size * ((float)inTargetDensity/inDensity) + 0.5)

How to load hdPI /photo1.jpg from B device:

InDesnity = 240 2, options. inDesnity = 420 3. Device B returns bitmap size =172 * 420/240 = 301px

The results are consistent with our previous debugging.

Density matching rule

How does the device decide to use the image resources under HDPI? According to the experiment (try to find the source, not how to understand, so just do the experiment, may be in different density devices to find rules are not the same) : First check whether xxHDPI has photo1.jpg. If not, go to a higher density. If not, go to a lower density and look for XHDPI instead of HDPI. It returns the constructed TypedValue, and the rest is what we analyzed earlier. Since we only want to put a slice at a certain density, which density should we put it at? From the perspective of finding rules in the system, it is more recommended to place them in a higher density, because if they are placed in a low density, the picture will be enlarged when running on a high density device, which may lead to unclear. I usually put it under xxhdPI.

Drawable and MIPMap folders with different densities

Android Studio creates mipmap folders of different densities by default, with ic_launcher.png placed by default. Should we put our normal cut diagram under Drawable or MipMap? There are different opinions on this issue on the Internet. In fact, for us, the focus is on the image in Drawable or MipMap, whether the loaded bitmap is different, if there is no difference, it depends on the habit. In practice, there is no difference between the bitmaps loaded from drawable and mipMap, but with Drawable you need to create folders of different densities. I’m used to putting it under Drawable (the startup logo is still under MipMap).

Screen width adaptation

We used dp to represent the size of the view. Why do they still look different? Let’s look at an example a little bit more visually. A device DPI =240 Density 1.5 resolution (width and height PX) : 480 * 800 B device DPI =420 Density 2.625 resolution (width and height PX) : 1080 * 1794 Converted to DP A device resolution: 320DP * 533DP B device resolution: 411DP * 683DP is still the above example:

<? The XML version = "1.0" encoding = "utf-8"? > <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"  android:id="@+id/big" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <View android:id="@+id/iv" android:background="@color/green" android:layout_gravity="center" android:layout_width="320dp" android:layout_height="320dp"/> </FrameLayout>Copy the code

Set the width and height of the view to 320dp and see what happens:





values-800×480

values-1794×1080

Let’s say the designer is drawing 800×480, so when we create the dimen file

<? The XML version = "1.0" encoding = "utf-8"? > <resources> <dimen name="px1">1px</dimen> <dimen name="px2">1px</dimen> ... <dimen name="px100">100px</dimen> <dimen name="px101">101px</dimen> </resources>Copy the code

The file is in the values-800×480 folder. The diMEN value of 1794×1080 was calculated according to the resolution ratio

<? The XML version = "1.0" encoding = "utf-8"? > <resources> <dimen name="px1">2.24px</dimen> <dimen name="px2">4.48px</dimen>... <dimen name="px100"> 24px</dimen> <dimen name="px101">226.24px</dimen> </resources>Copy the code

In this case, devices A and B use px in the corresponding resolution qualifier when loading resources. If they cannot find the default value, the problem of screen width fragmentation can be solved to A certain extent. But this can be very restrictive and requires testing in a wide range of resolutions, and Android has since introduced “slim-width” for short. Suppose the standard screen width of the designer is 320DP (device A), then you can define the following dimen.xml file

<? The XML version = "1.0" encoding = "utf-8"? > <resources> <dimen name="dp1">1dp</dimen> <dimen name="dp2">2dp</dimen> <dimen name="dp320">320dp</dimen> </resources>Copy the code

This file is placed in the values-SW320DP folder. According to the rules, dimen.xml of device B is calculated

Scale = targetWidth/baseWidth=411/320≈1.28 value = scale * baseValue

It is concluded that:

<? The XML version = "1.0" encoding = "utf-8"? > <resources> <dimen name="dp1">1dp</dimen> <dimen name="dp2">3dp</dimen> <dimen name="dp320">410dp</dimen> </resources>Copy the code

Now let’s move on to the view

<? The XML version = "1.0" encoding = "utf-8"? > <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"  android:id="@+id/big" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <View android:id="@+id/iv" android:background="@color/green" android:layout_gravity="center" android:layout_width="@dimen/dp320" android:layout_height="@dimen/dp320"/> </FrameLayout>Copy the code

By referring to dimen, device A looks for A dimen file of its own width and finds values -SW320dp, dp320=320dp. Device B looks for the dimen file with the same width as its own and finds values-sw411dp, dp320=410dp. In this way, the same DP320 can get different values, which ADAPTS to the problem of different screen widths. Look at the results:

If a device does not find values sw411DP, it will continue to look for values sw411DP. For example, if a device finds values sw390DP, it will use the values in it. Because we can roll vertically, we are less sensitive to height adaptation

In summary, dp+ smell-width is recommended for different screen sizes.

How do I get dPI

DisplayMetrics.java private static int getDeviceDensity() { // qemu.sf.lcd_density can be used to override ro.sf.lcd_density // when running in the emulator, allowing for dynamic configurations. // The reason for this is that ro.sf.lcd_density is write-once and is // set by the  init process when it parses build.prop before anything else. return SystemProperties.getInt("qemu.sf.lcd_density", SystemProperties.getInt("ro.sf.lcd_density", DENSITY_DEFAULT)); }Copy the code

This is where the device DPI is ultimately fetched, essentially reading the system configuration file. So we can also get adb shell:

HWTAS:/ $ wm size                                                                                                                                                                                   
Physical size: 1080x2340
HWTAS:/ $ 
HWTAS:/ $ getprop ro.sf.lcd_density
480
HWTAS:/ $ wm density 
Physical density: 480
HWTAS:/ $ 
Copy the code

It can be seen that DPI is configured by the system. Of course, the resolution of some mobile phones can be set. After setting, we can check the resolution:

HWTAS:/ $ wm density                                                                                                                                                                                
Physical density: 480
Override density: 320
HWTAS:/ $ 
HWTAS:/ $ 
HWTAS:/ $ wm size                                                                                                                                                                                   
Physical size: 1080x2340
Override size: 720x1560
HWTAS:/ $ 
Copy the code

The resolution is lower and the DPI is smaller.