Android 屏幕適配:最全面的解決方案


轉自:https://www.jianshu.com/p/ec5a1a30694b

 

前言

Android的屏幕適配一直以來都在折磨着我們Android開發者,本文將結合:

給你帶來一種全新、全面而邏輯清晰的Android屏幕適配思路,只要你認真閱讀,保證你能解決Android的屏幕適配問題!


目錄

 
Android屏幕適配解決方案.png

定義

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


相關重要概念

屏幕尺寸

  • 含義:手機對角線的物理尺寸
  • 單位:英寸(inch),1英寸=2.54cm

Android手機常見的尺寸有5寸、5.5寸、6寸等等

屏幕分辨率

  • 含義:手機在橫向、縱向上的像素點數總和
  1. 一般描述成屏幕的"寬x高”=AxB
  2. 含義:屏幕在橫向方向(寬度)上有A個像素點,在縱向方向
    (高)有B個像素點
  3. 例子:1080x1920,即寬度方向上有1080個像素點,在高度方向上有1920個像素點
  • 單位:px(pixel),1px=1像素點

UI設計師的設計圖會以px作為統一的計量單位

  • Android手機常見的分辨率:320x480、480x800、720x1280、1080x1920

屏幕像素密度

  • 含義:每英寸的像素點數
  • 單位:dpi(dots per ich)

假設設備內每英寸有160個像素,那么該設備的屏幕像素密度=160dpi

  • 安卓手機對於每類手機屏幕大小都有一個相應的屏幕像素密度:
密度類型 代表的分辨率(px) 屏幕像素密度(dpi)
低密度(ldpi) 240x320 120
中密度(mdpi) 320x480 160
高密度(hdpi) 480x800 240
超高密度(xhdpi) 720x1280 320
超超高密度(xxhdpi) 1080x1920 480

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

一部手機的分辨率是寬x高,屏幕大小是以寸為單位,那么三者的關系是:

 
三者關系示意圖

數學不太差的人應該能懂.....吧?

不懂沒關系,在這里舉個例子
假設一部手機的分辨率是1080x1920(px),屏幕大小是5寸,問密度是多少?
解:請直接套公式

 
解答過程

 

密度無關像素

  • 含義:density-independent pixel,叫dp或dip,與終端上的實際物理像素點無關。
  • 單位:dp,可以保證在不同屏幕像素密度的設備上顯示相同的效果
  1. Android開發時用dp而不是px單位設置圖片大小,是Android特有的單位
  2. 場景:假如同樣都是畫一條長度是屏幕一半的線,如果使用px作為計量單位,那么在480x800分辨率手機上設置應為240px;在320x480的手機上應設置為160px,二者設置就不同了;如果使用dp為單位,在這兩種分辨率下,160dp都顯示為屏幕一半的長度。
  • dp與px的轉換
    因為ui設計師給你的設計圖是以px為單位的,Android開發則是使用dp作為單位的,那么我們需要進行轉換:
