preface

As we all know, Android is licensed by numerous manufacturers and produces numerous models, resulting in serious fragmentation of size. Of course, it has been 9102 years, and we have gradually got the optimal solution. The mainstream models in China are basically hovering around 720, 1080 and 1440, with each having its own advantages at most, but there are still many mobile phones with other resolutions. Let’s take a look at a set of data (source: Umong)

As the chart shows, the above conclusion is correct, but it can be seen that every year a large proportion of other sizes of mobile phones occupy market share, not to mention those antique machines that are still in service. I believe that this part of the user base is impossible to be cut off by the product manager.

To solve this problem, we can of course —

  1. Good at using RelativeLayout, Linearlayout, ConstraintLayout;
  2. Proper use of wrap_content, match_content;
  3. Use attributes such as minHeight, minWidth, lines, ellipSize, etc.
  4. Use DP and SP units;
  5. Use different layouts, images, and dimen for different phones on a single page;

But I want to say that all of these are just the basic qualities that an Android developer should have. Some may ask, isn’t that enough? And dp, SP have not been officially adapted to the unit? Let’s analyze it step by step.

Why do officials need to use device-independent pixel adaptation

Device independent pixel (DP, SP), also known as logical pixel, is a size unit calculated by scale factor (scale), which has a certain conversion ratio with pixel and is not restricted by device resolution and density (PPI).

So what is resolution, what is PPI, what is DPI.

The resolution is defined as how many pixels there are on the phone’s screen. 1080×1920 is 1920 pixels high and 1080 pixels wide. When we continue to check the parameters of mobile phones, we will see the next indicator, called Pixels Per Inch (PPI), which indicates the number of Pixels Per Inch. A larger PPI means a finer screen, but exceeding the resolution of the naked eye is of little significance. Take Glory 10 for example, its resolution is 1080×2280. So the number of pixels in the diagonal is 2522.86, and the size of the main screen is 5.84 inches, so we can conclude that the number of pixels per inch is 431.997≈432, that is, PPI =432. So what is dpi? Dpi (Dots per Inch), which literally means Dots per Inch, but is now more commonly used to represent a parameter in the display strategy, in Android it can be set in the system, is variable, and is used to calculate the scaling factor. Perhaps we can see the statement that PPI IS DPI in many articles, but actually they are different from the original definition. Specifically referring to WHAT IS DPI, I think this article IS comprehensive, reliable and in line with the facts.

Why are individual pixels not subject to resolution and density?

We first understand, when we assume that pixels are tending to square, density can only affect the physical size and scale of visual presentation, screen pixels x are of high to width of a square, in the same resolution of the different density of mobile phone, they just size is not the same vision, but holds the scale of the screen is the same.

Then we just need to analyze why individual pixels are not resolution dependent.

As we know, fixed DPI is written in the system file of each mobile phone before delivery, and DPI is an important parameter to form independent pixels. We can calculate the conversion ratio between DP and PX according to DPI, which is also the scale factor. From the official document, we can get a formula

1dp = 1px * scale = 1px * dpi / 160
Copy the code

In other words, as long as we control the values of the DPI according to the same rules, we can achieve device-independent pixels that convert all resolutions into a single value at the latitude and width.

For example, the DPI of most 720×1280 mobile phones is 320 and the width is 360DP according to the formula, while the DPI of most 1080×1920 mobile phones is 480 and the width is 360DP according to the formula.

So what can individual pixels do with the same device? As long as we set the width to 180DP, it will always take up half the screen width.

However, if we use pixels as the unit of control directly, there is no guarantee that it will occupy the same proportion in different phone resolutions.

For example, if we set the width to 360px on a 720×1280 phone, it will take up half the screen, while on a 1080×1920 phone it will take up only one-third of the screen.

That’s why it’s officially recommended that we use device-independent pixels as units of size. So the question is, since device independent pixels are so good, let’s —

