Android屏幕适配方案

栏目: Android · 发布时间: 5年前

内容简介:android设备各种各样,手机、pad、电视、车载等不一而足。即使是相同分辨率的手机也可能参数不一致,比如1080P的手机 dpi 一般认为是480,但是 Google 的Pixel2(1920*1080)的 dpi 是420。此外,android设备的宽高比更是多种多样。这就导致App适配的工作异常困难。尤其是你的app要适配各种平台,比如手机、pad、车载、电视。在这种情形下,你面临的问题让你无所适从,因为你根本猜不到设备的参数和尺寸,更别提如何适配。android度量计算公式具体的含义自行搜索,d

android设备各种各样,手机、pad、电视、车载等不一而足。即使是相同分辨率的手机也可能参数不一致,比如1080P的手机 dpi 一般认为是480,但是 Google 的Pixel2(1920*1080)的 dpi 是420。此外,android设备的宽高比更是多种多样。这就导致App适配的工作异常困难。尤其是你的app要适配各种平台,比如手机、pad、车载、电视。在这种情形下,你面临的问题让你无所适从,因为你根本猜不到设备的参数和尺寸,更别提如何适配。

相关知识

android度量计算公式

  • px = density * dp
  • density = dpi / 160
  • px = dp * (dpi / 160)
  • DisplayMetrics.density
  • DisplayMetrics.densityDpi
  • DisplayMetrics.scaledDensity

具体的含义自行搜索,density 的差异导致适配困难;scaledDensity 是字体的缩放因子,scaledDensity 正常情况下和 density 相等,但是调节系统字体大小后会改变这个值。

查看源码,可以得知:DisplayMetrics 实例通过 Resources#getDisplayMetrics可以获得,而Resouces通过 Activity 或者 Application 的 Context 获得。

dp 和 px 的转换是通过 DisplayMetrics 中相关的值来计算的,view、bitmap 等元素在计算中的dp转换也是如此。

布局文件中 dp 的转换,最终都是调用 TypedValue#applyDimension(int unit, float value, DisplayMetrics metrics) 来进行转换。类似的,BitmapFactory#decodeResourceStream 方法也会应用 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;
    }
复制代码

今日头条方案

原理

  • density = px / dp

适配方案

  • 给定一个宽高大小固定的标准设计图,支持以宽或高一个维度自适应适配,保持改维度和设计图一致;
  • 支持dp和sp单位。

实现

修改application和activity的density,系统修改字体时打开App也能对应修改。scaledDensity计算根据系统原来的比值来获得现在修改后的值。

final float targetScaledDensity = targetDensity * (appDisplayMetrics.scaledDensity / appDisplayMetrics.density);
复制代码

在 Activity#onCreate 方法中调用下。代码比较简单,也没有涉及到系统非公开api的调用,因此理论上不会影响app稳定性。

/**
     * 头条处理多设备的方案  setCustomDensity(this, getApplication());
     *
     * @param activity
     * @param application
     */
    private void setCustomDensity(Activity activity, final Application application) {

        //application
        final DisplayMetrics appDisplayMetrics = application.getResources().getDisplayMetrics();

        if (sRoncompatDennsity == 0) {
            sRoncompatDennsity = appDisplayMetrics.density;
            sRoncompatScaledDensity = appDisplayMetrics.scaledDensity;
            application.registerComponentCallbacks(new ComponentCallbacks() {
                @Override
                public void onConfigurationChanged(Configuration newConfig) {
                    if (newConfig != null && newConfig.fontScale > 0) {
                        sRoncompatScaledDensity = application.getResources().getDisplayMetrics().scaledDensity;
                    }
                }

                @Override
                public void onLowMemory() {

                }
            });
        }

        //计算宽为360dp 同理可以设置高为640dp的根据实际情况
        final float targetDensity = appDisplayMetrics.widthPixels / 360;
        final float targetScaledDensity = targetDensity * (sRoncompatScaledDensity / sRoncompatDennsity);
        final int targetDensityDpi = (int) (targetDensity * 160);

        appDisplayMetrics.density = targetDensity;
        appDisplayMetrics.densityDpi = targetDensityDpi;
        appDisplayMetrics.scaledDensity = targetScaledDensity;

        //activity
        final DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics();

        activityDisplayMetrics.density = targetDensity;
        activityDisplayMetrics.densityDpi = targetDensityDpi;
        activityDisplayMetrics.scaledDensity = targetScaledDensity;
    }
复制代码

具体适配效果可以见参考资料链接,今日头条团队在各种手机上的适配效果和后期bug反馈。

思考一刻

这个方案的解决思路很简洁,参考资料也详细的列举了它的优点,非常吸引人。但是,最终我们公司的项目没有采用这个,而是采用下面的方案。理由很简单:一次修改,全局改变。后期维护无所适从。假如一处UI出问题了,你打算怎么改?你没法改,你怎么改。

后续

参考中《 骚年你的屏幕适配方式该升级了!-今日头条适配方案 》这篇文章进一步升级了这个思路,它最大的贡献是对单个 Activity 或 Fragment 可以取消适配。这个思路可以解决后期维护问题,我觉得这个方案这个时候就值得推荐和使用了。同时,它还能自定义以宽或者高为维度进行适配。

原理

  • 基于设计图的宽度值(或高度值)和对应的dpi适配,即根据设备的实际宽度(或高度)相对应的缩放view的尺寸。
  • 缩放比率 = value * ((float) actualWidth / (float) designWidth)

