Android应用篇 - UI 适配全集

今天来总结一下 Android 中的 UI 适配,主要从以下几个点来介绍:

  1. px、dp、dpi、density、sp
  2. 几种适配方案
  3. 图片资源适配

px、dp、dpi、density、sp

px :像素单位,比如我们通常说的手机分辨列表 800*400 都是 px 的单位。

dp :设备独立像素,以 dp 为尺寸单位的控件,在不同分辨率和尺寸的手机上代表了不同的真实像素,比如在分辨率较低的手机中,可能 1dp=1px,而在分辨率较高的手机中,可能 1dp=2px,这样的话,一个 96*96 dp 的控件,在不同的手机中就能表现出差不多的大小了。那么这个 dp 是如何计算的呢? 我们都知道一个公式:px = dp * (dpi/160)  系统都是通过这个来判断 px 和 dp 的数学关系。

dpi:像素密度,指的是在系统软件上指定的单位尺寸的像素数量,它往往是写在系统出厂配置文件的一个固定值。

density:设备的逻辑密度,是 dpi 的缩放因子。以 160 dpi 的屏幕为基线,density=dpi/160,所以 px = dp * density。

// 获取 density
getResources().getDisplayMetrics().density

sp:缩放独立像素,全称 scale independent pixel。类似于 dp,一般用于设置字体大小,可以根据用户设置的字体大小偏好来缩放。假如你不要字体大小随系统设置而改变,就直接使用 dp 做单位。

几种适配方案

大体的适配方案有以下几种:

  1. 根据设计图的尺寸,然后将设计图上标识的 px 尺寸,转化为百分比,为所有的主流屏幕去生成对应百分比的值,每个尺寸都会有一个 values/demins 文件。存在一些问题:产生大量的文件夹,适配不了特殊的尺寸 (必须建立默认的文件夹)。
  2. 基于 Google 推出的百分比布局 (percent-support-lib),已经很大程度解决了适配的问题。存在一些问题:使用起来比较反人类,因为设计图上标识的都是 px,所以需要去计算百分比,然后这个百分比还是依赖父容器的,设计图可能并不会将每个父容器的尺寸都标识出来,所有很难使用 (当然,有人已经采用自动化的工具去计算了)。还有个问题就是,因为依赖于父容器,导致ScrollView,ListView 等容器内高度无法使用百分比。
  3. AutoLayout 方案,直接布局中写 px,然后内部有程序会转换。
  4. 今日屏幕适配方案。

今天主要来讲讲后面两种。

1. AutoLayout

github: https://github.com/hongyangAndroid/AndroidAutoLayout

第一步,添加依赖:

dependencies {
    compile 'com.zhy:autolayout:1.4.5'
}

第二步,在 AndroidManifest 中注明你的设计稿的尺寸:

  <meta-data android:name="design_width" android:value="1080" />
  <meta-data android:name="design_height" android:value="1920" />

第三步,让你的 Activity 继承自 AutoLayoutActivity:

public class AdapterUIActivity extends AutoLayoutActivity {
}

如果你不希望继承 AutoLayoutActivity,可以在编写布局文件时,将

  • LinearLayout -> AutoLinearLayout
  • RelativeLayout -> AutoRelativeLayout
  • FrameLayout -> AutoFrameLayout

这样也可以完成适配。

目前支持属性:

  • layout_width
  • layout_height
  • layout_margin(left,top,right,bottom)
  • pading(left,top,right,bottom)
  • textSize
  • maxWidth, minWidth, maxHeight, minHeight

第四步,xml 中,宽高根据设计图标注的 px 写上即可:

  <Button
    android:layout_width="200px"
    android:layout_height="100px" />

可以看看它的源码,AutoLayoutActivity:

public class AutoLayoutActivity extends AppCompatActivity
{
    private static final String LAYOUT_LINEARLAYOUT = "LinearLayout";
    private static final String LAYOUT_FRAMELAYOUT = "FrameLayout";
    private static final String LAYOUT_RELATIVELAYOUT = "RelativeLayout";


