大家在Android开发时,必定会感觉屏幕适配是个尤其苦楚的事,各种屏幕尺寸适配起来巨烦无比。如果咱们换个角度咱们看下这个问题,不晓得大家有没有理解过web前端开发,或者说大家对于网页都不生疏吧,其实适配的问题在web页面的设计中实践上也存在,为什么这么说呢?电脑的显示器的分辨率、包含手机分辨率,我敢说分辨率的品种远超过Android设施的分辨率,那么有一个很奇怪的景象:

为什么Web页面设计人员素来没有说过,屏幕适配好麻烦?

那么,到底是什么起因,让网页的设计能够在千差万别的分辨率的分辨率中仍旧能给用户一个优质的体验呢?带着这个纳闷,我问了下一个前端敌人,敌人睁大眼睛问我:适配是什么?? 前端仿佛看来确实没有这类问题,起初在我认真的诘问后,她通知我,噢 这个尺寸呀,咱们个别都加个viewport,我都是设置为20%缩放的~~ 追根到底,其实就是一个起因,网页提供了缩放比计算大小。

同样的,大家拿到UI给的设计图当前,是不是埋怨过UI妹妹标识的都是px,而我我的项目外面用dp,这都什么玩意,和UI解释她也不了解,开发同样也是一脸无奈。所以能不能有一套完满的解决方案来解决Android工程师和UI妹妹间的矛盾,实现UI给出一个固定尺寸的设计稿,而后你在编写布局的时候不必思考,无脑照抄下面标识的像素值,就能达到完满适配。现实够饱满,但事实够残暴:

因为Android零碎的开放性,任何用户、开发者、OEM厂商、运营商都能够对Android进行定制,于是导致:

  • Android零碎碎片化:小米定制的MIUI、魅族定制的flyme、华为定制的EMUI等等,当然其都是基于Google原生零碎定制的
  • Android机型屏幕尺寸碎片化:5寸、5.5寸、6寸等等
  • Android屏幕分辨率碎片化:320x480、480x800、720x1280、1080x1920

    据友盟指数显示,统计至2015年12月,反对Android的设施共有27796种

当Android零碎、屏幕尺寸、屏幕密度呈现碎片化的时候,就很容易呈现同一元素在不同手机上显示不同的问题。

试想一下这么一个场景:

为4.3寸屏幕筹备的UI设计图,运行在5.0寸的屏幕上,很可能在右侧和下侧存在大量的空白;而5.0寸的UI设计图运行到4.3寸的设施上,很可能显示不下。

屏幕品种这么多,那么就须要一套完满的计划去解决适配问题,介绍屏幕适配计划之前,先简略介绍下Android屏幕中用到的一些相干重要概念::**

屏幕尺寸

· 含意:手机对角线的物理尺寸

· 单位:英寸(inch),1英寸=2.54cm

Android手机常见的尺寸有5寸、5.5寸、6寸等等

屏幕分辨率

· 含意:手机在横向、纵向上的像素点数总和

  1. 个别形容成屏幕的"宽x高”=AxB
  2. 含意:屏幕在横向方向(宽度)上有A个像素点,在纵向方向(高)有B个像素点

    例子:1080x1920,即宽度方向上有1080个像素点,在高度方向上有1920个像素点

  • 单位:px(pixel),1px=1个像素点
UI设计师的设计图会以px作为对立的计量单位
  • Android手机常见的分辨率:320x480、480x800、720x1280、1080x1920、 1080x2340

屏幕像素密度

  • 含意:每英寸的像素点数
  • 单位:dpi(dots per ich)
假如设施内每英寸有160个像素,那么该设施的屏幕像素密度=160dpi
  • 安卓手机对于每类手机屏幕大小都有一个相应的屏幕像素密度:
密度类型代表的分辨率(px)屏幕密度(dpi)
低密度(ldpi)240x320120
中密度(mdpi)320x480160
高密度(hdpi)480x800240
超高密度(xhdpi)720x1280320
超超高密度(xxhdpi)1080x1920480

