美文网首页
Android 屏幕适配

Android 屏幕适配

作者: 酷酷的Demo | 来源:发表于2019-06-26 22:14 被阅读0次

    传统dp适配方式的缺点

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

    • px = density * dp(dp = px / density);
    • density = dpi / 160;
    • px = dp * (dpi / 160);

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

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

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

    关系图

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

    今日头条屏幕适配

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

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

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

    • DisplayMetrics#density 就是上述的density
    • DisplayMetrics#densityDpi 就是上述的dpi
    • DisplayMetrics#scaledDensity 字体的缩放因子,正常情况下和density相等,但是调节系统字体大小后会改变这个值。如果在系统设置中切换字体,再返回应用,字体并没有变化。于是还得监听下字体切换,调用 Application#registerComponentCallbacks 注册下 onConfigurationChanged 监听即可。

    方案如下:

    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 尺寸了

    相关文章

      网友评论

          本文标题:Android 屏幕适配

          本文链接:https://www.haomeiwen.com/subject/hbxkcctx.html