浅谈屏幕适配

1. 概述

AndroidDevelop|屏幕兼容性概览

为什么会出现屏幕适配,首先我们思考一个问题,如果我们用dp作为单位,假设屏幕密度一样的两台手机,比如都是320dpi,但是他们的尺寸一个是4.3英寸,一个是5.0英寸,那么原先为4.3英寸适配的UI放到5.0中,则会出现差异,可能原先全屏适配的到了5.0会出现留白。

假设我们UI设计图是按屏幕宽度为360dp来设计的,那么在1080x1920,5英寸的设备上,屏幕宽度其实为1080/(440/160)=392.7dp,也就是屏幕是比设计图要宽的。

所以对于Android设备来说,屏幕适配的根本原因在于,Android屏幕碎片化太严重,没有遵循1:1.5:2:3:4的比例进行屏幕设计,所以即使用dp作为单位,也会出现上面那种情况。

2. 屏幕尺寸

屏幕尺寸大全

屏幕尺寸指屏幕的对角线的长度,单位是英寸,1英寸=2.54厘米,比如常见的屏幕尺寸有2.4、2.8、3.5、3.7、4.2、5.0、5.5、6.0等

3. 屏幕分辨率

屏幕分辨率是指在 纵横向上的像素点数,单位是px,1px=1个像素点。一般以纵向像素 x 横向像素,如1920 x1080。

分辨率和屏幕尺寸之间的关系

由于density在长和宽方向都是一致的,所以 屏幕尺寸长宽比 = 分辨率长宽比

比如 1080 * 1920 ——> 1080/x = 1920/y —— > 即:屏幕尺寸长宽比 = 分辨率之比;

综上:分辨率尺寸定好,手机的长宽就定死了;

来源今日头条适配

4. 屏幕像素密度

屏幕像素密度是指 每英寸上的像素点数,单位是dpi,即"dot per inch”的缩写。屏幕像素密度与屏幕 尺寸 和屏幕 分辨率 有关,在单一变化条件下,屏幕尺寸越小、分辨率越高,像素密度越大,反之越小。

设备密度:density = px / dpi, 比如: 2 = 1080px / 540dp

px = dp x (dpi/160) = dp x density
# 1080px = 540dp x (320dpi/160) = 540dp x 2

5. dp、sp、px

px我们应该是比较熟悉的,前面的分辨率就是用的像素为单位,大多数情况下,比如 UI设计Android原生API
都会以px作为统一的计量单位,像是获取屏幕宽高等。**像素(点)是组成图片的基本要素,单位面积上的像素点越多,这张图就越清晰;分辨率是指长和宽两个方向上各自拥有的像素点的个数;

dp,Density Independent Pixels的缩写,即密度无关像素,上面我们说过,dpi是屏幕像素密度,假如一英寸里面有160个像素,这个屏幕的像素密度就是160dpi,那么在这种情况下,dp和px如何换算呢?在Android中,规定以160dpi为基准(1英寸160px),1dp=1px,如果像素密度是320dpi(设备密度2),则1dp=2px,以此类推。

假如同样都是画一条160px的线,在480x800分辨率(标准设备密度1.5)手机上显示为1/3屏幕宽度,在320x480(标准设备密度1)的手机上则是1/2,如果换算成dp,则前者分辨率是 320 x 533,后者分辨率是320x 480,在这两种分辨率下,160dp都显示为屏幕一半的长度。这也是为什么在Android开发中,写布局的时候要尽量使用dp而不是px的原因。

而sp,即scale-independent pixels,与dp类似,但是可以根据文字大小首选项进行放缩,是设置字体大小的御
用单位。

// android\util\TypedValue.java
 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;
 }

6. mdpi、hdpi、xdpi…

mdpi、 hdpi、 xdpi、xxdpi用来修饰Android中的 drawable文件夹及values文件夹,用来区分不同像素密度下的 图片和dimen值

