性能优化Android CommunityAndroid 屏幕适配方案合集

【适配】【转】Android开发:最全面、最易懂的Android

2018-09-10  本文已影响1062人  dlihasa

原文地址:https://blog.csdn.net/wangwangli6/article/details/63258270/

前言

Android的屏幕适配一直以来都在折磨着我们Android开发者,本文将结合:

给你带来一种全新、全面而逻辑清晰的Android屏幕适配思路,只要你认真阅读,保证你能解决Android的屏幕适配问题!


目录

image

定义

使得某一元素在Android不同尺寸、不同分辨率的手机上具备相同的显示效果


相关重要概念

屏幕尺寸

屏幕分辨率

屏幕像素密度

密度类型 代表的分辨率(px) 屏幕像素密度(dpi)
低密度(ldpi) 240x320 120
中密度(mdpi) 320x480 160
高密度(hdpi) 480x800 240
超高密度(xhdpi) 720x1280 320
超超高密度(xxhdpi) 1080x1920 480

屏幕尺寸、分辨率、像素密度三者关系

一部手机的分辨率是宽x高,屏幕大小是以寸为单位,那么三者的关系是:

image

数学不太差的人应该能懂.....吧?

不懂没关系,在这里举个例子
假设一部手机的分辨率是1080x1920(px),屏幕大小是5寸,问密度是多少?
解:请直接套公式

image

密度无关像素

密度类型 代表的分辨率(px) 屏幕密度(dpi) 换算(px/dp) 比例
低密度(ldpi) 240x320 120 1dp=0.75px 3
中密度(mdpi) 320x480 160 1dp=1px 4
高密度(hdpi) 480x800 240 1dp=1.5px 6
超高密度(xhdpi) 720x1280 320 1dp=2px 8
超超高密度(xxhdpi) 1080x1920 480 1dp=3px 12

在Android中,规定以160dpi(即屏幕分辨率为320x480)为基准:1dp=1px

独立比例像素

请把上面的概念记住,因为下面讲解都会用到!


为什么要进行Android屏幕适配

由于Android系统的开放性,任何用户、开发者、OEM厂商、运营商都可以对Android进行定制,于是导致:

当Android系统、屏幕尺寸、屏幕密度出现碎片化的时候,就很容易出现同一元素在不同手机上显示不同的问题。

试想一下这么一个场景:
为4.3寸屏幕准备的UI设计图,运行在5.0寸的屏幕上,很可能在右侧和下侧存在大量的空白;而5.0寸的UI设计图运行到4.3寸的设备上,很可能显示不下。

为了保证用户获得一致的用户体验效果:

使得某一元素在Android不同尺寸、不同分辨率的手机上具备相同的显示效果

于是,我们便需要对Android屏幕进行适配。


屏幕适配问题的本质


解决方案

image

“布局”匹配

本质1:使得布局元素自适应屏幕尺寸

开发中,我们使用的布局一般有:

由于绝对布局(AbsoluteLayout)适配性极差,所以极少使用。

对于线性布局(Linearlayout)、相对布局(RelativeLayout)和帧布局(FrameLayout)需要根据需求进行选择,但要记住:

所以,对于屏幕适配来说,使用相对布局(RelativeLayout)将会是更好的解决方案

本质2:根据屏幕的配置来加载相应的UI布局

应用场景:需要为不同屏幕尺寸的设备设计不同的布局

尺寸(size)限定符

因此,我们可以使用尺寸限定符(layout-large)通过创建一个文件

res/layout-large/main.xml

来完成上述设定:

  • 让系统在屏幕尺寸>7英寸时采用适配平板的双面板布局
  • 反之(默认情况下)采用适配手机的单面板布局

文件配置如下:

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

请注意:

  • 两个布局名称均为main.xml,只有布局的目录名不同:第一个布局的目录名为:layout,第二个布局的目录名为:layout-large,包含了尺寸限定符(large)
  • 被定义为大屏的设备(7寸以上的平板)会自动加载包含了large限定符目录的布局,而小屏设备会加载另一个默认的布局

但要注意的是,这种方式只适合Android 3.2版本之前。

