一、背景
面对市场上五花八门的屏幕和分辨率,屏幕适配一直是android开发中让人头疼的问题。dp和res不能满足所有的适配需求,需要一种简单又方便的适配方案,可以弥补dp和res的不足。
二、方案
1、dp,看起来都一样大。
与px不同,dp是针对屏幕像素密度的一个单位,在小屏幕上1dp=1px,在大手机上1dp=5px,如果一个控件用dp单位来指定宽高的话,那么这个控件在不同手机上的大小都类似。
字不重要,看图:
以上是350dp的一个TextView在480*800,1080*1920上的效果图。这个TextView看起来是同样大小的,但是显示在屏幕上的效果完全不一样。对于要求所有手机屏幕显示效果一致的情况,显然不能满足需求。
2、res
res是通过设置不同的res,来达到适配效果。配合dp使用,可以适配大多数手机。但是缺点也十分明显,对于区分不同分辨率的粒度该怎么区分,并且使用起来也非常麻烦。
3、等比适配
通过阅读源码我们可以看到view是在onMeaure中通过LayoutParams属性来确定大小,在generateLayoutParams中获取LayoutParams对象。
源码调用过程:略。。。
所以,我们可以在这里截取到xml文件中的属性,根据我们针对屏幕计算出来的缩放比进行计算,把原来的LayoutParams换成我们自定义的LayoutParams。上代码:
//开始完成初始化工作(应该在app第一次安装的时候记录下来)
private
UIUtils(Context context) {
this
.context = context;
@SuppressLint
(
"ServiceCast"
) WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics displayMetrics =
new
DisplayMetrics();
if
(displayMetricsHeight ==
0
.0f || displayMetricsWidth ==
0
.0f) {
windowManager.getDefaultDisplay().getMetrics(displayMetrics);
}
//获取状态栏的高度
int
systemBarHeight = getSystemBarHeight(context);
//为了适配平板和手机
if
(displayMetrics.widthPixels > displayMetrics.heightPixels) {
//平板
}
else
{
//手机
this
.displayMetricsWidth = displayMetrics.widthPixels;
this
.displayMetricsHeight = displayMetrics.heightPixels - systemBarHeight;
}
}
//一般手机状态栏高度都是48dp,如果没有取到状态栏高度,就返回48
private
int
getSystemBarHeight(Context context) {
return
getValue(context, DIMEN_CLASS,
"system_bar_height"
,
48
);
}
private
int
getValue(Context context, String attrCroupClass, String system,
int
i) {
try
{
Class e = Class.forName(attrCroupClass);
Object object = e.newInstance();
Field field = e.getField(system);
int
x = Integer.parseInt(field.get(object).toString());
return
(
int
) context.getResources().getDimensionPixelOffset(x);
}
catch
(ClassNotFoundException e1) {
e1.printStackTrace();
return
i;
}
catch
(IllegalAccessException e) {
e.printStackTrace();
return
i;
}
catch
(InstantiationException e) {
e.printStackTrace();
return
i;
}
catch
(NoSuchFieldException e) {
e.printStackTrace();
return
i;
}
|
这段代码主要的作用就是获取当前手机与UI图上的缩放比。
下面是核心功能代码:
@Override
protected
void
onMeasure(
int
widthMeasureSpec,
int
heightMeasureSpec) {
//先拿到View的个数
int
count =
this
.getChildCount();
//获取缩放比
float
scalcX = UIUtils.getUIUtils(getContext()).getHorizontalScaleValue();
float
scalcY = UIUtils.getUIUtils(getContext()).getVeticalScaleValue();
//获取到所有控件上的属性
if
(flag) {
flag =
false
;
for
(
int
i =
0
; i < count; i++) {
View child =
this
.getChildAt(i);
//获取到布局参数
LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
//开始完成缩放
layoutParams.width = (
int
) (layoutParams.width * scalcX);
layoutParams.height = (
int
) (layoutParams.height * scalcY);
layoutParams.leftMargin = (
int
) (layoutParams.leftMargin * scalcX);
layoutParams.rightMargin = (
int
) (layoutParams.rightMargin * scalcX);
layoutParams.topMargin = (
int
) (layoutParams.topMargin * scalcY);
layoutParams.bottomMargin = (
int
) (layoutParams.bottomMargin * scalcY);
}
}
super
.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
//帮助我们把自定义属性封装起来
@Override
public
RelativeLayout.LayoutParams generateLayoutParams(AttributeSet attrs) {
return
new
LayoutParams(getContext(),attrs);
}
|
上面的flag是针对Measure会针对宽高会执行两次onMeasure方法,避免重复计算,导致比例失调。
以上就是全部核心代码,只需要事先在app中设置好UI图对应的手机分辨率,然后在xml文件中采用MyLayout就可以了,view控件的宽高直接按UI图上给的px直接设置上去就好。
以下是宽高皆为350px的TextView在480*800和1080*1920的屏幕上的显示效果。
这样显示效果就一样了,不需要再对px→dp进行计算。
此外,这种方式还有一个附带的属性:我们不止可以截取源生属性换成我们自己的属性,还可以在xml文件中设置自己的属性,在这里截取换成源生属性,例如:我需要子控件执行一个动画效果,我们只需在对应的控件上加上自定义属性,然后在MyLayout中解析,翻译成对应的动画效果,这个有时间我会再做一个dome。
三、总结
以上三种适配方案各有各的优点,我们结合实际情况做相应的取舍。
总的来说
- 如果需要在不同设备上显示的大小要几乎一样,就用dp(通常是内容显示类的View)。
- 如果要求在不同设备上显示的比例要一样,就用自定义Layout的方式(通常是动画一类的View)。
- 如果PM有特别的要求就要根据不同分辨率指定不同的res文件。