Android 屏幕适配

2019-06-26  本文已影响0人  酷酷的Demo

传统dp适配方式的缺点

android中的dp在渲染前会将dp转为px,计算公式:

而dpi是根据屏幕真实的分辨率和尺寸来计算的,每个设备都可能不一样的

屏幕尺寸、分辨率、像素密度三者关系

通常情况下,一部手机的分辨率是宽x高,屏幕大小是以寸为单位,那么三者的关系是:

关系图

举个例子:屏幕分辨率为:1920*1080,屏幕尺寸为5吋的话,那么dpi为440。

今日头条屏幕适配

根据dp和px的转换公式 :px = dp * density,想要保证在所有设备计算得出的px值都正好是屏幕宽度的话,我们只能修改 density 的值。

通过阅读源码,我们可以得知,density 是 DisplayMetrics 中的成员变量,而 DisplayMetrics 实例通过 Resources#getDisplayMetrics 可以获得,而Resouces通过Activity或者Application的Context获得。

先来熟悉下 DisplayMetrics 中和适配相关的几个变量:

方案如下:

private static float sNoncompatDensity;
private static float sNoncompatScaledDensity;

private static void setCustomDensity(@NonNull Activity activity,@NonNull final Application application){
    final DisplayDensity appDisplayMetrics = application.getResource().getDisplayMetrics();
    
    if(sNoncompatDensity == 0){
        sNoncompatDensity = appDisplayMetrics.density;
        sNoncompatScaledDensity = appDisplayMetrics.scaledDensity;
        application.registerComponentCallbacks(new ComponentCallbacks(){
            @Overried
            public void onConfigurationChanged(Configuration newConfig){
                if(newConfig != null && newConfig.fontScale > 0){
                   sNoncompatScaledDensity = application.getResources().scaledDensity; 
                }
            }
            @Overried
            public void onLowMemory(){
            }
          });
    }
    final float targetDensity = appDisplayMetrics / 360;
    final float targetScaledDensity = targetDensity * (sNoncompatScaledDensity / sNoncompatDensity); 
    final int targetDensityDpi = (int)(160 * targetDensity);
    
    appDisplayMetrics.density = targetDensity;
    appDisplayMetrics.scaledDensity = targetScaledDensity;
    appDisplayMetrics.densityDpi = targetDensityDpi;
    
    final DisplayMetrics activityDisplayDetrics = activity.getResources().getDisplayMetrics();
    activityDisplayDetrics.density = targetDensity;
    activityDisplayDetrics.scaledDensity = targetScaledDensity;
    activityDisplayDetrics.densityDpi = targetDensityDpi;
}

由于 API 26 及以上的 Activity#getResources()#getDisplayMetrics() 和 Application#getResources()#getDisplayMetrics() 是不同的引用,所以在 API 26 及以上适配是没有影响的,但在 API 26 以下 Activity#getResources()#getDisplayMetrics() 和 Application#getResources()#getDisplayMetrics() 是相同的引用,导致适配有问题。
如果我们以 xxhdpi 的 360dp 来适配的话,首先在 AS 中预览是个问题,在接入第三方 SDK 带有界面或者 View 的话会导致它的尺寸全然不对,因为我们那样适配后界面宽度只有 360dp,而第三方 SDK 中很有可能写的布局会超出 360dp,这便会引发新的问题,当然这也是有响应的解决之道,比如暂时取消适配,但我们有更好的方式,着重看下面介绍。

我着重推荐以 mdpi 为特例来适配,比如前面说到的 xxhdpi 的 360dp,那么在 mdpi 下就是 360 * 3 = 1080dp,这样我们新建一个宽为 1080px 的 mdpi 设备(可以通过修改设备尺寸来达到 mdpi),然后切换为该设备来预览布局就完美解决了以上问题,我们在写布局的时候设计图是 36px,那么我们直接就写 36dp 即可,设计图字体是 24px, 我们直接就写 24sp 即可,这样便可达到和设计图一致的效果。另外,图片资源放在需要适配的最高 dpi 下面即可,比如 drawable-xxhdpi 或者 drawable-xxxhdpi,这样在高清屏上也不会导致失真。

但是这样会导致获取状态栏和导航栏高度有问题,其获取状态栏高度代码为如下所示:

public static int getStatusBarHeight() {
    Resources resources = Application.getResources();
    int resourceId = resources.getIdentifier("status_bar_height", "dimen", "android");
    return resources.getDimensionPixelSize(resourceId);
}

