高级UI之Android屏幕适配全方位解析

1,361 阅读9分钟

前言

前面我们已经将android的绘制基础已经讲完,那么现在我们下面的两个内容点是事件分发问题,和屏幕适配相关。这篇我们主要来进android但中的各种屏幕适配问题

1.屏幕适配概念

而随着支持Android系统的设备(手机、平板、电视、手表)的增多,设备碎片化、品牌碎片化、系统碎片化、传感器碎片化和屏幕碎片化的程度也在不断地加深。而我们今天要探讨的,则是对我们开发影响比较大的——屏幕的碎片化。

下面这张图是Android屏幕尺寸的示意图,在这张图里面,蓝色矩形的大小代表不同尺寸,颜色深浅则代表所占百分比的大小。

image.png

下面是IOS的

image.png

通过对比可以很明显知道adnroid的屏幕到底有多少种了吧,而苹果只有5种包括现在最新的刘海屏。那么想要对屏幕适配的相关处理方案有一定的自己的心得,那么首先我们需要了解关于android屏幕的一定基础

2.屏幕适配基础

那么下面是我给大家写的一个屏幕适配基础的思维导图,基本为一个基础篇的大纲,这里我不会非常详细的给大家去过,就全部体现在脑图当中

image.png

那么屏幕适配相关概念上我们需要掌握最基础的3点,相对基础的内容是给段位比较低的同学,高段位可选择跳过

2.1 什么是屏幕尺寸,屏幕分辨率,屏幕像素密度

屏幕尺寸指的是:

image.png

分辨率:

image.png

屏幕像素密度(DPI)指每一英寸长度中,可显示输出的像素个数,DPI的数字受屏幕尺寸和分辨率所影响,DPI可以通过计算所得

image.png

上述内容在于扫盲..毕竟还是有不清楚的同学,而DPI跟下面内容结合比较密切所以啰嗦了两句

2.2 什么是dp,dip,sp,px?它们之间的关系?

  • px:构成图像的最小单位
  • dip(重点):Desity Independent pixels的缩写,即密度无关像素

android内部在识别图像像素时以160dpi为基准,1dip=1px或1dp=1px。例:在下列两台设备上使用DP进行操作

  1. 480 * 320 160dpi 那么这台机器上的1DP会被翻译成1px
  2. 800 * 480 240dpi 而这台机器上的1DP会被翻译成1.5px

也就是说当前我们设备的DP是由android给予的基础标准按比例进行翻译的,这也是为什么我们用DP能解决一部分适配的原因

2.3 mdpi,hdpi,xdpi,xxdpi,xxxdpi?如何计算和区分?

  名称	             像素密度范围	        图片大小
  mdpi	               120dp~160dp	       48×48px
  hdpi	               160dp~240dp	       72×72px
  xhdpi	               240dp~320dp	       96×96px
  xxhdpi	           320dp~480dp	       144×144px
  xxxhdpi	           480dp~640dp	       192×192px

在Google官方开发文档中,说明了mdpi:hdpi:xhdpi:xxhdpi:xxxhdpi=2:3:4:6:8 的尺寸比例进行缩放。例如,一个图标的大小为48×48dp,表示在mdpi上,实际大小为48×48px,在hdpi像素密度上,实际尺寸为mdpi上的1.5倍,即72×72px,以此类推,可以继续往后增加,不过一般情况下已经够用了,这种用来去适配手机和平板之间的图形问题

3.屏幕适配方案基础篇(常识,见思维导图,这里只详细讲一下限定符)

3.1使用 "wrap_content" 和 "match_parent"

3.2相对布局控制屏幕

3.3图的应用

上面三个都是最基本的android使用,我们只需要在平常应用是注意到就行了,这里不详细去讲

3.4 限定符

我们在做屏幕的适配时在屏幕 尺寸相差不大的情况下,dp可以使不同分辨率的设备上展示效果相似。但是在屏幕尺寸相差比较大的情况下(平板),dp就失去了这种效果。所以需要以下的限定符来约束,采用多套布局,数值等方式来适配。

那么其实所谓的限定符就是android在进行资源加载的时候会按照屏幕的相关信息对文件夹对应的名字进行识别,而这些特殊名字就是我们的限定符

限定符分类:
    屏幕尺寸	
        small	小屏幕
     	normal	基准屏幕
     	large	大屏幕
     	xlarge	超大屏幕
    屏幕密度
      	ldpi	<=120dpi
     	mdpi	<= 160dpi
     	hdpi	<= 240dpi
     	xhdpi	<= 320dpi
     	xxhdpi	<= 480dpi
     	xxhdpi	<= 640dpi(只用来存放icon)
     	nodpi	与屏幕密度无关的资源.系统不会针对屏幕密度对其中资源进行压缩或者拉伸
     	tvdpi	介于mdpi与hdpi之间,特定针对213dpi,专门为电视准备的,手机应用开发不需要关心这个密度值.
    屏幕方向	
        land	横向
     	port	纵向
    屏幕宽高比	
        long	比标准屏幕宽高比明显的高或者宽的这样屏幕
     	notlong	和标准屏幕配置一样的屏幕宽高比

