2013年3月20日 星期三

[Android] Android Developer Note Best Practices 2

Supporting Multiple Screens (見 Android Developer Supporting Multiple Screens)

序言
Android 能在多種裝置跑,各種裝置提供不同螢幕大小和解析度。
對於app來說,Android系統提供了一個固定的開發環境以跨不同的裝置,
且處理大多數的工作來調整app的使用者介面在螢幕所顯示的位置。
同時,系統也提供了API讓你可以去針對特定的大小、解析度去設計app的畫面。
舉例來說,你可能希望放在平板和手持裝置的UI要不一樣XD。

儘管系統會自動調整和縮放大小,建議最好還是自己對不同的螢幕狀況作最佳化。
這麼做可以最佳化使用者體驗,讓他們覺得備受尊榮,
好像這app是為他們量身打造的(無誤XD)。

藉由下面描述的實作你可以新增一個app來正確地顯示,
且在所有的螢幕設定下都能提供最佳化的使用者體驗,
僅僅使用單一個.apk檔。

Overview of Screens Support
下面會提供一個對於多種螢幕支援的概觀,
包含介紹術語和這裡會用到的API、短總結、以及API及關於螢幕相容的功能。

Terms and concepts
Screen size ->
   物理大小,以對角線量測。
   通常Android的分組是:small, normal, large, extra large這四種。
Screen density ->
   解析度,即單位區塊所能容納的像素(pixels),通常單位是dpi(dots per inch)。
   分組:low, medium, high, extra high這四種。
Orientation ->
   方向。依照使用者的角度來看,在執行時可以隨使用者轉裝置改變。
Resolution ->
   螢幕的總像素。因為螢幕大小不一樣,
   所以開發者應考慮解析度及螢幕大小為準。
Density-independent pixel(dp) ->
   虛擬像素單位,當定義使用者介面的layout的時候,
   能以跟解析度獨立的方式來描述。
   實作上請總是使用dp來定義app的使用者介面,
   藉此保證對於你的UI而言可以正確顯示在不同螢幕上。

Range of screen supported
從Android 1.6(API level 4)開始Android提供了對不同種類的螢幕支援,
反映了一台設備可能有許多不同的螢幕設定。
你可以藉由一些功能來做到最佳化在每一種螢幕上。

簡化設計需求,Android將其化簡四種generalized density:
ldpi, mdpi, hdpi, xhdpi.

當在設計UI的時候你會發現每個design都需要一個最小大小的空間。
所以每個一般化的螢幕大小有其被系統關聯的的最小解析度。
這些最小的size是以"dp"為單位的:同樣設計時請以dp為單位定義layout,
以避免螢幕解析度變化時出錯。
(單位為dp*dp)
xlarge 960*720
large 640*480
normal 470*320
small 426*320

 注意: 3.0以前的版本有些螢幕會沒有難以分類在normal和large之間。
然後有system bar的table會佔掉一點螢幕可用的空間,
所以實際能用的空間會小一點點。
 
要最佳化UI的話,你可以提供不同的資源給一般化的大小和解析度。
通常情況下,要給不同螢幕大小不同layout、給不同解析度的不同圖像。
然後跑的時候裝置就可以照其屬性來使用適當的資源。
當然也不一定絕對要提供,系統仍然會自動提供相容,
令其可以跑在不同的大小或解析度的螢幕上。

Density independence
當app可以保持在各種解析度的螢幕上UI的元件大小都一樣時,
我們稱其達到解析度獨立"density independence"。


(上): 沒有支援的狀況,元件隨著解析度增加,實際的物理大小變小了
(下): 有支援的狀況,無論在哪種解析度,實際的物理大小都一樣。



Android系統以兩種方式來幫忙達到density independence。
1. 系統縮放dp unit到適當的狀況給現在的螢幕解析度。
2. 系統縮放drawable資源到適當的大小(根據現在的螢幕解析度)(如果必要的話)。

