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

前言

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

1.屏幕适配概念

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

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

下面是IOS的

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

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

1.屏幕适配基础

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

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

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

屏幕尺寸指的是:

分辨率:

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

上述内容在于扫盲…毕竟还是有不清楚的同学,而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了。

  /**
   * 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之内)

那么传统做法:

改良后的做法:

获取最小宽度获取如下:

    DisplayMetrics dm = new DisplayMetrics();

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

    int widthPixels = dm.widthPixels;

    float density = dm.density;

    float widthDP = widthPixels / density;

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

更多Android 知识点可参考

Android 性能调优系列https://0a.fit/dNHYY

Android 车载学习指南https://0a.fit/jdVoy

Android Framework核心知识点笔记https://0a.fit/acnLL

Android 八大知识体系https://0a.fit/mieWJ

Android 中高级面试题锦https://0a.fit/YXwVq

原文地址:https://blog.csdn.net/weixin_61845324

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


更新Android SDK到3.0版本时,遇到Failed to rename directory E:\android\tools to E:\android\temp\ToolPackage.old01问题,导致无法更新,出现该问题的原因是由于3.0版本与较早的sdk版本之间文件结构有冲突,解决
Android 如何解决dialog弹出时无法捕捉Activity的back事件 在一些情况下,我们需要捕捉back键事件,然后在捕捉到的事件里写入我们需要进行的处理,通常可以采用下面三种办法捕捉到back事件: 1)重写onKeyDown或者onKeyUp方法 2)重写onBackPressed方
Android实现自定义带文字和图片的Button 在Android开发中经常会需要用到带文字和图片的button,下面来讲解一下常用的实现办法。一.用系统自带的Button实现 最简单的一种办法就是利用系统自带的Button来实现,这种方式代码量最小。在Button的属性中有一个是drawable
Android中的&quot;Unable to start activity ComponentInfo&quot;的错误 最近在做一款音乐播放器的时候,然后在调试的过程中发现一直报这个错误&quot;Unable to start activity ComponentInfo&quot;,从字面
Android 关于长按back键退出应用程序的实现最近在做一个Android上的应用,碰到一个问题就是如何实现长按back键退出应用程序。在网上查找了很多资料,发现几乎没有这样的实现,大部分在处理时是双击back键来退出应用程序。参考了一下双击back键退出应用程序的代码,网上主流的一种方法是下面
android自带的时间选择器只能精确到分,但是对于某些应用要求选择的时间精确到秒级,此时只有自定义去实现这样的时间选择器了。下面介绍一个可以精确到秒级的时间选择器。 先上效果图: 下面是工程目录: 这个控件我也是用的别人的,好像是一个老外写的,com.wheel中的WheelView是滑动控件的主
Android平台下利用zxing实现二维码开发 现在走在大街小巷都能看到二维码,而且最近由于项目需要,所以研究了下二维码开发的东西,开源的二维码扫描库主要有zxing和zbar,zbar在iPos平台上应用比较成熟,而在Android平台上主流还是用zxing库,因此这里主要讲述如何利用zxing
Android ListView的item背景色设置以及item点击无响应等相关问题 在Android开发中,listview控件是非常常用的控件,在大多数情况下,大家都会改掉listview的item默认的外观,下面讲解以下在使用listview时最常见的几个问题。1.如何改变item的背景色和按
如何向Android模拟器中导入含有中文名称的文件在进行Android开发的时候,如果需要向Android模拟器中导入文件进行测试,通过DDMS下手动导入或者在命令行下通过adb push命令是无法导入含有中文文件名的文件的。后来发现借用其他工具可以向模拟器中导入中文名称的文件,这个工具就是Ultr
Windows 下搭建Android开发环境一.下载并安装JDK版本要求JDK1.6+,下载JDK成功后进行安装,安装好后进行环境变量的配置【我的电脑】-——&gt;【属性】——&gt;【高级】 ——&gt;【环境变量】——&gt;【系统变量】中点击【新建】:变量名:CLASSPATH变量值:……
如何利用PopupWindow实现弹出菜单并解决焦点获取以及与软键盘冲突问题 在android中有时候可能要实现一个底部弹出菜单,此时可以考虑用PopupWindow来实现。下面就来介绍一下如何使用PopupWindow实现一个弹出窗。 主Activity代码:public void onCreat
解决Android中的ERROR: the user data image is used by another emulator. aborting的方法 今天调试代码的时候,突然出现这个错误,折腾了很久没有解决。最后在google上找到了大家给出的两种解决方案,下面给出这两种方法的链接博客:ht
AdvserView.java package com.earen.viewflipper; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory;
ImageView的scaleType的属性有好几种,分别是matrix(默认)、center、centerCrop、centerInside、fitCenter、fitEnd、fitStart、fitXY。 |值|说明| |:--:|:--| |center|保持原图的大小,显示在ImageVie
文章浏览阅读8.8k次,点赞9次,收藏20次。本文操作环境:win10/Android studio 3.21.环境配置 在SDK Tools里选择 CMAKE/LLDB/NDK点击OK 安装这些插件. 2.创建CMakeLists.txt文件 在Project 目录下,右键app,点击新建File文件,命名为CMakeLists.txt点击OK,创建完毕! 3.配置文件 在CMa..._link c++ project with gradle
文章浏览阅读1.2w次,点赞15次,收藏69次。实现目的:由mainActivity界面跳转到otherActivity界面1.写好两个layout文件,activity_main.xml和otherxml.xmlactivity_main.xml&lt;?xml version="1.0" encoding="utf-8"?&gt;&lt;RelativeLayout ="http://schemas..._android studio 界面跳转
文章浏览阅读3.8w次。前言:最近在找Android上的全局代理软件来用,然后发现了这两款神作,都是外国的软件,而且都是开源的软件,因此把源码下载了下来,给有需要研究代理这方面的童鞋看看。不得不说,国外的开源精神十分浓,大家相互使用当前基础的开源软件,然后组合成一个更大更强的大开源软件。好吧,废话不多说,下面简单介绍一下这两款开源项目。一、ProxyDroid:ProxyDroid功能比较强大,用到的技术也比较多,源码也_proxydroid
文章浏览阅读2.5w次,点赞17次,收藏6次。创建项目后,运行项目时Gradle Build 窗口却显示错误:程序包R不存在通常情况下是不会出现这个错误的。我是怎么遇到这个错误的呢?第一次创建项目,company Domain我使用的是:aven.com,但是创建过程在卡在了Building 'Calculator' Gradle Project info这个过程中,于是我选择了“Cancel”第二次创建项目,我还是使用相同的项目名称和项目路_r不存在
文章浏览阅读8.9w次,点赞4次,收藏43次。前言:在Android上使用系统自带的代理,限制灰常大,仅支持系统自带的浏览器。这样像QQ、飞信、微博等这些单独的App都不能使用系统的代理。如何让所有软件都能正常代理呢?ProxyDroid这个软件能帮你解决!使用方法及步骤如下:一、推荐从Google Play下载ProxyDroid,目前最新版本是v2.6.6。二、对ProxyDroid进行配置(基本配置:) (1) Auto S_proxydroid使用教程
文章浏览阅读1.1w次,点赞4次,收藏17次。Android Studio提供了一个很实用的工具Android设备监视器(Android device monitor),该监视器中最常用的一个工具就是DDMS(Dalvik Debug Monitor Service),是 Android 开发环境中的Dalvik虚拟机调试监控服务。可以进行的操作有:为测试设备截屏,查看特定进程中正在运行的线程以及堆栈信息、Logcat、广播状态信息、模拟电话_安卓摄像头调试工具