最小宽度(Smallest-width)限定符

  • 背景:上述提到的限定符“large”具体是指多大呢?似乎没有一个定量的指标,这便意味着可能没办法准确地根据当前设备的配置(屏幕尺寸)自动加载合适的布局资源
  • 例子:比如说large同时包含着5寸和7寸,这意味着使用“large”限定符的话我没办法实现为5寸和7寸的平板电脑分别加载不同的布局

于是,在Android 3.2及之后版本,引入了最小宽度(Smallest-width)限定符

定义:通过指定某个最小宽度(以 dp 为单位)来精确定位屏幕从而加载不同的UI资源

解决方案:您可以使用上文中所述的单面板和双面板这两种布局,但您应使用 sw600dp 指明双面板布局仅适用于最小宽度为 600 dp 的屏幕,而不是使用 large 尺寸限定符。

  • sw xxxdp,即small width的缩写,其不区分方向,即无论是宽度还是高度,只要大于 xxxdp,就采用次此布局
  • 例子:使用了layout-sw 600dp的最小宽度限定符,即无论是宽度还是高度,只要大于600dp,就采用layout-sw 600dp目录下的布局

代码展示:

<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>
  • 对于最小宽度≥ 600 dp 的设备
    系统会自动加载 layout-sw600dp/main.xml(双面板)布局,否则系统就会选择 layout/main.xml(单面板)布局
    (这个选择过程是Android系统自动选择的)

使用布局别名

设想这么一个场景

当你需要同时为Android 3.2版本前和Android 3.2版本后的手机进行屏幕尺寸适配的时候,由于尺寸限定符仅用于Android 3.2版本前,最小宽度限定符仅用于Android 3.2版本后,所以这会带来一个问题,为了很好地进行屏幕尺寸的适配,你需要同时维护layout-sw600dp和layout-large的两套main.xml平板布局,如下:

  • 适配手机的单面板(默认)布局:res/layout/main.xml
  • 适配尺寸>7寸平板的双面板布局(Android 3.2前):res/layout-large/main.xml
  • 适配尺寸>7寸平板的双面板布局(Android 3.2后)res/layout-sw600dp/main.xml

最后的两个文件的xml内容是完全相同的,这会带来:文件名的重复从而带来一些列后期维护的问题

于是为了要解决这种重复问题,我们引入了“布局别名”

还是上面的例子,你可以定义以下布局:

然后加入以下两个文件,以便进行Android 3.2前和Android 3.2后的版本双面板布局适配:

  1. res/values-large/layout.xml(Android 3.2之前的双面板布局)

    <resources> <item name="main" type="layout">@layout/main_twopanes</item></resources>
    
  2. res/values-sw600dp/layout.xml(Android 3.2及之后的双面板布局)

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

注:

  • 最后两个文件有着相同的内容,但是它们并没有真正去定义布局,它们仅仅只是将main设置成了@layout/main_twopanes的别名
  • 由于这些文件包含 large 和 sw600dp 选择器,因此,系统会将此文件匹配到不同版本的>7寸平板上:
    a. 版本低于 3.2 的平板会匹配 large的文件
    b. 版本高于 3.2 的平板会匹配 sw600dp的文件

这样两个layout.xml都只是引用了@layout/main_twopanes,就避免了重复定义布局文件的情况

屏幕方向(Orientation)限定符

方法是:

  1. 在 res/layout/ 目录下的某个 XML 文件中定义所需要的布局类别
    (单/双面板、是否带操作栏、宽/窄)
    res/layout/onepane.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/onepane_with_bar.xml:(单面板带操作栏)

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"      android:orientation="vertical"      android:layout_width="match_parent"      android:layout_height="match_parent">      <LinearLayout android:layout_width="match_parent"                     android:id="@+id/linearLayout1"                      android:gravity="center"                    android:layout_height="50dp">          <ImageView android:id="@+id/imageView1"                      android:layout_height="wrap_content"                     android:layout_width="wrap_content"                     android:src="@drawable/logo"                     android:paddingRight="30dp"                     android:layout_gravity="left"                     android:layout_weight="0" />          <View android:layout_height="wrap_content"                 android:id="@+id/view1"                android:layout_width="wrap_content"                android:layout_weight="1" />          <Button android:id="@+id/categorybutton"                  android:background="@drawable/button_bg"                  android:layout_height="match_parent"                  android:layout_weight="0"                  android:layout_width="120dp"                  style="@style/CategoryButtonStyle"/>      </LinearLayout>       <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/twopanes.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>