兩圖的(上)裡面,text view和bitmap drawable有著以pixels(px units)定義的座標,
所以view會物理上在低解析度的螢幕比較大,反之亦然。
因為雖然實際螢幕大小(screen size)一樣,但在高解析度的螢幕上,
單位物理大小所能容納的pixels就越多,所以看起來才會變成這樣子,
而(下)裡面的每個都是density-independent,
故實際大小並不跟著改變。

大多數的狀況你可以確保density independence在app中,
可以很簡單的定位所有layout dimension values,使用density-independent pixels(dp units),
或者使用"wrap_content"。系統會自動縮放適當大小來給現有的螢幕顯示。

但bitmap縮放可能導致模糊或 pixelated bitmaps,可能會是你有可'能注意到的螢幕狀況。
要避免這些狀況,我們應該要提供不同的bitmap的資源。
如果提供了高解析度的圖片,系統會用它們而就可以不用低解析度來縮放了。
下面的段落描述了支援不同資源給不同螢幕的設定。

How to Support Multiple Screens
基礎Android所支援的是去算圖並處理到適當的狀況給螢幕設定,
大部分可以運作正常,不過如果要更優雅地(是有多愛優雅阿XD)去處理不同螢幕設定,
你應該也:
1. 明確在manifest裡定義app支援的螢幕大小。
聲明app所支援的螢幕大小可以藉此確保你所支援的螢幕大小的裝置可以下載你的app。
聲明對不同螢幕大小的支援也會影響系統對於你的app作大螢幕繪圖的畫法,
特別在app跑在螢幕相容性模式(screen compatibility mode)的時候。
要聲明app所支援的螢幕大小,你應該要將 <supports-screens>包進manifest檔裡。

2. 為不同螢幕大小提供不同layout
有時候系統自動調整大小的方式會讓你的UI看起來不那麼好,
比如位置上可能會跑掉或大小不好。
你可以提供small, normal, large及xlarge的大小對應的資源,
比如給extra large的螢幕的layout就應該丟到layout-xlarge/裡。
Android 3.2(API level 13)開始,上面的size groups被deprecated(也就是建議不再使用)。
相對來說我們該用另一種 sw<N>dp 設定qualifier來取代,
這是以定義最小需求寬度來界定的。
比如一個設計給最少600dp的螢幕寬的平板的layout,
這些相對應的東西就該放在layout-sw600dp/裡。

3. 提供不同bitmap drawables給不同的螢幕解析度
由於Android會對圖形資源做縮放到實際的物理大小在每個裝置上,
所以當你只給mdpi的話,放到hdpi以上的話圖會怎樣?
當然就爆掉了阿XD! 會很模糊,因為解析度等於是變低了。
要保證bitmap看起來最好,你應該包含各種解析度的版本給各種螢幕解析度的裝置。
取名方式是drawable-<dpi>,比如extra high的話就要放在 drawable-xhdpi/裡。

在執行期間系統怎麼去決定將resource拿來縮放呢?
1. 剛好有定義好的resource就會直接用。
你的螢幕是hdpi,結果系統在drawable-hdpi/目錄底下有找到檔案,
它就拿來用啦XD
2. 沒有符合的就會用預設的resource然後縮放到符合螢幕。
預設的話就是不加任何後綴,比如drawable/目錄這樣的型式。
系統是假定預設的是給最基本的大小和解析度用的,
也就是normal螢幕大小以及medium解析度。
當然也不是每次一定會用預設的,系統會傾向於將最大圖縮成小圖(因為比較簡單)。
所以當它需要low-density而你只有high和medium時,
它會選擇拿high來縮。

Using configuration qualifiers
一個configuration qualifier是一個字串,讓你可以加上resource目錄在你的project裡,
而且定位內部設計的資源設定。