屏幕尺寸、分辨率、像素密度三者关系

一部手机的分辨率是宽*高,屏幕大小是以寸为单位,那么三者的关系是:

不懂没关系,在这里举个例子

假如一部手机的分辨率是1080x1920(px),屏幕大小是5寸,问密度是多少?

:请间接套公式

密度无关像素

  • 含意:density-independent pixel,叫dp或dip,与终端上的理论物理像素点无关。
  • 单位:dp,能够保障在不同屏幕像素密度的设施上显示雷同的成果

    1. Android开发时用dp而不是px单位设置图片大小,是Android特有的单位
    2. 场景:如果同样都是画一条长度是屏幕一半的线,如果应用px作为计量单位,那么在480x800分辨率手机上设置应为240px;在320x480的手机上应设置为160px,二者设置就不同了;如果应用dp为单位,在这两种分辨率下,160dp都显示为屏幕一半的长度。
  • dp与px的转换
    因为ui设计师给你的设计图是以px为单位的,Android开发则是应用dp作为单位的,那么咱们须要进行转换:
密度类型代表的分辨率(px)屏幕密度(dpi)换算(px/dp)
低密度(ldpi)240x3201201dp=0.75px
中密度(mdpi)320x4801601dp=1px
高密度(hdpi)480x8002401dp=1.5px
超高密度(xhdpi)720x12803201dp=2px
超超高密度(xxhdpi)1080x19204801dp=3px

在Android中,规定以160dpi(即屏幕分辨率为320x480)为基准:1dp=1px

独立比例像素

  • 含意:scale-independent pixel,叫sp或sip
  • 单位:sp

Android开发时用此单位设置文字大小,可依据字体大小首选项进行缩放。

举荐应用12sp、14sp、18sp、22sp作为字体设置的大小,不举荐应用奇数和小数,容易造成精度的失落问题;小于12sp的字体会太小导致用户看不清

请把下面的概念记住,因为上面解说都会用到!

适配计划比拟

1. dp原生计划

2. dimen基于px和dp的适配(宽高限定符和smallestWidth适配)

3. 头条屏幕适配计划

4. 头条适配计划改良版本

dp原生计划

前言:对立以px为单位有什么问题?

Android屏幕适配由来已久,关键在于屏幕尺寸与屏幕分辨率的变动微小,而很多UI工程师为了兼容iOS和Android的适配,这样导致设计进去的UI稿是以px单位标注的。在成千上百种机型背后,px单位已难以适应。

1.同样尺寸,不同分辨率:

1080px的宽度上显示100px 比例是100/1080

720px的宽度上显示100px 比例是100/720

2.同分辨率,不同尺寸:

1080px在4.7寸上显示100px

1080px在6.1寸上显示100px

如果应用多套px文件计划来适配,市面上少说上百种寸,须要的文件太多了

不同分辨率的屏幕该如何适配

这时候就须要用到dp计划来解决了,所以dp到底解决了什么问题?

以下公式示意了,同样尺寸上不同分辨率(不同density)的设施,每1dp所代表的像素数量是不一样的。

480 dpi上 1dp = 1 * 3 = 3px

320 dpi上 1dp = 1 * 2 = 2px

240 dpi上 1dp = 1 * 1.5 = 1.5px

160 dpi上 1dp = 1 * 1 = 1px

120 dpi上 1dp = 1 * 0.75 = 0.75px

然而所示意的物理长度(160dp=1in)是一样的。

160 dp在density=3上示意480px,物理长度为1 in

160 dp在density=2上示意320px,物理长度为1 in

160 dp在density=1.5上示意240px,物理长度为1 in

160 dp在density=1上示意160px,物理长度为1 in

160 dp在density=0.75上示意120px,物理长度为1 in