密度類型 代表的分辨率(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

獨立比例像素

  • 含義:scale-independent pixel,叫sp或sip
  • 單位:sp
  1. Android開發時用此單位設置文字大小,可根據字體大小首選項進行縮放
  2. 推薦使用12sp、14sp、18sp、22sp作為字體設置的大小,不推薦使用奇數和小數,容易造成精度的丟失問題;小於12sp的字體會太小導致用戶看不清

請把上面的概念記住,因為下面講解都會用到!


為什么要進行Android屏幕適配

由於Android系統的開放性,任何用戶、開發者、OEM廠商、運營商都可以對Android進行定制,於是導致:

  • Android系統碎片化:小米定制的MIUI、魅族定制的flyme、華為定制的EMUI等等

當然都是基於Google原生系統定制的

  • Android機型屏幕尺寸碎片化:5寸、5.5寸、6寸等等
  • Android屏幕分辨率碎片化:320x480、480x800、720x1280、1080x1920

據友盟指數顯示,統計至2015年12月,支持Android的設備共有27796種

當Android系統、屏幕尺寸、屏幕密度出現碎片化的時候,就很容易出現同一元素在不同手機上顯示不同的問題。

試想一下這么一個場景:
為4.3寸屏幕准備的UI設計圖,運行在5.0寸的屏幕上,很可能在右側和下側存在大量的空白;而5.0寸的UI設計圖運行到4.3寸的設備上,很可能顯示不下。

為了保證用戶獲得一致的用戶體驗效果:

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

於是,我們便需要對Android屏幕進行適配。


屏幕適配問題的本質

  • 使得“布局”、“布局組件”、“圖片資源”、“用戶界面流程”匹配不同的屏幕尺寸

使得布局、布局組件自適應屏幕尺寸;
根據屏幕的配置來加載相應的UI布局、用戶界面流程

  • 使得“圖片資源”匹配不同的屏幕密度

解決方案

  • 問題:如何進行屏幕尺寸匹配?
  • 答:
 
屏幕尺寸適配解決方案.png

“布局”匹配

本質1:使得布局元素自適應屏幕尺寸

  • 做法
    使用相對布局(RelativeLayout),禁用絕對布局(AbsoluteLayout)

開發中,我們使用的布局一般有:

  • 線性布局(Linearlayout)
  • 相對布局(RelativeLayout)
  • 幀布局(FrameLayout)
  • 絕對布局(AbsoluteLayout)

由於絕對布局(AbsoluteLayout)適配性極差,所以極少使用。

對於線性布局(Linearlayout)、相對布局(RelativeLayout)和幀布局(FrameLayout)需要根據需求進行選擇,但要記住:

  • RelativeLayout
    布局的子控件之間使用相對位置的方式排列,因為RelativeLayout講究的是相對位置,即使屏幕的大小改變,視圖之前的相對位置都不會變化,與屏幕大小無關,靈活性很強
  • LinearLayout
    通過多層嵌套LinearLayout和組合使
    用"wrap_content"和"match_parent"已經可以構建出足夠復雜的布局。但是LinearLayout無法准確地控制子視圖之間的位置關系,只能簡單的一個挨着一個地排列

所以,對於屏幕適配來說,使用相對布局(RelativeLayout)將會是更好的解決方案

本質2:根據屏幕的配置來加載相應的UI布局

應用場景:需要為不同屏幕尺寸的設備設計不同的布局

  • 做法:使用限定符
  • 作用:通過配置限定符使得程序在運行時根據當前設備的配置(屏幕尺寸)自動加載合適的布局資源
  • 限定符類型:
  • 尺寸(size)限定符
  • 最小寬度(Smallest-width)限定符
  • 布局別名
  • 屏幕方向(Orientation)限定符

尺寸(size)限定符

  • 使用場景:當一款應用顯示的內容較多,希望進行以下設置:
  • 在平板電腦和電視的屏幕(>7英寸)上:實施“雙面板”模式以同時顯示更多內容
  • 在手機較小的屏幕上:使用單面板分別顯示內容

因此,我們可以使用尺寸限定符(layout-large)通過創建一個文件

res/layout-large/main.xml

來完成上述設定:

  • 讓系統在屏幕尺寸>7英寸時采用適配平板的雙面板布局
  • 反之(默認情況下)采用適配手機的單面板布局

文件配置如下:

  • 適配手機的單面板(默認)布局: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> 
  • 適配尺寸>7寸平板的雙面板布局::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> 

請注意:

  • 兩個布局名稱均為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資源

  • 使用場景

你需要為標准 7 英寸平板電腦匹配雙面板布局(其最小寬度為 600 dp),在手機(較小的屏幕上)匹配單面板布局

解決方案:您可以使用上文中所述的單面板和雙面板這兩種布局,但您應使用 sw600dp 指明雙面板布局僅適用於最小寬度為 600 dp 的屏幕,而不是使用 large 尺寸限定符。

  • sw xxxdp,即small width的縮寫,其不區分方向,即無論是寬度還是高度,只要大於 xxxdp,就采用次此布局
  • 例子:使用了layout-sw 600dp的最小寬度限定符,即無論是寬度還是高度,只要大於600dp,就采用layout-sw 600dp目錄下的布局

代碼展示:

  • 適配手機的單面板(默認)布局: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> 
  • 適配尺寸>7寸平板的雙面板布局:res/layout-sw600dp/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> 
  • 對於最小寬度≥ 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內容是完全相同的,這會帶來:文件名的重復從而帶來一些列后期維護的問題

於是為了要解決這種重復問題,我們引入了“布局別名”

還是上面的例子,你可以定義以下布局:

  • 適配手機的單面板(默認)布局:res/layout/main.xml
  • 適配尺寸>7寸平板的雙面板布局:res/layout/main_twopanes.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> 
  1. 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)限定符

  • 使用場景:根據屏幕方向進行布局的調整

