安卓屏幕适配踩坑笔记(手机+平板)

前言

目前做APP的时候,尤其应用跑在平板上的时候,发现了一个比较难解决的安卓碎片化的问题:同样分辨率的平板,屏幕密度是不相同的,甚至于不同品牌的手机也是不相同的。

下面随便举了一些例子:

型号 类型 屏幕宽度 屏幕高度 屏幕密度
rockchip 平板 1920 1128 1.5
华为C5 平板 1920 1200 2
华为C3 平板 1280 800 1.275
小米10 手机 1080 2120 2.75
华为nova4 手机 1080 2108 3
vivo 手机      

屏幕上显示的单位都是以px为准的,而我们代码中一般设置的单位是dp。

换算关系如下:px=dp*density。

比如说我代码中设置一个button的高度为20dp,在华为C5平板上的高度就是40px,在rockchip平板上显示的高度就是30px。

但是这两个平板的分辨率几乎是一样的,这样就会产生一个问题,根据UI稿,如果我们按照C5平板的density来设计,那么在rockchip平板上就会显示的很大,会比设计稿大30%。

 

方案一:设置不同分辨率的dimens文件

如下图所示,针对不同的分辨率创建不同的values文件夹,并且添加不同的dimens文件。而dimens针对不同的dp配置不同的px值

安卓屏幕适配踩坑笔记(手机+平板)_第1张图片安卓屏幕适配踩坑笔记(手机+平板)_第2张图片

使用的时候我们直接使用dipxx代替原来的dp值,如下。

 

1280x800的分辨率下,我配置的dip16=32px

1920x1080的分辨率下,我配置的dip16=48px

则上面的TextView,在1280x800展示大小为32px*32px,在1920x1080展示的大小为48px*48px

代码中所有使用dp的地方,全部改为dipxx替代。这样等于针对不同的分辨率直接使用px,从而解决了density不一致的问题。

 

附送values文件生成代码:

public class DimenCreate {

    private String dirStr = "./res";

    private final static String TemplateDP = "{1}\n";
    private final static String Template_DP = "{1}\n";
    private final static String TemplateSP = "{1}\n";

    /**
     * {0}-HEIGHT
     */
    private final static String VALUE_TEMPLATE = "values-{0}x{1}";

    public static void main(String[] args) {

        HashMap params = new HashMap<>();
        params.put("0,0", 1.0f);
        params.put("1080,1920", 3.0f);
        params.put("720,1280", 2.0f);
        params.put("800,1223", 2.0f);
        params.put("708,1366", 2.0f);
        params.put("1200,1920", 3.0f);
        params.put("1080,2107", 3.0f);
        params.put("360,640", 1.0f);

        new DimenCreate().generate(params);
    }


    public void generate(HashMap params) {
        for (String key : params.keySet()) {
            String[] wh = key.split(",");//宽高
            Float density = params.get(key);
            generateXmlFile(Integer.parseInt(wh[0]), Integer.parseInt(wh[1]), density);
        }
    }

