<适配>Android应用如何支持多种屏幕

我们在项目中,处理页面布局和绘制的时候,常常会考虑如何更好的做到多种机型和屏幕的最大程度的支持,亦即屏幕适配的问题。我为何以“Android应用如何支持多屏幕”为题,而非用“Android应用如何做屏幕适配”为题呢?这是因为在官方文档中,便是以“支持多种屏幕”为题来讲解的,为了统一吧,固以此为题。

一、支持多种屏幕的原因

1、Android 可在各种具有不同屏幕尺寸和密度的设备上运行。
2、基于国情和现状,各种屏幕尺寸和密度的设备机型各不相同。
3、对于应用,Android 系统在不同设备中提供一致的开发环境,可以处理大多数工作,将每个应用的用户界面调整为适应其显示的屏幕。
4、为了提高开发效率,避免重复开发项目。

二、术语和概念

1、屏幕尺寸:
按屏幕对角测量的实际物理尺寸。

为简便起见,Android 将所有实际屏幕尺寸分组为四种通用尺寸:小、正常、大和超大。

2、屏幕密度:
屏幕物理区域中的像素量;通常称为 dpi(每英寸 点数)。例如,与“正常”或“高”密度屏幕相比,“低”密度屏幕在给定物理区域的像素较少。

为简便起见,Android 将所有屏幕密度分组为六种通用密度: 低、中、高、超高、超超高和超超超高。

3、方向:
从用户视角看屏幕的方向,即横屏还是竖屏,分别表示屏幕的纵横比是宽还是高。

请注意,不仅不同的设备默认以不同的方向操作,而且方向在运行时可随着用户旋转设备而改变。

4、分辨率:
屏幕上物理像素的总数。添加对多种屏幕的支持时, 应用不会直接使用分辨率;而只应关注通用尺寸和密度组指定的屏幕 尺寸及密度。

5、密度无关像素 (dp):
在定义 UI 布局时应使用的虚拟像素单位,用于以密度无关方式表示布局维度 或位置。

密度无关像素等于 160 dpi 屏幕上的一个物理像素,这是系统为“中”密度屏幕假设的基线密度。在运行时,系统根据使用中屏幕的实际密度按需要以透明方式处理 dp 单位的任何缩放 。dp 单位转换为屏幕像素很简单: px = dp * (dpi / 160)。 例如,在 240 dpi 屏幕上,1 dp 等于 1.5 物理像素。在定义应用的 UI 时应始终使用 dp 单位 ,以确保在不同密度的屏幕上正常显示 UI。

三、支持的屏幕范围

从 Android 1.6(API 级别 4)开始,Android 支持多种屏幕尺寸和密度,反映设备可能具有的多种不同屏幕配置。 您可以使用 Android 系统的功能优化应用在各种屏幕配置下的用户界面 ,确保应用不仅正常渲染,而且在每个屏幕上提供 最佳的用户体验。

具体内容见下表:

屏幕配置

四、如何支持多种屏幕

支持多种屏幕的目标是创建一款在 Android 系统支持的通用屏幕尺寸上都可以 正常运行且显示良好的应用。

概括:

1、在 XML 布局文件中指定尺寸时使用 wrap_content、match_parent 或 dp 单位 。

2、不要在应用代码中使用硬编码的像素值

3、不要使用 AbsoluteLayout(已弃用)

4、为不同屏幕密度提供替代位图可绘制对象

详解:

1、对布局尺寸使用 wrap_content、match_parent 或 dp 单位
为 XML 布局文件中的视图定义 android:layout_widthandroid:layout_height 时,使用"wrap_content"、"match_parent" 或 dp单位可确保在当前设备屏幕上为视图提供适当的尺寸。

例如,layout_width="100dp"
的视图在中密度屏幕上测出宽度为 100 像素,在高密度屏幕上系统会将其扩展至 150 像素宽, 因此视图在屏幕上占用的物理空间大约相同。

类似地,您应选择 sp(缩放独立的像素)来定义文本 大小。sp 缩放系数取决于用户设置,系统会像处理 dp
一样缩放大小。