Why the secondary adaptation

In the example above, most 720×1280 and 1080×1920 phones are 360DP wide, while most 480×800 phones (DPI =240) are 320DP wide. What happens when the design is 360DP?

For example, as shown in the figure below, the two devices have the same resolution, but different DPI. The former is 480dpi, while the latter is 540dpi (ps: Don’t ask if there is such a machine, nove4e is like this), the design is 360DP based, the width ratio of heat ranking to contribution ranking is 3:4, we can see that it does not perform well in 320DP, no matter how we lay it out, how we use attributes, it will never be perfect, The logical width is always 40dp less than the design draft.

Besides 320DP and 360DP, the logical width of domestic mobile phones alone is 345.6 DP, 375.6 DP, 392.7 DP, 411.4 DP, 423.5 DP and so on. Of course, in theory more logical width should show more content, but in reality this is often not allowed, which means —

  1. Need to design multiple sets of maps;
  2. Heavy development work and difficult maintenance;
  3. Increase package size, after all, unlike iOS has App Slicing;

In short, the human cost is too high. But through secondary adaptation, we can achieve a set of design for “all” devices, a set of layout for “the whole family”. Maybe this is not the best plan, but it is the most appropriate plan and the most cost-effective plan from a comprehensive perspective. So we’re going to —

How to do secondary adaptation

There are many ways to do secondary adaptation, which can be roughly divided into exhaustive and Hook.

Note: Because the current application scene of most apps is only in portrait screen, even if the interface has landscape screen, it only needs to keep the same height and adapt to the width. To say the least, there are really individual page height also need to adapt, can be specific scene specific analysis, even if do not do two adaptation, is OK. Therefore, the following adaptation methods are described only from the latitude of the width.

Enumerates the width and height qualifier

As we all know, width and height qualifiers match if both sides are smaller than the nearest value of screen resolution. Following this rule, we enumerate as many resolutions as possible (there are many, but we just enumerate by width, with height set to slightly greater than width). According to the resolution of testin and wetest, we can get the file structure as follows:

+-- res
|   +-- values
|   +-- values-330x320
|   +-- values-490x480
|   +-- values-550x540
|   +-- values-650x640
|   +-- values-730x720
|   +-- values-780x768
|   +-- values-810x800
|   +-- values-1100x1080
|   +-- values-1160x1152
|   +-- values-1210x1200
|   +-- values-1450x1440
|   +-- values-2170x2160
Copy the code

Then, based on 1080px, calculate the equal ratio of 1px in other resolutions (default values= VALUes-1100×1080). Assuming that the width of the target resolution is W, the formula is as follows:

px' = W/1080
Copy the code

For example, the dimens value at 720px is:

<resources>
    <dimen name="x1"> 0.66 px < / dimen > < dimen name ="x2"> 1.33 px < / dimen > < dimen name ="x3"> 2.0 px < / dimen > < dimen name ="x4"> 2.66 px < / dimen > < dimen name ="x5"> 3.33 px < / dimen > < dimen name ="x6"> 4.0 px < / dimen > < dimen name ="x7"> 4.66 px < / dimen > < dimen name ="x8"> 5.33 px < / dimen > < dimen name ="x9"> 6.0 px < / dimen > < dimen name ="x10">6.66px</dimen>.. <dimen name="x1080">720px</dimen>
</resources>
Copy the code

Once configured, let’s select 9 samples from the above resolutions to see how it works in practice:

As can be seen from the figure above, the running result is very in line with our expected value. The space between heat ranking and contribution ranking is almost the same. The only obvious thing is the word count of each line ±1, which is caused by the decimal point in the pixel after conversion, but this is acceptable.

