由于Android碎片化严重,屏幕分辨率千奇百怪,而想要在各种分辨率的设备上显示基本一致的效果,适配成本越来越高。虽然Android官方提供了dp单位来适配,但其在各种奇怪分辨率下表现却不尽如人意,因此下面探索一种简单且低侵入的适配方式。
通常情况下,一部手机的分辨率是宽x高,屏幕大小是以寸为单位,那么三者的关系是:
以华为P7为例,计算其dpi值。先利用勾股定理得其对角线的像素值为2202.91,再除以对角线的大小5,即2202.91/5=440.582;此处计算出的440.58便是该设备的真实屏幕密度dpi。
现在我们再通过代码来获取设备的dpi值
private void getDisplayInfo(){
Resources resources=getResources();
DisplayMetrics displayMetrics = resources.getDisplayMetrics();
float density = displayMetrics.density;
int densityDpi = displayMetrics.densityDpi;
Log.i(TAG, "density = " + density);
Log.i(TAG, "densityDpi = " + densityDpi);
}
输出:
density = 3.0
densityDpi = 480
发现代码中获取到的densityDpi=480和我们计算出来的屏幕实际密度值440.582不一样。因为在每部手机出厂时都会为该手机设置屏幕密度,若其屏幕的实际密度是440dpi那么就会将其屏幕密度设置为与之接近的480dpi;如果实际密度为325dpi那么就会将其屏幕密度设置为与之接近的320dpi。
这也就是说常见的屏幕密度是与每个显示级别的最大值相对应的,比如:120、160、240、320、480、640等。顺便说一下,看到代码中的density么?其实它就是一个倍数关系,它表示当前设备的densityDpi和160的比值,480/160=3倍关系属于xxhdpi。从而逻辑分辨率为640dp * 360dp
其实,关于这一点,我们从Android源码对于densityDpi的注释也可以看到一些端倪:
The screen density expressed as dots-per-inch.
May be either DENSITY_LOW,DENSITY_MEDIUM or DENSITY_HIGH
请注意这里的措辞”May be”,它也没有说一定非要是DENSITY_LOW、DENSITY_MEDIUM、 DENSITY_HIGH这些系统常量。 这就是Android”碎片化”的一个佐证。
假设我们UI设计图是按屏幕宽度为360dp来设计的,如果屏幕宽度为1080/(440/160)=392.7dp,也就是屏幕是比设计图要宽的。这种情况下, 即使使用dp也是无法在不同设备上显示为同样效果的。 同时还存在部分设备屏幕宽度不足360dp,这时就会导致按360dp宽度来开发实际显示不全的情况。
首先来梳理下我们的需求,一般我们设计图都是以固定的尺寸来设计的。比如以分辨率1920px * 1080px来设计,以density为3来标注,也就是屏幕其实是640dp * 360dp。如果我们想在所有设备上显示完全一致,其实是不现实的,因为屏幕高宽比不是固定的,16:9、4:3甚至其他宽高比层出不穷,宽高比不同,显示完全一致就不可能了。但是通常下,我们只需要以宽或高一个维度去适配,比如我们Feed是上下滑动的,只需要保证在所有设备中宽的维度上显示一致即可,再比如一个不支持上下滑动的页面,那么需要保证在高这个维度上都显示一致,尤其不能存在某些设备上显示不全的情况。
因此,总结下大致需求如下:
布局文件中unit转换,最终都是调用 TypedValue#applyDimension(int unit, float value, DisplayMetrics metrics) 来进行转换:
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:
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;
}
各种单位说明:
PT(point)点: 一个专用的印刷单位“点”, 也是一种像素单位
IN: 英寸
MM: 毫米.
根据公式很容易得出 1 IN = 25.4MM = 72PT.
对DIP和SP下手对于老项目不够友好, 只能选择这三个单位. 又会发现这三个单位转换得到像素值的时候都会与metrics.xdpi
有关
xdpi: The exact physical pixels per inch of the screen in the X dimension. 其实说白了就是X横轴方向的dpi.
一般给的图都是以像素为单位的. 例如1920*1080 5寸屏的我们如果有1pt = 1px. 则如果需要120px的宽度, 我们不用想写成120pt就OK了.
要求得的1pt实际对应的px / 屏幕宽度px = 1px / 设计图宽度px
要求得的1pt实际对应的px = 屏幕宽度px / 设计图宽度px
然后
metrics.xdpi * (1.0f/72) = 对于1pt表示的像素
metrics.xdpi = 1*72=72
当前情况下容易得出 xdpi = 72
, 我们还是算出原来的xdpi为440, 也就是大概差了6倍.如果假设1pt = 1px, 在使用过程中发现1pt变现为6px, 也就是突然变大了, 你就知道pt失效导致的.自己去找原因并解决.
final Point size = new Point();
((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getSize(size);
final Resources resources = context.getResources();
resources.getDisplayMetrics().xdpi = size.x / designWidth * 72f;
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PT, value, metrics)
这是因为有一个已知条件
1pt = 1px
则等价于xdpi = 72
因为1334px * 750px
, 则对角线px = 1530.3px = 1530.3pt = 21.25 inch
如果采用1920px*1080px
的屏幕同理啦.
该方案由于不是自己原创, 我偷偷贴个代码, 没人发现吧
package xxx.yyy.zzz;
import java.lang.reflect.Field;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Point;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.WindowManager;
/**
* Created by Caodongyao
* 转载请联系作者并注明出处 http://www.jianshu.com/p/b6b9bd1fba4d
* 使用方法: Application#onCreate中调用一次即可
*/
public class ScreenHelper {
/**
* 重新计算displayMetrics.xhdpi, 使单位pt重定义为设计稿的相对长度
*
* @see #activate()
*
* @param context
* @param designWidth
* 设计稿的宽度
*/
public static void resetDensity(Context context, float designWidth) {
if (context == null) return;
final Point size = new Point();
((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getSize(size);
final Resources resources = context.getResources();
resources.getDisplayMetrics().xdpi = size.x / designWidth * 72f;
DisplayMetrics metrics = getMetricsOnMiui(resources);
if (metrics != null) {
metrics.xdpi = size.x / designWidth * 72f;
}
}
/**
* 恢复displayMetrics为系统原生状态,单位pt恢复为长度单位磅
*
* @see #inactivate()
*
* @param context
*/
public static void restoreDensity(Context context) {
context.getResources().getDisplayMetrics().setToDefaults();
DisplayMetrics metrics = getMetricsOnMiui(context.getResources());
if (metrics != null)
metrics.setToDefaults();
}
// 解决MIUI更改框架导致的MIUI7+Android5.1.1上出现的失效问题(以及极少数基于这部分miui去掉art然后置入xposed的手机)
private static DisplayMetrics getMetricsOnMiui(Resources resources) {
if ("MiuiResources".equals(resources.getClass().getSimpleName())
|| "XResources".equals(resources.getClass().getSimpleName())) {
try {
Field field = Resources.class.getDeclaredField("mTmpMetrics");
field.setAccessible(true);
return (DisplayMetrics) field.get(resources);
} catch (Exception e) {
return null;
}
}
return null;
}
private Application.ActivityLifecycleCallbacks mActivityLifecycleCallbacks;
private Application mApplication;
private float mDesignWidth;
/**
*
* @param application
* application
* @param width
* 设计稿宽度
*/
public ScreenHelper(Application application, float width) {
mApplication = application;
mDesignWidth = width;
mActivityLifecycleCallbacks = new Application.ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
// 通常情况下application与activity得到的resource虽然不是一个实例,但是displayMetrics是同一个实例,只需调用一次即可
// 为了面对一些不可预计的情况以及向上兼容,分别调用一次较为保险
resetDensity(mApplication, mDesignWidth);
resetDensity(activity, mDesignWidth);
}
@Override
public void onActivityStarted(Activity activity) {
resetDensity(mApplication, mDesignWidth);
resetDensity(activity, mDesignWidth);
}
@Override
public void onActivityResumed(Activity activity) {
resetDensity(mApplication, mDesignWidth);
resetDensity(activity, mDesignWidth);
}
@Override
public void onActivityPaused(Activity activity) {
}
@Override
public void onActivityStopped(Activity activity) {
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
}
@Override
public void onActivityDestroyed(Activity activity) {
}
};
}
/**
* 激活本方案
*/
public void activate() {
resetDensity(mApplication, mDesignWidth);
mApplication.registerActivityLifecycleCallbacks(mActivityLifecycleCallbacks);
}
/**
* 恢复系统原生方案
*/
public void inactivate() {
restoreDensity(mApplication);
mApplication.unregisterActivityLifecycleCallbacks(mActivityLifecycleCallbacks);
}
/**
* 转换pt为px
* @param context context
* @param value 需要转换的pt值,若context.resources.displayMetrics经过resetDensity()的修改则得到修正的相对长度,否则得到原生的磅
* @return px值
*/
public static float pt2px(Context context, float value){
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PT, value, context.getResources().getDisplayMetrics());
}
}
若存在webview导致适配失效的问题
可以先继承WebView并重写setOverScrollMode(int mode)
方法,在方法中调用super之后调用一遍ScreenHelper.resetDensity(getContext(), designWidth)
规避
若存在dialog中适配失效的问题
可以在dialog的oncreate中调用一遍ScreenHelper.resetDensity(getContext(), designWidth)
规避
旋转屏幕之后适配失效
可以在onConfigurationChanged中调用ScreenHelper .resetDensity(getContext(), designWidth)
规避
特定国产机型ROM中偶先fragment失效
可以在fragment的onCreateView中调用ScreenHelper .resetDensity(getContext(), designWidth)
规避
1920px*1080px
16:9的屏幕,一般而言可以做到手机和Pad通吃,如果你们公司遵循"更大的屏幕显示更多的内容", 可以和美工协商规划.