code
2018年7月31日 星期二
2018年6月21日 星期四
Android Architecture Components - Life Cycle Aware Components
簡介
這是Android官方推薦“正確寫一個Android App"的方法,這個官方推薦的方法由一些非常基本library以及pattern組成,稱為Architecture components,包括以下:Lifecycles: 每個app必考慮的東西
Lifecycle-aware observables: 觀察lifecycles的方法
Lightweight ViewModel: 這是獨立於Activity和Fragment之外,可以單獨測試邏輯之處
Object mapping for sqlite
要解決什麼問題?
通常programmer把所有的邏輯(商業/UI/Data/與OS互動等)都寫在activity或是fragment中,不過activity事實上是設計來操縱UI與OS互動,越簡單越能避免life cycle造成的問題,主要是因為activity / fragment 這類型的class,programmer並沒有絕對掌控權,從instance creation到destruction都是OS決定,所以要盡量減少對這些class的dependency。此外model應該永遠獨立於view之外,永遠不受到OS相關決定(回收記憶體之類)的影響。
Life-cycle Aware Components
我們通常會把某些邏輯或是object寫在一個activity或是fragment裡面,應且在life cycle callback中作出對應的動作。例如以下MyLocationListener instance會在MyActivity的onStart()和onStop() 時也去做start() / stop() 動作:class MyLocationListener {
public MyLocationListener(Context context, Callback callback) {
// ...
}
void start() {
// connect to system location service
}
void stop() {
// disconnect from system location service
}
}
class MyActivity extends AppCompatActivity {
private MyLocationListener myLocationListener;
@Override
public void onCreate(...) {
myLocationListener = new MyLocationListener(this, (location) -> {
// update UI
});
}
@Override
public void onStart() {
super.onStart();
myLocationListener.start();
// manage other components that need to respond
// to the activity lifecycle
}
@Override
public void onStop() {
super.onStop();
myLocationListener.stop();
// manage other components that need to respond
// to the activity lifecycle
}
}
但是如果有很多類似的component在這個activity裡面,那他的life cycle callbacks就會充滿這類code,變得複雜難maintain。
此外我們不能保證async operations會在我們要的life cycle callbacks回來:
class MyActivity extends AppCompatActivity {
private MyLocationListener myLocationListener;
public void onCreate(...) {
myLocationListener = new MyLocationListener(this, location -> {
// update UI
});
}
@Override
public void onStart() {
super.onStart();
Util.checkUserStatus(result -> {
// what if this callback is invoked AFTER activity is stopped?
if (result) {
myLocationListener.start();
}
});
}
@Override
public void onStop() {
super.onStop();
myLocationListener.stop();
}
}
可以看到在onStart()中,此myLocationListener要等待某個operation result回來為true才會啟動,但是有可能此Activity onStop()先發生了,然後此operation result才回來,所以又啟動了這個myLocationListener。這有可能造成了這個listener在我們未預期的狀況下活動。android.arch.lifecycle
提供了一些design pattern來幫助我們免於煩這些(鳥)事。Life Cycle
LifeCycle這個class把某個有life cycle概念的component (e.g. Activity / Fragment / Service) 的life cycle給抽象化了,它有states和events:
所以對這個component life cycle有興趣的人,就可以implement LifeCycleObserver:
public class MyObserver implements LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
public void connectListener() {
...
}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
public void disconnectListener() {
...
}
}
myLifecycleOwner.getLifecycle().addObserver(new MyObserver());
Life Cycle Owner
擁有LifeCycle的components必須implement LifeCycleOwner interface (AppCompatActivity的某個super class是implement這個interface的),所以最快的讓自己的activity或是fragment變成life cycle owner就是去繼承有implement這個interface的base class:class MyActivity extends AppCompatActivity {
private MyLocationListener myLocationListener;
public void onCreate(...) {
myLocationListener = new MyLocationListener(this, getLifecycle(), location -> {
// update UI
});
Util.checkUserStatus(result -> {
if (result) {
myLocationListener.enable();
}
});
}
}
剛剛的MyLocationListener就可以implement LifeCycleObserver:class MyLocationListener implements LifecycleObserver { private boolean enabled = false; public MyLocationListener(Context context, Lifecycle lifecycle, Callback callback) { ... } @OnLifecycleEvent(Lifecycle.Event.ON_START) void start() { if (enabled) { // connect } } public void enable() { enabled = true; if (lifecycle.getCurrentState().isAtLeast(STARTED)) { // connect if not connected } } @OnLifecycleEvent(Lifecycle.Event.ON_STOP) void stop() { // disconnect if connected } }
可以看到MyLocationListener並沒有override什麼interface method,而是依賴於annotations。我們說MyLocationListener是一個life cycle aware object。
如果不繼承已知的life cycle owner class的話,也可以自己去implement LifeCycleOwner interface,但是要透過以下的templates:
public class MyActivity extends Activity implements LifecycleOwner {
private LifecycleRegistry mLifecycleRegistry;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mLifecycleRegistry = new LifecycleRegistry(this);
mLifecycleRegistry.markState(Lifecycle.State.CREATED);
}
@Override
public void onStart() {
super.onStart();
mLifecycleRegistry.markState(Lifecycle.State.STARTED);
}
@NonNull
@Override
public Lifecycle getLifecycle() {
return mLifecycleRegistry;
}
}
2018年6月10日 星期日
Google IO 2018 - Android Vitals
startup time
1. slow cold start: cold start超過五秒者:定義為:
1. app 不在memory
2. app經過onCreate() -> Activity running
2. slow warm start: app已經在memory,重新叫起超過2秒者:
定義為:
1. app 在memory
2. app經過onCreate() -> Activity running
3. slow hot start: 某個已經launched activity又restart 到 onResume的時間的時間超過1.5秒者:
定義為:
1. app 在memory
2. app經過onRestart() -> Activity running
Detecting Crashes
Android Vitals是一個platform tool,所以可以在3rd party crash-detection sdk (e.g. Fabric Crashlytics)還沒起來前,就能偵測到crash!Battery
這是最重要的效能指標,因為mobile device是很resource-limited。1. 不要直接使用wake lock api,有可能因為bug造成螢幕/CPU永不關閉。改用:
2. 盡量少wake up
Permission
統計看來,user並不喜歡grant permissions:Vitals會把這些permission列出來:
不過目前應該還是在beta吧?! 我們的app都看不到這些資訊。奇怪。
ANRs
這個是Android系統中惡名昭彰的代名詞,android vitals也是有偵測這個東西,這怎麼發生呢?當main thread (UI thread) 被block住,無法處理render / ui input時,系統偵測到此狀態維持幾秒鐘之後,就會出現ANR警告。block的原因可能包括:
1. I/O operations: network, disk
上面這個function call看似沒有read/write operations,但實際上一但被instantiated,就馬上有read operations (SharedPreference implementation就是如此)。所以看似無害的code,實際上在main thread做了I/O operation了。
下面是另一個I/O example:
上面的code實際上在 access headerFields就會真的去讀取connection,甚至在某些版本的Android,連if ()中的URL equality comparison就會進行IO了!
不過developers哪有辦法知道所有這些眉角?????
還好Android提供一個debug api,可以讓我們在debug time抓出這些在main thread不該做的事:
penaltyDeath() 會造成runtime crash,所以記得不要在release版本時候打開!!!當違反strict mode crash之後,可以從log找到發生的行數:
2. long computation
這個就不用多說了,肯定是block。
要常態性使用Android Profiler來看哪些code有可能造成long computation。
Strict mode api也提供自己加入註解一段slow code,讓strict mode偵測到此slow code被main thread上執行時,可以penalty crash。
3. Interprocess communication (IPC)
既然另外一個process (通常是另外一個app)不受我們控制,我們當然不能在main thread去做IPC,否則有可能造成main thread上做一些blocking operations。
4. Multithreaded programming
使用一些synchronization primitives的時候,要很確定自己知道自己在幹嘛,否則有可能造成dead lock => ANR
android vitals的log可以幫助我們找出這類型的問題:
5. Slow BroadcastReceiver handling
我們宣告BroadcastReceiver在manifest中,如果一個broadcast進來,handler是在main thread上跑的! 所以不該做任何以上四點提到的operations。如果你的handler在10秒內沒exit,OS會殺掉整個process (app)。
所以要在handler中用另一個thread來做上面四點的事:
Crashes
我們常常為了怕某些狀況下會crash,而做一堆null check或是try-catch,不過目前Android官方建議全部使用Jetpack的架構來解決這些問題:另外使用Kotlin的built-in nullity check,也是有幫助。
2018年5月16日 星期三
Google IO 2018 - 跟Android有關的talk
Android
App performance:Android vitals: debug app performance and reap rewards
What's new in Android Runtime
Drawn out: how Android renders
Understanding Android memory usage
Best practices using compilers in Android Studio
Best practices for text on Android
What's new with the Android build system
Don't let your app drain your users' battery
App development
What's new in Android development tools
The future of apps on Android and Google Play: modular, instant, and dynamic
Build the new, modular Android App Bundle
Protips: a fresh look at advanced topics for Android experts
Android Jetpack: what's new in Architecture Components
Effective ProGuard keep rules for smaller applications
What's new with ConstraintLayout and Android Studio design tools
Android Slices: build interactive results for Google Search
Building AR apps with the Sceneform SDK
Android Jetpack
Modern Android development: Android Jetpack, Kotlin, and more
Android Jetpack: manage infinite lists with RecyclerView and Paging
Android Jetpack: how to smartly use Fragments in your UI
Android Jetpack: manage UI navigation with Navigation Controller
Android Jetpack: what’s new in Android Support Library
Android Jetpack: easy background processing with WorkManager
Modern Android development: Android Jetpack, Kotlin, and more
Android Jetpack: manage infinite lists with RecyclerView and Paging
Android Jetpack: how to smartly use Fragments in your UI
Android Jetpack: manage UI navigation with Navigation Controller
Android Jetpack: what’s new in Android Support Library
Android Jetpack: easy background processing with WorkManager
Frictionless Android testing: write once, run everywhere
Autonomous and customized pre-launch testing in the Google Play Console
App Security
What's new in Android security
App Release Management
Release management: successful launches and updates on Google Play
Android TV: What’s new with Android TV
Google Play
Analyze your audience and benchmark metrics to grow on Google Play
Google Play Instant: how app developers are finding success
Android Car
What’s new in automotive
Wear OS
What's new in Wear OS by Google
2017年12月23日 星期六
iOS Universal Links and Android App Links
Android App Links
我們可以把app中的content透過連結,讓使用者直接開啟app到那個特定的content,有以下兩種連結方方法可以做到:
Deep Links: 這是使用URL的方法,我們必須在app中加入intent filters,然後解析intent帶來的data來開啟相對應的activity。潛在問題是其他的app如果也listen相同的intent,則可以獲取此連結開啟權力。
Android App Links (Android 6.0+ / api 23+): 設定一個app為某種link的default handler,使用者可以在Android系統設定中更改此設定。好處包括:
- 需驗證HTTP URL domain ownership,不會被其他app冒用
- 如果使用者沒安裝app的話,會直接導向web
- support Instant Apps (不用安裝app就可以使用app)
- 可以從以下地方導到app content:
- mobile browser中點擊Google search結果
- Google Search app
- Android screen search
- Google Assistant
可以說App Links是強化版本的URL scheme deep links。怎麼實現App Links呢?
簡單來說:
1. 放一個json file到一個https的網站,讓Android用來驗證網站所有權以及app關聯性。
2. 在app manifest宣告某個activity相關的intent filter,以及要設定autoVerify為true,這樣Android才會在launch activity的第一次就驗證關聯性。
很簡單,困難點反而是在怎麼建立一個https網站,如果對我這種backend白癡來說的話。
簡單來說:
1. 放一個json file到一個https的網站,讓Android用來驗證網站所有權以及app關聯性。
2. 在app manifest宣告某個activity相關的intent filter,以及要設定autoVerify為true,這樣Android才會在launch activity的第一次就驗證關聯性。
很簡單,困難點反而是在怎麼建立一個https網站,如果對我這種backend白癡來說的話。
iOS Universal Links
iOS Universal Links跟Android App Links就是一樣的東西,行為:- 如果有安裝app,則點擊網頁中的link會開啟app中的某個特定畫面
- 如果沒安裝app則開啟Safari導到這個link連結的網頁
- 不會被開啟其他宣稱能handle此url的app (同樣經過網站所有權驗證)
- 不過如果原本已經開啟此domain web page,就不會開啟app
上面9JA89QQLNQ是team id,可以在developer portal找到,後面接的就是app id,整個構起一個identify這個app的key, value是一個array (paths),標明哪些path可以由app來handle,如果整個網站都在app有對應的handler,那就像上面黃色字樣寫的"*"。
或是部分被app handle的話:
結論
這兩個平台提供的Dee[ Link方法對server端幾乎沒有影響,設計很好!2017年11月5日 星期日
Android Test 1 - Overview
這是來自Android官方網站的建議,摘要在這篇blog裡面。
較大的cycle:較小cycle完成後,代表requirement做完了,進入正式的UI test (QC部門)
tests有三種類型,依照數量比例(建議 7 : 2 : 1)會形成一個testing pyramid:
unit test: 這是通常是最獨立的,沒有其他module dependency的測試,如果牽涉到其他module的input的話,就必須要做mockup(假資料),但是即便通過所有的unit test不能給予此app正確運作的信心,只能說個別unit通過了自己設定的tests。
integration test: 這是比較中型規模的test,會把幾個相關聯的modules一起寫入test cases測試。
ui test: 這是以end user眼光來測試整個ui flow,是最大規模的tests。
1. 一個open source Java-based Android framework mock是 Roboletric (根據google IO 2017,現在是google內部使用以及維護的open source project)
2. 另一個是Mockito,這提供一個改變過的android.jar,裡面沒有任何code,但是提供Mock介面
3. instrumented unit tests: 這沒用到mocks,但是由於遠比上面兩種花時間,所以最好用來測試在硬體上(而非模擬器)
基本上medium tests還是isolated tests。
這邊可以支援的工具有:
AndroidJUnitRunner class
Android Testing Support Library
採用iterative development workflow
較小的cycle: 這要求programmer寫unit test,確保test cases都完整且通過較大的cycle:較小cycle完成後,代表requirement做完了,進入正式的UI test (QC部門)
tests有三種類型,依照數量比例(建議 7 : 2 : 1)會形成一個testing pyramid:
unit test: 這是通常是最獨立的,沒有其他module dependency的測試,如果牽涉到其他module的input的話,就必須要做mockup(假資料),但是即便通過所有的unit test不能給予此app正確運作的信心,只能說個別unit通過了自己設定的tests。
integration test: 這是比較中型規模的test,會把幾個相關聯的modules一起寫入test cases測試。
ui test: 這是以end user眼光來測試整個ui flow,是最大規模的tests。
Small tests (Unit tests)
這通常不需要在模擬器上跑,可以直接寫test code在開發環境中執行,如果需要使用到Android framework的話,通常使用mocked methods。1. 一個open source Java-based Android framework mock是 Roboletric (根據google IO 2017,現在是google內部使用以及維護的open source project)
2. 另一個是Mockito,這提供一個改變過的android.jar,裡面沒有任何code,但是提供Mock介面
3. instrumented unit tests: 這沒用到mocks,但是由於遠比上面兩種花時間,所以最好用來測試在硬體上(而非模擬器)
Medium tests (Integration tests)
經過unit tests之後,我們可以做一些medium tests來看某些units交集的運作情況,由於數量仍然較large tests多,所以為節省時間主要是在模擬器或是cloud test services上測試,盡量測試各種hardware profiles。基本上medium tests還是isolated tests。
Large tests (UI / use cases test)
這類tests要在前兩種較小的tests都通過後,才會進行,由於定義的test cases牽涉到了use cases,所以花費大量時間,在testing pyramid中規模最大,數量最少。這邊可以支援的工具有:
AndroidJUnitRunner class
Android Testing Support Library
- JUnit4Rules
- Espresso
- UI Automator
- Android Test Orchestrator
2017年10月20日 星期五
Android Performance Enhancement 1 - Overdraw problem
第一守則:不要解決非瓶頸部分
除非你很閒,否則CP值很低。Rendering通常是最大效能瓶頸
在android上安排UI或是畫圖,若是沒有理解Android的繪圖流程,很容易造成畫面效能低落。如果要達成流暢的60 fps,我們需要在每16 ms的time frame中完成每一個frame的繪製:
算法就是螢幕refresh rate = 60Hz = 60 次/sec,所以相當於每 1/60 = 0.016秒 = 16ms refresh一次。
如果你的drawing無法在16 ms中完成的話,當然就無法達成60 fps,此時就稱為drop frame:
Rendering Pipeline
繪製一個frame需要兩個component去計算,一個是CPU,主要負責項目如下:layout hierarchy過於龐大深度,會造成layout重複計算,是主要原因。
GPU的主要負責項目只有幫pixel上色 (rasterization,非常耗時):
GPU最可能發生的效能問題就是overdraw,重複畫在同一個區域。
Android UI & GPU
真正做rasterization的是GPU,但是CPU也扮演著把UI object計算過,並且透過openGL ES上傳到GPU memory的角色,這兩個動作都是非常耗時的:所以最好要減少計算UI Object的時間,以及減少上傳到GPU的次數。
Honeycomb 之後的android使用GPU rendering不需要programmer去操心了,除了overdraw的問題。
Overdraw problem
overdraw定義:一個frame中,某個pixel被重複畫了的次數。這可能發生在重疊的view,但是CPU和GPU不能罵他笨,他們只是忠實的重現programmer的意志:上面這樣堆疊主要是浪費GPU 無謂rasterization的時間。
Android 使用者開發選項有一個 “show GPU overdraw”,打開後可以檢視目前螢幕上的app的overdraw程度:
可以藉由重新layout xml來調教:
fix overdraw problem
1. 我們的activity如果有apply theme,則他會有一個background color被指定要繪製,這可能跟actitvy本身的content layout的background color不一樣,所以壓在layout下面,看不到,此時應該要把這個theme本身的background 拿掉:這樣可以減少一次重繪。
2. 檢查所有重疊view的background,只留下你真正需要的background設定。
3. 調整邏輯,某些時候重複繪製是可以靠程式邏輯去避免的。
Clipping
Android UI rendering基本上會自己計算那些UI 部分是重疊的而不去繪製被壓在下面看不見得部分,但是當我們自己extends View,並且override onDraw()的時候,等於就放棄了這個系統的判斷,所以要跟Android明確定義出visible rect:這樣的動作稱為clipping。
舉例來說,以下是一個custom view:
可以看到onDraw如下:
卡片有重疊部分還是重畫了,沒有做任何處理。
現在來看使用clipRect要怎麼改:
所以基本上clipRect是改變canvas state,設定一個只能在clipRect,則rendering system只會畫在這個clipRect中間的顏色。但是由於每張卡片的clipRect都不一樣,所以要記得canvas.save()來儲存canvas原本的state,之後繪製目前clipRect之後還要再restore到沒有設定clipRect的state。
當然最後一張卡片由於是沒有被任何其他卡片覆蓋住的,所以不用設定clipRect,直接畫出整張卡片即可。
改善結果如下:
CPU optimization
CPU負責把layout xml file轉換成openGL需要的display list,如此GPU才知道要畫些什麼:所以這個display list會被cache在GPU memory,任何只是單純改變屬性 (這邊定義不太清楚)的狀況,都不會改變這個display list,所以這個display list就是這個Button的繪製藍圖。
但是一旦Button的任何pixel內容改變了,例如多出陰影或是按下的動畫等等,這個view就需要被invalidate,告訴CPU要重新製作display list,所以上面的流程都要重新跑一次,耗費了CPU與GPU的資源。
以scale button為例:
scaling會觸發Measure過程,因為系統要知道你的最後大小與位置,另外也有可能影響到其他UI component的位置與大小,所以有可能觸發Layout過程,接下來就是invalidate被影響view去重新計算display list交給GPU去重畫。
注意Measure和Layout會讓Android重新評估所有受影響的UI components,這個過程耗費的資源和整個xml定義的view hierarchy息息相關,所以調整view hierarchy是重要的,不要設計無謂龐大的hierarchy。
Hierarchy Viewer
主要目的:1. 用android rendering system的視角來看layout hierarchy
2. 提供hierarchy扁平化的可能性,加速rendering和memory consumption。
(不過這個工具已經obsolete,這邊不做筆記了)
以下是兩個一模一樣的視覺呈現,但是layout hierarchy不一樣,可以看到上面那個row的階層比較深,performance的差異如下:
2017年10月18日 星期三
Fabric for Android
Modular kits
fabric有很多modular kits,類似plugin或是packages可以安裝。Crashlytics Kit
這是fabric的crash reporting solution,提供real-time analytics,建議透過Crashlytics來安裝Answers kit。Answers Kit
這是fabric的核心引擎,提供real-time analytics,建議透過Crashlytics來安裝Answers kit,安裝了Answers之後就會有以下這些東西:Top-level metrics
1. active users: 五分鐘內正在使用的使用者
2. daily active users: 某天內的使用者
3. daily new users
4. monthly active users: 最後30天內的使用者
5. cash-free users: DAU中沒有crash的佔比
6. sessions: 一天中所有版本的app使用的session數目,session定義為一個foreground -> background event,並且在背景中至少30秒
Answers Kit Twitter Integration: Answers能使用twitter的audience insight。
Answers Kit Event Tracking: 使用者可以track最多3000個不同的event,每個event都沒有log數量的上限,每個event可以有最多20個custom events。
Android Studio Plugin
可以search "fabric for android"在Android Studio的preference中安裝。很白痴的是,plugin介面中沒有提供切換帳號的按鈕,要記住快速鍵 ctrl - L來登出。
2016年8月5日 星期五
Android支援多種螢幕大小和螢幕密度
Android device實在是亂成一團!不搞清楚怎麼正確在各種螢幕大小與螢幕密度的話,真的會一個頭兩個大!
每個等級有定義出最小的虛擬解析度(螢幕上能夠表現出的dp總數):
xlarge >= 960dp x 720dp
large >= 640dp x 480dp
normal >= 470dp x 320dp
small >= 426dp x 320dp
知道了以上的名詞定義,我們可以簡單列出兩個螢幕之間的關係:
如果螢幕A和螢幕B有相同解析度,而且螢幕A的尺寸比螢幕B大的話,那按照定義螢幕A的dpi值一定小於螢幕B的dpi值,這是簡單的數學,也可以說螢幕A的一個pixel的物理尺寸是大於螢幕B的一個pixel的物理尺寸。
有時候這不會不好,如果兩者的解析度一樣的話就只是等比例縮放而已,但是通常解析度不會一樣,這時候dpi的差距常會造成usability的問題,例如按鈕變得太小按不到,或是widget變得太大超出螢幕範圍。
不過這還是會造成一些問題,如果在大螢幕(例如tablet)上的UI用了實際尺寸較大的widget,到小螢幕時還是會一樣大! 所以如果螢幕大小差距太大時,兩者需要不同的layout file,不能依賴android runtime縮放來保證usability。另外一個問題是android runtime自動放大補償的時候,圖像品質就會下降。
所幸android支援alternative configuration給不同的density不同的bitmaps/layout,也就是我們常會在layout和drawables目錄下看到各種xhdpi, xxhdpi目錄的原因。不過現在已經改成要用sw<N>dp configuration qualifier了,之前講的screen size定義在API level 13以後都deprecated。
Screen size
是指device真正的物理尺寸,例如對角線長度為五寸的手機,其screen size就是5寸。Android api把物理尺寸分成四個等級: small, normal, large, extra-large。不過在API 13以上,這個等級就已經deprecated,詳見Android文件。每個等級有定義出最小的虛擬解析度(螢幕上能夠表現出的dp總數):
xlarge >= 960dp x 720dp
large >= 640dp x 480dp
normal >= 470dp x 320dp
small >= 426dp x 320dp
Screen density
螢幕密度是指螢幕在某個物理尺寸內所能表現的pixel數量,所以是密度的概念,通常稱為dpi (dots per inch)。 Android把螢幕密度分成五個等級:low, medium, high, extra-high, extra-extra-high, extra-extra-extra-high。 (Google工程師真的有比較聰明嗎?看這命名...)Resolution
解析度通常會跟螢幕密度搞混了,這是指一個螢幕「所有」能表現的pixel數目,是總量的概念,不是密度的概念,通常程式中不會直接面對resolutoin。Density-independent pixel (dp)
這是一個虛擬的pixel單位,讓我們定義UI的時候,不用去考慮螢幕密度的問題。1dp相當於medium screen density (160dpi)時的一個pixel大小 (白話文就是 當你的螢幕每一英吋能表現160個pixel的時候,這時候一個pixel就可以稱為1dp)。Android runtime會自動做縮放補償,例如一個長度為1dp的image,如果在mdpi的螢幕上,那就剛好用一個pixel表現,但是如果在240dpi的螢幕上,就需要用 (240/160) * 1dp = 1.5個pixel來「補償」放大原本1pixel的區域。知道了以上的名詞定義,我們可以簡單列出兩個螢幕之間的關係:
如果螢幕A和螢幕B有相同解析度,而且螢幕A的尺寸比螢幕B大的話,那按照定義螢幕A的dpi值一定小於螢幕B的dpi值,這是簡單的數學,也可以說螢幕A的一個pixel的物理尺寸是大於螢幕B的一個pixel的物理尺寸。
如果只用pixel當UI單位的話有什麼問題?
如果用pixel當單位的話,就會在不同的density螢幕中看到依照兩者density的倍數縮放的widget,這會造成widget在較低的dpi螢幕上的物理尺寸變大,而在較高dpi螢幕上物理尺寸較小。舉例來說,一張160 pixel x 160 pixel的影像,在160dpi我們看到的實際尺寸是1 inch x 1 inch,但是在420dpi的螢幕上,1 inch相當於420個pixel的寬度,所以160個pixel的長度會變成 160/420 inch,反之在較低dpi螢幕上(例如80dpi)會變成看到160/80 inch的長度。有時候這不會不好,如果兩者的解析度一樣的話就只是等比例縮放而已,但是通常解析度不會一樣,這時候dpi的差距常會造成usability的問題,例如按鈕變得太小按不到,或是widget變得太大超出螢幕範圍。
如何達成在各種screen density螢幕中正確表現?
由上可知,在UI中使用dp當作單位的好處就是,在不同的density螢幕中,android runtime會把這個因為單一pixel物理尺寸不同所造成的widget膨脹或縮減作物理尺寸的補償,這會讓使用者看到近似的物理尺寸的widget。不過這還是會造成一些問題,如果在大螢幕(例如tablet)上的UI用了實際尺寸較大的widget,到小螢幕時還是會一樣大! 所以如果螢幕大小差距太大時,兩者需要不同的layout file,不能依賴android runtime縮放來保證usability。另外一個問題是android runtime自動放大補償的時候,圖像品質就會下降。
所幸android支援alternative configuration給不同的density不同的bitmaps/layout,也就是我們常會在layout和drawables目錄下看到各種xhdpi, xxhdpi目錄的原因。不過現在已經改成要用sw<N>dp configuration qualifier了,之前講的screen size定義在API level 13以後都deprecated。
訂閱:
文章 (Atom)