Then we analyze the extreme case. Firstly, since we are exhaustive resolution, we do not need to consider DPI here, and since the width of our exhaustive resolution is 320-2160, we can consider the boundary value from this Angle:

  1. The width is less than 320px and matches the default values.
  2. The width is equal to some enumerated value (let’s say 720px) and the height is exactly less than 730px, matching values one level higher, but since we set the height only slightly greater than the width, we can assume that this case does not exist;
  3. Wider than one enumeration value, but less than the width of the next level (say 1600px), matching values-1450×1440;
  4. Width less than one enumeration value, but larger than the width of the previous level (say 1300px), matching values-1210×1200, equivalent to point 3;
  5. Width 2160px, match values-2170×2160, equal to point 3;

Let’s see how 3, 4, and 5 work:

As you can see from the results, our extreme cases are all wider than expected because our resolution qualifier is downmatched.

From what has been discussed above, we can draw the conclusion that:

  1. In the equipment with known resolution, this method can be used perfectly.
  2. It is recommended to use PX ‘everywhere, including fonts, custom controls, etc. Otherwise it will not be compatible;
  3. The app font size is not affected by the system setting — the font display size — because the font also needs to use PX ‘.
  4. Because of the use of PX ‘, you have to pay extra attention to units in your code to dynamically set the spacing and so on;
  5. Because non-1080px is a conversion scale, there must be a decimal point, so there will be a drop error;
  6. The enumeration resolution is too high, resulting in too many Dimens files and a slightly larger package size. If all 1080 pixels are mapped, the enumeration values in the example will be about 0.5MB more.
  7. Because of the presence of field resolution, even if the use of this adaptation, still can not blindly use absolute value, or with other control attributes together to use;
  8. It is highly invasive and depends on the quality of technical personnel.

Enumerate the minimum width qualifier

The minimum width qualifier limits the logical width of resources that are less than and closest to the screen width. The logical width (W’) can be obtained from resolution (W) and DPI:

W' = W / ( dpi / 160 )
Copy the code

We can enumerate all possible logical widths just as we can enumerate resolution qualifiers. For analysis, our tentative file structure is as follows:

+-- res
|   +-- values
|   +-- values-sw320dp
|   +-- values-sw360dp
|   +-- values-sw411dp
Copy the code

Then take 360DP as the base, calculate the equal ratio of 1DP to other logical widths (note: default values= values-SW360DP), assume that the target logical width is W, then the formula is:

dp' = W/360
Copy the code

Similarly, for example, the logical width of 320dp is dimens:

<resources>
    <dimen name="dp_1"Dp > 0.89 < / dimen > < dimen name ="dp_2"Dp > 1.78 < / dimen > < dimen name ="dp_3"Dp > 2.67 < / dimen > < dimen name ="dp_4"Dp > 3.56 < / dimen > < dimen name ="dp_5"Dp > 4.44 < / dimen > < dimen name ="dp_6"Dp > 5.33 < / dimen > < dimen name ="dp_7"Dp > 6.22 < / dimen > < dimen name ="dp_8"Dp > 7.11 < / dimen > < dimen name ="dp_9"Dp > 8.00 < / dimen > < dimen name ="dp_10">8.89dp</dimen>.. <dimen name="dp_360">320dp</dimen>
</resources>
Copy the code

Let’s look at this in action:

Obviously, it is in line with our expectations, but it is inevitable that there is still a problem with the logical width (see the reason at the beginning of the article — why the secondary adaptation), such as 384DP (Nexus 4), 392DP (XiaoMi MIX2), so let’s look at the extreme situation again:

  1. The logical width is less than 320dp, although there is no data support, we assume that this is the minimum width, or there is always a minimum value (later statistics, fill in relevant data);
  2. The logical width is between two enumerated values, such as 384dp;
  3. Logical width > 411dp;

Ok, since the minimum width qualifier is still matched downward, thus returning to exactly the same situation as in the previous section — the extreme case is wider than expected, so we will not repeat the texture here.