由上可知,dp单位的应用就意味着你在这些同样尺寸然而不同分辨率的设施上看到的大小一样,此时各设施上显示的比例也就统一了。

dp计划没有解决什么问题

举个例子:

屏幕分辨率为:1920*1080,屏幕尺寸为5吋的话,那么dpi为440。假如咱们UI设计图是按屏幕宽度为360dp来设计的,那这样会存在什么问题呢?

在上述设施上,屏幕宽度其实为1080/(440/160)=392.7dp,也就是屏幕是比设计图要宽的。这种状况下, 即便应用dp也是无奈在不同设施上显示为同样成果的。 同时还存在局部设施屏幕宽度有余360dp,这时就会导致按360dp宽度来开发理论显示不全的状况。

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

dimen基于px和dp的适配(宽高限定符和smallestWidth适配)

dimen基于dp适配 SmallestWidth限定符

原理:

这种适配根据的是最小宽度限定符。指的是Android会辨认屏幕可用高度和宽度的最小尺寸的dp值(其实就是手机的宽度值),而后依据辨认到的后果去资源文件中寻找对应限定符的文件夹下的资源文件。这种机制和上文提到的宽高限定符适配原理上是一样的,都是零碎通过特定的规定来抉择对应的文件。

举个例子,小米5的dpi是480,横向像素是1080px,依据px=dp(dpi/160),横向的dp值是1080/(480/160),也就是360dp,零碎就会去寻找是否存在value-sw360dp的文件夹以及对应的资源文件。

smallestWidth限定符适配和宽高限定符适配最大的区别在于,有很好的容错机制,如果没有value-sw360dp文件夹,零碎会向下寻找,比方离360dp最近的只有value-sw350dp,那么Android就会抉择value-sw350dp文件夹上面的资源文件。这个个性就完满的解决了上文提到的宽高限定符的容错问题。

毛病:

  • 侵入性强
  • Android 私人订制的起因,宽度方面参差不齐,不可能适配所有的手机。
  • 我的项目中减少了N个文件夹,上拉下拉查看文件十分不不便:想看string或者color资源文件须要拉很多再能达到。
  • 通过宽度限定符就近查找的原理,能够看进去匹配进去的大小不够精确。
  • 是在Android 3.2 当前引入的,Google的本意是用它来适配平板的布局文件(然而实际上显然用于diemns适配的成果更好),不过目前SPX所有的我的项目应该最低反对版本应该都是5.1了,所以这问题其实也不重要了。

dimens基于px的适配 宽高限定符适配

原理:

依据市面上手机分辨率的占比剖析,咱们选定一个占比例值大的(比方1280*720)设定为一个基准,而后其余分辨率依据这个基准做适配。

基准的意思(比方320*480的分辨率为基准)是:

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

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

例如对于800 * 480的分辨率设施来讲,须要在我的项目中values-800x480目录下的dimens.xml文件中的如下设置(当然了,能够通过工具主动生成):

<resources><dimen name="x1">1.5px</dimen><dimen name="x2">3.0px</dimen><dimen name="x3">4.5px</dimen><dimen name="x4">6.0px</dimen><dimen name="x5">7.5px</dimen></pre>

能够看到x1 = 480 / 基准 = 480 / 320 = 1.5 ;它的意思就是同样的1px,在320/480分辨率的手机上是1px,在480/800的分辨率的手机上就是1*1.5px,px会依据咱们指定的不同values文件夹主动适配为适合的大小。

验证计划:

简略通过计算验证下这种计划是否能达到适配的成果,例如设计图上有一个宽187dp的View。

分辨率为480 * 800

  • 设计图占宽比: 187dp / 375dp = 0.498
  • 理论在480 800占宽比 = 187 1.28px / 480 = 0.498

分辨率为1080 * 1920

  • 设计图占宽比: 187dp / 375dp = 0.498
  • 理论在1080 1920占宽比 = 187 2.88px / 1080 = 0.498
  • 计算高同理

