Android’s screen fragmentation problem has a long history. Generally, we use the same physical size on different devices. Such a solution does not completely restore the design draft, and the situation of individual adaptation will appear more frequently as the size of the project increases.

Solution

In May 2018, the ByteDance technical team released an article “一种极低成本的Android屏幕适配方式(A very low-cost Android screen adaptation solution)”. This solution proposes to modify the density for screen adaptation. Recently, our project has been modified and adapted regarding this solution, and it is currently operating well.

Principle

As it is describe in the article:

px = density * dp;

density = dpi / 160;

px = dp * (dpi / 160);

Generally, the design draft will fix a width value, and it may even be a design draft shared with iOS, and then the unit conversion is performed according to the platform.

如果设计图宽为360dp,想要保证在所有设备计算得出的px值都正好是屏幕宽度的话,我们只能修改 density 的值(If the design width is 360dp and we want to ensure that the px value calculated on all devices is exactly the screen width, we can only modify the density value.).

Implementation

So we can write utils file for encapsulation, as the code provided by the article, we can write a function about changing density.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
private static final int DEFAULT_WIDTH = 375;
private static float sNoncompatDensity;
private static float sNoncompatScaledDensity;

public static void setCustomDensity(@NotNull Activity activity, @NotNull final Application application) {
final DisplayMetrics appDisplayMetrics = application.getResources().getDisplayMetrics();

if (sNoncompatDensity == 0) {
sNoncompatDensity = appDisplayMetrics.density;
sNoncompatScaledDensity = appDisplayMetrics.scaledDensity;
application.registerComponentCallbacks(new ComponentCallbacks() {
@Override
public void onConfigurationChanged(Configuration newConfig) {
if (newConfig != null && newConfig.fontScale > 0) {
sNoncompatScaledDensity = application.getResources().getDisplayMetrics().scaledDensity;
}
}

@Override
public void onLowMemory() {
}
});
}

final float targetDensity = (appDisplayMetrics.widthPixels * 1.0f) / DEFAULT_WIDTH;
final float targetScaledDensity = targetDensity * (sNoncompatScaledDensity / sNoncompatDensity);
final int targetDensityDpi = (int) (160 * targetDensity);

appDisplayMetrics.density = targetDensity;
appDisplayMetrics.scaledDensity = targetScaledDensity;
appDisplayMetrics.densityDpi = targetDensityDpi;

final DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics();
activityDisplayMetrics.density = targetDensity;
activityDisplayMetrics.scaledDensity = targetScaledDensity;
activityDisplayMetrics.densityDpi = targetDensityDpi;

}

We should run this function before setContentView() method within Activity‘s OnCreated() method.

Problem

Question 1:
The system will reset the density value in some situations as WebView disappear.

Solution:
Change the density value again after the reset problem happened. For example, we can override setOverScrollMode() method to solve the WebView problem.

Question 2:
Some components, like Bitmap, using the raw density to calculate but not the density we changed.

Solution:
Read the source code of Bitmap, we can find mDensity is the variable that using in the calculation.

1
2
/** @hide */
public int mDensity = getDefaultDensity();

And the process of initialization is as below.

1
2
3
4
5
6
7
static int getDefaultDensity() {
if (sDefaultDensity >= 0) {
return sDefaultDensity;
}
sDefaultDensity = DisplayMetrics.DENSITY_DEVICE;
return sDefaultDensity;
}

The sDefaultDensity initialized as -1.

1
2
3
4
5
6
7
8
9
10
private static volatile int sDefaultDensity = -1;

/**
* For backwards compatibility, allows the app layer to change the default
* density when running old apps.
* @hide
*/
public static void setDefaultDensity(int density) {
sDefaultDensity = density;
}

It can be seen that when the value of density is not passed in, the value calculated by Bitmap is DisplayMetrics.DENSITY_DEVICE, andmDensity itself is a variable marked with @ hide, which means that in newer versions We can’t get and modify it through reflection. Although sDefaultDensity is not marked, it is also on the official hidden API list.

Therefore, the solution is as follows: On the old version of the system, we modify the value of sDefaultDensity by reflection. On the new version, we avoid using Bitmap directly but pass in the Density through BitmapFactory to generate.

Question 3:
How are third-party libraries compatible if they are not designed to that size?

Solution:
The best solution is to fork the third-party library and modify it by yourself. If this method is difficult to complete, you can consider other screen adaptation solutions, such as inserting stubs before and after calling the code of the third-party library to restore and modify the density.

Question 4:
How compatible is the solution for horizontal and vertical screen adaptation?

Solution:
Out project only supports the use of vertical screens, so the requirement for vertical screens is mandatory for all pages. This is also the task of this adaptation. Pay attention to the crash of transparent pages compatible with the Android8.0 system version. For scenes with horizontal and vertical screen requirements, you can make relevant judgments in the tool code above.