按照 AndroidDevelopers|提供备用位图 按下列来划分

名称像素密度范围
mdpi120dpi~160dpi (0.75~1)——1
hdpi160dpi~240dpi (1~1.5) ——1.5
xhdpi240dpi~320dpi (1.5~2) ——2
xxhdpi320dpi~480dpi (2~3) ——3
xxxhdpi480dpi~640dpi (3~4) ——4

在设计图标时,对于五种主流的像素密度(MDPI、HDPI、XHDPI、XXHDPI和XXXHDPI)应按照2:3:4:6:8
的比例进行缩放。例如,一个启动图标的尺寸为48x48 dp,在mdpi的屏幕上其实际尺寸应为 48x48
pX,在hdmi的屏幕上其实际大小是mdpi的1.5倍(72x72 px),在xdpi的屏幕上其实际大小是mdpi的2
倍(96x96 px),依此类推。

屏幕密度图标尺寸
mdpi48x48px
hdpi72x72px
xhdpi96x96px
xxhdpi144x144px
xxxhdpi192x192px

7. 屏幕分辨率限定符

也有说 宽高限定符

可以参考鸿神提供的思路,即每种屏幕分辨率的设备需要定义一套 dimens.xml文件。

用的是百分比的思路,src\main\res下创建一些常见的手机像素比例,系统会自动去找对应的dimens文件

# - src
#	- main
#		- res
#			- values-480x320
#			- values-800x480
#			- values-854x480
#			- values-xxxxxxx
#			- values-2560x1440

假设设计图是以480x320为基准作图,那么

  • 宽度为320,将任何分辨率的宽度分为320份,取值为x1-x320

  • 高度为480,将任何分辨率的高度分为480份,取值为y1-y480

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <dimen name="x1">1px</dimen>
        <dimen name="x2">2px</dimen>
        ...
        <dimen name="x320">320px</dimen>
    </resources>
    

对于800x480来说,它对应的就是,480/320 = 1.5,纵向同样的思路创建

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <dimen name="x1">1.5px</dimen>
    <dimen name="x2">3px</dimen>
    ...
    <dimen name="x320">480px</dimen>
</resources>

其它的依次类推

8. 最小宽度限定符

wildma/ScreenAdaptation

  • 适配手机的单面板(默认)布局:res/layout/main.xml
  • 适配尺寸>7寸平板的双面板布局(Android 3.2前):res/layout-large/main.xml
  • 适配尺寸>7寸平板的双面板布局(Android 3.2后):res/layout-sw600dp/main.xml

Android 3.2 版引入。最小宽度限定符可让您通过指定某个最小宽度(以dp为单位)来定位屏幕。

  1. 屏幕分辨率限定符适配是根据屏幕分辨率的,Android 设备分辨率一大堆,而且还要考虑虚拟键盘,这样就需要大量的 dimens.xml文件。因为无论手机屏幕的像素多少,密度多少,90% 的手机的最小宽度都为 360dp(2022不止了),所以采用 smallestWidth 限定符适配只需要少量dimens.xml文件即可。
  2. 屏幕分辨率限定符适配采用的是px单位,而smallestWidth限定符适配采用的单位是dp和sp,dp和sp是google推荐使用的计量单位。又由于很多应用要求字体大小随系统改变,所以字体单位使用 sp 也更灵活。
  3. 屏幕分辨率限定符适配需要设备分辨率与 values-xx 文件夹完全匹配才能达到适配,而smallestWidth 限定符适配寻找dimens.xml文件的原理是从大往小找,例如设备的最小宽度为 360dp,就会先去找values-360dp,发现没有则会向下找values-320dp,如果还是没有才找默认的 values下的 demens.xml文件,所以即使没有完全匹配也能达到不错的适配效果。

8.1 获取设计图最小宽度(dp)

