響應式 Android 應用
引言
開發一款移動應用是一個創造性的過程。你一定會想要創作一款這樣的應用,它美觀實用,它在任何設備上都能運行流暢,它讓用戶覺得賞心悅目,它讓自己引以為傲,下面我就會告訴你我是如何創作具有這些特性的安卓應用的。
對于安卓開發存在一個普遍的誤解,那就是安卓設備尺寸的多樣性使得開發出具有上述特性的應用變得十分棘手。 你肯定已經看過《安卓可視化碎片》這篇文章了( Android Fragmentation Visualized),文章列舉出了驚人數目的不同安卓設備.
事實上,你確實需要在設計上花費很多心思,但絕對不必比在其他平臺上花費得多。安卓開發者擁有高性能的工具去應對設備配置的多樣性,并確保應用在所有設備上都能完美運行。
在這篇文章中,我主要會涉及三方面的內容,它們包括:安卓設備多樣性的三個方面,以及這種多樣性是如何影響開發的,還有安卓應用的設計。我會從一個較高的層次并從 iOS 開發者角度來談論這些內容:
安卓開發者是怎樣針對屏幕尺寸的微小差異進行優化的? 怎樣處理不同設備的寬高差異?
安卓開發者是怎樣考慮屏幕密度差異(screen density variations)問題的?
應用怎樣才能夠被優化去適應不同的設備? 怎樣才能制作一款在手機和平板上都完美運行的應用呢?
屏幕尺寸 (Screen Size)
我們首先回顧一下 iOS 設備的屏幕尺寸。實際上有三種: 3.5 英寸 iPhone,4 英寸 iPhone,以及 iPad。盡管 iPad mini 比 iPad 小,但從開發者角度,這僅僅只是按比例縮小了。對許多應用來說,3.5 英寸和 4 英寸的設備屏幕尺寸的差別幾乎沒有影響,因為僅僅只是高度改變了。
iOS 繪畫系統使用點(points)而不是像素(pixels),因此屏幕是否是視網膜屏不會影響頁面布局。頁面布局或是靜態的 (針對每種設備用編程方式設計精確到點,或者使用設備相應的 xib 文件) 或動態的 (使用自動布局 Auto Layout 或者自適應 Autoresizing Masks)。
相比之下,在安卓平臺上,有數量驚人的不同尺寸的屏幕需要支持。安卓開發者們是如何確保他們的應用在所有設備上都運行流暢呢?
在許多方面,安卓設計和網頁設計很相似。網頁設計必須支持任何可能存在的瀏覽器尺寸。類似的,安卓應用設計是建立在預期了屏幕尺寸改變的前提下的。我們設計的視圖能夠按照自身限制條件自動填充空間和內容。
既然你在設計時必須將不同的屏幕尺寸都考慮到,那么支持設備橫屏也理所應當需要被考慮。當一款應用需要支持任何尺寸的屏幕大小時,橫向僅僅就只是設備的一項附加配置。
布局文件
讓我們深入到布局系統的更多細節中。布局文件是描述用戶界面的 XML 文件。
如下圖所示,我們創建了一個布局文件樣本。這個文件被用作應用的登錄視圖:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="14dp"> <EditText android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="username"/> <EditText android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="password"/> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="Login"/> </LinearLayout>
在上面的布局文件中,線性布局 LinearLayout
被用來線性排列 視圖Views
。我們實例化了 線性布局
中的三個視圖: 一個用戶名 EditText
,一個密碼 EditText
,和一個登錄按鈕。
需要注意的是在布局文件中的每個視圖的寬 layout_width
和高 layout_height
。這些屬性被用來設置視圖的具體寬高。我們使用兩個常量來設置每個屬性: wrap_content
和 match_parent
。如果用 wrap_content
來設置視圖的高度,那么那個視圖會根據它需要呈現的內容調整至相應高度。如果用 match_parent
來設置視圖的寬,那么那個視圖會和它的父視圖一樣寬。
通過使用 wrap_content
和 match_parent
的值,我們設計了一款可以自動伸縮去適應任何屏幕的視圖。
和 iOS 最重要的區別在于,布局 XML 文件和在其中設置的視圖并未設置大小。事實上,在布局文件的視圖被放置到屏幕上之前,并沒有被設置任何大小相關聯的值。
屏幕密度 (Screen Density)
安卓開發中視圖可變性的另一個影響因素是屏幕密度。你怎樣才能編寫一款可以適應不同密度屏幕的應用的呢?
你應該知道,iOS 開發里會考慮到兩種屏幕: 普通屏幕和 Retina 屏幕。如果文件名里 @2x
后綴被使用,系統會自動根據設備種類選擇合適的圖像。
安卓應用屏幕密度適配的原理和 iOS 相似,但是可變性更強。不同于 iOS 有兩個圖片容器 (image buckets),安卓開發者有很多。我們標準的圖片容器大小是 mdpi
(Median Dots Per Inch),或者稱作中密度。這個 mdpi
容器和普通的iOS圖片尺寸一致。然后,hdpi
(High Dots Per Inch),或者高密度,是中密度 mdpi
的 1.5 倍。最后,xhdpi
(Extra High Dots Per Inch),或稱為超高密度,是普通尺寸的 2 倍,這和 iOS 的 Retina 高清屏尺寸一致。安卓還能利用其它的圖像容器,包含 xxhdpi
和 xxxhdpi
。
資源選定 (Resource Qualifiers)
我們對增添多種圖片尺寸似乎無計可施,但是安卓使用了一種健壯的資源選定系統來挑選具體應該使用的資源。
下面,將介紹一個關于資源選定如何處理圖片的例子。在一個安卓項目中,有一個 res
文件夾,這里放置 app 所要使用的所有資源。包含圖片,以及布局文件,還有一些其他項目資源。
這里,ic_launcher.png
圖片重復出現在下面三個文件夾: drawable-hdpi
,drawable-mdpi
,和 drawable-xhdpi
。當請求名為 ic_launcher
的圖片時,系統運行時會根據設備配置自動選擇適應的圖片.
這能讓我們根據不同屏幕尺寸最優化圖片,但是重復存儲的圖片勢必浪費資源。
這些屏幕密度就是模糊限制因素 (fuzzy qualifiers)。如果你使用和上面的例子中的帶有 xxhdpi
屏幕的設備,系統將自動選擇 xhdpi
版本的圖像然后根據屏幕密度縮放圖片。這種特性允許我們只須創建一個版本的圖像就可以按需為其他密度的屏幕優化。
一個普遍模型是提供高密度的圖片,然后讓安卓去將圖像縮小,以適應低屏幕密度的設備。
密度獨立像素 (DIPs)
應對屏幕密度變化做出的最后調整是在布局文件里設置準確的尺寸。想像你希望你的應用支持屏幕外部填充。我們怎樣設置元素值使得視圖能夠根據不同設備自動匹配屏幕密度?
好吧,iOS 開發者可以將這樣的填充精確至像素點。在非 Retina 屏設備上,原像素值會被使用,而在 Retina 設備上,系統會自動 double 該像素值。
在安卓上,你也可以以原像素點為單位設置圖像填充,但是那些值不會縮放以適用于高密度屏幕的設備。相反的,安卓開發者會設置在密度獨立像素里的測量單元 (通常被稱作密度獨立像素,或者設備像素單元)。這些單元會像 iOS 自適應大小一樣 根據設備密度自動做出縮放調整。
設備種類
最后需要考慮的一點是在安卓開發中不同種類的設備是怎樣被管理的。值得注意的是 iOS 有兩個獨立的類: iPhone 和 iPad。但是,安卓截然不同,因為安卓擁有一系列不同的種類,而且手機和平板之間的差別可以是任意的。先前提到的資源選定體系被重度使用來支持這一范圍的屏幕尺寸變化。
對于簡單屏幕,它們可以基于設備大小尺寸并根據內容調整填充。例如,我們可以檢驗一下維度資源 (dimension resources).我們可以在一個普通的位置定義一個維度值并在我們的布局文件中引用它:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margin="@dimen/container_margin" > ... </LinearLayout>
注意 @dimen/container_margin
的值。這代表了一個存儲在資源系統中的被命名值。我們能夠定義一個基本的邊距屬性值作為默認值:
在 res/values/dimens.xml
里:
<resouces> <dimen name=”container_margin”>4dp</dimen> </resources>
然后,我們為平板創建一個合格版本的填充:
在 res/values-w600dp/dimens.xml
中:
<resouces> <dimen name=”container_margin”>96dp</dimen> </resources>
現在,在寬度至少有 600 個設備點單元的設備上,較大的容器邊緣間距值會被系統選擇。這個多出來的邊界可以調整我們的用戶界面,這樣一來,這款在手機上表現良好的應用,現在在平板上就不只是一個簡單的拉伸版本而已了。
分割視圖
上面的尺寸實例在某些情況下是一個很實用的工具,但是,常常會出現一種情況,那就是由于平板尺寸較大,有了放置額外的應用組件的空間,所以應用可以變得更為有用。
在 iOS 應用中,一個通常的方式是使用一個分離的視圖控制器。UISplitViewController 允許你控制兩個視圖控制器,并將它們同時顯示在 iPad 應用的一個屏幕上。
在安卓上,我們有一個相似的系統,但卻可以有更多的控制和更多的選擇用于擴展。你的應用的核心部件可以被分為可重用的部分,就是 fragments,類似于 iOS 里面的視圖控制器。所有應用里的任一個單屏幕的控制邏輯都能夠被以一個 fragment 的形式呈現出來。當在手機上的時候,我們向用戶呈現一個 fragment。當在平板上時,我們可以向用戶呈現兩個 (或更多的) fragments。
我們可以再次依賴資源選定系統為手機或者平板供應一個獨特的布局文件,這將允許我們在平板上控制兩個 fragment,而在手機上控制一個。
例如,下面定義的布局文件會被用在手機設備上:
在 res/layout/activity_home.xml
中:
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" />
這個被 container
ID 定義的 FrameLayout
會包含我們應用的主視圖,并且會為其提供主 fragment 作為容器。
我們可以為平板設備創建這個文件的對應的版本:
在 res/layout-sw600dp/activity_home.xml
中:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="match_parent"> <FrameLayout android:id="@+id/container" android:layout_width="250dp" android:layout_height="match_parent" /> <FrameLayout android:id="@+id/detail_container" android:layout_weight="1" android:layout_width="0dp" android:layout_height="match_parent" /> </LinearLayout>
現在,當我們在平板設備上使用 activity_home 布局文件時,我們會有兩個窗口,而不是一個,這意味著我們能夠支配兩個 fragment 視圖。我們現在可以幾乎不用修改代碼就能在一個屏幕上呈現主視圖和詳情視圖。在運行時候,系統會根據設備的配置決定布局文件的使用版本。
結論
除了 sw600dp
資源選定以外,這篇文章提到的所有工具都可以供任何你支持的任何安卓設備使用。其實有一種在 sw600dp
加入之前就存在的更老,粒度更小的資源選定方式,一般用于那些更早的的設備上。
正如上面所示,安卓開發者擁有適用于任何設備的優化工具。你會發現一些安卓應用在許多設備上并不非常適合 (這種情況其實蠻常見)。我想要強調的是 從已有平臺上把設計從既存的平臺上強塞到安卓里這種行為其實并不合適。我強烈建議你重新思考一下你的設計并為你的用戶提供一種更愉悅的體驗.