使用方式:
1. 在project裡開一個res/目錄並使用這樣的格式來命名:
<resources_name>-<qualifier>
然後<resources_name>是標準的資源名稱(比如drawable或layout),
<qualifier>是一個從下表而來的configuration qualifier,
用來定義螢幕設定該用那些資源(比如hdpi或xlarge)。

2. 存適當的設定資源在新的資料夾。資源檔必須要剛好命名的跟預設資源檔一樣。
舉例來說,xlarge是給extra large的螢幕的configuration qualifier。
當那麼加上字串到資源目錄的名稱(如layout-xlarge),這是對系統指示這些資源,
要被用在有extra large螢幕的裝置。
Screen characteristicQualifierDescription
SizesmallResources for small size screens.
normalResources for normal size screens. (This is the baseline size.)
largeResources for large size screens.
xlargeResources for extra large size screens.
DensityldpiResources for low-density (ldpi) screens (~120dpi).
mdpiResources for medium-density (mdpi) screens (~160dpi). (This is the baseline density.)
hdpiResources for high-density (hdpi) screens (~240dpi).
xhdpiResources for extra high-density (xhdpi) screens (~320dpi).
nodpiResources for all densities. These are density-independent resources. The system does not scale resources tagged with this qualifier, regardless of the current screen's density.
tvdpiResources for screens somewhere between mdpi and hdpi; approximately 213dpi. This is not considered a "primary" density group. It is mostly intended for televisions and most apps shouldn't need it—providing mdpi and hdpi resources is sufficient for most apps and the system will scale them as appropriate. If you find it necessary to provide tvdpi resources, you should size them at a factor of 1.33*mdpi. For example, a 100px x 100px image for mdpi screens should be 133px x 133px for tvdpi.
OrientationlandResources for screens in the landscape orientation (wide aspect ratio).
portResources for screens in the portrait orientation (tall aspect ratio).
Aspect ratiolongResources for screens that have a significantly taller or wider aspect ratio (when in portrait or landscape orientation, respectively) than the baseline screen configuration.
notlongResources for use screens that have an aspect ratio that is similar to the baseline screen configuration.

舉例來說,下面這是一個app裡提供不同layout和繪圖給不同的螢幕。

res/layout/my_layout.xml             // layout for normal screen size ("default")
res/layout-small/my_layout.xml       // layout for small screen size
res/layout-large/my_layout.xml       // layout for large screen size
res/layout-xlarge/my_layout.xml      // layout for extra large screen size
res/layout-xlarge-land/my_layout.xml // layout for extra large in landscape orientation

res/drawable-mdpi/my_icon.png        // bitmap for medium density
res/drawable-hdpi/my_icon.png        // bitmap for high density
res/drawable-xhdpi/my_icon.png       // bitmap for extra high density

要注意的是當系統挑資源的時候是有一個決定最適合的資源的方法的,
所以你不用剛好完全有match人家螢幕的設定。
但如果你只有比現在螢幕大的資源又沒有預設的資源的時候,
程式就會crash掉,比如你的size是normal但程式只提供xlarge,那麼系統是不會用的。

註:
如果你確定你完全都調好,系統完全不須縮放時,
應該將它用nodpi的configuration quilifier的資料夾裡,這告訴系統說,
它不須也不用去縮放這個資料夾裡的資源,直接用就行了。

Designing alternative layouts and drawables
這部分提供一點關於使用大小和解析度的quilifiers來提供不同layout和drawables的總結。

Alternative layouts:
大致上在不同螢幕上測試後,你會知道你需不需要不同layout。
舉例來說:
1. 在小螢幕上測可能會發現比如Button塞不下的狀況。
那你就需要特別的layout給小螢幕,或者調整排版的位置。
2. 當在特大螢幕測的時候,你可能會發現螢幕的空間你沒有妥善利用,
而有些元件看起來就是拉過大去填滿整個螢幕而沒有意義。
這時你也可以考慮重新設計UI來最佳化像平板之類的體驗XD。
3. 而且當在測不同的方向的時候,UI的排版也會有問題。
所以你的layout應該要:
1. 作配合小螢幕適合的設計
2. 最佳化給大螢幕以好好利用額外的空間
3. 最佳化給直橫立兩種使用狀況