一般来说,UI设计师提供的设计图分以下几种

  • 蓝湖:开发平台切换到 Android,设计图宽度即为最小宽度。
  • psd 源文件:用像素大厨查看,设计图宽度即为最小宽度。(注意像素大厨需要选择与设计图对应的 dpi 进行显示)
  • dp单位的设计图:设计图宽度即为最小宽度。
  • px单位的设计图:问UI设计师是几倍图,然后最小宽度=设计图宽度/倍数。

8.2 生成对应的dimens.xml文件

可以借助 ScreenMatch 插件(原理可以参考鸿洋屏幕分辨率限定符)快速生成。安装插件后,会自动在根目录生成screenMatch.propertiesscreenMatch_example_dimens.xml示例文件

在这里插入图片描述

base_dp=360
# System default values is 240,320,384,392,400,410,411,480,533,592,600,640,662,720,768,800,811,820,960,961,1024,1280,1365
# 可根据实际情况修改下值
match_dp=392.7272
ignore_dp=240,320,384,392,400,410,411,480,533,592,600,640,662
# 以哪个module为基准module生成相应的dimens.xml
match_module=app
  1. base_dp:最小宽度基/准值,填写设计图的最小宽度值即可。
  2. system default...:插件默认适配的最小宽度值,默认生成的一系列的dimens.xml文件。
  3. match_dp:需要适配的最小宽度值(如果是小数,则保留4位小数。例如 392.727272…,则取392.7272),即你想生成哪些dimens.xml文件。
  4. ignore_dp:忽略不需要适配的最小宽度值,即忽略掉插件默认生成的 dimens.xml文件。
  5. match_module:适配其它module时候,不需要每份都生成一套dimens.xml,只需要相应的module中的values文件夹下有一套与match_module一样的dimens文件即可

按照上面示例生成的文件夹为下图:

img

当然也可以自己写一个方法来生成这些文件,以宽高的最小值为基准,此处我们以宽370为基准,在根目录下生成相应src-dpsrc-px目录,并生成相应的dimens.xml

import java.io.File
import java.io.FileOutputStream
import kotlin.math.min

private const val XML_FILE_NAME = """dimens.xml"""
private const val XML_HEADER = """<?xml version="1.0" encoding="utf-8"?>"""
private const val XML_RESOURCE_START = """<resources>"""
private const val XML_SW_DP_TAG = """<string name="sw_dp">%ddp</string>"""
private const val XML_DIMEN_TEMPLATE_TO_DP = """<dimen name="DIMEN_%ddp">%.2fdp</dimen>"""
private const val XML_DIMEN_TEMPLATE_TO_PX = """<dimen name="DIMEN_%dpx">%.2fdp</dimen>"""
private const val XML_RESOURCE_END = """</resources>"""

private const val DESIGN_WIDTH_DP = 370
private const val DESIGN_HEIGHT_DP = 667

private const val DESIGN_WIDTH_PX = 1080
private const val DESIGN_HEIGHT_PX = 1920

fun main() {
    val designWidthDp = min(DESIGN_WIDTH_DP, DESIGN_HEIGHT_DP)
    val srcDirFileDp = File("src-dp")
    makeDimens(designWidthDp, srcDirFileDp, XML_DIMEN_TEMPLATE_TO_DP)

    val designWidthPx = min(DESIGN_WIDTH_PX, DESIGN_HEIGHT_PX)
    val srcDirFilePx = File("src-px")
    makeDimens(designWidthPx, srcDirFilePx, XML_DIMEN_TEMPLATE_TO_PX)
}

private fun makeDimens(designWidth: Int, srcDirFile: File, xmlDimenTemplate: String) {
    if (srcDirFile.exists() && !srcDirFile.deleteRecursively()) {
        return
    }
    srcDirFile.mkdirs()
    val smallestWidthList = mutableListOf<Int>().apply {
        for (i in 320..460 step 10) {
            add(i)
        }
    }.toList()
    for (smallestWidth in smallestWidthList) {
        makeDimensFile(designWidth, smallestWidth, xmlDimenTemplate, srcDirFile)
    }
}

