今日头条屏幕适配方案
原理
1.无论我们在xml中使用何种尺寸单位(dp、sp、pt......),最后在绘制时都会给我们转成px
2.我们选定一种尺寸单位(dp、sp、pt ......)作为我们的适配单位,然后篡改这个单位与px之间的转化比例,然后在xml中使用选定的单位做适配
原理解析
px值 = dp值 * metrics.density
解释:当前设备屏幕总像素/ 设计图总宽度(单位为 dp) = density
density 的意思就是 1dp 占当前设备多少像素
常规的density 在每个设备上都是固定的,DPI / 160 = density,屏幕的总 px 宽度 / density = 屏幕的总 dp 宽度
举个例子:
设备 1,屏幕宽度为 1080px,480DPI,屏幕总 dp 宽度为 1080 / (480 / 160) = 360dp
设备 2,屏幕宽度为 1440,560DPI,屏幕总 dp 宽度为 1440 / (560 / 160) = 411dp
屏幕的总 dp 宽度在不同的设备上是会变化的,但是我们在布局中填写的 dp 值却是固定不变的
这会导致什么呢?假设我们布局中有一个 View 的宽度为 100dp,在设备 1 中 该 View 的宽度占整个屏幕宽度的 27.8% (100 / 360 = 0.278)
但在设备 2 中该 View 的宽度就只能占整个屏幕宽度的 24.3% (100 / 411 = 0.243),可以看到这个 View 在像素越高的屏幕上,dp 值虽然没变,但是与屏幕的实际比例却发生了较大的变化,所以肉眼的观看效果,会越来越小,这就导致了传统的填写 dp 的屏幕适配方式产生了较大的误差
这时我们要想完美适配,那就必须保证这个 View 在任何分辨率的屏幕上,与屏幕的比例都是相同的
1.改变每个 View 的 dp 值?不现实,在每个设备上都要通过代码动态计算 View 的 dp 值,工作量太大——————pass
2.每个 View 的 dp 值是固定不变的,那我们只要保证每个设备的屏幕总 dp 宽度不变,就能保证每个 View 在所有分辨率的屏幕上与屏幕的比例都保持不变,从而完成等比例适配,并且这个屏幕总 dp 宽度如果还能保证和设计图的宽度一致的话,那我们在布局时就可以直接按照设计图上的尺寸填写 dp 值
屏幕的总 px 宽度 / density = 屏幕的总 dp 宽度
这个公式就是把上面公式中的 屏幕的总 dp 宽度 换成 设计图总宽度,原理都是一样的,只要 density 根据不同的设备进行实时计算并作出改变
density是指的手机的屏幕密度,由系统提供,不同的手机的density可能不同;所以我们不能直接使用系统的density,需要篡改density来达到适配的目的
方案实现
一般只对宽度适配,毕竟高度可以滑动解决!所以方案中给出的是用dp适配宽度
建议
选用dp作为适配单位,给出的理由是项目中大部分都是使用dp做单位
即使适配方案万一失效了呢,咋办?选用dp至少很大程度可以防止出现页面显示不全,显示效果太差的问题! 侵入性低
优点
1.在页面布局时不需要额外的代码和操作
2.侵入性非常低,该方案和项目完全解耦,在项目布局时不会依赖哪怕一行该方案的代码,而且使用的还是 Android 官方的 API,意味着当你遇到什么问题无法解决,想切换为其他屏幕适配方案时,基本不需要更改之前的代码
3.可适配三方库的控件和系统的控件(不止是 Activity 和 Fragment,Dialog、Toast 等所有系统控件都可以适配),由于修改的 density 在整个项目中是全局
4.不会有任何性能的损耗
缺点
当某个系统控件或三方库控件的设计图尺寸和和我们项目自身的设计图尺寸差距非常大时,这个问题就越严重
具体的就是比如第三方库的设计图是给的640dp的,而自身项目给的是320dp,当一个View 64dp宽的时候,第三方中就是10% ,而在自身项目中就是20% ,相当于差距比较大的时候,就存在问题
处理方式就是针对不同的activity 去做特殊适配就可以
代码
首先要在Application初始化,针对封装的可以在BaseActivity中初始化,放在onCreate()之前,也可以单个activity不适配,看具体注释
/**
* 通过修改系统参数来适配android设备
*/
public class DensityUtils {
private static float appDensity;
private static float appScaledDensity;
private static DisplayMetrics appDisplayMetrics;
private static int barHeight;
public final static String WIDTH = "width";
public final static String HEIGHT = "height";
/**
* 在Application里初始化一下
* @param application
*/
public static void setDensity(@NonNull final Application application) {
//获取application的DisplayMetrics
appDisplayMetrics = application.getResources().getDisplayMetrics();
//获取状态栏高度
barHeight = getStatusBarHeight(application);
if (appDensity == 0) {
//初始化的时候赋值
appDensity = appDisplayMetrics.density;
appScaledDensity = appDisplayMetrics.scaledDensity;
//添加字体变化的监听
application.registerComponentCallbacks(new ComponentCallbacks() {
@Override
public void onConfigurationChanged(Configuration newConfig) {
//字体改变后,将appScaledDensity重新赋值
if (newConfig != null && newConfig.fontScale > 0) {
appScaledDensity = application.getResources().getDisplayMetrics().scaledDensity;
}
}
@Override
public void onLowMemory() {
}
});
}
}
/**
* 此方法在BaseActivity中做初始化(如果不封装BaseActivity的话,直接用下面那个方法就好)
* 在setContentView()之前设置
* @param activity
*/
public static void setDefault(Activity activity) {
setAppOrientation(activity, WIDTH);
}
/**
* 此方法用于在某一个Activity里面更改适配的方向
* 在setContentView()之前设置
* @param activity
* @param orientation
*/
public static void setOrientation(Activity activity, String orientation) {
setAppOrientation(activity, orientation);
}
/**
* targetDensity
* targetScaledDensity
* targetDensityDpi
* 这三个参数是统一修改过后的值
* orientation:方向值,传入width或height
*/
private static void setAppOrientation(@Nullable Activity activity, String orientation) {
float targetDensity;
if (orientation.equals("height")) {
targetDensity = (appDisplayMetrics.heightPixels - barHeight) / 667f;//设计图的高度 单位:dp
} else {
targetDensity = appDisplayMetrics.widthPixels / 360f;//设计图的宽度 单位:dp
}
float targetScaledDensity = targetDensity * (appScaledDensity / appDensity);
int targetDensityDpi = (int) (160 * targetDensity);
/**
*
* 最后在这里将修改过后的值赋给系统参数
* 只修改Activity的density值
*/
DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics();
activityDisplayMetrics.density = targetDensity;
activityDisplayMetrics.scaledDensity = targetScaledDensity;
activityDisplayMetrics.densityDpi = targetDensityDpi;
}
/**
* 获取状态栏高度
*
* @param context
* @return
*/
public static int getStatusBarHeight(Context context) {
int result = 0;
int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
result = context.getResources().getDimensionPixelSize(resourceId);
}
return result;
}
}