Alternative drawables:
大概每個app都得有不同的drawable resources,
畢竟從螢幕上app的icon開始就給人醜醜的體驗就別玩了XDDDDD
提供的時候應該要遵循3:4:6:8縮放比例原則。
比如說對medium來說你想要給一個48*48 pixels的bitmap drawable的話,
那個所有的size應該是:
36*36 low-density
48*48 medium-density
72*72 high-density
96*96 extra high-density


Declaring Tablet Layouts for Android 3.2
第一代平板的版本Android 3.0時,正確地定義layout方式是用xlarge,
為了相容其他種類的平板,3.2版以後提供了特別的定義資源給更多固定螢幕大小。
(最基本就是就是前面提到的sw<N>dp定義方法)

Using new size qualifiers
基本有三種:sw<N>dp、w<N>dp、h<N>dp。
Screen configurationQualifier valuesDescription
smallestWidthsw<N>dp

Examples:
sw600dp
sw720dp
The fundamental size of a screen, as indicated by the shortest dimension of the available screen area. Specifically, the device's smallestWidth is the shortest of the screen's available height and width (you may also think of it as the "smallest possible width" for the screen). You can use this qualifier to ensure that, regardless of the screen's current orientation, your application's has at least <N> dps of width available for it UI.
For example, if your layout requires that its smallest dimension of screen area be at least 600 dp at all times, then you can use this qualifer to create the layout resources, res/layout-sw600dp/. The system will use these resources only when the smallest dimension of available screen is at least 600dp, regardless of whether the 600dp side is the user-perceived height or width. The smallestWidth is a fixed screen size characteristic of the device; the device's smallestWidth does not change when the screen's orientation changes.
The smallestWidth of a device takes into account screen decorations and system UI. For example, if the device has some persistent UI elements on the screen that account for space along the axis of the smallestWidth, the system declares the smallestWidth to be smaller than the actual screen size, because those are screen pixels not available for your UI.
This is an alternative to the generalized screen size qualifiers (small, normal, large, xlarge) that allows you to define a discrete number for the effective size available for your UI. Using smallestWidth to determine the general screen size is useful because width is often the driving factor in designing a layout. A UI will often scroll vertically, but have fairly hard constraints on the minimum space it needs horizontally. The available width is also the key factor in determining whether to use a one-pane layout for handsets or multi-pane layout for tablets. Thus, you likely care most about what the smallest possible width will be on each device.
Available screen widthw<N>dp

Examples:
w720dp
w1024dp
Specifies a minimum available width in dp units at which the resources should be used—defined by the <N> value. The system's corresponding value for the width changes when the screen's orientation switches between landscape and portrait to reflect the current actual width that's available for your UI.
This is often useful to determine whether to use a multi-pane layout, because even on a tablet device, you often won't want the same multi-pane layout for portrait orientation as you do for landscape. Thus, you can use this to specify the minimum width required for the layout, instead of using both the screen size and orientation qualifiers together.
Available screen heighth<N>dp

Examples:
h720dp
h1024dp
etc.
Specifies a minimum screen height in dp units at which the resources should be used—defined by the <N> value. The system's corresponding value for the height changes when the screen's orientation switches between landscape and portrait to reflect the current actual height that's available for your UI.
Using this to define the height required by your layout is useful in the same way as w<N>dp is for defining the required width, instead of using both the screen size and orientation qualifiers. However, most apps won't need this qualifier, considering that UIs often scroll vertically and are thus more flexible with how much height is available, whereas the width is more rigid.

1. 依照當前的最小寬度(不隨轉向而變)
2. 可用的螢幕寬
3. 可用的螢幕高

