高级UI---LSN-9-1-android屏幕适配全方位解析

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

1.屏幕适配概念

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

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

image.png

下面是IOS的

image.png

通过对比可以很明显知道adnroid的屏幕到底有多少种了吧。而苹果只有5种包括现在最新的刘海屏

那么想要对屏幕适配的相关处理方案有一定的自己的心得,那么首先我们需要了解关于android屏幕的一定基础

1.屏幕适配基础

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

屏幕适配.png

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

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

屏幕尺寸指的是:


屏幕尺寸.png

分辨率:


image.png

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

计算公式:sqrt(1080^2+1920^2)/屏幕尺寸

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

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

px:构成图像的最小单位
dip(重点):Desity Independent pixels的缩写,即密度无关像素
android内部在识别图像像素时以160dpi为基准,1dip=1px或1dp=1px
例:在下列两台设备上使用DP进行操作
480 * 320 160dpi 那么这台机器上的1DP会被翻译成1px
800 * 480 240dpi 而这台机器上的1DP会被翻译成1.5px
也就是说当前我们设备的DP是由android给予的基础标准按比例进行翻译的,这也是为什么我们用DP能解决一部分适配的原因

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,以此类推,可以继续往后增加,不过一般情况下已经够用了,这种用来去适配手机和平板之间的图形问题

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

2.1使用 "wrap_content" 和 "match_parent"
2.2相对布局控制屏幕
2.3. .9图的应用
上面三个都是最基本的android使用,我们只需要在平常应用是注意到就行了,这里不详细去讲

2.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.xml、
res/values-large/layout.xml、
res/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>

那么上述是最基本的屏幕适配的解决方案
这里找到一个神人给官方适配方案做的翻译推给大家参考
https://blog.csdn.net/wzy_1988/article/details/52932875

3.屏幕适配解决方案:

基础篇结束之后,我们市场上最常用的解决方案我给大家总结了两种
1.通过自定义布局组件来完成


有听过我公开课的同学应该知道我当时写了一套,其核心原理是根据一个参照分辨率进行布局,然后再各个机器上提取当前机器分辨率换算出系数之后,然后再通过重新测量的方式来达到适配的效果,这一套方案基本能适用于95以上的机型,那么今天到时候再加上刘海屏的适配就OK了。
下面是代码,
公开课地址:链接:https://pan.baidu.com/s/1xaRi6574Tq98wDvr19AOvA 密码:zcn5

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

}

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;

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

著作:Kerwin Barry
邮箱:kerwin0210@sina.com
原创博客,转载请注明出处.....

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容