private fun makeDimensFile(designWidth: Int, smallestWidth: Int, xmlDimenTemplate: String, srcDirFile: File) {
    val dimensFolderName = "values-sw" + smallestWidth + "dp"
    val dimensFile = File(srcDirFile, dimensFolderName)
    dimensFile.mkdirs()
    val fos = FileOutputStream(dimensFile.absolutePath + File.separator + XML_FILE_NAME)
    fos.write(generateDimens(designWidth, smallestWidth, xmlDimenTemplate).toByteArray())
    fos.flush()
    fos.close()
}

private fun generateDimens(designWidth: Int, smallestWidth: Int, xmlDimenTemplate: String): String {
    val sb = StringBuilder()
    sb.append(XML_HEADER)
    sb.append("\n")
    sb.append(XML_RESOURCE_START)
    sb.append("\n")
    sb.append("    ")
    sb.append(String.format(XML_SW_DP_TAG, smallestWidth))
    sb.append("\n")
    for (i in 1..designWidth) {
        val dpValue = i.toFloat() * smallestWidth / designWidth
        sb.append("    ")
        sb.append(String.format(xmlDimenTemplate, i, dpValue))
        sb.append("\n")
    }
    sb.append(XML_RESOURCE_END)
    return sb.toString()
}

8.3 尺寸限定符

但要注意的是,这种方式只适合 Android 3.2版本之前

<!-- res/layout/main.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical"
  android:layout_width="match_parent"
  android:layout_height="match_parent">
 
  <fragment android:id="@+id/headlines"
            android:layout_height="match_parent"
            android:layout_width="match_parent" />
</LinearLayout>
<!-- res/layout-large/main.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal">
    <fragment android:id="@+id/headlines"
              android:layout_height="fill_parent"
              android:layout_width="400dp"
              android:layout_marginRight="10dp"/>
    <fragment android:id="@+id/article"
              android:layout_height="match_parent"
              android:layout_width="match_parent" />
</LinearLayout>
  • 两个布局名称均为main.xml,只有布局的目录名不同:第一个布局的目录名为:layout,第二个布局的目录名为:layout-large,包含了 尺寸限定符(large)
  • 被定义为大屏的设备(7寸以上的平板)会自动加载包含了large限定符目录的布局,而小屏设备会加载另一个默认的布局

8.4 其它

  1. 图片资源匹配,.9图片类型

  2. 布局组件,使用wrap_contentmatch_parentweight

  3. 布局别名



9. 今日头条相关

一种极低成本的Android屏幕适配方式

9.1 系统状态栏获取不对问题

由于使用的是 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的获取

// 比如状态栏、导航栏
Resources systemRes = Resources.getSystem();
// applicaiton
Resources appRes = Application.getResources();
// acttivity
Resources actRes = activity.getResources();

9.2 autosize

JessYanCoding/AndroidAutoSize

其实这里并没有用到什么 黑科技,原理反而非常简单,只需要声明—个ContentProvider,在它的onCreate方法中启动框架即可,在App 启动时,系统会在App 的主进程中自动实例化你声明的这个ContentProvider,并调用它的 onCreate 方法,执行时机比 Application#onCreate 还靠前,可以做一些初始化的工作

// me.jessyan.autosize.InitProvider.java
public class InitProvider extends ContentProvider {
   @Override
    public boolean onCreate() {
        Context application = getContext().getApplicationContext();
        if (application == null) {
            application = AutoSizeUtils.getApplicationByReflect();
        }
        AutoSizeConfig.getInstance()
                .setLog(true)
                .init((Application) application)
                .setUseDeviceSize(false);
        return true;
    } 
}

这里是今日头条适配方案的核心代码, 核心在于根据当前设备的实际情况做自动计算并转换 DisplayMetrics.density、 DisplayMetrics.scaledDensity、DisplayMetrics.densityDpi 这三个值, 额外增加 DisplayMetrics.xdpi 以支持单位 pt、in、mm