Configuration examples
典型有4種: 320dp(手機螢幕) 480dp(小型平板) 600dp(7吋平板) 720dp(10吋平板)
比如你要提供600dp最小寬的支援的話可以提供這兩組:

res/layout/main_activity.xml           # For handsets
res/layout-sw600dp/main_activity.xml   # For tablets

這個狀況下螢幕的最小寬必須是600dp。
比如你還想支援7吋甚至10吋,就在往上加上去:

res/layout/main_activity.xml           # For handsets (smaller than 600dp available width)
res/layout-sw600dp/main_activity.xml   # For 7” tablets (600dp wide and bigger)
res/layout-sw720dp/main_activity.xml   # For 10” tablets (720dp wide and bigger)

注意到前兩個是用sw<N>dp,也就是說螢幕不論現在轉向,
都是算兩方向的最小端。所以這是對於可以不去考慮轉向的一個簡單作法。
不過有時候,你可能只是需要要求在任何狀況"寬"需要最少多少的大小值,
這時候就該用w<N>dp。

res/layout/main_activity.xml         # For handsets (smaller than 600dp available width)
res/layout-w600dp/main_activity.xml  # Multi-pane (any screen with 600dp available width or more)

另外h<N>dp也是一樣的道理。

Declaring screen size support
一旦實作了layout,相應在manifest裡就要定義,
這兩者同等重要,少了任一你都無法使用該layout。

Android 3.2開始引入了新的屬性(attributes)給<supports-screens>  manifest元素:

android:requiresSmallestWidthDp
定義了最小的最小所需寬(螢幕的最小的座標方向的長度,比如720*640->640) (好拗口@@),
螢幕的最小寬必須大於等於它,否則無法執行。
例:

<manifest ... >
    <supports-screens android:requiresSmallestWidthDp="600" />
    ...</manifest>

但如果你是所有的都支援的話就不用定義這個了。

android:compatibleWidthLimitDp
這個屬性定義了最大的你可以跑的最小寬,超過的話還是可以跑,
但可能就會作延展之類的。(在螢幕相容性模式(screen compatibility mode),
系統不會預設打開,但會顯示出通知來供使用者開關)

android:largestWidthLimitDp
這個屬性讓你可以強制打開screen compatibility mode,其他基本上和前者一樣,
而且使用者不能主動關掉相容性模式。

注意:
開發3.2以後的版本時,你不該用舊的螢幕大小屬性和現在新的作組合。
組合起來可能發生不可預期的結果。

Best Practices
簡單的檢查支援不同螢幕的方法:
1. 用wrap_contentfill_parentdp單位當在XML layout file裡定位座標。
2. 別在app的code用硬(hard)coded的像素,也就是別用絕對值的方式。
3. 別用 AbsoluteLayout (它已經被deprecated了!)。
4. 為不同螢幕解析度提供不同的bitmap drawables支援。

1. Use wrap_content, fill_parent, or the dp unit for layout dimensions
當定位view的兩個元素(android:layout_width and android:layout_height)的時候,
使用wrap_content, fill_parent, 或者 dp可以讓view在當前的螢幕上有更恰當的大小。
舉例來說,一個view用layout_width="100dp"會在medium-density的螢幕上用100 pixels寬,
而在high-density的螢幕上,系統會放到150 pixels寬,
這麼一來在螢幕上view所佔的相對位置大致上會相同。
與其相似的,可以用sp(scale-independent pixel)來定義文字的大小,
就如同dp的感覺一樣。
2. Do not use hard-coded pixel values in your application code.
使用絕對的數值來計算的話當density改變時,
這種計算就會出現致命的漏洞。比如在程式碼拿元件寬度,
不同螢幕給出的pixel會不一樣,這點要特別注意。

3. Do not use AbsoluteLayout
它在Android 1.5版本裡就被deprecated了。
因為全部都用固定的位置定位,所以在不同的顯示螢幕上,
很容易會出現UI位置不好的狀況。
建議上改用RelativeLayout。