取以下為例子:

  • 小屏幕, 豎屏: 單面板
  • 小屏幕, 橫屏: 單面板
  • 7 英寸平板電腦,縱向:單面板,帶操作欄
  • 7 英寸平板電腦,橫向:雙面板,寬,帶操作欄
  • 10 英寸平板電腦,縱向:雙面板,窄,帶操作欄
  • 10 英寸平板電腦,橫向:雙面板,寬,帶操作欄
  • 電視,橫向:雙面板,寬,帶操作欄

方法是:

  • 先定義類別:單/雙面板、是否帶操作欄、寬/窄

定義在 res/layout/ 目錄下的某個 XML 文件中

  • 再進行相應的匹配:屏幕尺寸(小屏、7寸、10寸)、方向(橫、縱)

使用布局別名進行匹配

  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“來控制視圖組件的寬度和高度
  • "wrap_content"
    相應視圖的寬和高就會被設定成所需的最小尺寸以適應視圖中的內容
  • "match_parent"(在Android API 8之前叫作"fill_parent")
    視圖的寬和高延伸至充滿整個父布局
  • "weight"
    1.定義:是線性布局(Linelayout)的一個獨特比例分配屬性
    2.作用:使用此屬性設置權重,然后按照比例對界面進行空間的分配,公式計算是:控件寬度=控件設置寬度+剩余空間所占百分比寬幅
    具體可以參考這篇文章,講解得非常詳細

通過使用"wrap_content"、"match_parent"和"weight"來替代硬編碼的方式定義視圖大小&位置,你的視圖要么僅僅使用了需要的那邊一點空間,要么就會充滿所有可用的空間,即按需占據空間大小,能讓你的布局元素充分適應你的屏幕尺寸


“圖片資源”匹配

本質:使得圖片資源在不同屏幕密度上顯示相同的像素效果

  • 做法:使用自動拉伸位圖:Nine-Patch的圖片類型
    假設需要匹配不同屏幕大小,你的圖片資源也必須自動適應各種屏幕尺寸

使用場景:一個按鈕的背景圖片必須能夠隨着按鈕大小的改變而改變。
使用普通的圖片將無法實現上述功能,因為運行時會均勻地拉伸或壓縮你的圖片

  • 解決方案:使用自動拉伸位圖(nine-patch圖片),后綴名是.9.png,它是一種被特殊處理過的PNG圖片,設計時可以指定圖片的拉伸區域和非拉伸區域;使用時,系統就會根據控件的大小自動地拉伸你想要拉伸的部分

1.必須要使用.9.png后綴名,因為系統就是根據這個來區別nine-patch圖片和普通的PNG圖片的;

2.當你需要在一個控件中使用nine-patch圖片時,如

android:background="@drawable/button" 

系統就會根據控件的大小自動地拉伸你想要拉伸的部分


”用戶界面流程“匹配

  • 使用場景:我們會根據設備特點顯示恰當的布局,但是這樣做,會使得用戶界面流程可能會有所不同。
  • 例如,如果應用處於雙面板模式下,點擊左側面板上的項即可直接在右側面板上顯示相關內容;而如果該應用處於單面板模式下,點擊相關的內容應該跳轉到另外一個Activity進行后續的處理。

本質:根據屏幕的配置來加載相應的用戶界面流程

  • 做法
    進行用戶界面流程的自適應配置:
  1. 確定當前布局
  2. 根據當前布局做出響應
  3. 重復使用其他活動中的片段
  4. 處理屏幕配置變化
  • 步驟1:確定當前布局
    由於每種布局的實施都會稍有不同,因此我們需要先確定當前向用戶顯示的布局。例如,我們可以先了解用戶所處的是“單面板”模式還是“雙面板”模式。要做到這一點,可以通過查詢指定視圖是否存在以及是否已顯示出來。
public class NewsReaderActivity extends FragmentActivity { boolean mIsDualPane; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main_layout); View articleView = findViewById(R.id.article); mIsDualPane = articleView != null && articleView.getVisibility() == View.VISIBLE; } } 

這段代碼用於查詢“報道”面板是否可用,與針對具體布局的硬編碼查詢相比,這段代碼的靈活性要大得多。

  • 步驟2:根據當前布局做出響應
    有些操作可能會因當前的具體布局而產生不同的結果。

