对于 android 屏幕适配大家可能比较头疼,因为android 设备碎片化太严重。
大家可能使用过的适配方案有:
1. 百分比布局。
2. 针对不同分辨率或最小宽度生成不同dimens.xml。
3. 鸿神的AutoLayout动态换算等适配方案。
但是这些方案或多或少都有些问题,所以自己写了一种适配方案,感觉效果不错,可以较完美兼容各种pad、手机,在系统版本为8.1的设备上也测试通过,就拿出来分享了。
接下来先给大家展示下某款开源应用仿「单读」App在使用本方案适配前后的效果对比(为了效果明显直接拿 pad 和手机进行对比,共4套图):
适配前
适配后
看到效果后是不是对本方案产生了兴趣?做到这种程度的适配我也仅仅是在 Application 中加了一行代码而已:
ScreenUtil.adaptDensity(this, 375, 667, true, ScreenUtil.MODE_FORCE_ADAPT_LONG_SIDE);
参数依次是:
1. Application 实例。
2. 设计稿短边尺寸 单位是 dp。(单位是其他的换成 dp)
3. 设计稿长边尺寸 单位同上。
4. 是否把字体大小设置为系统默认。(忽略用户在系统设置里面的字体大小设置)。
5. 适配模式3种。(依照自己项目选择)
- MODE_ADAPT_TWO_SIDE 适配兼顾宽高;
- MODE_FORCE_ADAPT_SHORT_SIDE 强制适配短边;
- MODE_FORCE_ADAPT_LONG_SIDE 强制适配长边;
使用须知:
- 此方案基于screen density实现,项目里布局要使用 dp 和 sp。(dp、sp 是 android 上最佳尺寸单位!)
- 若是已完工的项目有适配需求,布局使用的单位却不是 dp、sp,需要先全局修改,可以 写脚本使用正则方式替换。
- 此方案只适用于minSdkVersion>=17的应用。
- 注意没有完美适配这一说,因为手机屏幕比例不尽相同,有16:9、16:10、18:9、18.5:9等。
- 获取系统元素宽高有变化,可参考ScreenUtil 中获取状态栏高度 getStatusHeight() 及 recoverToSystemValueIfNeed()。
- 方案中提到的【长边】及【短边】是为了兼容横竖屏应用的说法。
- 不能保证完全没有兼容性问题,遇到请反馈给我。(将 pad 应用移植到手机上我遇到了1.弹窗样式Activity显示不全、Dialog 不居中或显示不全的问题,但对项目代码简单修改也正常了。)
- 此适配是缩放的方式,若要对pad屏幕充分利用,还需要单独设计布局。
这里贴下 ScreenUtil ,快拿去试试吧:
下篇博文会讲解下实现原理。
package com.***.library.utils;
import android.app.Activity;
import android.app.Application;
import android.content.ComponentCallbacks;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.webkit.WebView;
public class ScreenUtil {
public static final int MODE_ADAPT_TWO_SIDE = 1;
public static final int MODE_FORCE_ADAPT_SHORT_SIDE = 2;
public static final int MODE_FORCE_ADAPT_LONG_SIDE = 3;
private static int screenHeightWithPx = 0;
private static int screenWidthWithPx = 0;
private static float systemDensityRatio = 0;
private static float adaptDensityRatio = 0;
private static DisplayMetrics displayMetrics;
/**
* 获取屏幕宽度
*
* @return
*/
public static int getWidth() {
return screenWidthWithPx;
}
/**
* 获取屏幕高度
*
* @return
*/
public static int getHeight() {
return screenHeightWithPx;
}
private static boolean webViewHasDestroyed = false;
/**
* 最小宽度适配 phone、tablet、tv;
* 1.实现了与设计稿不同比例设备的兼容; e.g. 设计比例:16:9 设备比例 4:3 按照16:9显示 但是直接造成缺陷:不能充分使用屏幕像素;
* 2.支持minSdkVersion>=17;
*
* @param application {@link android.app.Application or it's subClass}
*/
public static void adaptDensity(final Application application, final int shortSideLengthWidthDp, final int longSideLengthWithDp, final boolean isSetFontSizeToDefault, final int adaptMode) {
application.registerComponentCallbacks(new ComponentCallbacks() {
@Override
public void onConfigurationChanged(Configuration config) {
Resources resources = application.getResources();
updateConfig(application, resources, isSetFontSizeToDefault);
}
@Override
public void onLowMemory() {
}
});
if (Build.VERSION.SDK_INT >= 26) {
application.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
Resources resources = activity.getResources();
updateConfig(activity, resources, isSetFontSizeToDefault);
}
@Override
public void onActivityStarted(Activity activity) {
}
@Override
public void onActivityResumed(Activity activity) {
}
@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) {
}
});
}
//设计稿宽高比
float aspectRatio = longSideLengthWithDp / (float) shortSideLengthWidthDp;
Resources resources = application.getResources();
Configuration configuration = resources.getConfiguration();
displayMetrics = new DisplayMetrics();
WindowManager windowManager = (WindowManager) application.getSystemService(Context.WINDOW_SERVICE);
windowManager.getDefaultDisplay().getRealMetrics(displayMetrics);
//屏幕物理高 px
screenHeightWithPx = displayMetrics.heightPixels;
//屏幕物理宽 px
screenWidthWithPx = displayMetrics.widthPixels;
Logger.d("adaptDensity", "screenHeightWithPx =" + screenHeightWithPx);
Logger.d("adaptDensity", "screenWidthWithPx =" + screenWidthWithPx);
//屏幕最小宽度 px
int screenSwWithPx;
//屏幕长边 px
int screenLongSideLengthWithPx;
if (screenHeightWithPx > screenWidthWithPx) {
screenSwWithPx = screenWidthWithPx;
screenLongSideLengthWithPx = screenHeightWithPx;
} else {
screenSwWithPx = screenHeightWithPx;
screenLongSideLengthWithPx = screenWidthWithPx;
}
Logger.d("adaptDensity", "screenSwWithPx =" + screenSwWithPx);
Logger.d("adaptDensity", "screenLongSideLengthWithPx =" + screenLongSideLengthWithPx);
//屏幕最小宽度 dp
int screenSwWithDp = configuration.smallestScreenWidthDp;
//屏幕密度比
systemDensityRatio = displayMetrics.density;
Logger.d("adaptDensity", "screenSwWithDp =" + screenSwWithDp);
Logger.d("adaptDensity", "systemDensityRatio =" + systemDensityRatio);
int adaptSwWithDp;
int adaptLongSideLengthWithDp;
adaptSwWithDp = screenSwWithDp;
adaptLongSideLengthWithDp = (int) (adaptSwWithDp * aspectRatio);
Logger.d("adaptDensity", "adaptSwWithDp =" + adaptSwWithDp);
Logger.d("adaptDensity", "adaptLongSideLengthWithDp =" + adaptLongSideLengthWithDp);
if (adaptMode == MODE_ADAPT_TWO_SIDE) {
float ratio1 = systemDensityRatio;
float ratio2 = screenLongSideLengthWithPx / (float) adaptLongSideLengthWithDp;
adaptDensityRatio = ratio1 > ratio2 ? ratio2 : ratio1;
} else if (adaptMode == MODE_FORCE_ADAPT_SHORT_SIDE) {
adaptDensityRatio = systemDensityRatio;
} else if (adaptMode == MODE_FORCE_ADAPT_LONG_SIDE) {
adaptDensityRatio = screenLongSideLengthWithPx / (float) adaptLongSideLengthWithDp;
}
adaptDensityRatio *= screenSwWithDp / (float) shortSideLengthWidthDp;
Logger.d("adaptDensity", "update systemDensityRatio " + systemDensityRatio + " ==> " + adaptDensityRatio);
updateConfig(application, resources, isSetFontSizeToDefault);
}
private static void destroyWebView(Context context) {
try {//Caused by android.webkit.WebViewFactory$MissingWebViewPackageException
new WebView(context).destroy();//see https://stackoverflow.com/questions/40398528/android-webview-language-changes-abruptly-on-android-n
webViewHasDestroyed = true;
} catch (Exception e) {
e.printStackTrace();
}
}
private static void updateConfig(Context context, Resources resources, boolean isSetFontSizeToDefault) {
if (!webViewHasDestroyed) {
destroyWebView(context);
}
Configuration newConfig = resources.getConfiguration();
newConfig.densityDpi = (int) (adaptDensityRatio * DisplayMetrics.DENSITY_DEFAULT);
if (isSetFontSizeToDefault) {
newConfig.fontScale = 1;
}
resources.updateConfiguration(newConfig, displayMetrics);
}
/**
* dip2px
*
* @param context
* @param dpValue
* @return
*/
public static int dip2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
/**
* px2dip
*
* @param context
* @return
*/
public static int px2dip(Context context, float pxValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (pxValue / scale + 0.5f);
}
/**
* px2sp
*
* @param context
* @return
*/
public static int px2sp(Context context, float pxValue) {
final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (pxValue / fontScale + 0.5f);
}
/**
* sp2px
*
* @param spValue
* @return
*/
public static int sp2px(Context context, float spValue) {
final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (spValue * fontScale + 0.5f);
}
/**
* 获得状态栏的高度
*
* @param context
* @return
*/
public static int getStatusHeight(Context context) {
int statusHeight = -1;
try {
Class clazz = Class.forName("com.android.internal.R$dimen");
int height = Integer.parseInt(clazz.getField("status_bar_height")
.get(null).toString());
statusHeight = context.getResources().getDimensionPixelSize(height);
statusHeight = recoverToSystemValueIfNeed(statusHeight);
} catch (Exception e) {
e.printStackTrace();
}
return statusHeight;
}
private static int recoverToSystemValueIfNeed(int valueWithPx) {
if (systemDensityRatio != adaptDensityRatio) {
valueWithPx = (int) (valueWithPx / adaptDensityRatio * systemDensityRatio);
}
return valueWithPx;
}
}