2、不要在应用代码中使用硬编码的像素值
由于性能的原因和简化代码的需要,Android 系统使用像素作为 表示尺寸或坐标值的标准单位。这意味着,视图的尺寸在代码中始终以像素表示,但始终基于当前的屏幕密度。

例如,如果 myView.getWidth()
返回 10,则表示视图在 当前屏幕上为 10 像素宽,但在更高密度的屏幕上,返回的值可能是 15。如果 在应用代码中使用像素值来处理预先未针对 当前屏幕密度缩放的位图,您可能需要缩放代码中使用的像素值,以与未缩放的位图来源匹配。

如果应用在运行时操作位图或处理像素值,请参阅有关其他密度注意事项的一节

3、不要使用 AbsoluteLayout
与其他布局小工具不同,AbsoluteLayout会强制 使用固定位置放置其子视图,很容易导致 在不同显示屏上显示效果不好的用户界面。因此,AbsoluteLayout在 Android 1.5(API 级别 3)上便已弃用。

您应改用 RelativeLayout,它会使用相对定位来放置其子视图。例如,您可以指定按钮小部件显示在文本小工具的“右边”。

4、使用尺寸和密度特定资源
虽然系统会根据当前屏幕配置扩展布局,但您在不同的屏幕尺寸上可能要调整 UI,以及提供针对不同密度优化的可绘制对象。

如果需要精确控制应用在不同 屏幕配置上的外观,请在配置特定的 资源目录中调整您的布局和位图可绘制对象。例如,考虑要显示在中密度和高密度屏幕上的图标。只需创建两种不同大小的图标 (例如中密度使用 100x100,高密度使用 150x150),然后使用适当的限定符 以适当的方向放置两个变体:

res/drawable-mdpi/icon.png   //for medium-density screens
res/drawable-hdpi/icon.png   //for high-density screens

如需了解有效配置限定符的更多信息,请参阅使用配置限定符

额外的

5、在清单中显式声明您的应用支持哪些屏幕尺寸
通过声明您的应用支持哪些屏幕尺寸,可确保只有 其屏幕受支持的设备才能下载您的应用。声明对不同屏幕尺寸的支持也可影响系统如何在较大 屏幕上绘制您的应用 — 特别是,您的应用是否在屏幕兼容模式中运行。

要声明应用支持的屏幕尺寸,应在清单文件中包含 <supports-screens> 元素。

6、使用可绘制对象
默认情况下,Android 会缩放位图可绘制对象(.png、.jpg 和 .gif 文件)和九宫格可绘制对象(.9.png 文件),使它们以适当的物理尺寸显示在每部设备上。

一些通用的按钮和简单的图标,可以通过九宫格可绘制对象(.9.png 文件)或者通过xml(使用shape等)进行绘制,则可在不同设备上进行适当的缩放,已达到无损效果。

注:您只需要为位图文件(.png、.jpg 或 .gif)和九宫格文件 (.9.png) 提供密度特定的可绘制对象。如果您使用 XML 文件定义形状、颜色或其他可绘制对象资源,应该将一个副本放在默认可绘制对象目录中 (drawable/)。

五、屏幕常用适配总结:

UI适配 — 常用方式:

  • 1、单位适配:
  • 在XML布局中尽量使用wrap_content、match_parent 或 dp 单位 。
  • 在代码中不要使用硬编码的像素值。
  • 2、位图资源适配:
  • 提供不同屏幕密度的图片资源,若考虑包体积,一般选用drawable-xxxhdpi
  • 使用 .9 的图片自适应
  • svg矢量图(可缩放)
  • 3、布局控件组件适配:
  • 尽量使用约束布局ConstraintLayout
  • 使用 Fragment 将界面组件模块化,可复用
  • 4、布局资源配置适配:即 创建备用布局
  • 创建特定于屏幕尺寸的布局
  • 使用最小宽度限定符,即sw限定符适配,系统识别屏幕可用高度和宽度的最小尺寸的dp值(其实就是手机的宽度值)
  • 使用可用宽/高度限定符,如创建目录:layout-h600dp
  • 添加屏幕方向限定符:提供横向(layout-land)或纵向(layout-port)的布局目录
  • 5、系统属性配置适配:
  • 使用预缩放的配置值:
    使用 ViewConfiguration 类来获取 Android 系统常用的距离、速度和时间。
    例如,可通过 getScaledTouchSlop() 来获取框架用作滚动阈值的距离(以像素为单位)
  • 声明最大屏幕尺寸:<supports-screens>
    Android 可自动调整应用以适应大屏幕的方式。
    若有特殊需求,可通过在 <supports-screens> 清单标记中指定largestWidthLimitDp 属性
  • 限制应用仅支持平板电脑或电视:使用 <supports-screens> 清单元素来阻止手机设备下载您的应用。
  • 限制应用仅支持特定尺寸和密度:使用 <compatible-screens> 清单元素来指定应用支持的确切屏幕尺寸和密度。【不推荐】