例如,在新聞閱讀器示例中,如果用戶界面處於雙面板模式下,那么點擊標題列表中的標題就會在右側面板中打開相應報道;但如果用戶界面處於單面板模式下,那么上述操作就會啟動一個獨立活動:

@Override public void onHeadlineSelected(int index) { mArtIndex = index; if (mIsDualPane) { /* display article on the right pane */ mArticleFragment.displayArticle(mCurrentCat.getArticle(index)); } else { /* start a separate activity */ Intent intent = new Intent(this, ArticleActivity.class); intent.putExtra("catIndex", mCatIndex); intent.putExtra("artIndex", index); startActivity(intent); } } 
  • 步驟3:重復使用其他活動中的片段
    多屏幕設計中的重復模式是指,對於某些屏幕配置,已實施界面的一部分會用作面板;但對於其他配置,這部分就會以獨立活動的形式存在。

例如,在新聞閱讀器示例中,對於較大的屏幕,新聞報道文本會顯示在右側面板中;但對於較小的屏幕,這些文本就會以獨立活動的形式存在。

在類似情況下,通常可以在多個活動中重復使用相同的 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(); 
  • 步驟3:處理屏幕配置變化
    如果我們使用獨立Activity實施界面的獨立部分,那么請注意,我們可能需要對特定配置變化(例如屏幕方向的變化)做出響應,以便保持界面的一致性。

例如,在運行 Android 3.0 或更高版本的標准 7 英寸平板電腦上,如果新聞閱讀器示例應用運行在縱向模式下,就會在使用獨立活動顯示新聞報道;但如果該應用運行在橫向模式下,就會使用雙面板布局。

也就是說,如果用戶處於縱向模式下且屏幕上顯示的是用於閱讀報道的活動,那么就需要在檢測到屏幕方向變化(變成橫向模式)后執行相應操作,即停止上述活動並返回主活動,以便在雙面板布局中顯示相關內容:

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


總結

經過上面的介紹,對於屏幕尺寸大小適配問題應該是不成問題了。


解決方案

  • 問題:如何進行屏幕密度匹配?
  • 答:
 
屏幕密度匹配解決方案.png

“布局控件”匹配

本質:使得布局組件在不同屏幕密度上顯示相同的像素效果

  • 做法1:使用密度無關像素
    由於各種屏幕的像素密度都有所不同,因此相同數量的像素在不同設備上的實際大小也有所差異,這樣使用像素(px)定義布局尺寸就會產生問題。
    因此,請務必使用密度無關像素 dp 或**獨立比例像素 sp **單位指定尺寸。
  • 相關概念介紹
    密度無關像素
  • 含義:density-independent pixel,叫dp或dip,與終端上的實際物理像素點無關。
  • 單位:dp,可以保證在不同屏幕像素密度的設備上顯示相同的效果
  1. Android開發時用dp而不是px單位設置圖片大小,是Android特有的單位
  2. 場景:假如同樣都是畫一條長度是屏幕一半的線,如果使用px作為計量單位,那么在480x800分辨率手機上設置應為240px;在320x480的手機上應設置為160px,二者設置就不同了;如果使用dp為單位,在這兩種分辨率下,160dp都顯示為屏幕一半的長度。
  • dp與px的轉換
    因為ui給你的設計圖是以px為單位的,Android開發則是使用dp作為單位的,那么該如何轉換呢?

