開發(fā)一款移動應(yīng)用是一個創(chuàng)造性的過程。你一定會想要創(chuàng)作一款這樣的應(yīng)用,它美觀實(shí)用,它在任何設(shè)備上都能運(yùn)行流暢,它讓用戶覺得賞心悅目,它讓自己引以為傲,下面我就會告訴你我是如何創(chuàng)作具有這些特性的安卓應(yīng)用的。
對于安卓開發(fā)存在一個普遍的誤解,那就是安卓設(shè)備尺寸的多樣性使得開發(fā)出具有上述特性的應(yīng)用變得十分棘手。 你肯定已經(jīng)看過《安卓可視化碎片》這篇文章了( Android Fragmentation Visualized),文章列舉出了驚人數(shù)目的不同安卓設(shè)備.
事實(shí)上,你確實(shí)需要在設(shè)計上花費(fèi)很多心思,但絕對不必比在其他平臺上花費(fèi)得多。安卓開發(fā)者擁有高性能的工具去應(yīng)對設(shè)備配置的多樣性,并確保應(yīng)用在所有設(shè)備上都能完美運(yùn)行。
在這篇文章中,我主要會涉及三方面的內(nèi)容,它們包括:安卓設(shè)備多樣性的三個方面,以及這種多樣性是如何影響開發(fā)的,還有安卓應(yīng)用的設(shè)計。我會從一個較高的層次并從 iOS 開發(fā)者角度來談?wù)撨@些內(nèi)容:
我們首先回顧一下 iOS 設(shè)備的屏幕尺寸。實(shí)際上有三種: 3.5 英寸 iPhone,4 英寸 iPhone,以及 iPad。盡管 iPad mini 比 iPad 小,但從開發(fā)者角度,這僅僅只是按比例縮小了。對許多應(yīng)用來說,3.5 英寸和 4 英寸的設(shè)備屏幕尺寸的差別幾乎沒有影響,因?yàn)閮H僅只是高度改變了。
iOS 繪畫系統(tǒng)使用點(diǎn)(points)而不是像素(pixels),因此屏幕是否是視網(wǎng)膜屏不會影響頁面布局。頁面布局或是靜態(tài)的 (針對每種設(shè)備用編程方式設(shè)計精確到點(diǎn),或者使用設(shè)備相應(yīng)的 xib 文件) 或動態(tài)的 (使用自動布局 Auto Layout 或者自適應(yīng) Autoresizing Masks)。
相比之下,在安卓平臺上,有數(shù)量驚人的不同尺寸的屏幕需要支持。安卓開發(fā)者們是如何確保他們的應(yīng)用在所有設(shè)備上都運(yùn)行流暢呢?
在許多方面,安卓設(shè)計和網(wǎng)頁設(shè)計很相似。網(wǎng)頁設(shè)計必須支持任何可能存在的瀏覽器尺寸。類似的,安卓應(yīng)用設(shè)計是建立在預(yù)期了屏幕尺寸改變的前提下的。我們設(shè)計的視圖能夠按照自身限制條件自動填充空間和內(nèi)容。
既然你在設(shè)計時必須將不同的屏幕尺寸都考慮到,那么支持設(shè)備橫屏也理所應(yīng)當(dāng)需要被考慮。當(dāng)一款應(yīng)用需要支持任何尺寸的屏幕大小時,橫向僅僅就只是設(shè)備的一項附加配置。
讓我們深入到布局系統(tǒng)的更多細(xì)節(jié)中。布局文件是描述用戶界面的 XML 文件。
如下圖所示,我們創(chuàng)建了一個布局文件樣本。這個文件被用作應(yīng)用的登錄視圖:
<?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>
http://wiki.jikexueyuan.com/project/objc/images/11-12.png" alt="" />
http://wiki.jikexueyuan.com/project/objc/images/11-13.png" alt="" />
在上面的布局文件中,線性布局 LinearLayout 被用來線性排列 視圖Views。我們實(shí)例化了 線性布局中的三個視圖: 一個用戶名 EditText,一個密碼 EditText,和一個登錄按鈕。
需要注意的是在布局文件中的每個視圖的寬 layout_width 和高 layout_height 。這些屬性被用來設(shè)置視圖的具體寬高。我們使用兩個常量來設(shè)置每個屬性: wrap_content 和 match_parent。如果用 wrap_content 來設(shè)置視圖的高度,那么那個視圖會根據(jù)它需要呈現(xiàn)的內(nèi)容調(diào)整至相應(yīng)高度。如果用 match_parent 來設(shè)置視圖的寬,那么那個視圖會和它的父視圖一樣寬。
通過使用 wrap_content 和 match_parent 的值,我們設(shè)計了一款可以自動伸縮去適應(yīng)任何屏幕的視圖。
和 iOS 最重要的區(qū)別在于,布局 XML 文件和在其中設(shè)置的視圖并未設(shè)置大小。事實(shí)上,在布局文件的視圖被放置到屏幕上之前,并沒有被設(shè)置任何大小相關(guān)聯(lián)的值。
安卓開發(fā)中視圖可變性的另一個影響因素是屏幕密度。你怎樣才能編寫一款可以適應(yīng)不同密度屏幕的應(yīng)用的呢?
你應(yīng)該知道,iOS 開發(fā)里會考慮到兩種屏幕: 普通屏幕和 Retina 屏幕。如果文件名里 @2x 后綴被使用,系統(tǒng)會自動根據(jù)設(shè)備種類選擇合適的圖像。
安卓應(yīng)用屏幕密度適配的原理和 iOS 相似,但是可變性更強(qiáng)。不同于 iOS 有兩個圖片容器 (image buckets),安卓開發(fā)者有很多。我們標(biāo)準(zhǔn)的圖片容器大小是 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。
我們對增添多種圖片尺寸似乎無計可施,但是安卓使用了一種健壯的資源選定系統(tǒng)來挑選具體應(yīng)該使用的資源。
下面,將介紹一個關(guān)于資源選定如何處理圖片的例子。在一個安卓項目中,有一個 res 文件夾,這里放置 app 所要使用的所有資源。包含圖片,以及布局文件,還有一些其他項目資源。
http://wiki.jikexueyuan.com/project/objc/images/11-14.png" alt="" />
這里,ic_launcher.png 圖片重復(fù)出現(xiàn)在下面三個文件夾: drawable-hdpi,drawable-mdpi,和 drawable-xhdpi。當(dāng)請求名為 ic_launcher 的圖片時,系統(tǒng)運(yùn)行時會根據(jù)設(shè)備配置自動選擇適應(yīng)的圖片.
這能讓我們根據(jù)不同屏幕尺寸最優(yōu)化圖片,但是重復(fù)存儲的圖片勢必浪費(fèi)資源。
這些屏幕密度就是模糊限制因素 (fuzzy qualifiers)。如果你使用和上面的例子中的帶有 xxhdpi 屏幕的設(shè)備,系統(tǒng)將自動選擇 xhdpi 版本的圖像然后根據(jù)屏幕密度縮放圖片。這種特性允許我們只須創(chuàng)建一個版本的圖像就可以按需為其他密度的屏幕優(yōu)化。
一個普遍模型是提供高密度的圖片,然后讓安卓去將圖像縮小,以適應(yīng)低屏幕密度的設(shè)備。
應(yīng)對屏幕密度變化做出的最后調(diào)整是在布局文件里設(shè)置準(zhǔn)確的尺寸。想像你希望你的應(yīng)用支持屏幕外部填充。我們怎樣設(shè)置元素值使得視圖能夠根據(jù)不同設(shè)備自動匹配屏幕密度?
好吧,iOS 開發(fā)者可以將這樣的填充精確至像素點(diǎn)。在非 Retina 屏設(shè)備上,原像素值會被使用,而在 Retina 設(shè)備上,系統(tǒng)會自動 double 該像素值。
在安卓上,你也可以以原像素點(diǎn)為單位設(shè)置圖像填充,但是那些值不會縮放以適用于高密度屏幕的設(shè)備。相反的,安卓開發(fā)者會設(shè)置在密度獨(dú)立像素里的測量單元 (通常被稱作密度獨(dú)立像素,或者設(shè)備像素單元)。這些單元會像 iOS 自適應(yīng)大小一樣 根據(jù)設(shè)備密度自動做出縮放調(diào)整。
最后需要考慮的一點(diǎn)是在安卓開發(fā)中不同種類的設(shè)備是怎樣被管理的。值得注意的是 iOS 有兩個獨(dú)立的類: iPhone 和 iPad。但是,安卓截然不同,因?yàn)榘沧繐碛幸幌盗胁煌姆N類,而且手機(jī)和平板之間的差別可以是任意的。先前提到的資源選定體系被重度使用來支持這一范圍的屏幕尺寸變化。
對于簡單屏幕,它們可以基于設(shè)備大小尺寸并根據(jù)內(nèi)容調(diào)整填充。例如,我們可以檢驗(yàn)一下維度資源 (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 的值。這代表了一個存儲在資源系統(tǒng)中的被命名值。我們能夠定義一個基本的邊距屬性值作為默認(rèn)值:
在 res/values/dimens.xml 里:
<resouces>
<dimen name=”container_margin”>4dp</dimen>
</resources>
然后,我們?yōu)槠桨鍎?chuàng)建一個合格版本的填充:
在 res/values-w600dp/dimens.xml 中:
<resouces>
<dimen name=”container_margin”>96dp</dimen>
</resources>
現(xiàn)在,在寬度至少有 600 個設(shè)備點(diǎn)單元的設(shè)備上,較大的容器邊緣間距值會被系統(tǒng)選擇。這個多出來的邊界可以調(diào)整我們的用戶界面,這樣一來,這款在手機(jī)上表現(xiàn)良好的應(yīng)用,現(xiàn)在在平板上就不只是一個簡單的拉伸版本而已了。
上面的尺寸實(shí)例在某些情況下是一個很實(shí)用的工具,但是,常常會出現(xiàn)一種情況,那就是由于平板尺寸較大,有了放置額外的應(yīng)用組件的空間,所以應(yīng)用可以變得更為有用。
在 iOS 應(yīng)用中,一個通常的方式是使用一個分離的視圖控制器。UISplitViewController 允許你控制兩個視圖控制器,并將它們同時顯示在 iPad 應(yīng)用的一個屏幕上。
在安卓上,我們有一個相似的系統(tǒng),但卻可以有更多的控制和更多的選擇用于擴(kuò)展。你的應(yīng)用的核心部件可以被分為可重用的部分,就是 fragments,類似于 iOS 里面的視圖控制器。所有應(yīng)用里的任一個單屏幕的控制邏輯都能夠被以一個 fragment 的形式呈現(xiàn)出來。當(dāng)在手機(jī)上的時候,我們向用戶呈現(xiàn)一個 fragment。當(dāng)在平板上時,我們可以向用戶呈現(xiàn)兩個 (或更多的) fragments。
我們可以再次依賴資源選定系統(tǒng)為手機(jī)或者平板供應(yīng)一個獨(dú)特的布局文件,這將允許我們在平板上控制兩個 fragment,而在手機(jī)上控制一個。
例如,下面定義的布局文件會被用在手機(jī)設(shè)備上:
在 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 會包含我們應(yīng)用的主視圖,并且會為其提供主 fragment 作為容器。
我們可以為平板設(shè)備創(chuàng)建這個文件的對應(yīng)的版本:
在 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>
現(xiàn)在,當(dāng)我們在平板設(shè)備上使用 activity_home 布局文件時,我們會有兩個窗口,而不是一個,這意味著我們能夠支配兩個 fragment 視圖。我們現(xiàn)在可以幾乎不用修改代碼就能在一個屏幕上呈現(xiàn)主視圖和詳情視圖。在運(yùn)行時候,系統(tǒng)會根據(jù)設(shè)備的配置決定布局文件的使用版本。
除了 sw600dp 資源選定以外,這篇文章提到的所有工具都可以供任何你支持的任何安卓設(shè)備使用。其實(shí)有一種在 sw600dp 加入之前就存在的更老,粒度更小的資源選定方式,一般用于那些更早的的設(shè)備上。
正如上面所示,安卓開發(fā)者擁有適用于任何設(shè)備的優(yōu)化工具。你會發(fā)現(xiàn)一些安卓應(yīng)用在許多設(shè)備上并不非常適合 (這種情況其實(shí)蠻常見)。我想要強(qiáng)調(diào)的是 從已有平臺上把設(shè)計從既存的平臺上強(qiáng)塞到安卓里這種行為其實(shí)并不合適。我強(qiáng)烈建議你重新思考一下你的設(shè)計并為你的用戶提供一種更愉悅的體驗(yàn).