So let’s conclude this section:

  1. In the enumerated logical widths, the device is almost perfectly matched;
  2. References to DP ‘are required for consistency in XML, code, and custom controls.
  3. We need to configure another set of TextSize. As for dp or SP, it’s different for different people (wechat doesn’t use SP).
  4. Enumeration value maps have fewer dp’ values than width and height qualifiers;
  5. It is slightly more compatible than the width and height qualifier, even if it is written as DP in some places.
  6. Also because it is the conversion ratio, there must be a decimal point, the final application of PX, there may be a little error;
  7. Packet size is proportional to the number of enumerations of Dimens;
  8. There is also an off-field DPI, so absolute value cannot be blindly used;
  9. It is highly invasive and depends on the quality of technical personnel.

The above mentioned how to do exhaustive adaptation, but how to exhaustive both complete and concise is a difficult point, so is it possible to have a measurement end point, all the spacing, size, size will pass through here, we carry out automatic adaptation at this end point? Of course there is.

Select onMeasure to Hook

As we know, view needs to measure first, then layout and then draw, so the entry point is onMeasure.

A good example is AndroidAutoLayout, which is detailed in ReadMe and won’t be covered here. The core idea is that by overwriting its onMeasure before calling super.onMeasure(widthMeasureSpec, heightMeasureSpec), it resets the values of the related properties based on the screen width and height. Such as padding, margin, height, width, textSize.

Of course, the last code submitted by AndroidAutoLayout was 4 Yeas ago. At the beginning of its design, it was assumed that the aspect ratio of all phones was in an appropriate range, such as 720×1280, so its height and width were scaled in different proportions. In 9102, however, there was clearly a wide variety of aspect ratios. So AndroidAutoLayout has entered its limitations. However, it is still a target we can learn from, we just need to scale its height according to the width of the zoom ratio, or high or low mobile phone to adapt to the height. (Interested students can try, here only talk about how to hook~)

So how do you hook onMeasure? In AndroidAutoLayout, there are a lot of custom viewGroups, such as AutoLinearLayout, AutoRelativeLayout, AutoFrameLayout. Let’s take AutoLinearLayout as an example

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if(! IsInEditMode ()) {isInEditMode() = adjustChildren(); } super.onMeasure(widthMeasureSpec, heightMeasureSpec); }Copy the code

AdjustChildren (adjustChildren) AndroidAutoLayout (adjustChildren)

public void adjustChildren() { AutoLayoutConifg.getInstance().checkParams(); // Check the library configurationfor (int i = 0, n = mHost.getChildCount(); i < n; i++) {
        View view = mHost.getChildAt(i);
        ViewGroup.LayoutParams params = view.getLayoutParams();

        if (params instanceof AutoLayoutParams) {
            AutoLayoutInfo info = ((AutoLayoutParams) params).getAutoLayoutInfo();
            if(info ! = null) { info.fillAttrs(view); }}}}Copy the code

AdjustChildren (adjustChildren) loop takes all of the AutoLayoutParams from the parent class LayoutParams. AdjustChildren (adjustChildren) loop takes all of the AutoLayoutParams from the parent class LayoutParams. It doesn’t do anything.

public static AutoLayoutInfo getAutoLayoutInfo(Context context,AttributeSet attrs) { ... // Width attributes are scaled by width and height attributes are scaled by height. However, there are always exceptions, so baseWidth and baseHeight are used to enforce constraints on the zoom direction. int baseWidth = a.getInt(R.styleable.AutoLayout_Layout_layout_auto_basewidth, 0); int baseHeight = a.getInt(R.styleable.AutoLayout_Layout_layout_auto_baseheight, 0); .for (int i = 0; i < n; i++) {
        ...
        switch (index) {
            case INDEX_TEXT_SIZE:
                info.addAttr(new TextSizeAttr(pxVal, baseWidth, baseHeight));
                break;
            case INDEX_PADDING:
                info.addAttr(new PaddingAttr(pxVal, baseWidth, baseHeight));
                break; . }}return info;
}
Copy the code