4. Use size and density-specific resources
雖然系統會按照目前的螢幕設定來縮放layout和drawable,
也許你會想自己提供不同的resource以達到最佳化。
最簡單的做法就是你知道目標是那些種類的螢幕,
就提供相對應的layout或者圖給它。
例如:

res/drawable-mdpi/icon.png   //for medium-density screens
res/drawable-hdpi/icon.png   //for high-density screens

Additional Density Considerations

1. Pre-scaling of resources(such as bitmap drawables)
大小符合->直接用不調整。
沒有符合的->
找預設來縮放,這時候你跟系統要size相關的資訊時,
它會告訴你縮放過的狀況,這叫做pre-scaling。
比如說你的default是50*50 pixels的bitmap(也就是mdpi),
現在的螢幕是hdpi,於是當你問它圖的size時,
系統會回答你75*75。
不想讓它這麼做的話,請將resource放到nodpi的資料夾,
比如: res/drawable-nodpi/icon.png,這樣系統就會直接用而不會縮放。

2. Auto-scaling of pixel dimensions and coordinates
想關掉pre-scaling -> 在manifest檔裡設android:anyDensity 為 "false"
或者將Bitmap的inScaled屬性設為false。
這種方法被稱為Auto-scale,會在實際上縮放成絕對的物理大小,
且座標也會相對應轉換。接著你問的時候,
系統會回報app縮放後的pixel(對應到現在的螢幕解析度)。

通常你不該關掉pre-scaling就是了XD。

如果程式操作bitmap或者直接和pixels用別的方法互動,
你可能會需要額外的步驟來支援不同螢幕解析度,
比如你支援了觸控手勢,那你可能要取得獨立於解析度的pixel值,
而非實際的pixels。

Scaling Bitmap objects created at runtime


有提供不同的bimap圖片會比縮放的看起來好一點。
可以透過BitmapFactory.Options來定義bitmap的屬性,
且使用inDensity的field可以定義圖片所對應解析度。
把inScaled設為false可以將pre-scaling改成auto-scale(也就是在draw time才處理)。
這樣會叫耗CPU資源,但用較少記憶體。

Converting dp units to pixel units
在一些狀況下你需要去用dp來表達座標,然後轉換成pixels。
比如觸控手勢滑過的長度界限:

// The gesture threshold expressed in dp
private static final float GESTURE_THRESHOLD_DP = 16.0f;
// Get the screen's density scale
final float scale = getResources().getDisplayMetrics().density;
// Convert the dps to pixels, based on density scale
mGestureThreshold = (int) (GESTURE_THRESHOLD_DP * scale + 0.5f);
// Use mGestureThreshold as a distance in pixels...

 DisplayMetrics.density field定出了換算的參數,
我們就可以用這個scale來計算實際的狀況。

How to Test Your Application on Multiple Screens
我們可以用AVD Manager來開不同的虛擬Android的機器來作測試。


 
                                 Low density (120), ldpi     Medium density (160), mdpiHigh density (240), hdpiExtra high density (320), xhdpi

SmallscreenQVGA (240x320)480x640
NormalscreenWQVGA400 (240x400)
WQVGA432 (240x432)
HVGA (320x480)WVGA800 (480x800)
WVGA854 (480x854) 
600x1024
640x960
LargescreenWVGA800** (480x800)
WVGA854** (480x854)
WVGA800* (480x800)
WVGA854* (480x854) 
600x1024
Extra Largescreen1024x600WXGA (1280x800)
1024x768
1280x768
1536x1152
1920x1152 
1920x1200
2048x1536
2560x1536 
2560x1600

上面這些是各種相對應的設定代表的解析度等等。

建議上我們能設置的跟實體條件越接近越好。
如果用命令列來執行也是可以的,
使用-scale的參數來設定scale:

emulator -avd <avd_name> -scale 96dpi






沒有留言:

張貼留言