前言
首先是今日说法很早就提到的适配方案一种极低成本的Android屏幕适配方式
原理是修改手机屏幕的density和dpi实现所有屏幕的宽度都被强制设置成和设计图上面的宽度一致。这个方案是和在开发中xml布局查看时切换不同的屏幕时效果。
下面可以看下xml布局查看的效果
Nexus4
Nexus5
Pixel 2
可以看到屏幕变了之后,确实只有宽度随着变动了,高度没有变过。
原理分析
一切以转换公式 px = dp * density 为主。
首先我们先设置设计图宽度为360dp
在density为3的屏幕中,实际px为1080 对应上面的Nexus5(1080)
在density为2的屏幕中,实际px为720 对应上面的Nexus4(768)
在density为2.625的屏幕中,实际px为945 对应上面的Pixel 2(1080)
这就导致了如果我们是定值的dp单位会在不同density的屏幕下出现不同的宽度。
那么按照公式,我们如何让一个设计图上面360dp扩展到全部的屏幕呢?
我们将density = px / dp计算出来,得到如下
- 1080 / 360 = 3
- 768 / 360 = 2.1333
- 1080 / 360 = 3
我们可以看到,明明1和3应该要属于同一个density然而实际一个是3一个是2.625,那是因为dpi和屏幕的尺寸有关。
如果我们将所有的屏幕的dpi都设置成符合dp转px的dpi不就可以适配全部的宽度了!
即我们将Nexus4的density 由2改为2.1333,Pixel 2的density 由2.625改为3。
代码分析
首先我们要确定到底是系统的dpi是不是DisplayMetrics的值
在源码TypedValue.applyDimension
中可以看到sp px 等值都是调用了DisplayMetrics的值
/**
* 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;
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;
}
那么按照上面的原理分析,我们就可以开始为了适配宽度进行代码编写了。
我先将我写好的工具类AutoScreenDpUtils贴下
package com.gjn.autoscreenlibrary;
import android.app.Activity;
import android.app.Application;
import android.content.ComponentCallbacks;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.support.annotation.NonNull;
import android.util.DisplayMetrics;
import android.util.Log;
/**
* @author gjn
* @time 2018/8/22 18:26
*/
public class AutoScreenDpUtils {
private static final String TAG = "AutoScreenDpUtils";
private final static String WIDTH_DP = "set_width_dp";
public static boolean isDebug = false;
private static Application mApplication;
private static float w = 0;
private static float oldDensity = -1;
private static int oldDensityDpi = -1;
private static float oldScaledDensity = -1;
private static float newDensity = -1;
private static int newDensityDpi = -1;
private static float newScaledDensity = -1;
public static void init(@NonNull Application application) {
init(application, 0);
}
public static void init(@NonNull Application application, boolean debug) {
init(application, 0, debug);
}
public static void init(@NonNull Application application, float widthDp) {
init(application, widthDp, false);
}
public static void init(@NonNull Application application, float widthDp, boolean debug) {
isDebug = debug;
if (widthDp != 0) {
w = widthDp;
} else {
try {
ApplicationInfo info = application.getPackageManager().getApplicationInfo(application.getPackageName(), PackageManager.GET_META_DATA);
w = info.metaData.getInt(WIDTH_DP, 0);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
}
if (w == 0) {
Log.e(TAG, "w is null.");
return;
}
if (mApplication == null) {
mApplication = application;
}
log("-----------------------------------------------");
log("默认AutoScreenDpUtils.isDebug为false");
initApplication();
log("-----------------------------------------------");
}
private static void initApplication() {
final DisplayMetrics metrics = mApplication.getResources().getDisplayMetrics();
if (oldDensity == -1) {
oldDensity = metrics.density;
newDensity = metrics.widthPixels / w;
}
if (oldDensityDpi == -1) {
oldDensityDpi = metrics.densityDpi;
newDensityDpi = (int) (newDensity * 160);
}
if (oldScaledDensity == -1) {
oldScaledDensity = metrics.scaledDensity;
newScaledDensity = newDensity * (oldScaledDensity / oldDensity);
}
mApplication.registerComponentCallbacks(new ComponentCallbacks() {
@Override
public void onConfigurationChanged(Configuration newConfig) {
if (newConfig != null && newConfig.fontScale > 0) {
log("fontScale is change.");
newScaledDensity = newDensity * newConfig.fontScale;
oldScaledDensity = oldDensity * newConfig.fontScale;
}
}
@Override
public void onLowMemory() {
}
});
log("oldDensity = " + oldDensity);
log("oldDensityDpi = " + oldDensityDpi);
log("oldScaledDensity = " + oldScaledDensity);
log("newDensity => " + metrics.widthPixels + " / " + w + " = " + newDensity);
log("newDensityDpi => " + newDensity + " * 160 = " + newDensityDpi);
log("newScaledDensity => " + newDensity + " * (" + oldScaledDensity + " / " + oldDensity + ") = " + newScaledDensity);
}
public static void setCustomDensity(@NonNull final Activity activity) {
if (mApplication == null) {
Log.e(TAG, "application is null.");
return;
}
changeDensity(activity);
}
private static void changeDensity(@NonNull Activity activity) {
float density;
int densityDpi;
float scaledDensity;
final DisplayMetrics activityMetrics = activity.getResources().getDisplayMetrics();
if (activity instanceof IAutoCancel) {
density = oldDensity;
densityDpi = oldDensityDpi;
scaledDensity = oldScaledDensity;
log("default Density");
}else if (activity instanceof IAutoChange) {
density = activityMetrics.widthPixels / ((IAutoChange) activity).newWidth();
densityDpi = (int) (density * 160);
scaledDensity = densityDpi * (oldScaledDensity / oldDensity);
log("new dp width = " + ((IAutoChange) activity).newWidth());
log("new Density = " + density);
}else {
density = newDensity;
densityDpi = newDensityDpi;
scaledDensity = newScaledDensity;
log("new dp width = " + w);
log("new Density = " + density);
}
activityMetrics.density = density;
activityMetrics.densityDpi = densityDpi;
activityMetrics.scaledDensity = scaledDensity;
}
private static void log(String msg) {
if (isDebug) {
Log.d(TAG, msg);
}
}
}
下面是流程整理
- 保存旧参数
我们先将旧的density
、densityDpi
、scaledDensity
保存下来,随时为了还原适配。
既代码中的
oldDensity = metrics.density;
oldDensityDpi = metrics.densityDpi;
oldScaledDensity = metrics.scaledDensity;
- 生成新参数
由当前的widthPixels
除去设计图的dp宽度生成新的density
然后由新的density
生成新的densityDpi
和新的scaledDensity
既代码中的
newDensity = metrics.widthPixels / w;
newDensityDpi = (int) (newDensity * 160);
newScaledDensity = newDensity * (oldScaledDensity / oldDensity);
这边说下为何要用旧的计算一下,因为正常scaledDensity
是等于density
但是如果用户修改过系统的字体大小。那么他们之间是有差值的,需要先用旧的计算出差值在设置成新的。
- 监听系统修改变化
这边主要是为了监听修改字体后的变化。
既代码中的
mApplication.registerComponentCallbacks(new ComponentCallbacks() {
@Override
public void onConfigurationChanged(Configuration newConfig) {
if (newConfig != null && newConfig.fontScale > 0) {
log("fontScale is change.");
newScaledDensity = newDensity * newConfig.fontScale;
oldScaledDensity = oldDensity * newConfig.fontScale;
}
}
@Override
public void onLowMemory() {
}
});
- 取消适配和单独适配说明
这边写了两个interface用来区别是否适配和单独修改适配。 IAutoCancel
代表取消适配 IAutoChange
代表修改新的适配宽度
- 使用
按照上面的流程,我们有了新的参数和旧的参数。只要在需要调用适配的地方加入AutoScreenDpUtils.setCustomDensity(@NonNull final Activity activity)
就可以实现适配了。
注: 这句话需要写在setContentView
之前
工具类使用
- 依赖
allprojects {
repositories {
google()
jcenter()
maven { url 'https://jitpack.io' }
}
}
dependencies {
implementation 'com.github.Gaojianan2016:AutoScreenDpUtils:1.0.2'
}
- 自定义Application
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
//第二个参数为设计图上面的宽度以dp为单位 第三个参数为显示log
AutoScreenDpUtils.init(this, 384, true);
}
}
或者直接
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
AutoScreenDpUtils.init(this, true);
}
}
在清单文件中加入
<application
.....>
<meta-data android:name="set_width_dp" android:value="384" />
</application>
在Activity设置布局之前加入AutoScreenDpUtils.setCustomDensity(this);
效果
在小米max中
第一个Activity取消了适配
第二个Activity进行适配
以上就是工具类里面的demo使用而已。
资料
骚年你的屏幕适配方式该升级了!-今日头条适配方案
一种极低成本的Android屏幕适配方式