res/layout/twopanes_narrow.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="200dp"              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>

2.使用布局别名进行相应的匹配
(屏幕尺寸(小屏、7寸、10寸)、方向(横、纵))
res/values/layouts.xml:(默认布局)

<resources>      <item name="main_layout" type="layout">@layout/onepane_with_bar</item>      <bool name="has_two_panes">false</bool>  </resources>

可为resources设置bool,通过获取其值来动态判断目前已处在哪个适配布局

res/values-sw600dp-land/layouts.xml
(大屏、横向、双面板、宽-Andorid 3.2版本后)

<resources>    <item name="main_layout" type="layout">@layout/twopanes</item>    <bool name="has_two_panes">true</bool></resources>

res/values-sw600dp-port/layouts.xml
(大屏、纵向、单面板带操作栏-Andorid 3.2版本后)

<resources>    <item name="main_layout" type="layout">@layout/onepane</item>    <bool name="has_two_panes">false</bool></resources>

res/values-large-land/layouts.xml
(大屏、横向、双面板、宽-Andorid 3.2版本前)

<resources>    <item name="main_layout" type="layout">@layout/twopanes</item>    <bool name="has_two_panes">true</bool></resources>

res/values-large-port/layouts.xml
(大屏、纵向、单面板带操作栏-Andorid 3.2版本前)

<resources>    <item name="main_layout" type="layout">@layout/onepane</item>    <bool name="has_two_panes">false</bool></resources>

这里没有完全把全部尺寸匹配类型的代码贴出来,大家可以自己去尝试把其补充完整


“布局组件”匹配

本质:使得布局组件自适应屏幕尺寸

通过使用"wrap_content"、"match_parent"和"weight"来替代硬编码的方式定义视图大小&位置,你的视图要么仅仅使用了需要的那边一点空间,要么就会充满所有可用的空间,即按需占据空间大小,能让你的布局元素充分适应你的屏幕尺寸


“图片资源”匹配

本质:使得图片资源在不同屏幕密度上显示相同的像素效果


”用户界面流程“匹配

  • 使用场景:我们会根据设备特点显示恰当的布局,但是这样做,会使得用户界面流程可能会有所不同。
  • 例如,如果应用处于双面板模式下,点击左侧面板上的项即可直接在右侧面板上显示相关内容;而如果该应用处于单面板模式下,点击相关的内容应该跳转到另外一个Activity进行后续的处理。

本质:根据屏幕的配置来加载相应的用户界面流程

  1. 确定当前布局
  2. 根据当前布局做出响应
  3. 重复使用其他活动中的片段
  4. 处理屏幕配置变化

在类似情况下,通常可以在多个活动中重复使用相同的 Fragment 子类以避免代码重复。例如,在双面板布局中使用了 ArticleFragment:

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

然后又在小屏幕的Activity布局中重复使用了它 :

ArticleFragment frag = new ArticleFragment();getSupportFragmentManager().beginTransaction().add(android.R.id.content, frag).commit();

也就是说,如果用户处于纵向模式下且屏幕上显示的是用于阅读报道的活动,那么就需要在检测到屏幕方向变化(变成横向模式)后执行相应操作,即停止上述活动并返回主活动,以便在双面板布局中显示相关内容:

