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屏幕适配方案》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

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

性能之巅

性能之巅

Brendan Gregg / 徐章宁、吴寒思、陈磊 / 电子工业出版社 / 2015-8-15 / 128

《性能之巅:洞悉系统、企业与云计算》基于Linux 和Solaris 系统阐述了适用于所有系统的性能理论和方法,Brendan Gregg 将业界普遍承认的性能方法、工具和指标收集于本书之中。阅读本书,你能洞悉系统运作的方式,学习到分析和提高系统与应用程序性能的方法,这些性能方法同样适用于大型企业与云计算这类最为复杂的环境的性能分析与调优。一起来看看 《性能之巅》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

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

HTML 编码/解码

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试