毛病:

  • 侵入性强
  • 须要精准命中资源文件能力适配,比方1920x1080的手机就肯定要找到1920x1080的限定符,否则就只能用对立的默认的dimens文件了。而应用默认的尺寸的话,UI就很可能变形,简略说,就是容错机制很差。
  • Android不同分辨率的手机切实太多了,可能你说支流就能够,确实小公司支流就能够,淘宝这种App必定不能只适配支流手机。控件在设计图上显示的大小以及控件之间的间隙在小分辨率和大分辨率手机上天壤之别,你会发现大屏幕手机上控件超级大。可能你会感觉失常,毕竟分辨率不同。但实际效果大的有些夸大。
  • 占据资源大:好几百KB,甚至多达1M或跟多。

头条屏幕适配计划

梳理需要:

首先来梳理下咱们的需要,个别咱们设计图都是以固定的尺寸来设计的。比方以分辨率1920px 1080px来设计,以density为3来标注,也就是屏幕其实是640dp 360dp。如果咱们想在所有设施上显示完全一致,其实是不事实的,因为屏幕高宽比不是固定的,16:9、4:3甚至其余宽高比层出不穷,宽高比不同,显示完全一致就不可能了。然而通常下,咱们只须要以宽或高一个维度去适配,比方咱们Feed是高低滑动的,只须要保障在所有设施中宽的维度上显示统一即可,再比方一个不反对高低滑动的页面,那么须要保障在高这个维度上都显示统一,尤其不能存在某些设施上显示不全的状况。同时思考到当初根本都是以dp为单位去做的适配,如果新的计划不反对dp,那么迁徙老本也十分高。

因而,总结下大抵需要如下:

  • 反对以宽或者高一个维度去适配,放弃该维度上和设计图统一;
  • 反对dp和sp单位,管制迁徙老本到最小。

找计划兼容突破口

从dp和px的转换公式 :

