Android屏幕适配讲解和实战

本文深入解析屏幕适配的必要性,探讨dpi、ppi与dp之间的关系,以及它们如何影响不同设备上的显示效果。提出了以宽度或高度适配的思路,并提供了一种具体的适配方案,确保设计图在各种设备上的一致显示。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文是基于字节跳动的文章整理的,原文链接如下:
https://mp.weixin.qq.com/s/d9QCoBP6kV9VSWvVldVVwA
也整合了很多其他的大佬的文章和自己查看源码,文章后面有附录的文章,可以看看。

一、为什么要屏幕适配

基础:
1、dpi:屏幕像素密度,指的是在系统软件上指定的单位尺寸的像素数量,它往往是写在系统出厂配置文件的一个固定值;
2、ppi:也是屏幕像素密度,但这个是物理上的概念,它是客观存在的不会改变。dpi是软件参考了物理像素密度后,人为指定的一个值,这样保证了某一个区间内的物理像素密度在软件上都使用同一个值;
3、dp加上自适应布局和weight比例布局能解决90%的适配问题。因为并不是所有的1080P的手机dpi都是480,比如Google 的Pixel2(1920*1080)的dpi是420;
4、宽高限定符适配:穷举市面上所有的Android手机的宽高像素值,设定一个基准的分辨率,其他分辨率都根据这个基准分辨率来计算,在不同的尺寸文件夹内部,根据该尺寸编写对应的dimens文件。但其有一个致命的缺陷,那就是需要精准命中才能适配,App包体积也会变大

为什么要屏幕适配:
不管我们在布局文件里面用的是什么单位,最后都会转换为px,这是Android的标准单位。我们就先来看看dp是如何转化为px的,代码如下:

 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;
    }

关注dp转换为px这一项,来看value * metrics.density这一项,value是我们写布局时候的dp值,metrics.density跟踪代码就是context.getResources().getDisplayMetrics().density,而density=densityDpi/160,也就是density是屏幕密度dpi和160的一个比值,到这里就能知道,px转化为dp和屏幕密度dpi有关。

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

px = density * dp;

density = dpi / 160;

px = dp * (dpi / 160);

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

屏幕尺寸、分辨率、像素密度三者关系:
通常情况下,一部手机的分辨率是宽x高,屏幕大小是以寸为单位,那么三者的关系是:
在这里插入图片描述举个例子:屏幕分辨率为:1920*1080,屏幕尺寸为5吋的话,那么dpi为440。

在这里插入图片描述

这样会存在什么问题呢?
假设我们UI设计图是按屏幕宽度为360dp来设计的,那么在上述设备上,屏幕宽度其实为1080/(440/160)=392.7dp,也就是屏幕是比设计图要宽的。这种情况下, 即使使用dp也是无法在不同设备上显示为同样效果的。 同时还存在部分设备屏幕宽度不足360dp,这时就会导致按360dp宽度来开发实际显示不全的情况。

而且上述屏幕尺寸、分辨率和像素密度的关系,很多设备并没有按此规则来实现, 因此dpi的值非常乱,没有规律可循,从而导致使用dp适配效果差强人意。

二、适配思路

首先来梳理下我们的需求,一般我们设计图都是以固定的尺寸来设计的。比如以分辨率1920px * 1080px来设计,以density为3来标注,也就是屏幕其实是640dp * 360dp。如果我们想在所有设备上显示完全一致,其实是不现实的,因为屏幕高宽比不是固定的,16:9、4:3甚至其他宽高比层出不穷,宽高比不同,显示完全一致就不可能了。但是通常下,我们只需要以宽或高一个维度去适配,比如我们Feed是上下滑动的,只需要保证在所有设备中宽的维度上显示一致即可,再比如一个不支持上下滑动的页面,那么需要保证在高这个维度上都显示一致,尤其不能存在某些设备上显示不全的情况。同时考虑到现在基本都是以dp为单位去做的适配,如果新的方案不支持dp,那么迁移成本也非常高。

因此,总结下大致需求如下:

1、支持以宽或者高一个维度去适配,保持该维度上和设计图一致;

2、支持dp和sp单位,控制迁移成本到最小。

找兼容突破口

从dp和px的转换公式 :px = dp * density

可以看出,如果设计图宽为360dp,想要保证在所有设备计算得出的px值都正好是屏幕宽度的话,我们只能修改 density 的值。

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

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

DisplayMetrics#density 就是上述的density

DisplayMetrics#densityDpi 就是上述的dpi

DisplayMetrics#scaledDensity 字体的缩放因子,正常情况下和density相等,但是调节系统字体大小后会改变这个值

那么是不是所有的dp和px的转换都是通过 DisplayMetrics 中相关的值来计算的呢?

首先来看看布局文件中dp的转换,最终都是调用 TypedValue#applyDimension(int unit, float value, DisplayMetrics metrics) 来进行转换:

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;
}

这里用到的DisplayMetrics正是从Resources中获得的。

再看看图片的decode,BitmapFactory#decodeResourceStream方法:

 @Nullable
    public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value,
            @Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts) {
        validate(opts);
        if (opts == null) {
            opts = new Options();
        }

        if (opts.inDensity == 0 && value != null) {
            final int density = value.density;
            if (density == TypedValue.DENSITY_DEFAULT) {
                opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
            } else if (density != TypedValue.DENSITY_NONE) {
                opts.inDensity = density;
            }
        }
        
        if (opts.inTargetDensity == 0 && res != null) {
            opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
        }
        
        return decodeStream(is, pad, opts);
    }

可见也是通过 DisplayMetrics 中的值来计算的。

当然还有些其他dp转换的场景,基本都是通过 DisplayMetrics 来计算的,这里不再详述。因此,想要满足上述需求,我们只需要修改 DisplayMetrics 中和 dp 转换相关的变量即可。

三、具体方案

下面假设设计图宽度是360dp,以宽维度来适配。

那么适配后的 density = 设备真实宽(单位px) / 360,接下来只需要把我们计算好的 density 在系统中修改下即可,代码实现如下:

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

字体的适配

在上面的适配中,我们忽略了DisplayMetrics#scaledDensity的特殊性,将DisplayMetrics#scaledDensity和DisplayMetrics#density设置为同样的值,从而某些用户在系统中修改了字体大小失效了,但是我们还不能直接用原始的scaledDensity,直接用的话可能导致某些文字超过显示区域,因此我们可以通过计算之前scaledDensity和density的比获得现在的scaledDensity,方式如下:

在这里插入图片描述如果在系统设置中切换字体,再返回应用,字体并没有变化。于是还得监听下字体切换,调用 Application#registerComponentCallbacks 注册下 onConfigurationChanged 监听即可。

因此最终方案如下:
在这里插入图片描述

封装好的git库:

https://github.com/JessYanCoding/AndroidAutoSize

只需一步:

在这里插入图片描述

参考文章:https://juejin.im/post/6844903697000972295

https://juejin.im/post/6844904177903075342

https://mp.weixin.qq.com/s/d9QCoBP6kV9VSWvVldVVwA

https://mp.weixin.qq.com/s/X-aL2vb4uEhqnLzU5wjc4Q

https://www.open-open.com/lib/view/open1462929484471.html#articleHeader1

https://github.com/JessYanCoding/AndroidAutoSize

https://developer.android.com/guide/practices/screens_support.html

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值