    @Override
    public View onCreateView(String name, Context context, AttributeSet attrs)
    {
        View view = null;
        if (name.equals(LAYOUT_FRAMELAYOUT))
        {
            view = new AutoFrameLayout(context, attrs);
        }

        if (name.equals(LAYOUT_LINEARLAYOUT))
        {
            view = new AutoLinearLayout(context, attrs);
        }

        if (name.equals(LAYOUT_RELATIVELAYOUT))
        {
            view = new AutoRelativeLayout(context, attrs);
        }

        if (view != null) return view;

        return super.onCreateView(name, context, attrs);
    }
}

在 AppCompatActivity 解析 xml 后,构造 View 时,会回调 onCreateView,这边可以实现一个拦截,构造自己的 View。拿 AutoFrameLayout 为例,继续看:

public class AutoFrameLayout extends FrameLayout
{
    private final AutoLayoutHelper mHelper = new AutoLayoutHelper(this);

    public AutoFrameLayout(Context context)
    {
        super(context);
    }

    public AutoFrameLayout(Context context, AttributeSet attrs)
    {
        super(context, attrs);
    }

    public AutoFrameLayout(Context context, AttributeSet attrs, int defStyleAttr)
    {
        super(context, attrs, defStyleAttr);
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public AutoFrameLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs)
    {
        return new LayoutParams(getContext(), attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
    {
        if (!isInEditMode())
        {
            mHelper.adjustChildren();
        }
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom)
    {
        super.onLayout(changed, left, top, right, bottom);
    }

    public static class LayoutParams extends FrameLayout.LayoutParams
            implements AutoLayoutHelper.AutoLayoutParams
    {
        private AutoLayoutInfo mAutoLayoutInfo;

        public LayoutParams(Context c, AttributeSet attrs)
        {
            super(c, attrs);

            mAutoLayoutInfo = AutoLayoutHelper.getAutoLayoutInfo(c, attrs);
        }

        public LayoutParams(int width, int height)
        {
            super(width, height);
        }

        public LayoutParams(int width, int height, int gravity)
        {
            super(width, height, gravity);
        }

        public LayoutParams(ViewGroup.LayoutParams source)
        {
            super(source);
        }

        public LayoutParams(MarginLayoutParams source)
        {
            super(source);
        }

        public LayoutParams(FrameLayout.LayoutParams source)
        {
            super((MarginLayoutParams) source);
            gravity = source.gravity;
        }

        public LayoutParams(LayoutParams source)
        {
            this((FrameLayout.LayoutParams) source);
            mAutoLayoutInfo = source.mAutoLayoutInfo;
        }

        @Override
        public AutoLayoutInfo getAutoLayoutInfo()
        {
            return mAutoLayoutInfo;
        }
    }
}

最后调用的核心代码:

    public static int getPercentWidthSizeBigger(int val)
    {
        int screenWidth = AutoLayoutConifg.getInstance().getScreenWidth();
        int designWidth = AutoLayoutConifg.getInstance().getDesignWidth();

        int res = val * screenWidth;
        if (res % designWidth == 0)
        {
            return res / designWidth;
        } else
        {
            return res / designWidth + 1;
        }

    }

这边会根据屏幕宽高和设计图的宽高做一个比例换算,最终应用到控件的各种属性上,有点类似换肤的设计。但是这种方案目前在 issues 中反应出了一些问题:

首先一个问题就是不继承 view 的控件例如:alertDialog 或者 popupwindow 无法适配。还有一个问题是:这个适配方案没有考虑 statusbar 和 navagation bar,在带虚拟按键的手机上面会很明显 (目前已解决,提供了 API 设置)。

2. 今日屏幕适配方案

今日头条屏幕适配方案的核心原理在于,根据以下公式算出 density:

当前设备屏幕总宽度 (单位为像素) / 设计图总宽度 (单位为 dp) = density

density 的意思就是 1 dp 占当前设备多少像素。为什么要算出 density,这和屏幕适配有什么关系呢?

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

大家都知道,不管你在布局文件中填写的是什么单位,最后都会被转化为 px,系统就是通过上面的方法,将你在项目中任何地方填写的单位都转换为 px 的。

所以我们常用的 px 转 dp 的公式 dp = px / density,就是根据上面的方法得来的,density 在公式的运算中扮演着至关重要的一步。要看懂下面的内容,还得明白,今日头条的适配方式,今日头条适配方案默认项目中只能以高或宽中的一个作为基准,进行适配,为什么不像 AndroidAutoLayout 一样,高以高为基准,宽以宽为基准,同时进行适配呢?

这就引出了一个现在比较棘手的问题,大部分市面上的 Android 设备的屏幕高宽比都不一致,特别是现在大量全面屏的问世,这个问题更加严重,不同厂商推出的全面屏手机的屏幕高宽比都可能不一致。这时我们只以高或宽其中的一个作为基准进行适配,就会有效的避免布局在高宽比不一致的屏幕上出现变形的问题,明白这个后,我再来说说 density,density 在每个设备上都是固定的,DPI / 160 = density,屏幕的总 px 宽度 / density = 屏幕的总 dp 宽度:

  • 设备 1,屏幕宽度为 1080px,480DPI,屏幕总 dp 宽度为 1080 / (480 / 160) = 360 dp。
  • 设备 2,屏幕宽度为 1440px,560DPI,屏幕总 dp 宽度为 1440 / (560 / 160) = 411 dp。

可以看到屏幕的总 dp宽度在不同的设备上是会变化的,但是我们在布局中填写的 dp 值却是固定不变的,这会导致什么呢?假设我们布局中有一个 View 的宽度为 100dp,在设备 1 中 该 View 的宽度占整个屏幕宽度的 27.8%(100 / 360 = 0.278),但在设备 2 中该 View 的宽度就只能占整个屏幕宽度的 24.3%(100 / 411 = 0.243),可以看到这个 View 在像素越高的屏幕上,dp 值虽然没变,但是与屏幕的实际比例却发生了较大的变化,所以肉眼的观看效果,会越来越小,这就导致了传统的填写 dp 的屏幕适配方式产生了较大的误差。

这时我们要想完美适配,那就必须保证这个 View 在任何分辨率的屏幕上,与屏幕的比例都是相同的。这时我们该怎么做呢?改变每个 View 的 dp 值?不现实,在每个设备上都要通过代码动态计算 View 的 dp 值,工作量太大。如果每个 View 的 dp 值是固定不变的,那我们只要保证每个设备的屏幕总 dp 宽度不变,就能保证每个 View 在所有分辨率的屏幕上与屏幕的比例都保持不变,从而完成等比例适配,并且这个屏幕总 dp 宽度如果还能保证和设计图的宽度一致的话,那我们在布局时就可以直接按照设计图上的尺寸填写 dp 值。

屏幕的总 px 宽度 / density = 屏幕的总 dp 宽度

在这个公式中我们要保证屏幕的总 dp 宽度和设计图总宽度一致,并且在所有分辨率的屏幕上都保持不变,我们需要怎么做呢?屏幕的总 px 宽度每个设备都不一致,这个值是肯定会变化的,这时今日头条的公式就派上用场了。

当前设备屏幕总宽度 (单位为像素) / 设计图总宽度 (单位为 dp) = density

这个公式就是把上面公式中的屏幕的总 dp 宽度换成设计图总宽度,原理都是一样的,只要 density 根据不同的设备进行实时计算并作出改变,就能保证设计图总宽度不变,也就完成了适配。

Android屏幕适配方案(出自今日头条)

图片资源适配

mipmap:

采用 Android Studio 开发 Android APP,在项目的 res 目录下,会多出几个以 mipmap 开头的文件夹。


根据 Android 官方的描述,mipmap 仅仅用于存放 app 启动图标,可由 Image Asset Studio 生成。

drawable:

Android 系统可以在具有不同屏幕尺寸和密度的设备上运行,并将每个应用的用户界面调整为适应其显示的屏幕,会进行缩放和大小调整。为了最大程序优化更多设备上的用户体验,开发者需要针对不同的屏幕尺寸和密度优化应用。对于 Android 智能手机来说,屏幕大小、分辨率、密度均不尽相同,那么图片适配就成了 Android 中优化应用必不可少的环节之一。

  • ldpi(低)    ~120dpi
  • mdpi(中)    ~160dpi
  • hdpi(高)    ~240dpi
  • xhdpi(超高)    ~320dpi
  • xxhdpi(超超高)    ~480dpi
  • xxxhdpi(超超超高)    ~640dpi

Android 为了更好地优化应用在不同屏幕密度下的用户体验,在项目的 res 目录下可以创建 drawab-[density] (density 为6种通用密度名) 目录,开发者在进行 app 开发时,针对不同的屏幕密度,将图片放置于对应的 drawable-[density] 目录,Android 系统会依据特定的原则来查找各 drawable 目录下的图片。查找流程为: 

  • 1. 先查找和屏幕密度最匹配的文件夹。如当前设备屏幕密度 dpi 为 160,则会优先查找 drawable-mdpi 目录;如果设备屏幕密度 dpi 为 420,则会优先查找 drawable-xxhdpi 目录。 
  • 2. 如果在最匹配的目录没有找到对应图片,就会向更高密度的目录查找,直到没有更高密度的目录。例如,在最匹配的目录drawable-mdpi 中没有查找到,就会查找 drawable-hdpi 目录,如果还没有查找到,就会查找 drawable-xhdpi 目录,直到没有更高密度的 drawable-[density] 目录。 
  • 3. 如果一直往高密度目录均没有查找,Android 就会查找 drawable-nodpi 目录。drawable-nodpi 目录中的资源适用于所有密度的设备,不管当前屏幕的密度如何,系统都不会缩放此目录中的资源。因此,对于永远不希望系统缩放的资源,最简单的方法就是放在此目录中;同时,放在该目录中的资源最好不要再放到其他 drawable 目录下了,避免得到非预期的效果。 
  • 4. 如果在 drawable-nodpi 目录也没有查找到,系统就会向比最匹配目录密度低的目录依次查找,直到没有更低密度的目录。例如,最匹配目录是 xxhdpi,更高密度的目录和 nodpi 目录查找不到后,就会依次查找 drawable-xhdp、drawable-hdpi、drawable-mdpi、drawable-ldpi。

举个例子,假如当前设备的 dpi 是 320,系统会优先去 drawable-xhdpi 目录查找,如果找不到,会依次查找 xxhdpi → xxxhdpi → nodpi hdpi → hdpi → mdpi → ldpi。对于不存在的 drawable-[density] 目录直接跳过,中间任一目录查找到资源,则停止本次查找。

图片的缩放:

前述说到 Android 为了能够更好地适配各种屏幕,会依据当前设备的 dpi 对 drawable-[density] 目录中的图片进行缩放,那么什么情况下图片被放大,什么情况下图片被缩小呢?

为了更好的描述,把"符合当前设备 dpi 的 drawable 目录"表示为"匹配目录"。比如,设备的 dpi 为 320,这匹配目录为drawable-xhdpi;设备的 dpi 为 150,则匹配目录为 drawable-mdpi。图片的放大和缩小遵循以下规律:

  • 如果图片所在目录为匹配目录,则图片会根据设备 dpi 做适当的缩放调整。
  • 如果图片所在目录 dpi 低于匹配目录,那么该图片被认为是为低密度设备需要的,现在要显示在高密度设备上,图片会被放大。
  • 如果图片所在目录 dpi 高于匹配目录,那么该图片被认为是为高密度设备需要的,现在要显示在低密度设备上,图片会被缩小。
  • 如果图片所在目录为 drawable-nodpi,则无论设备 dpi 为多少,保留原图片大小,不进行缩放。

那么六种通用密度下的缩放倍数是多少呢?以 mdpi 为基线,各密度目录下的放大倍数 (即缩放因子 density) 如下:

密度 放大倍数
ldpi 0.75
mdpi 1.0
hdpi 1.5
xhdpi 2.0
xxhdpi 3.0
xxxhdpi 4.0

公式为:

缩放因子计算公式

猜你喜欢

转载自blog.csdn.net/u014294681/article/details/89211744