2.4.1使用尺寸限定符:

当我们要在大屏幕上显示不同的布局,就要使用large限定符。例如,在宽的屏幕左边显示列表右边显示列表项的详细信息,在一般宽度的屏幕只显示列表,不显示列表项的详细信息,我们就可以使用large限定符。

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="fill_parent"
          android:name="com.example.android.newsreader.HeadlinesFragment"
          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:name="com.example.android.newsreader.HeadlinesFragment"
          android:layout_width="400dp"
          android:layout_marginRight="10dp"/>
<!-- 列表项的详细信息 -->
<fragment android:id="@+id/article"
          android:layout_height="fill_parent"
          android:name="com.example.android.newsreader.ArticleFragment"
          android:layout_width="fill_parent" />
</LinearLayout>

如果这个程序运行在屏幕尺寸大于7inch的设备上,系统就会加载res/layout-large/main.xml 而不是res/layout/main.xml,在小于7inch的设备上就会加载res/layout/main.xml。

需要注意的是,这种通过large限定符分辨屏幕尺寸的方法,适用于android3.2之前。在android3.2之后,为了更精确地分辨屏幕尺寸大小,Google推出了最小宽度限定符。

2.4.2 使用最小宽度限定符

最小宽度限定符的使用和large基本一致,只是使用了具体的宽度限定。

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="fill_parent"
          android:name="com.example.android.newsreader.HeadlinesFragment"
          android:layout_width="match_parent" />
</LinearLayout>

res/layout-sw600dp/main.xml,双面板布局: Small Width 最小宽度

<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:name="com.example.android.newsreader.HeadlinesFragment"
          android:layout_width="400dp"
          android:layout_marginRight="10dp"/>
<fragment android:id="@+id/article"
          android:layout_height="fill_parent"
          android:name="com.example.android.newsreader.ArticleFragment"
          android:layout_width="fill_parent" />
</LinearLayout>

这种方式是不区分屏幕方向的。这种最小宽度限定符适用于android3.2之后,所以如果要适配android全部的版本,就要使用large限定符和sw600dp文件同时存在于项目res目录下。

这就要求我们维护两个相同功能的文件。为了避免繁琐操作,我们就要使用布局别名。

2.4.3使用布局别名

  • res/layout/main.xml: 单面板布局
  • res/layout-large/main.xml: 多面板布局
  • res/layout-sw600dp/main.xml: 多面板布局

由于后两个文具文件一样,我们可以用以下两个文件代替上面三个布局文件:

  • res/layout/main.xml 单面板布局
  • res/layout/main_twopanes.xml 双面板布局

然后在res下建立res/values/layout.xmlres/values-large/layout.xmlres/values-sw600dp/layout.xml三个文件。

**默认布局:**res/values/layout.xml:

<resources>
    <item name="main" type="layout">@layout/main</item>
</resources>

**Android3.2之前的平板布局:**res/values-large/layout.xml:

<resources>
    <item name="main" type="layout">@layout/main_twopanes</item>
</resources>

**Android3.2之后的平板布局:**res/values-sw600dp/layout.xml:

<resources>
    <item name="main" type="layout">@layout/main_twopanes</item>
</resources>

这样就有了main为别名的布局。

在activity中setContentView(R.layout.main); 这样,程序在运行时,就会检测手机的屏幕大小,如果是平板设备就会加载res/layout/main_twopanes.xml,如果是手机设备,就会加载res/layout/main.xml 。我们就解决了只使用一个布局文件来适配android3.2前后的所有平板设备。

2.4.4使用屏幕方向限定符

如果我们要求给横屏、竖屏显示的布局不一样。就可以使用屏幕方向限定符来实现。例如,要在平板上实现横竖屏显示不用的布局,可以用以下方式实现。

res/values-sw600dp-land/layouts.xml:横屏

<resources>
    <item name="main" type="layout">@layout/main_twopanes</item>
</resources>

res/values-sw600dp-port/layouts.xml:竖屏

<resources>
    <item name="main" type="layout">@layout/main</item>
</resources>

那么上述是最基本的屏幕适配的解决方案

这里找到一个神人给官方适配方案做的翻译推给大家参考:blog.csdn.net/wzy_1988/ar…

4.屏幕适配解决方案:

基础篇结束之后,我们市场上最常用的解决方案我给大家总结了两种

4.1 通过自定义布局组件来完成

核心原理是根据一个参照分辨率进行布局,然后再各个机器上提取当前机器分辨率换算出系数之后,然后再通过重新测量的方式来达到适配的效果,这一套方案基本能适用于95以上的机型,到时候再加上刘海屏的适配就OK了。

  /**
   * Created by barry on 2018/6/7.
   */