| 密度類型 | 代表的分辨率(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

獨立比例像素

  • 含義:scale-independent pixel,叫sp或sip
  • 單位:sp
  1. Android開發時用此單位設置文字大小,可根據用戶的偏好文字大小/字體大小首選項進行縮放
  2. 推薦使用12sp、14sp、18sp、22sp作為字體設置的大小,不推薦使用奇數和小數,容易造成精度的丟失問題;小於12sp的字體會太小導致用戶看不清

所以,為了能夠進行不同屏幕像素密度的匹配,我們推薦:

  • 使用dp來代替px作為控件長度的統一度量單位
  • 使用sp作為文字的統一度量單位

可是,請看以下一種場景:

Nexus5的總寬度為360dp,我們現在在水平方向上放置兩個按鈕,一個是150dp左對齊,另外一個是200dp右對齊,那么中間留有10dp間隔;但假如同樣地設置在Nexus S(屏幕寬度是320dp),會發現,兩個按鈕會重疊,因為320dp<200+150dp

從上面可以看出,由於Android屏幕設備的多樣性,如果使用dp來作為度量單位,並不是所有的屏幕的寬度都具備相同的dp長度

再次明確,屏幕寬度和像素密度沒有任何關聯關系

所以說,dp解決了同一數值在不同分辨率中展示相同尺寸大小的問題(即屏幕像素密度匹配問題),但卻沒有解決設備尺寸大小匹配的問題。(即屏幕尺寸匹配問題)

當然,我們一開始討論的就是屏幕尺寸匹配問題,使用match_parent、wrap_content和weight,盡可能少用dp來指定控件的具體長寬,大部分的情況我們都是可以做到適配的。

那么該如何解決控件的屏幕尺寸和屏幕密度的適配問題呢?

從上面可以看出:

  • 因為屏幕密度(分辨率)不一樣,所以不能用固定的px
  • 因為屏幕寬度不一樣,所以要小心的用dp

因為本質上是希望使得布局組件在不同屏幕密度上顯示相同的像素效果,那么,之前是繞了個彎使用dp解決這個問題,那么到底能不能直接用px解決呢?

即根據不同屏幕密度,控件選擇對應的像素值大小

接下來介紹一種方法:百分比適配方法,步驟如下:

  1. 以某一分辨率為基准,生成所有分辨率對應像素數列表
  2. 將生成像素數列表存放在res目錄下對應的values文件下
  3. 根據UI設計師給出設計圖上的尺寸,找到對應像素數的單位,然后設置給控件即可

步驟1:以某一分辨率為基准,生成所有分辨率對應像素數列表

現在我們以320x480的分辨率為基准:

  • 將屏幕的寬度分為320份,取值為x1~x320
  • 將屏幕的高度分為480份,取值為y1~y480

然后生成該分辨率對應像素數的列表,如下圖:

  • lay_x.xml(寬)
<?xml version="1.0" encoding="utf-8"?> <resources><dimen name="x1">1.0px</dimen> <dimen name="x2">2.0px</dimen> <dimen name="x3">3.0px</dimen> <dimen name="x4">4.0px</dimen> <dimen name="x5">5.0px</dimen> <dimen name="x6">6.0px</dimen> <dimen name="x7">7.0px</dimen> <dimen name="x8">8.0px</dimen> <dimen name="x9">9.0px</dimen> <dimen name="x10">10.0px</dimen> ... <dimen name="x300">300.0px</dimen> <dimen name="x301">301.0px</dimen> <dimen name="x302">302.0px</dimen> <dimen name="x303">303.0px</dimen> <dimen name="x304">304.0px</dimen> <dimen name="x305">305.0px</dimen> <dimen name="x306">306.0px</dimen> <dimen name="x307">307.0px</dimen> <dimen name="x308">308.0px</dimen> <dimen name="x309">309.0px</dimen> <dimen name="x310">310.0px</dimen> <dimen name="x311">311.0px</dimen> <dimen name="x312">312.0px</dimen> <dimen name="x313">313.0px</dimen> <dimen name="x314">314.0px</dimen> <dimen name="x315">315.0px</dimen> <dimen name="x316">316.0px</dimen> <dimen name="x317">317.0px</dimen> <dimen name="x318">318.0px</dimen> <dimen name="x319">319.0px</dimen> <dimen name="x320">320px</dimen> </resources> 
  • lay_y.xml(高)
<?xml version="1.0" encoding="utf-8"?> <resources><dimen name="y1">1.0px</dimen> <dimen name="y2">2.0px</dimen> <dimen name="y3">3.0px</dimen> <dimen name="y4">4.0px</dimen> ... <dimen name="y480">480px</dimen> </resources> 

找到基准后,是時候把其他分辨率補全了,現今以寫1080x1920的分辨率為例:

因為基准是320x480,所以1080/320=3.375px,1920/480=4px,所以相應文件應該是

  • lay_x.xml
<?xml version="1.0" encoding="utf-8"?> <resources><dimen name="x1">3.375px</dimen> <dimen name="x2">6.65px</dimen> <dimen name="x3">10.125px</dimen> ... <dimen name="x320">1080px</dimen> </resources> 
  • lay_y.xml
<?xml version="1.0" encoding="utf-8"?> <resources><dimen name="y1">4px</dimen> <dimen name="y2">8px</dimen> <dimen name="y3">12px</dimen> <dimen name="y4">16px</dimen> ... <dimen name="y480">1920px</dimen> </resources> 

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

作為程序猿的我們當然不會做手寫的這些蠢事!!!多謝 @鴻洋大神 提供了自動生成工具(內置了常用的分辨率),大家可以直接點擊這里下載
注:工具默認基准為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文件(注意寬、高要對應),如下圖:

 
res目錄下對應的values文件

注:

  • 分辨率為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作為長度的度量單位,所以和google的要求使用dp作為度量單位會有所背離
  • 必須盡可能多的包含所有分辨率,因為這個是使用這個方案的基礎,如果有某個分辨率缺少,將無法完成該屏幕的適配
  • 過多的分辨率像素描述xml文件會增加軟件包的大小和維護的難度

“圖片資源”匹配

本質:使得圖片資源在不同屏幕密度上顯示相同的像素效果

  • 做法:提供備用位圖(符合屏幕尺寸的圖片資源)
    由於 Android 可在各種屏幕密度的設備上運行,因此我們提供的位圖資源應該始終可以滿足各類密度的要求:
密度類型 代表的分辨率(px) 系統密度(dpi)
低密度(ldpi) 240x320 120
中密度(mdpi) 320x480 160
高密度(hdpi) 480x800 240
超高密度(xhdpi) 720x1280 320
超超高密度(xxhdpi) 1080x1920 480
  • 步驟1:根據以下尺寸范圍針對各密度生成相應的圖片。

比如說,如果我們為 xhdpi 設備生成了 200x200 px尺寸的圖片,就應該按照相應比例地為 hdpi、mdpi 和 ldpi 設備分別生成 150x150、100x100 和 75x75 尺寸的圖片

即一套分辨率=一套位圖資源(這個當然是Ui設計師做了)

  • 步驟2:將生成的圖片文件放在 res/ 下的相應子目錄中(mdpi、hdpi、xhdpi、xxhdpi),系統就會根據運行您應用的設備的屏幕密度自動選擇合適的圖片
  • 步驟3:通過引用 @drawable/id,系統都能根據相應屏幕的 屏幕密度(dpi)自動選取合適的位圖。

注:

  • 如果是.9圖或者是不需要多個分辨率的圖片,放在drawable文件夾即可
  • 對應分辨率的圖片要正確的放在合適的文件夾,否則會造成圖片拉伸等問題。

更好地方案解決“圖片資源”適配問題

上述方案是常見的一種方案,這固然是一種解決辦法,但缺點在於:

  • 每套分辨率出一套圖,為美工或者設計增加了許多工作量
  • 對Android工程文件的apk包變的很大

那么,有沒有一種方法:

  • 保證屏幕密度適配
  • 可以最小占用設計資源
  • 使得apk包不變大(只使用一套分辨率的圖片資源)

下面我們就來介紹這個方法

  • 只需選擇唯一一套分辨率規格的圖片資源

方法介紹

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應該是首選

原因如下:

  • xhdpi分辨率以內的手機需求量最旺盛
    目前市面上最普遍的高端機的分辨率還多集中在720X1080范圍內(xhdpi),所以目前來看xhpdi規格的圖片資源成為了首選
  • 節省設計資源&工作量
    在現在的App開發中(iOS和Android版本),有些設計師為了保持App不同版本的體驗交互一致,可能會以iPhone手機為基礎進行設計,包括后期的切圖之類的。
    設計師們一般都會用最新的iPhone6和iPhone5s(5s和5的尺寸以及分辨率都一樣)來做原型設計,所有參數請看下圖
機型 分辨率(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

  • ImageView的ScaleType屬性
    設置不同的ScaleType會得到不同的顯示效果,一般情況下,設置為centerCrop能獲得較好的適配效果。

  • 動態設置

使用場景:有些情況下,我們需要動態的設置控件大小或者是位置,比如說popwindow的顯示位置和偏移量等

這時我們可以動態獲取當前的屏幕屬性,然后設置合適的數值

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開發中的相關知識,有興趣可以繼續關注Carson_Ho的安卓開發筆記



作者:Carson_Ho
鏈接:https://www.jianshu.com/p/ec5a1a30694b
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權並注明出處。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM