为什么要适配?
1.Android随着不断的发展壮大,设备碎片已越发严重,各大厂商对屏幕参数没有统一的标准,导致UI组件在不同的屏幕尺寸上展示效果显示不一致。
2.屏幕适配的最终目的,让组件在与给定的UI标准匹配到不同的屏幕。使显示效果尽可能达到一致。
自定义像素适配
1.通常在开发的过程,视觉总是以一套标准的屏幕来给出对应的设计如(720x1280),像素匹配则根据当前屏幕的实际像素换算出目标像素作用到控件上,达到适配的效果。
2.以标准屏幕(720x1280)为标准视觉标注,(1080x1920)为目标设备进行适配。若某个组件在标准设备的显示效果为屏幕一半(360px),则目标屏幕的宽度应该为(1080/ 720 x 360 = 540px)。则可以编写工具类: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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104/**
* 布局文件中android:layout_width="px"
*/
public class DisplayUtils {
/**
* 视觉给定的标准屏幕宽度
*/
private static final float STAND_WIDTH = 720;
/**
* 视觉给定的标准屏幕长度
*/
private static final float STAND_HEIGHT = 1280;
private static DisplayUtils displayUtils;
/**
* 目标屏幕宽度
*/
private int targetWidth;
/**
* 目标屏幕长度
*/
private int targetHeight;
private DisplayUtils(Context context) {
if (null == context) {
return;
}
if (0 == targetWidth || 0 == targetHeight) {
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
if (null != windowManager) {
DisplayMetrics metrics = new DisplayMetrics();
windowManager.getDefaultDisplay().getMetrics(metrics);
//横屏
if (metrics.widthPixels > metrics.heightPixels) {
targetWidth = metrics.heightPixels;
targetHeight = metrics.widthPixels;
} else {
targetWidth = metrics.widthPixels;
targetHeight = metrics.heightPixels - getStatusBarHeight(context);
}
}
}
}
public static DisplayUtils getInstance(Context context) {
if (null == displayUtils) {
displayUtils = new DisplayUtils(context.getApplicationContext());
}
return displayUtils;
}
/**
* statusBar 的高度
*
* @param context
*/
private int getStatusBarHeight(Context context) {
if (null == context) {
return 0;
}
int resID = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resID > 0) {
return context.getResources().getInteger(resID);
}
return 0;
}
/**
* 水平方向缩放比例
*/
public float getHorizontalScale() {
return targetWidth / STAND_WIDTH;
}
/**
* 竖直方向缩放比例
*/
public float getVerticalScale() {
return targetHeight / STAND_HEIGHT;
}
}
使用-->
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
/**
* 自定义view继承自LinearLayout,若继承自RelativeLayout,保证布局测量一次,应设置flag
* if(!flag) {
* do something
* }
* flag = true
*/
float scaleX = DisplayUtils.getInstance(getContext()).getHorizontalScale();
float scaleY = DisplayUtils.getInstance(getContext()).getVerticalScale();
int count = getChildCount();
for (int i = 0; i < count; i++) {
View childView = getChildAt(i);
LayoutParams params = (LayoutParams) childView.getLayoutParams();
params.width = (int) (params.width * scaleX);
params.height = (int) (params.height * scaleY);
//margin信息也是同样的处理方式
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
小结
1.可以看出自定义像素适配方式更多的适合使用在自定义View
中,对通用的布局支持不是很友好。
自定义百分比布局适配
1.Andorid
支持库Android-percent-support
支持百分比布局,一定程度上可以解决屏幕的适配问题。按照官方的实现思路,作为开发者可以自定义实现自身的百分比布局。以RelativeLayout
为例,实现自己的百分比布局。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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RelativeLayout;
/**
* Created by Sai
*/
public class PercentLayout extends RelativeLayout {
public PercentLayout(Context context) {
super(context);
}
public PercentLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public PercentLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
/**获取父容器的size*/
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
/**遍历子控件*/
int count = getChildCount();
for (int i = 0; i < count; i++) {
View childView = getChildAt(i);
ViewGroup.LayoutParams params = childView.getLayoutParams();
changeToPercentSize(params, widthSize, heightSize);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
/**
* 根据父容器尺寸计算子view尺寸
*/
private void changeToPercentSize(ViewGroup.LayoutParams params, int widthSize, int heightSize) {
if (checkLayoutParams(params)) {
LayoutParams layoutParams = (LayoutParams) params;
float widthPercent = layoutParams.widthPercent;
float heightPercent = layoutParams.heightPercent;
float marginLeftPercent = layoutParams.marginLeftPercent;
float marginRightPercent = layoutParams.marginRightPercent;
float marginTopPercent = layoutParams.marginTopPercent;
float marginBottomPercent = layoutParams.marginBottomPercent;
/** 保证在未使用自定义属性时相当于使用普通RelativeLayout */
if (widthPercent > 0) {
params.width = (int) (widthSize * widthPercent);
}
if (heightPercent > 0) {
params.height = (int) (heightSize * heightPercent);
}
if (marginLeftPercent > 0) {
((LayoutParams) params).leftMargin = (int) (widthSize * marginLeftPercent);
}
if (marginRightPercent > 0) {
((LayoutParams) params).rightMargin = (int) (widthSize * marginRightPercent);
}
if (marginTopPercent > 0) {
((LayoutParams) params).topMargin = (int) (heightSize * marginTopPercent);
}
if (marginBottomPercent > 0) {
((LayoutParams) params).bottomMargin = (int) (heightSize * marginBottomPercent);
}
}
}
/**
* 使用自定的LayoutParams,包含了百分比的属性
*
* @param attrs 布局参数
*/
public RelativeLayout.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
/**
* 判断是否是自定义的LayoutParams,保留原有RelativeLayout相关属性,开关(使用默认属性)
*
* @param params 是否为自定义的LayoutParams
*/
protected boolean checkLayoutParams(ViewGroup.LayoutParams params) {
return params instanceof LayoutParams;
}
private static class LayoutParams extends RelativeLayout.LayoutParams {
private float widthPercent;
private float heightPercent;
private float marginLeftPercent;
private float marginRightPercent;
private float marginTopPercent;
private float marginBottomPercent;
private LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray typedArray = c.obtainStyledAttributes(attrs, R.styleable.PercentLayout_Layout);
widthPercent = typedArray.getFloat(R.styleable.PercentLayout_Layout_widthPercent, 0);
heightPercent = typedArray.getFloat(R.styleable.PercentLayout_Layout_heightPercent, 0);
marginLeftPercent = typedArray.getFloat(R.styleable.PercentLayout_Layout_marginLeftPercent, 0);
marginRightPercent = typedArray.getFloat(R.styleable.PercentLayout_Layout_marginRightPercent, 0);
marginTopPercent = typedArray.getFloat(R.styleable.PercentLayout_Layout_marginTopPercent, 0);
marginBottomPercent = typedArray.getFloat(R.styleable.PercentLayout_Layout_marginBottomPercent, 0);
typedArray.recycle();
}
}
}
----->
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="PercentLayout_Layout">
<attr name="widthPercent" format="float" />
<attr name="heightPercent" format="float" />
<attr name="marginLeftPercent" format="float" />
<attr name="marginRightPercent" format="float" />
<attr name="marginTopPercent" format="float" />
<attr name="marginBottomPercent" format="float" />
</declare-styleable>
</resources>
使用------>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.example.PercentLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv_handler"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/colorAccent"
android:gravity="center"
android:text="Hello World!"
app:heightPercent="0.75"
app:widthPercent="0.3" />
</com.example.PercentLayout>
</layout>
app:heightPercent="0.75",在定义时我们直接可以使用浮点数,在使用时候会比较方便相比较与管方`75%`。
Tip
1.同自定义像素类似,自定义百分比是直接作用在View
上,灵活性相对较差,无法实现真正的通用(自定义view)。但是也能比较很好的达到适配的效果。
修改Density、DensityDpi进行适配
1.看TypedValue
中的一段代码: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/**
* Converts an unpacked complex data value holding a dimension to its final floating
* point value. The two parameters <var>unit</var> and <var>value</var>
* are as in {@link #TYPE_DIMENSION}.
*
* @param unit The unit to convert from.
* @param value The value to apply the unit to.
* @param metrics Current display metrics to use in the conversion --
* supplies display density and scaling information.
*
* @return The complex floating point value multiplied by the appropriate
* metrics depending on its unit.
*/
public static float applyDimension(int unit, float value,
DisplayMetrics metrics)
{
switch (unit) {
case COMPLEX_UNIT_PX:
return value;//换算成px
case COMPLEX_UNIT_DIP:
return value * metrics.density;
case COMPLEX_UNIT_SP:
return value * metrics.scaledDensity;//好比字体缩放
case COMPLEX_UNIT_PT:
return value * metrics.xdpi * (1.0f/72);
case COMPLEX_UNIT_IN:
return value * metrics.xdpi;
case COMPLEX_UNIT_MM:
return value * metrics.xdpi * (1.0f/25.4f);
}
return 0;
}
不管使用sp、dp,最终都会换算成像素px,不同的设备density是不一样的,同一个分辨率下的density也有可能是不一样的。基于一个标准值修改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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110import android.app.Activity;
import android.app.Application;
import android.content.ComponentCallbacks;
import android.content.res.Configuration;
import android.util.DisplayMetrics;
import androidx.annotation.NonNull;
/**
* Created by Sai
*/
public class DensityUtil {
/**
* 标准参考值
* eg:如果要设置屏幕的一半宽度则 360/2=180
*/
private static final float STAND_WIDTH = 360;
/**
* 标准密度
*/
private static final int STAND_DPI = 160;
/**
* 屏幕的密度
*/
private static float appDensity;
/**
* 字体缩放比例
*/
private static float appScaleDensity;
/**
* 方法作用于setContentView之前
*
* @param application app
* @param activity target
*/
public static void setDensity(final Application application, Activity activity) {
if (null == application || null == activity) {
return;
}
DisplayMetrics metrics = application.getResources().getDisplayMetrics();
if (0 == appDensity) {
appDensity = metrics.density;
appScaleDensity = metrics.scaledDensity;
/** 监听用户改变字体大小,重新赋值 */
application.registerComponentCallbacks(new ComponentCallbacks() {
public void onConfigurationChanged(@NonNull Configuration newConfig) {
if (newConfig.fontScale > 0) {
appScaleDensity = application.getResources().getDisplayMetrics().scaledDensity;
}
}
public void onLowMemory() {
}
});
}
/**基于标准值进行计算*/
float targetDensity = metrics.widthPixels / STAND_WIDTH;
float targetScaleDensity = targetDensity * (appScaleDensity / appDensity);
int targetDensityDpi = (int) (targetDensity * STAND_DPI);
/**替换目标activity的属性值*/
DisplayMetrics activityMetrics = activity.getResources().getDisplayMetrics();
activityMetrics.density = targetDensity;
activityMetrics.scaledDensity = targetScaleDensity;
activityMetrics.densityDpi = targetDensityDpi;
}
}
----->布局中使用
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv_handler"
android:layout_width="180dp"
android:layout_height="180dp"
android:background="@color/colorAccent"
android:gravity="center"
android:text="Hello World!" />
<TextView
android:layout_width="180dp"
android:layout_height="180dp"
android:layout_below="@+id/tv_handler"
android:layout_toEndOf="@+id/tv_handler"
android:background="@color/colorAccent"
android:gravity="center"
android:text="Hello World!" />
</RelativeLayout>
</layout>
------->
public class MainActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
DensityUtil.setDensity(getApplication(), this);
ActivityMainBinding binding = DataBindingUtil.
setContentView(this, R.layout.activity_main);
setContentView(binding.getRoot());
}
}
因为基于360的标准来定义,180dp则为屏幕的一半,当然基于什么标准要看视觉给予我们什么样的设计稿;显然的是,density运行时修改,所以在布局预览的时候会存在一定的偏差。在实际项目中对多个activity的适配,可以考虑在baseAolication中使用registerActivityLifecycleCallbacks实现ActivityLifecycleCallbacks接口中的方法。
优势与缺点
1.可以看到,修改density并没有明显的缺点,如果应用在老的项目当中,由于布局参数会需要做必要的调整。另外由于不知道第三方库的设计标准,因此也存在一定的问题。但是优势也很明显,比较完美的适配。
最小宽度适配SW
1.安卓中,Android会识别屏幕可用高度和宽度的最小尺寸的dp值(手机的宽度值),然后根据识别到的结果去资源文件中寻找对应限定符的文件夹下的资源文件。假如模拟器的dpi为400,规格为(1080x1920)横向像素为1080px。px=dp(dpi/160),得出dp = 432。系统会去匹配value-sw432dp对应下的资源。如果没有value-sw432dp的文件夹,则会寻找与之大小最接近的文件夹如value-sw420dp。对比与精准大小的适配方案value-1080x1920,这个方案最大的优势在于有备选值。而精准大小匹配这种方案要包括的资源文件太过于多,手机屏幕规格发展太快。容错机制不太友好。
思考对于最小宽度适配,手机为什么会去匹配对应的宽度文件夹?
1.安卓获取资源是分有优先级的:values-mcc310-en-sw320dp-w720dp-h720dp-large-long-port-car-night-ldpi-notouch-keysexposed-nokeys-navexposed-nonav-v7
。匹配是从最高优先级别的属性开始,排除跟系统配置不同的资源文件,而不是优先选择匹配到的属性数量最多的资源文件。
2.如果属性是分辨率,向下读取与它最为接近的适配文件。