public class ArticleActivity extends FragmentActivity {    int mCatIndex, mArtIndex;     @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        mCatIndex = getIntent().getExtras().getInt("catIndex", 0);        mArtIndex = getIntent().getExtras().getInt("artIndex", 0);         // If should be in two-pane mode, finish to return to main activity        if (getResources().getBoolean(R.bool.has_two_panes)) {            finish();            return;        }        ...}

通过上面一系列步骤,我们就完全可以建立一个可以根据用户界面配置进行自适应的应用程序App了。


总结

经过上面的介绍,对于屏幕尺寸大小适配问题应该是不成问题了。


解决方案

image

“布局控件”匹配

本质:使得布局组件在不同屏幕密度上显示相同的像素效果

密度类型 代表的分辨率(px) 屏幕密度(dpi) 换算(px/dp) 比例
低密度(ldpi) 240x320 120 1dp=0.75px 3
中密度(mdpi) 320x480 160 1dp=1px 4
高密度(hdpi) 480x800 240 1dp=1.5px 6
超高密度(xhdpi) 720x1280 320 1dp=2px 8
超超高密度(xxhdpi) 1080x1920 480 1dp=3px 12

在Android中,规定以160dpi(即屏幕分辨率为320x480)为基准:1dp=1px

独立比例像素

所以,为了能够进行不同屏幕像素密度的匹配,我们推荐:

可是,请看以下一种场景:

Nexus5的总宽度为360dp,我们现在在水平方向上放置两个按钮,一个是150dp左对齐,另外一个是200dp右对齐,那么中间留有10dp间隔;但假如同样地设置在Nexus S(屏幕宽度是320dp),会发现,两个按钮会重叠,因为320dp<200+150dp

从上面可以看出,由于Android屏幕设备的多样性,如果使用dp来作为度量单位,并不是所有的屏幕的宽度都具备相同的dp长度

再次明确,屏幕宽度和像素密度没有任何关联关系

所以说,dp解决了同一数值在不同分辨率中展示相同尺寸大小的问题(即屏幕像素密度匹配问题),但却没有解决设备尺寸大小匹配的问题。(即屏幕尺寸匹配问题)

当然,我们一开始讨论的就是屏幕尺寸匹配问题,使用match_parent、wrap_content和weight,尽可能少用dp来指定控件的具体长宽,大部分的情况我们都是可以做到适配的。

那么该如何解决控件的屏幕尺寸和屏幕密度的适配问题呢?

从上面可以看出:

因为本质上是希望使得布局组件在不同屏幕密度上显示相同的像素效果,那么,之前是绕了个弯使用dp解决这个问题,那么到底能不能直接用px解决呢?

即根据不同屏幕密度,控件选择对应的像素值大小

接下来介绍一种方法:百分比适配方法,步骤如下:

  1. 以某一分辨率为基准,生成所有分辨率对应像素数列表
  2. 将生成像素数列表存放在res目录下对应的values文件下
  3. 根据UI设计师给出设计图上的尺寸,找到对应像素数的单位,然后设置给控件即可

步骤1:以某一分辨率为基准,生成所有分辨率对应像素数列表

现在我们以320x480的分辨率为基准:

然后生成该分辨率对应像素数的列表,如下图:

找到基准后,是时候把其他分辨率补全了,现今以写1080x1920的分辨率为例:

因为基准是320x480,所以1080/320=3.375px,1920/480=4px,所以相应文件应该是

用上面的方法把你需要适配的分辨率的像素列表补全吧~

作为程序猿的我们当然不会做手写的这些蠢事!!!多谢 @鸿洋大神 提供了自动生成工具(内置了常用的分辨率),大家可以直接点击这里下载
注:工具默认基准为400*320,当然对于特殊需求,通过命令行指定即可:

java -jar 文件名.jar 基准宽 基准高 额外支持尺寸1的宽,额外支持尺寸1的高_额外支持尺寸2的宽,额外支持尺寸2的高:

例如:需要设置的基准是800x1280,额外支持尺寸:735x1152 ;3200x4500;

java -jar 文件名.jar 800 1280 735,1152_3200,4500

步骤2:把生成的各像素数列表放到对应的资源文件

将生成像素数列表(lay_x.xml和lay_y.xml)存放在res目录下对应的values文件(注意宽、高要对应),如下图:

image

注:

  • 分辨率为480x320的资源文件应放在res/values-480x320文件夹中;同理分辨率为1920x1080的资源文件应放在res/values-1920x1080文件夹中。(其中values-480x320是分辨率限定符)

  • 必须在默认values里面也创建对应默认lay_x.xml和lay_y.xml文件,如下图
    lay_x.xml

    <?xml version="1.0" encoding="utf-8"><resources><dimen name="x1">1.0dp</dimen><dimen name="x2">2.0dp</dimen>...</resources>
    