适配方案

  • 给定一个宽高大小固定的标准设计图,支持以宽或高一个维度自适应适配,保持改维度和设计图一致;
  • 支持dp和sp单位。

实现

遍历 ViewGroup 获取所有子 View 的尺寸参数,重新计算 View 的WidthHeightFont、Padding、LayoutMargin。

/**
 * Only adapter width/height/padding/margin
 * Created by zhangyuwan0 on 2018/3/21.
 */

public class SimpleConversion implements IConversion {

    @Override
    public void transform(View view, AbsLoadViewHelper loadViewHelper) {
        if (view.getLayoutParams() != null) {
            loadViewHelper.loadWidthHeightFont(view);
            loadViewHelper.loadPadding(view);
            loadViewHelper.loadLayoutMargin(view);
        }
    }

}
复制代码

Activity、Fragment、自定义 View 等加载view后,手动调用LoadViewHelper#loadView 方法重计算一遍所有view。本质的转化方法是计算缩放因子。

private float calculateValue(float value) {
        if ("px".equals(unit)) {
            return value * ((float) actualWidth / (float) designWidth);
        } else if ("dp".equals(unit)) {
            int dip = dp2pxUtils.px2dip(actualDensity, value);
            value = ((float) designDpi / 160) * dip;
            return value * ((float) actualWidth / (float) designWidth);

        }
        return 0;
    }
复制代码

项目实际反馈

  • 简单衡量头条和AndroidScreenAdaptation的优缺点后,我们最终选择这个方案。原因:虽然所有布局都需要手动调用 ScreenAdapterTools # getInstance() # loadView(view) 方法,工作量大;但是,优点也是这个。任何 View 的适配都是可以调整和修改的,而且不会影响其它布局。
/**
     * Created by guokun on 2018/7/21.
     * Description: 标准宽高640x360(16:9) density = 1.0 dpi = 160
     * 1. 高度低于设计高度,以高度作标准缩放;
     * 2. 高度高于设计高度,但是高度:宽度 < 9:16,以高度作标准缩放;
     * 3. 其余以宽度作标准缩放;
     * @param
     * @return
     */
    public float calculateValue(float value) {
        if ("px".equals(unit)) {
            return value * ((float) actualWidth / (float) designWidth);
        } else if ("dp".equals(unit)) {
            int dip = dp2pxUtils.px2dip(actualDensity, value);
            value = ((float) designDpi / 160) * dip;

            if (actualHeight < designHeight || actualWidth * designHeight / designWidth > actualHeight) {
                return value * ((float) actualHeight / (float) designHeight);
            }
            return value * ((float) actualWidth / (float) designWidth);
        }
        return 0;
    }
复制代码
  • 自定义 View 基本不支持。每个自定义 View 你需要查看源码调用 calcualteValue 重新计算参数。幸运的是项目自定义 View 不是很多和复杂。最致命的是:wrapcontent 不适配,所有的 View 必须给定尺寸;SeekBarProgress 不支持,手动反射调用方法处理适配问题。
@Override
    public void transform(View view, AbsLoadViewHelper loadViewHelper) {
        /**Created by guokun on 2018/7/28.
         * Description: MyLinearLayout_h381特殊处理
         * 1. MyLinearLayout键盘的高度大于标准高度360;
         * 2. 这里UI标准图设计bug,未加上20dp 键盘top;
         * */
        if (view.getTag() != null && (Integer)view.getTag() == MyLinearLayout_h381.getCustomHeight(view.getContext())) {
            int defaultDesign = loadViewHelper.getDesignHeight();
            loadViewHelper.setDesignHeight((Integer) view.getTag());
            if (view.getLayoutParams() != null) {
                loadViewHelper.loadWidthHeightFont(view);
                loadViewHelper.loadPadding(view);
                loadViewHelper.loadLayoutMargin(view);
            }
            loadViewHelper.setDesignHeight(defaultDesign);
        }else {
            if (view.getLayoutParams() != null) {
                loadViewHelper.loadWidthHeightFont(view);
                loadViewHelper.loadPadding(view);
                loadViewHelper.loadLayoutMargin(view);
            }
        }
    }
复制代码

思考一刻

放弃这个项目吧,不值得。光是缺点,你都改不过来。

大总结

android 适配一直是个悬而未决的大难题。Google 提供的思路对于国内复杂的设备环境和小团队而言,代价很高。综合项目实际场景再权衡各种方案才是解决之道,因为这些方案本身并不是很大的工程。

参考资料


以上所述就是小编给大家介绍的《Android屏幕适配方案》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

算法详解(卷1)——算法基础

算法详解(卷1)——算法基础

[美]蒂姆·拉夫加登(Tim Roughgarden) / 徐波 / 人民邮电出版社 / 2019-1-1 / 49

算法是计算机科学领域最重要的基石之一。算法是程序的灵魂,只有掌握了算法,才能轻松地驾驭程序开发。 算法详解系列图书共有4卷,本书是第1卷——算法基础。本书共有6章,主要介绍了4个主题,它们分别是渐进性分析和大O表示法、分治算法和主方法、随机化算法以及排序和选择。附录A和附录B简单介绍了数据归纳法和离散概率的相关知识。本书的每一章均有小测验、章末习题和编程题,这为读者的自我检查以及进一步学习提......一起来看看 《算法详解(卷1)——算法基础》 这本书的介绍吧!

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具