Of course, different autoattrs implement their own scaling methods, which are simply calculated as the ratio of the width of the design to the width of the screen, and then multiplied by the original attribute value to get the final attribute value.

public void apply(View view) {
    int val;
    if (useDefault()) {
        val = defaultBaseWidth() ? getPercentWidthSize() : getPercentHeightSize();
    } else if (baseWidth()) {
        val = getPercentWidthSize();
    } else {
        val = getPercentHeightSize();
    }
    if(val > 0) { val = Math.max(val, 1); //forvery thin divider } execute(view, val); // Perform the zoom and set it to view or layoutParams}Copy the code

AdjustChildren is called in adjustChildren when fillAttrs is used.

So the hook is done. Of course, AutoAttr, Helper, AutoLayoutParams, and internally packaged AutoLayoutActivity and AutoUtils are all design ideas, but the important thing is implementation ideas, and you can also simply mix them together

Since it is a perfect fit, then just paste a few random resolution bar ~

As usual, the following is the summary of this section:

  1. Using onMeasure as a pointcut, it can be almost perfectly adapted to the screen with almost no performance loss.
  2. Similarly, 1 is also a disadvantage, which must be supported by viewGroup and attribute implementation.
  3. For custom controls, adaptation is cumbersome;
  4. For controls whose properties need to be modified, see 3.
  5. AndroidAutoLayout is not very friendly for dynamically added controls, you need to manually call Autoutils.auto (view) after adding, this will bring extra overhead, of course users can extend to support this;
  6. It is also highly invasive and depends very much on the quality of technical personnel;
  7. The original library, height and width is scaled in different proportions, but now the mobile phone height and width ratio difference is relatively large, so in accordance with the old rules will be very poor display effect, although you can be forced to specify the control of the zoom direction, but the workload will be more cumbersome, so you may need to modify the source code;
  8. Of course, AndroidAutoLayout intercepts and replaces px, if necessary, can be replaced with intercepts DP, SP, this is not important, important is hook point.

In fact, onMeasure is a shallow hook point. Although it has obvious advantages, it also has obvious disadvantages. Is there a breakthrough point that can automatically adapt without writing so much code and is not highly intrusive? The bytedance team gave us the answer.

Choose DisplayMetrics. DensityDpi Hook

The relationship between DP and PX is as follows:

1dp = 1px * dpi / 160
Copy the code

There must be a place in the system to convert these units, such as in TypedValue:

public static float applyDimension(int unit, float value,DisplayMetrics metrics)
{
    switch (unit) {
        case COMPLEX_UNIT_PX:
            return value;
        case COMPLEX_UNIT_DIP:
            return value * metrics.density;
        case COMPLEX_UNIT_SP:
            return value * metrics.scaledDensity;
        case COMPLEX_UNIT_PT:
            returnValue * metrics. Xdpi * (1.0f/72);case COMPLEX_UNIT_IN:
            return value * metrics.xdpi;
        case COMPLEX_UNIT_MM:
            returnValue * metrics. Xdpi * (1.0f/25.4f); }return 0;
}
Copy the code

For example in BitmapFactory:

public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value,@Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts) {
    validate(opts);
    if (opts == null) {
        opts = new 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; / /}}inDensity refers to the Density of the Drawable folder where the resource resides,inTargetDensity refers to screen densityif(opts.inTargetDensity == 0 && res ! = null) { opts.inTargetDensity = res.getDisplayMetrics().densityDpi; }return decodeStream(is, pad, opts);
}

Copy the code

We watch as you can see, they all use the DisplayMetrics. DensityDpi this property, so we only need according to the density of the screen to modify the property value, can pretend to be the logical width of the screen for 360 dp forever.

DisplayMetrics comes from three things:

// The system screen size val systemMetrics = resources.getSystem ().displayMetrics Application. The resources. The displayMetrics / / the activity screen size val activityMetrics = activity. The resources. The displayMetricsCopy the code