  • 因为对于没有生成对应分辨率文件的手机,会使用默认values文件夹,如果默认values文件夹没有(即没有对应的分辨率、没有对应dimen)就会报错,从而无法进行屏幕适配。
    (注意对应单位改为dp,而不同于上面的px。因为不知道机型的分辨率,所以默认分辨率文件只好默认为x1=1dp以保证尽量兼容(又回到dp老方法了),这也是这个解决方案的一个弊端)

步骤3:根据UI设计师给出某一分辨率设计图上的尺寸,找到对应像素数的单位,然后设置给控件即可

如下图:

<FrameLayout >     <Button        android:layout_gravity="center"        android:gravity="center"        android:text="@string/hello_world"        android:layout_width="@dimen/x160"        android:layout_height="@dimen/y160"/> </FrameLayout>

总结

使用上述的适配方式,应该能进行90%的适配了,但其缺点还是很明显:


“图片资源”匹配

本质:使得图片资源在不同屏幕密度上显示相同的像素效果

密度类型 代表的分辨率(px) 系统密度(dpi)
低密度(ldpi) 240x320 120
中密度(mdpi) 320x480 160
高密度(hdpi) 480x800 240
超高密度(xhdpi) 720x1280 320
超超高密度(xxhdpi) 1080x1920 480

即一套分辨率=一套位图资源(这个当然是Ui设计师做了)

注:

  • 如果是.9图或者是不需要多个分辨率的图片,放在drawable文件夹即可
  • 对应分辨率的图片要正确的放在合适的文件夹,否则会造成图片拉伸等问题。

更好地方案解决“图片资源”适配问题

上述方案是常见的一种方案,这固然是一种解决办法,但缺点在于:

那么,有没有一种方法:

下面我们就来介绍这个方法:

方法介绍

1. 先来理解下Android 加载资源过程
Android SDK会根据屏幕密度自动选择对应的资源文件进行渲染加载(自动渲染)

比如说,SDK检测到你手机的分辨率是320x480(dpi=160),会优先到drawable-mdpi文件夹下找对应的图片资源;但假设你只在xhpdi文件夹下有对应的图片资源文件(mdpi文件夹是空的),那么SDK会去xhpdi文件夹找到相应的图片资源文件,然后将原有大像素的图片自动缩放成小像素的图片,于是大像素的图片照样可以在小像素分辨率的手机上正常显示。
具体请看http://blog.csdn.net/xiebudong/article/details/37040263
所以理论上来说只需要提供一种分辨率规格的图片资源就可以了。
那么应该提供哪种分辨率规格呢?
如果只提供ldpi规格的图片,对于大分辨率(xdpi、xxdpi)的手机如果把图片放大就会不清晰

所以需要提供一套你需要支持的最大dpi分辨率规格的图片资源,这样即使用户的手机分辨率很小,这样图片缩小依然很清晰。那么这一套最大dpi分辨率规格应该是哪种呢?是现在市面手机分辨率最大可达到1080X1920的分辨率(dpi=xxdpi=480)吗?

2. xhdpi应该是首选

原因如下:

机型 分辨率(px) 屏幕尺寸(inch) 系统密度(dpi)
iPhone 5s 640X1164 4 332
iPhone 6 1334x750 4.7 326
iPhone 6 Plus 1080x1920 5 400

iPhone主流的屏幕dpi约等于320, 刚好属于xhdpi,所以选择xhdpi作为唯一一套dpi图片资源,可以让设计师不用专门为Android端切图,直接把iPhone的那一套切好的图片资源放入drawable-xhdpi文件夹里就好,这样大大减少的设计师的工作量!

额外小tips

这时我们可以动态获取当前的屏幕属性,然后设置合适的数值

public class ScreenSizeUtil {   public static int getScreenWidth(Activity activity) {     return activity.getWindowManager().getDefaultDisplay().getWidth(); }   public static int getScreenHeight(Activity activity) {     return activity.getWindowManager().getDefaultDisplay().getHeight();    }}

总结

本文根据现今主流Android的适配方法,以逻辑清晰的方式进行了主流Android适配方法的全面整理,接下来我会介绍继续介绍Android开发中的相关知识

上一篇下一篇

猜你喜欢

热点阅读