$\color{red}{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相等,然而调节零碎字体大小后会扭转这个值

是不是Android中所有的dp和px的换算都是通过 DisplayMetrics 中相干的值来计算的呢?

  • 首先来看看布局文件中的dp转化,最终都是调用TypedValue#applyDimension来进行住转化

  • 图片的decode,BitmapFactory#decodeResourceStream办法:

当然还有些其余dp转换的场景,根本都是通过 DisplayMetrics 来计算的,这里不再详述。因而,想要满足上述需要,咱们只须要批改DisplayMetrics 中和 dp 转换相干的变量即可。

最终计划:

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

那么适配后 自定义density = 设施实在宽(单位px) / 360,接下来只须要把咱们计算好的 density 在零碎中批改下即可,代码实现如下:

同时在 Activity#onCreate 办法中调用下。代码比较简单,也没有波及到零碎非公开api的调用,因而实践上不会影响app稳定性。

毛病:

  • 只能反对以高或宽中的一个作为基准进行适配。
  • 只须要批改一次 density,我的项目中的所有中央都会主动适配,这个看似解放了双手,缩小了很多操作,然而实际上反馈了一个毛病,那就是只能一刀切的将整个我的项目进行适配,但适配范畴是不可控的。我的项目中如果采纳了零碎控件、三方库控件、等不是咱们我的项目本身设计的控件,这时就会呈现和咱们我的项目本身的设计图尺寸差距十分大的问题。

头条适配计划改良版本

大抵思路:在头条适配计划的根底上,通过重写Activity的getResources(),重写冷门单位pt作为基准单位,它是Android 中的一个长度单位:示意一个点,是屏幕的物理尺寸,其大小为 1 英寸的 1 / 72,也就是 72pt 等于 1 英寸

  • AdaptScreenUtils
public final class AdaptScreenUtils {private static List<Field> sMetricsFields;private AdaptScreenUtils() {    throw new UnsupportedOperationException("u can't instantiate me...");}/** * Adapt for the horizontal screen, and call it in {@link android.app.Activity#getResources()}. */public static Resources adaptWidth(final Resources resources, final int designWidth) {    float newXdpi = (resources.getDisplayMetrics().widthPixels * 72f) / designWidth;    applyDisplayMetrics(resources, newXdpi);    return resources;}/** * Adapt for the vertical screen, and call it in {@link android.app.Activity#getResources()}. */public static Resources adaptHeight(final Resources resources, final int designHeight) {    return adaptHeight(resources, designHeight, false);}/** * Adapt for the vertical screen, and call it in {@link android.app.Activity#getResources()}. */public static Resources adaptHeight(final Resources resources, final int designHeight, final boolean includeNavBar) {    float screenHeight = (resources.getDisplayMetrics().heightPixels            + (includeNavBar ? getNavBarHeight(resources) : 0)) * 72f;    float newXdpi = screenHeight / designHeight;    applyDisplayMetrics(resources, newXdpi);    return resources;}private static int getNavBarHeight(final Resources resources) {    int resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android");    if (resourceId != 0) {        return resources.getDimensionPixelSize(resourceId);    } else {        return 0;    }}/** * @param resources The resources. * @return the resource */public static Resources closeAdapt(final Resources resources) {    float newXdpi = Resources.getSystem().getDisplayMetrics().density * 72f;    applyDisplayMetrics(resources, newXdpi);    return resources;}/** * Value of pt to value of px. * * @param ptValue The value of pt. * @return value of px */public static int pt2Px(final float ptValue) {    DisplayMetrics metrics = FWAdSDK.sContext.getResources().getDisplayMetrics();    return (int) (ptValue * metrics.xdpi / 72f + 0.5);}/** * Value of px to value of pt. * * @param pxValue The value of px. * @return value of pt */public static int px2Pt(final float pxValue) {    DisplayMetrics metrics = FWAdSDK.sContext.getResources().getDisplayMetrics();    return (int) (pxValue * 72 / metrics.xdpi + 0.5);}private static void applyDisplayMetrics(final Resources resources, final float newXdpi) {    resources.getDisplayMetrics().xdpi = newXdpi;    FWAdSDK.sContext.getResources().getDisplayMetrics().xdpi = newXdpi;    applyOtherDisplayMetrics(resources, newXdpi);}static void preLoad() {    applyDisplayMetrics(Resources.getSystem(), Resources.getSystem().getDisplayMetrics().xdpi);}private static void applyOtherDisplayMetrics(final Resources resources, final float newXdpi) {    if (sMetricsFields == null) {        sMetricsFields = new ArrayList<>();        Class resCls = resources.getClass();        Field[] declaredFields = resCls.getDeclaredFields();        while (declaredFields != null && declaredFields.length > 0) {            for (Field field : declaredFields) {                if (field.getType().isAssignableFrom(DisplayMetrics.class)) {                    field.setAccessible(true);                    DisplayMetrics tmpDm = getMetricsFromField(resources, field);                    if (tmpDm != null) {                        sMetricsFields.add(field);                        tmpDm.xdpi = newXdpi;                    }                }            }            resCls = resCls.getSuperclass();            if (resCls != null) {                declaredFields = resCls.getDeclaredFields();            } else {                break;            }        }    } else {        applyMetricsFields(resources, newXdpi);    }}private static void applyMetricsFields(final Resources resources, final float newXdpi) {    for (Field metricsField : sMetricsFields) {        try {            DisplayMetrics dm = (DisplayMetrics) metricsField.get(resources);            if (dm != null) dm.xdpi = newXdpi;        } catch (Exception e) {            Log.e("AdaptScreenUtils", "applyMetricsFields: " + e);        }    }}private static DisplayMetrics getMetricsFromField(final Resources resources, final Field field) {    try {        return (DisplayMetrics) field.get(resources);    } catch (Exception e) {        Log.e("AdaptScreenUtils", "getMetricsFromField: " + e);        return null;    }}}
  • 应用办法
    以宽度320为基准进行适配
@Overridepublic Resources getResources() {    return AdaptScreenUtils.adaptWidth(super.getResources(),320);}

假如我当初须要在屏幕核心有个按钮,宽度和高度为咱们屏幕宽度的1/2,我能够怎么编写布局文件呢?

<FrameLayout><Button    android:layout_gravity="center"    android:gravity="center"    android:text="@string/hello_world"    android:layout_width="160pt"    android:layout_height="160pt"/></FrameLayout>

成果

480 x 800 - mdpi(160dpi)

720 x 1280 - xhdpi(320dpi)

1080 x 1920 - xxhdpi(480dpi)

能够看到效果图中 WebView 对之后的 View 并没有产生适配生效的问题,这是之前适配所不能解决的问题。

长处

1. 无侵入性
用了这个之后仍然能够应用dp包含其余任何单位,对从前应用的布局不会造成任何影响,在老我的项目中开发新性能你能够胆大地退出该适配计划,新我的项目的话更能够毫不犹豫地采纳该适配,并且在敞开该敞开后,pt 成果等同于 dp 哦。

2. 灵活性高
如果你想要对某个 View 做到不同分辨率的设施下,使其尺寸在适配维度上所占比例统一的话,那么对它应用 pt 单位即可,如果你不想要这样的成果,而是想要更大尺寸的设施显示更多的内容,那么能够像从前那样写 dpsp 什么的即可,联合这两点,在界面布局上你就能够熟能生巧地做到你想要的成果。

3. 不会影响零碎 View 和三方 View 的大小
这点其实在无侵入性中曾经体现进去了,因为头条的计划是间接批改 DisplayMetrics#densitydp 适配,这样会导致系统 View 尺寸和原先不统一,比方 DialogToast、 尺寸,同样,三方 View 的大小也会和原先成果不统一,这也就是抉择 pt 适配的起因之一。

4. 不会生效
因为不管头条的适配还是其余三方库适配,都会存在 DisplayMetrics#density 被还原的状况,须要本人从新设置回去,最显著的就是界面中存在 WebView 的话,因为其初始化的时候会还原 DisplayMetrics#density 的值导致适配生效,当然这点曾经有解决方案了,但还会有很多其余状况会还原 DisplayMetrics#density 的值导致适配生效。而我这计划就是为了解决这个痛点,不让 DisplayMetrics 中的值被还原导致适配生效。

毛病:

只能适配宽或者高其中一边,但这也是绝大部分适配计划的痛点所在,长和宽只能适配其一,好在大部分公司在采纳这些计划去适配是都采纳优先适配宽,而后在长下面以滑动模式去进行解决;

小结:

尽管 dimen基于px和dp的适配这种计划能涵盖市面上所有机型屏幕的适配,然而冗余的dimen文件会让工程师们生不如死,而且这种计划侵入性十分强,一旦应用将使得回退变得十分的艰难;头条适配计划和头条适配优化计划作为一种侵入性不是很强的形式进行接入,能完满解决代码冗余问题,而且总体方案灵活性很高,但只能抉择宽或者高作为惟一维度去进行适配;

上述计划都能用来解决屏幕适配问题,每种计划都有其独特的优缺点,因而最终选取哪种计划因人而异

参考文章:

Android屏幕适配和计划【整顿】

Android 屏幕适配:最全面的解决方案

Android 屏幕适配计划

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

Android据说你还在用dp单位做屏幕适配?

❤️ 谢谢反对

以上便是本次分享的全部内容,心愿对你有所帮忙^_^

喜爱的话别忘了 分享、点赞、珍藏 三连哦~。

欢送关注公众号 程序员巴士,一辆乏味、有范儿、有温度的程序员巴士,涉猎大厂面经、程序员生存、实战教程、技术前沿等内容,关注我,交个敌人。