We only need to modify the application and activity, system is not recommended to change, it is used to retain a copy of the original data, and even if changed, it is useless, it is used to obtain system resources.

From the above we can easily know that the current screen logical width is:

* 1. Available display width may be smaller than real width, although it is the same in most real scenes; * 2. In the scene of 1 (say there is a decoration bar on the left and right of the screen?) , we might say that the application uses available display width and the activity uses real width, * but if the activity uses application.resource, So the spacing is going to be a little bit smaller, and that doesn't matter. * Conversely, if we modify it with Real Width, it will not show up. */ val widthInDp = resources.displayMetrics.run { widthPixels / (densityDpi / 160) }Copy the code

So we can set densityDpi to:

Val targetDpi = resources. DisplayMetrics. WidthPixels * / 360/160/360 is our design draft the logical width of the val sysMetrics = Resources.getSystem().displayMetrics resources.displayMetrics.run { densityDpi = targetDpi density = targetDpi / 160f ScaledDensity = density * sysMetrics. ScaledDensity/sysMetrics. So we need according to the original proportion to get new scaledDensity} application. The resources. The displayMetrics. Run {densityDpi = targetDpi density = targetDpi / 160f scaledDensity = density * sysMetrics.scaledDensity / sysMetrics.density }Copy the code

Use for recovery:

/** * We use sysMetrics directly here for two * 1 reasons. 2. If you change the system font size while using the application, sysMetrics will synchronize the change. Don't listen registerComponentCallbacks * / val sysMetrics = Resources. GetSystem () displayMetrics Resources. The displayMetrics. Run { densityDpi = sysMetrics.densityDpi density = sysMetrics.density scaledDensity = sysMetrics.scaledDensity } application.resources.displayMetrics.run { densityDpi = sysMetrics.densityDpi density = sysMetrics.density scaledDensity  = sysMetrics.scaledDensity }Copy the code

Look at the adaptation effect ~

Looks perfect, doesn’t it? The code is simple, isn’t it? It doesn’t seem invasive, does it? But everything has both advantages and disadvantages:

  1. If we modify the application.resource, the tripartite library will be affected if it is useful;
  2. If we modify by registerActivityLifecycleCallbacks activity. The resource, the third party libraries activity would be affected;
  3. [Fixed] Libraries may be modified simultaneously without control.
  4. System controls will also be affected, such as TOAST, so it is especially not recommended to set the logical width of the design draft to an extreme. For example, to copy from the design draft and save trouble, set it to 1080dp.
  5. During webView initialization, the density value will be restored, resulting in adaption failure. Modify as follows:
    /** * override funsetOverScrollMode(mode: Int) {super.seToverScrollMode (mode) adaptDensityDpi()} ** * or override fun getResources(): Resources {adaptDensityDpi() // Be careful to avoid endless loops and repeated changesreturn super.getResources() 
    }
    Copy the code
  6. Some systems may fail to modify DisplayMetrics due to framework modifications, such as MIUI7 + Android5.1.1;
  7. Need to consider the special activity does not need to adapt;
  8. Because the fragment actually uses the resource of the activity, if one of the fragments does not need to be adapted, you need to consider the adaptation reset when switching the fragment.

So the above code can only be used in the demo to prove that we are on the right track. The rest of us still need to further solve the problems listed above, also need to adapt, need to package, need to improve the usability, robustness ~ such as AndroidAutoSize.

finish

Well, that’s the end of this article. Having said all that, PERSONALLY I prefer the last option. Of course, the maturity of a program needs to grow, so does the project and the people. The rest is a matter of opinion.


In this paper, the author: timedance this article links: www.tktimedance.com/posts/a5563… Demo address: github.com/timedance/S… Copyright Notice: All articles on this blog are licensed BY-NC-ND unless otherwise stated. Reprint please indicate the source!