    private void generateXmlFile(int w, int h, float density) {

        StringBuffer sbForWidth = new StringBuffer();
        sbForWidth.append("\n");
        sbForWidth.append("\n");
        int bigger = w > h ? w : h;
        if (bigger == 0) {
            bigger = 640;
        }
        float baseFloat = bigger / density;

        System.out.println("width : " + w + ",height : " + h + ",bigger:" + bigger + ",baseFloat:" + baseFloat);

        //spxx
        for (int i = 1; i <= 50; i++) {
            if (w == 0) {
                sbForWidth.append(TemplateSP.replace("{0}", i + "").replace("{1}",
                        change(density * i, w, "sp") + ""));
            } else {
                sbForWidth.append(TemplateSP.replace("{0}", i + "").replace("{1}",
                        change(density * i, w, "sp") + ""));
            }
        }

        //dip_xx
        for (int i = 1; i <= 50; i++) {
            sbForWidth.append(Template_DP.replace("{0}", i + "").replace("{1}",
                    change(density * i * -1, w, "dp")));
        }


        //dipxx
        for (int i = 1; i <= baseFloat; i++) {
            sbForWidth.append(TemplateDP.replace("{0}", i + "").replace("{1}",
                    change(density * i, w, "dp") + ""));
        }
        sbForWidth.append("");

        File fileDir = new File(dirStr + File.separator
                + VALUE_TEMPLATE.replace("{0}", h + "")//
                .replace("{1}", w + ""));
        fileDir.mkdir();

        File layxFile = new File(fileDir.getAbsolutePath(), "common_dimens.xml");
        try {
            PrintWriter pw = new PrintWriter(new FileOutputStream(layxFile));
            pw.print(sbForWidth.toString());
            pw.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
    //values中的也要考虑到


    //保留一位小数
    public static String change(float a, float w, String unit) {
        if (w == 0) {
            return a + unit;
        }
        int temp = (int) (a * 100);
        return String.format("%.1f", temp / 100f) + "px";
    }


}

 

优点:对原生无侵入,原理简单。

缺点:

1.对原有的代码代码量较大,所以使用到dp的地方都需要替换。(可以写脚本进行这种替换)

2.横竖屏切换的时候会导致不适配。横竖屏切换的时候,上述配置文件是失效的。

 

因为横竖屏切换的时候会导致配置文件失效,这时候展示出来的界面控件大小是有问题的,所以这种方案使用了一段之后,最终被放弃。

但是如果没有横竖屏切换的需求,全部都是mainfest里面写死的横屏或者竖屏,是没有的问题。

 

方案二:针对横竖屏使用新尺寸限定符

上面说的是针对分辨率设置不同的配置文件,在横竖屏切换的时候有问题,那么能否按照竖屏的方式再去配置一套呢?搜了一圈,果然可以。

如下图所示,同样1920*1080分辨率情况下,密度不同,则dp值不同。如果我设置两个dp值适配的文件进行适配,则可以解决这个问题。

其中编号3中的dimens文件,大小可以设置为编号1中的1.5倍。

编号 分辨率 密度 dp值 缩放比
1 1920*1080 3 360 1
2 1280*720 2 360 1
3 1920*1080 2 540 1.5
4 1280*720 1.5 480 1.33

w360dp设置:

安卓屏幕适配踩坑笔记(手机+平板)_第3张图片

w540dp中设置:

安卓屏幕适配踩坑笔记(手机+平板)_第4张图片

这里要额外说一下,这套适配和上面方案一的适配是可以同时使用的。方案二的优先级要高于方案一,即如果手机或平板同时命中方案一的分辨率和方案二的dp值时,优先按照方案二进行适配。

 

方案二其实已经满足我的需求了,因为它已经解决了同样分辨率不同密度展示不相同的问题。但是由于我之前的方案使用的是方案一,所以使用方案二的话会导致方案一完全失效,会影响所有的控件。风险性较大。

 

另外方案二还有一个缺陷,就是横竖屏切换的时候,适配的是不同的文件夹。比如我1920*1200,屏幕密度是2。那么横屏显示的时候适配的是w960dp的文件夹,竖屏显示的时候适配的是w600dp的文件夹。这时候如果我要设置宽度占横屏时屏幕的一半,则应该dip320,实际展示出来,宽度为2*1.5*320=960px,正好为屏幕的一半宽度。

这时候如果有一个屏幕分辨率是1800*1080的,屏幕密度是3的。则也适用w600dp的文件夹。这时候dip320的时候,宽度为3*1.5*320=1440,那这时候就有问题了。

 

方案三:修改设备密度density

这个是参照今日头条的一个方案,所有安卓设备最终展示在屏幕上面的都是像素px,既然是像素,那么我们代码中写的dp,sp什么的,就一定有一个地方转换为像素值,这个地方就是TypedValue类的applyDimension方法:

 /**
     * Converts an unpacked complex data value holding a dimension to its final floating 
     * point value. The two parameters unit and value
     * 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;
    }

这里我们就举一个例子就好了,TextView设置字号:

如下图所示,setTextSize默认传入的是sp,调用setTextSizeInternal方法,通过TypedValue.applyDimension方法把sp转换为最终的像素值,传入setRawTextSize方法。setRawTextSize中把size设置给TextPaint,所以我们也知道了,TextPaint接收的单位是像素值。

public void setTextSize(float size) {
        setTextSize(TypedValue.COMPLEX_UNIT_SP, size);
    }

 public void setTextSize(int unit, float size) {
        if (!isAutoSizeEnabled()) {
            setTextSizeInternal(unit, size, true /* shouldRequestLayout */);
        }
    }
 private void setTextSizeInternal(int unit, float size, boolean shouldRequestLayout) {
        Context c = getContext();
        Resources r;

        if (c == null) {
            r = Resources.getSystem();
        } else {
            r = c.getResources();
        }

        setRawTextSize(TypedValue.applyDimension(unit, size, r.getDisplayMetrics()),
                shouldRequestLayout);
    }

    private void setRawTextSize(float size, boolean shouldRequestLayout) {
        if (size != mTextPaint.getTextSize()) {
            mTextPaint.setTextSize(size);

            if (shouldRequestLayout && mLayout != null) {
                // Do not auto-size right after setting the text size.
                mNeedsAutoSizeText = false;
                nullLayouts();
                requestLayout();
                invalidate();
            }
        }
    }

OK,回归正题,如何修改像素值呢?这个很简单,都不需要反射,直接通过resources获取DisplayMetrics对象,然后直接粗暴的改掉就好了,简单的有点不可思议。由于进程是共享一个resources的,所以直接在application中进行一次设置,整个APP都生效了。我们公司的设计一般是按照360*640进行设置的,所以我只要按照这个比例去设置密度值即可:

比如:

1920*1080/1920*1200等,密度值是3

1280*720/1280*800等,密度值是2

代码如下,在闪屏页进行加载。

public static void setCustomDensity(Activity activity, Application application) {
        //application
        final DisplayMetrics appDisplayMetrics = application.getResources().getDisplayMetrics();
        int max = Math.max(appDisplayMetrics.widthPixels, appDisplayMetrics.heightPixels);
        //计算宽为360dp 同理可以设置高为640dp的根据实际情况
        final float targetDensity = Math.round((double) max / 640);
        final int targetDensityDpi = (int) (targetDensity * 160);

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

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

        activityDisplayMetrics.density = appDisplayMetrics.scaledDensity = targetDensity;
        activityDisplayMetrics.densityDpi = targetDensityDpi;
    }

然后我就惊喜的发现,所有界面都已经生效了。

这个方案虽然好,但是使用的过程中也发现了一些问题。于是整理了一下原因和解决方案,汇总如下:

问题点1:有的时候发现密度值会失效,恢复到未修改的原始的状态

这个最终调研下来,发现自定义加载布局的时候就会出现这种问题,getWindow().getLayoutInflater().inflate(id, parent, false)。

具体原因没有根究,就简单的改为在每个onCreate方法调用setCustomDensity方法。

 

你可能感兴趣的:(安卓)