由于使用的是 Application#getResources,这会导致最后计算状态栏高度使用的是修改过后的 density,通过Resources.getSystem() 来获取系统的 Resources,果不其然可以获取到正确高度的状态栏高度,代码如下所示:

public static int getStatusBarHeight() {
    Resources resources = Resources.getSystem();
    int resourceId = resources.getIdentifier("status_bar_height", "dimen", "android");
    return resources.getDimensionPixelSize(resourceId);
}

考虑到了 Resources.getSystem(),那么我们在适配上岂不是可以更方便,不用区分版本什么的 Activity#getResources()#getDisplayMetrics() 和 Application#getResources()#getDisplayMetrics(),也不需要什么中间变量来记录适配前的值,那些值我们直接在 Resources#getSystem()#getDisplayMetrics() 中获取 density、densityDpi、scaledDensity 即可,而且在修改系统字体的时候,Resources#getSystem()#getDisplayMetrics() 也会相应地改变,这样也就不需要注册 registerComponentCallbacks 来监听系统字体的改变,所以最终的源码很是简洁

最终方案:

/**
 * Adapt the screen for vertical slide.
 *
 * @param activity        The activity.
 * @param designWidthInPx The size of design diagram's width, in pixel.
 */
public static void adaptScreen4VerticalSlide(final Activity activity,
                                             final int designWidthInPx) {
    adaptScreen(activity, designWidthInPx, true);
}
/**
 * Adapt the screen for horizontal slide.
 *
 * @param activity         The activity.
 * @param designHeightInPx The size of design diagram's height, in pixel.
 */
public static void adaptScreen4HorizontalSlide(final Activity activity,
                                               final int designHeightInPx) {
    adaptScreen(activity, designHeightInPx, false);
}


private static void adaptScreen(final Activity activity,
                                final int sizeInPx,
                                final boolean isVerticalSlide) {
    final DisplayMetrics systemDm = Resources.getSystem().getDisplayMetrics();
    final DisplayMetrics appDm = Utils.getApp().getResources().getDisplayMetrics();
    final DisplayMetrics activityDm = activity.getResources().getDisplayMetrics();
    if (isVerticalSlide) {
        activityDm.density = activityDm.widthPixels / (float) sizeInPx;
    } else {
        activityDm.density = activityDm.heightPixels / (float) sizeInPx;
    }
    activityDm.scaledDensity = activityDm.density * (systemDm.scaledDensity / systemDm.dens
    activityDm.densityDpi = (int) (160 * activityDm.density);
    appDm.density = activityDm.density;
    appDm.scaledDensity = activityDm.scaledDensity;
    appDm.densityDpi = activityDm.densityDpi;
}
/**
 * Cancel adapt the screen.
 *
 * @param activity The activity.
 */
public static void cancelAdaptScreen(final Activity activity) {
    final DisplayMetrics systemDm = Resources.getSystem().getDisplayMetrics();
    final DisplayMetrics appDm = Utils.getApp().getResources().getDisplayMetrics();
    final DisplayMetrics activityDm = activity.getResources().getDisplayMetrics();
    activityDm.density = systemDm.density;
    activityDm.scaledDensity = systemDm.scaledDensity;
    activityDm.densityDpi = systemDm.densityDpi;
    appDm.density = systemDm.density;
    appDm.scaledDensity = systemDm.scaledDensity;
    appDm.densityDpi = systemDm.densityDpi;
}
/**
 * Return whether adapt screen.
 *
 * @return {@code true}: yes<br>{@code false}: no
 */
public static boolean isAdaptScreen() {
    final DisplayMetrics systemDm = Resources.getSystem().getDisplayMetrics();
    final DisplayMetrics appDm = Utils.getApp().getResources().getDisplayMetrics();
    return systemDm.density != appDm.density;
}

建议

新老项目都可以用这套方案,老项目中如果有新的 Activity 加进来,那么可以对其使用该方案来适配,然后在启动其他老的 Activity 时候 cancelAdaptScreen 即可。新项目我建议采用我工具类中的使用,可以让你爽到极致,在 BaseActivity 中 setContentView(xx) 之前调用适配代码即可,记得第二个参数一定要传入设计图的实际像素尺寸,不再是曾经的 dp 尺寸了

上一篇下一篇

猜你喜欢

热点阅读