// me.jessyan.autosize\AutoSize.java
public static void autoConvertDensity(Activity act, float sizeInDp, boolean isBaseOnWidth){
    //...
    
    if (displayMetricsInfo == null) {
        if (isBaseOnWidth) {
            targetDensity = AutoSizeConfig.getInstance().getScreenWidth() * 1.0f / sizeInDp;
        } else {
            targetDensity = AutoSizeConfig.getInstance().getScreenHeight() * 1.0f / sizeInDp;
        }
    }
    //...
    setDensity(activity, targetDensity, targetDensityDpi, targetScaledDensity, targetXdpi);
    setScreenSizeDp(activity, targetScreenWidthDp, targetScreenHeightDp);
}

setDensity中有两种类型的DisplayMetrics,分别获取并设置其属性

private static void setDensity(Activity activity, float density, int densityDpi, float scaledDensity, float xdpi) {
   // Activity 的 DM
    DisplayMetrics activityDM = activity.getResources().getDisplayMetrics();
    setDensity(activityDisplayMetrics, density, densityDpi, scaledDensity, xdpi);
// App 的 DM
    DisplayMetrics appDM = AutoSizeConfig.getInstance().getApplication().getResources().getDisplayMetrics();
    setDensity(appDisplayMetrics, density, densityDpi, scaledDensity, xdpi);
}

setScreenSizeDpConfiguration赋值,在Activity配置变化时能够同步变更

private static void setScreenSizeDp(Configuration configuration, int screenWidthDp, int screenHeightDp) {
    configuration.screenWidthDp = screenWidthDp;
    configuration.screenHeightDp = screenHeightDp;
}

scaleDensity

显示器上显示的字体的缩放因子。这与density是相同的,只是在运行时可以根据用户对字体大小的偏好以较小的增量进行调整。

density本质上是同一个值,为了单独调整字体而设置

// density 和 scaledDensity 的关系
public static void autoConvertDensity(Activity act, float sizeInDp, boolean isBaseOnWidth){
    //...
    // 是否屏蔽系统字体大小对AutoSize的影响,否的话就随系统改变,设置字体大小后,计算缩放比(scaledDensity/density)
    float systemFontScale = AutoSizeConfig.getInstance().isExcludeFontScale() ? 1 : AutoSizeConfig.getInstance().getInitScaledDensity() * 1.0f / AutoSizeConfig.getInstance().getInitDensity();
    // 
    targetScaledDensity = targetDensity * systemFontScale;
}

在这里插入图片描述

我们以 pixel2xl 为验证机型,当字体大小调节为默认时候,调整显示大小时,我们看输出:设备密度与字体密度都会变,但比例不变

# 小
E/测试: scanledDensity/density=1.0
E/测试: 设备屏幕密度:density:2.9750001	scaledDensity:2.9750001	densityDpi(dpi):476
# 默认
E/测试: scanledDensity/density=1.0
E/测试: 设备屏幕密度:density:3.5	scaledDensity:3.5	densityDpi(dpi):560
# 大
E/测试: scanledDensity/density=1.0
E/测试: 设备屏幕密度:density:3.825	scaledDensity:3.825	densityDpi(dpi):612

显示大小为默认时,调节字体大小,我们看输出:设备密度不变,字体密度会变

# 小
E/测试: scanledDensity/density=0.85
E/测试: 设备屏幕密度:density:3.5	scaledDensity:2.9750001	densityDpi(dpi):560
# 默认
E/测试: scanledDensity/density=1.0
E/测试: 设备屏幕密度:density:3.5	scaledDensity:3.5	densityDpi(dpi):560
# 大
E/测试: scanledDensity/density=1.15
E/测试: 设备屏幕密度:density:3.5	scaledDensity:4.025	densityDpi(dpi):560
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值