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