参考链接:
官方:设备兼容性

UI适配 — 修改density值

  • density值:
    代表 1dp 占当前设备多少像素,即 density = dpi / 160
px = density * dp;
density = dpi / 160;
px = dp * (dpi / 160);
  • ppi像素密度
    指每英寸包含的物理像素的数量。
    ppi 是设备在物理上的属性值,取决于屏幕自身,计算公式如下所示。被除数和除数都属于客观不可改变的值,所以 ppi 也是 无法修改 的,是硬件上一个客观存在无法改变的值
    ppi
  • dpi屏幕像素密度
    软件概念 上单位距离对应的像素总数。
    是手机在出厂时就会被写入系统配置文件中的一个属性值,一般情况下 用户是无法修改 该值的,但在开发者模式中有修改该值的入口,是 软件上可修改 的一个值。

  • 说明:
    Android系统渲染UI前,最终都是通过转换成px来计算的,通过调用 TypedValueapplyDimension 方法。系统的转换代码如下:

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

图片中也会用到 density

   @Nullable
   public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value,
           @Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts) {
       validate(opts);
       if (opts == null) {
           opts = new Options();
       }

       if (opts.inDensity == 0 && value != null) {
           final int density = value.density;
           if (density == TypedValue.DENSITY_DEFAULT) {
               opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
           } else if (density != TypedValue.DENSITY_NONE) {
               opts.inDensity = density;
           }
       }
       
       // 此处用到了densityDpi
       if (opts.inTargetDensity == 0 && res != null) {
           opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
       }
       
       return decodeStream(is, pad, opts);
   }

  • 核心原理:
    即今日头条适配方案,该方案的核心在于:将不同尺寸分辨率手机的宽度dp值改成一个统一的值,从而解决屏幕适配的问题。

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

   private static final float defaultWidth = 360;
   private static float appDensity;
   private static float appScaleDensity;

   public static void setCustomDensity(Application application, Activity activity){
       DisplayMetrics displayMetrics = application.getResources().getDisplayMetrics();
       if (appDensity == 0){
           appDensity = displayMetrics.density;
           appScaleDensity = displayMetrics.scaledDensity;
           //设置修改系统字体以后的监听
           application.registerComponentCallbacks(new ComponentCallbacks() {
               @Override
               public void onConfigurationChanged(@NonNull Configuration newConfig) {
                   if (newConfig != null && newConfig.fontScale >0){
                       appScaleDensity = application.getResources().getDisplayMetrics().scaledDensity;
                   }
               }

               @Override
               public void onLowMemory() {

               }
           });
       }
       final float targetDensity = displayMetrics.widthPixels/defaultWidth;
       final float targetScaleDensity = targetDensity *(appScaleDensity/appDensity);
       final int  targetDensityDpi = (int) (targetDensity * 160);
       displayMetrics.density = targetDensity;
       displayMetrics.scaledDensity = targetScaleDensity;
       displayMetrics.densityDpi = targetDensityDpi;
       final DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics();
       activityDisplayMetrics.density = targetDensity;
       activityDisplayMetrics.scaledDensity = targetScaleDensity;
       activityDisplayMetrics.densityDpi = targetDensityDpi;
   }
  • 不足之处:
    对于平板适配来说不太友好,本质上就是自动拉伸控件的效果。

  • 三方库:AutoSize

参考链接:
一种极低成本的Android屏幕适配方式
Android 简单好用的屏幕适配方案
Android 屏幕适配

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

推荐阅读更多精彩内容