public class ScreenAdaptationRelaLayout extends RelativeLayout {
public ScreenAdaptationRelaLayout(Context context) {
    super(context);
}

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

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

static boolean isFlag = true;

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {


    if(isFlag){
        int count = this.getChildCount();
        float scaleX =  UIUtils.getInstance(this.getContext()).getHorizontalScaleValue();
        float scaleY =  UIUtils.getInstance(this.getContext()).getVerticalScaleValue();

        Log.i("testbarry","x系数:"+scaleX);
        Log.i("testbarry","y系数:"+scaleY);
        for (int i = 0;i < count;i++){
            View child = this.getChildAt(i);
            //代表的是当前空间的所有属性列表
            LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
            layoutParams.width = (int) (layoutParams.width * scaleX);
            layoutParams.height = (int) (layoutParams.height * scaleY);
            layoutParams.rightMargin = (int) (layoutParams.rightMargin * scaleX);
            layoutParams.leftMargin = (int) (layoutParams.leftMargin * scaleX);
            layoutParams.topMargin = (int) (layoutParams.topMargin * scaleY);
            layoutParams.bottomMargin = (int) (layoutParams.bottomMargin * scaleY);
        }
        isFlag = false;
    }



    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
}
}




public class UIUtils {

private Context context;

private static UIUtils utils ;

public static UIUtils getInstance(Context context){
    if(utils == null){
        utils = new UIUtils(context);
    }
    return utils;
}


//参照宽高
public final float STANDARD_WIDTH = 720;
public final float STANDARD_HEIGHT = 1232;

//当前设备实际宽高
public float displayMetricsWidth ;
public float displayMetricsHeight ;

private  final String DIMEN_CLASS = "com.android.internal.R$dimen";


private UIUtils(Context context){
    this.context = context;
    //
    WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);

    //加载当前界面信息
    DisplayMetrics displayMetrics = new DisplayMetrics();
    windowManager.getDefaultDisplay().getMetrics(displayMetrics);

    if(displayMetricsWidth == 0.0f || displayMetricsHeight == 0.0f){
        //获取状态框信息
        int systemBarHeight = getValue(context,"system_bar_height",48);

        if(displayMetrics.widthPixels > displayMetrics.heightPixels){
            this.displayMetricsWidth = displayMetrics.heightPixels;
            this.displayMetricsHeight = displayMetrics.widthPixels - systemBarHeight;
        }else{
            this.displayMetricsWidth = displayMetrics.widthPixels;
            this.displayMetricsHeight = displayMetrics.heightPixels - systemBarHeight;
        }

    }
}

//对外提供系数
public float getHorizontalScaleValue(){
    return displayMetricsWidth / STANDARD_WIDTH;
}

public float getVerticalScaleValue(){

    Log.i("testbarry","displayMetricsHeight:"+displayMetricsHeight);
    return displayMetricsHeight / STANDARD_HEIGHT;
}



public int getValue(Context context,String systemid,int defValue) {

    try {
        Class<?> clazz = Class.forName(DIMEN_CLASS);
        Object r = clazz.newInstance();
        Field field = clazz.getField(systemid);
        int x = (int) field.get(r);
        return context.getResources().getDimensionPixelOffset(x);

    } catch (Exception e) {
       return defValue;
    }
}

}

4.2 给各个分辨率单独适配,res,dimens里设置各个对应的px,再统一调用,由系统筛选。

这种方式比较久远了,但是确实还是有很多项目在使用到这种方式。其原理就是据设备屏幕的分辨率各自写一套dimens.xml文件,然后根据一个基准分辨率(例如720x1080),将宽度分成720份,取值为1px——720px,将高度分成1080份,取值为1px——1080px。生成各自dimens.xml文件对应的值。

但是今天我根据这个方法,在这个方案的基础之上给大家做了一次改变,运用之前所见的DP的概念,结合之前讲的限定符,用DP来升级了这种方案,dp适配原理与px适配一样,区别就在于px适配是根据屏幕分辨率,即拿px值等比例缩放,而dp适配是拿dp值来等比缩放而已。

既然原理都一样,都需要多套dimens.xml文件,为什么说dp适配就比px适配好呢?因为px适配是根据屏幕分辨率的,Android设备分辨率一大堆,而且还要考虑虚拟键盘。而dp适配无论手机屏幕的像素多少,密度比值多少,80%的手机的最小宽度dp值(widthPixels / density)都为360dp,这样就大大减少了dimens.xml文件

PS:(现在基本上手机的dpi都在350+以上 那么按最低算 350/160=2.1 那么360 * 2.1 = 720+ 基本上手机的分辨率都会在360dp之内 上面例子19201080的情况 500/160=3.125 那么 3603.125=1125其实也在360之内),

传统做法:

image.png

改良后的做法:

image.png

获取最小宽度获取如下:

    DisplayMetrics dm = new DisplayMetrics();

    getWindowManager().getDefaultDisplay().getMetrics(dm);

    int widthPixels = dm.widthPixels;

    float density = dm.density;

    float widthDP = widthPixels / density;

所以通过这种两种形式的结合能够达到我们整体适配任意机型的目的

看完三件事❤️

如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

  1. 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  2. 关注公众号 『 小新聊Android 』,不定期分享原创知识
  3. 同时可以期待后续文章ing🚀