Android 讀書筆記

《Android開發藝術探索》

一、Activity 的生命週期和啓動模式

1. 當前 Activity 爲 A,此時打開 Activity B:A.onPause() → B.onCreate() → B.onStart() → B.onResume() → A.onStop(),故不能在 onPause 中做重量級操作,使新 Activity 儘快顯示出來並切換到前臺。


2. 當系統內存不足時,系統會按照 [ 後臺 Activity → 可見但非前臺 Activity → 前臺 Activity ] 的優先級殺死目標 Activity 所在的進程。如果一個進程中沒有四大組件在執行,那麼這個進程將很快被系統殺死 → 故 將後臺工作放入 Service 中從而保證進程有一定的優先級,不易被輕易殺死。


3. Activity 的 LaunchMode

(1) standard 標準模式(系統默認)—— 多實例實現

每次啓動一個 Activity 都會重新創建一個新的實例,不管這個實例是否已經存在,被創建的實例的生命週期符合典型情況下 Activity 的生命週期。

一個任務棧中可以有多個實例,每個實例也可以屬於不同的任務棧。

誰啓動了這個 Activity ,這個 Activity 就運行在啓動它的那個 Activity 所在的棧中。

當用 ApplicationContext 啓動 standard 模式的 Activity 時會報錯,因爲非 Activity 類型的 Context 並沒有所謂的任務棧,解決:爲待啓動的 Activity 指定 FLAG_ACTIVITY_NEW_TASK 標記位,這樣啓動的時候會爲它創建一個新的任務棧,此時待啓動 Activity 實際是以 singleTask 模式啓動的。

(2)singleTop 棧頂複用模式

若新 Activity 已經位於任務棧的棧頂,那麼此 Activity 不會被重新創建,同時它的 onNewIntent 方法會被回調,通過此方法的參數可以取出當前請求的信息。此 Activity 的 onCreate、onStart 不會被調用,因爲它並沒有發生改變。若不在棧頂,則會重新創建。

(3)singleTask 棧內複用模式 —— 一種單實例模式

只要 Activity 在一個棧中存在,那麼多次啓動此 Activity 都不會重新創建實例。也會回調 onNewIntent。

如 Activity A 爲 singleTask 模式啓動後,系統首先尋找是否存在 A 想要的任務棧,若不存在,則重新創建一個任務棧,然後創建 A 的實例放入棧中。若存在 A 所需的任務棧,若棧中有 A 的實例存在,系統就把 A 調到棧頂並調用 onNewIntent 方法,同時由於singleTask 默認具有 clearTop 效果,會導致棧內所有在 A 上面的 Activity 全部出棧;若實例不存在,則創建並壓入棧中。

(4)singleInstance 單實例模式 —— 加強的 singleTask

具有此種模式的 Activity 只能單獨地位於一個任務棧中。

4. Activity 所需的任務棧

參數 TaskAffinity 標識了 Activity 所需的任務棧的名字,其值爲字符串,且中間必須含有包名分隔符“.”。默認所有 Activity 所需的任務棧名字爲應用的包名。也可爲每個 Activity 單獨指定 TaskAffinity 屬性,但其值不能和包名相同。該屬性主要和 singleTask 啓動模式或 allowTaskReparenting 屬性配對使用。

任務棧分爲前臺任務棧和後臺任務棧,後臺任務棧中的 Activity 位於暫停狀態。

(1)TaskAffinity 和 singleTask 結合使用:TaskAffinity 是該 Activity 的目前任務棧的名字,待啓動的 Activity 會運行在名字和 TaskAffinity 相同的任務棧中。

(2)TaskAffinity 和 allowTaskReparenting 結合使用:當應用 A 啓動了應用 B 的某個 Activity 後,若這個 Activity 的 allowTaskReparenting 屬性爲 true,則當 B 被啓動後,此 Activity 會直接從 A 的任務棧轉移到 B 的任務棧中。


5. IntentFilter 匹配規則

(1)action

要求 Intent 中必須有一個 action,且必須能夠和過濾規則中的某個 action 相同

(2)category

Intent 中可以沒有 category,但一旦有 category,則不管有幾個必須每個都要能和過濾規則中任何一個 category 相同。

爲了 Activity 能夠接收隱式調用,必須在 intent-filter 中指定 “android.intent.category.DEFAULT”。

(3)data

語法:

<span style="font-size:14px;"><span style="font-size:14px;"><span style="font-size:14px;"><data android:scheme="string" //URI 的模式,如 http、file、content。若未指定 scheme,則整個 URI 無效。默認爲 content 和 file。
android:host="string" //主機名,如 www.baidu.com。若未指定,則 URI 無效。
android:port="string" //端口號,僅當 URI 中指定了 scheme 和 host 時纔有效。
android:path="string"
android:pathPattern="string"
android:pathPrefix="string" 
android:mimeType="string"/></span></span></span>
data 由 2 部分組成,mimeType (媒體類型)和 URI。

URI 結構:

<span style="font-size:14px;"><span style="font-size:14px;"><span style="font-size:14px;"><scheme>://<host>:<port>/[<path>|<pathPrefix>|<pathPattern>]</span></span></span>

Intent 中必須含有 data,且 data 能完全匹配過濾規則中的某一個 data。

<span style="font-size:14px;"><span style="font-size:14px;"><span style="font-size:14px;">android:pathPattern="string"</span></span></span>

注:

入口 Activity 會出現在系統應用列表中,二者缺一不可。
<span style="font-size:14px;"><span style="font-size:14px;"><span style="font-size:14px;"><action android:name="android.intent.action.MAIN/>
<category android:name="android.intent.category.LAUNCHER"/></span></span></span>


二、IPC 機制

1. IPC:Inter-Process Communication,進程間通信 / 跨進程通信。

2. 主線程也叫 UI 線程,在 UI 線程裏才能操作界面元素。

3. Android 中的多進程模式

在 Android 中使用多進程只有一種方法:給四大組件在 AndroidMenifest 指定

<span style="font-size:14px;"><span style="font-size:14px;"><span style="font-size:14px;">android:process    //若未指定,默認進程名是包名</span></span></span>

屬性,即我們無法給一個線程或一個實體類指定其運行時所在的進程。

進程名以 “:” 開頭的進程屬於當前應用的私有進程,其他應用的組件不可以和它跑在同一個進程中;

進程名不以 ":" 開頭的屬於全局進程,其他應用通過 ShareUID 方式可以和它跑在同一個進程中。


Android 系統會爲每一個應用分配一個唯一的 UID,具有相同 UID 的應用才能共享數據。

只有 2 個應用有相同的 ShareUID 且簽名相同纔可以跑在同一個進程中。此時不管它們是否在同一個進程中都可以互相訪問對方的私有數據,如 data 目錄、組件信息等;若在同一個進程中,則還可以共享內存數據。

Android 爲每個進程都分配一個獨立的虛擬機(相當於把應用重啓了一遍),不同的虛擬機在內存分配上有不同的地址空間,導致在不同的虛擬機中訪問同一個類對象會產生多份副本。

所有運行在不同進程中的四大組件,只要它們之間需要通過內存來共享數據,都會共享失敗。

使用多進程有如下問題:

(1)靜態成員和單例模式完全失效。

(2)線程同步機制完全失效。

(3)SharedPreferences 的可靠性下降。(不支持 2 個進程同時寫)

(4)Application 會多次創建。

4. 序列化

(1)Serializable 接口

是 Java 提供的序列化接口,是一個空接口,爲對象提供標準的序列化和反序列化操作。使用簡單,但開銷大,需要大量I/O操作。

使用 Serializable 實現序列化只需這個類實現 Serializable 接口並聲明如下標識即可自動實現默認的序列化過程:

<span style="font-size:14px;"><span style="font-size:14px;"><span style="font-size:14px;">private static final long serialVersionUID = 87254421243431L;</span></span></span>

若不聲明會對反序列化產生影響。
注:靜態成員變量屬於類不屬於對象,故不會參與序列化過程;用 transient 關鍵字標記的成員變量不參與序列化過程。

(2)Parcelable 接口

Android 提供的序列化接口。使用稍麻煩,但效率高。

Android 序列化首選 Parcelable,主要用在內存序列化上;將對象序列化到存儲設備或通過網絡傳輸應使用 Serializable。


5. Android 中的 IPC 方式

(1)Bundle

傳輸的數據必須能被序列化,如 基本類型、實現了 Parcelable 或 Serializable 接口的對象以及一些 Android 支持的特殊對象。

(2)文件共享

對文件格式沒有要求,只要讀 /  寫雙方約定數據格式即可。

注:SharedPreferences 是 Android 中提供的輕量級存儲方案,也是文件的一種,但系統對它的讀 / 寫有一定的緩存策略,即在內存中會有一份 SharedPreferences 文件的緩存,在多進程模式下,系統對它的讀 / 寫就變得不靠譜,當面對高併發的讀 / 寫訪問時會有很大機率丟失數據。故不建議在進程間通信中使用 SharedPreferences。

(3)Messenger

一種輕量級的 IPC 方案,底層實現是 AIDL。

一次處理一個請求,故在服務端不用考慮線程同步的問題,因爲服務端中不存在併發執行的問題。

串行方式,不適合大量併發請求。

(4)AIDL

(5)ContentProvider

Android 提供的專門用於不同應用間進行數據共享的方式。底層實現是 Binder。

主要以表格的形式組織數據,還支持文件數據。

需要註冊,

<span style="font-size:14px;"><span style="font-size:14px;"><span style="font-size:14px;">android:authorities    //ContentProvider 的唯一標識。</span></span></span>

(6)Socket 套接字
①流式套接字 —— TCP 協議,是面向連接的協議,提供穩定的雙向通信功能,三次握手,超時重傳機制。
②用戶數據報套接字 —— UDP 協議,無連接,不穩定的單向通信。效率更高,但不能保證數據一定能正確傳輸。

聲明權限:
<span style="font-size:14px;"><span style="font-size:14px;"><span style="font-size:14px;"><uses-permission android:name="android.permission.INTERNER"/>
<uses-permission android:name="android.permission.ACCESS_NEWWORD_STATE"/></span></span></span>
不能在主線程中訪問網絡

7. Binder 連接池

8. 小結
名稱 優點 缺點 適用場景
Bundle 簡單易用 只能傳輸 Bundle 支持的數據 四大組件間的進程間通信
文件共享 簡單易用 不適合高併發場景,無法做到進程間的即時通信 無併發訪問情形,交換簡單的數據實時性不高的場景
AIDL 功能強大,支持一對多併發通信,支持實時通信 使用稍複雜,需要處理好線程同步 一對多通信且有 RPC 需求
Messenger 功能一般,支持一對多串行通信,支持實時通信 不能很好處理高併發情形,不支持 RPC,數據通過 Message 進行傳輸,只能傳輸 Bundle 支持的數據類型 低併發的一對多即時通信,無 RPC 需求,或者無須返回結果的 RPC 需求
ContentProvider 在數據源訪問方面功能強大,支持一對多併發數據共享,可通過 Call 方法擴展其他操作 可理解爲受約束的 AIDL,主要提供數據源的 CRUD 操作 一對多的進程間的數據共享
Socket 功能強大,可以通過網絡傳輸字節流,支持一對多併發實時通信 實現細節稍有繁瑣,不支持直接的 RPC 網絡數據交換

三、View 的事件體系

1.View 的基礎知識 
(1)View 的位置參數

這些座標是相對於父容器的。
top、left:View 原始左上角座標,平移過程中不變。
平移時以下4 個參數會變化:
x、y:View 左上角座標。
translationX、translationY:View 左上角相對於父容器的偏移量,默認值是 0。
<span style="font-size:14px;"><span style="font-size:14px;"><span style="font-size:14px;">x = left + tanslationX;
y = top + translationY;</span></span></span>

(2)
① MotionEvent
  • ACTION_DOWN
  • ACTON_MOVE
  • ACTION_UP
<span style="font-size:14px;"><span style="font-size:14px;"><span style="font-size:14px;">getX / getY:相對於當前 View 左上角的 x 和 y 座標;
getRawX / getRawY:相對於手機屏幕左上角的 x 和 y;</span></span></span>

② TouchSlop
是系統所能識別出的被認爲是滑動的最小距離。(當手指在屏幕上滑動時,2 次滑動之間的距離小於這個常量的話,系統就不認爲這是在進行滑動操作)
這是常量,和設備有關。
獲取方式:
<span style="font-size:14px;"><span style="font-size:14px;"><span style="font-size:14px;">ViewConfiguration.get(getContext()).getScaledTouchSlop();</span></span></span>

(3) 
VelocityTracker 速度追蹤
① 在 View 的 onTouchEvent 方法中追蹤當前單擊事件的速度:
<span style="font-size:14px;"><span style="font-size:14px;"><span style="font-size:14px;">VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);</span></span></span>

② 獲取速度:
<span style="font-size:14px;"><span style="font-size:14px;"><span style="font-size:14px;">velocityTracker.computeCurrentVelocity(1000);//計算速度:在一段時間內手指所劃過的像素數,如 1000ms
int xVelocity = (int) velocityTracker.getXVelocity();
int yVelocity = (int) velocityTracker.getYVelocity();</span></span></span>

③ 最後,不需要使用時,應用 clear 方法來重置並回收內存:
<span style="font-size:14px;"><span style="font-size:14px;"><span style="font-size:14px;">velocityTracker.clear();
velocityTracker.recycle();</span></span></span>

GestureDetector 手勢檢測,用於輔助檢測用戶的單擊、滑動、長按、雙擊等行爲。
① 創建 GestureDetector 對象並實現 OnGestureListener 接口:
<span style="font-size:14px;"><span style="font-size:14px;"><span style="font-size:14px;">GestureDetector mGestureDetector = new GestureDetector(this);
//解決長按屏幕後無法拖動的現象
mGestureDetector.setIsLongpressEnabled(false);</span></span></span>

② 接管目標 View 的 onTouchEvent 方法,在待監聽 View 的 onTouchEvent 方法中添加如下實現:
<span style="font-size:14px;"><span style="font-size:14px;"><span style="font-size:14px;">boolean consume = mGestureDetector.onTouchEvent(event);
return consume;</span></span></span>

完成上述 2 步後就可以有選擇地實現 OnGestureListener 和 OnDoubleTapListener 中的方法了。

Scroller 彈性滑動對象
當使用 View 的 scrollTo / scrollBy 方法滑動時,過程是瞬間完成的。
可使用 Scroller 來實現有過渡效果的滑動,是在一定時間間隔內完成的。
Scroller 本身無法讓 View 彈性滑動,需要和 View 的 computeScroll 方法配合使用。

2. View 的滑動
(1)scrollTo / scrollBy —— 對 View 內容的滑動
只能改變 View 中內容的位置,而不能改變 View 在佈局中的位置(即滑動 View 裏的內容,而非 View 本身)。
不影響內部元素的單擊事件。
View 內部 2 個屬性 mScrollX 和 mScrollY可以由 getScrollX() 和 getScrollY() 得到。
在滑動過程中,
<span style="font-size:14px;"><span style="font-size:14px;"><span style="font-size:14px;">mScrollX = View 左邊緣 - View 內容左邊緣;
mScrollY = View 上邊緣 - View 內容上邊緣;</span></span></span>
即從右向左滑動 mScrollX 爲正,從下往上滑動 mScrollY 爲正。

(2)使用動畫 —— 操作簡單,適用於沒有交互的 View 和實現複雜的動畫效果
View 動畫(xml)是對 View 的影像做操作,它並不能真正改變 View 的位置參數,包括寬 / 高,故會影響 View 的單擊等交互事件。若希望動畫後的位置狀態得以保留還必須將 fillAfter 屬性設置爲 true。
使用屬性動畫(Java 代碼)不存在此問題。

(3)改變佈局參數 LayoutParams —— 操作稍複雜,適用於有交互的 View
適用於有交互性的 View。
<span style="font-size:14px;"><span style="font-size:14px;"><span style="font-size:14px;">MarginLayoutParams params = (MarginLayoutParams) btn.getLayoutParams();
params.width += 100;
params.leftMargin += 100;
btn.requestLayout();
//或 btn.setLayoutParams(params);</span></span></span>

在 Button 左邊放一個空的 View,默認寬度爲 0,當需要將 Button 右移時,可以重新設置空 View 的寬度即可。

3. 彈性滑動
(1)Scroller
(2)動畫
動畫本身就是一種漸進的過程。
(3)延時策略
通過發送一系列延時消息從而達到一種漸進式的效果。
① Handler 或 View 的 postDelayed 方法,延時發送一個消息,在消息中來進行 View 的滑動,若接連不斷地發送這種延時消息,就可以實現彈性滑動的效果。
② 線程的 sleep 方法,在 while 中不斷地滑動 View 和 sleep 來實現。

4. View 的事件分發機制
(1)MotionEvent 點擊事件的傳遞規則
① public boolean dispatchTouchEvent(MotionEvent ev)
進行事件的分發。若事件能傳遞給當前 View,那麼此方法一定會被調用,返回結果表示是否消耗當前事件。
② public boolean onInterceptTouchEvent(MotionEvent ev)
在 ① 內部調用,用來判斷是否攔截某個事件。若當前 View 攔截了某個事件,則在同一個事件序列當中,此方法不會再次被調用,返回結果表示是否攔截當前事件。
③ public boolean onTouchEvent(MotionEvent ev)
在 ① 內部調用,用來處理點擊事件,返回結果表示是否消耗當前事件。若不消耗,則在同一個事件序列中,當前 View 無法再次接收到事件。

三者關係可用如下僞代碼表示:
<span style="font-size:14px;"><span style="font-size:14px;"><span style="font-size:14px;">public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean consume = false;
    if (onInterceptTouchEvent(ev)) {
        consume = onTouchEvent(ev);
    } else {
        consume = child.dispatchTouchEvent(ev);
    }
    return consume;
}</span></span></span>

點擊事件優先級:
OnTouchListener 的 onTouch → onTouchEvent → OnClickListener 的 onClick。

點擊事件的傳遞過程:
Activity → Window → View。
若 View 的 onTouchEvent 返回 false,那麼其父容器的 onTouchEvent 將會被調用,依此類推。若所有元素都不處理這個事件,則會最終傳遞給 Activity,即 Activity 的 onTouchEvent 會被調用。

結論:
(1)同一個事件序列是指從手指接觸屏幕的那一刻起,到手指離開屏幕的那一刻結束,在這個過程中產生的一系列事件。以 down 事件開始,中間含有數量不定的 move 事件,最終以 up 事件結束。
(2)正常情況下,一個事件序列只能被一個 View 攔截消耗。但 View 也可以將本該自己處理的事件通過 onTouchEvent 強行傳遞給其他 View 處理。
(3)某個 View 一旦決定攔截,那麼這個事件序列都只能由它來處理,並且它的 onInterceptTouchEvent 不會再被調用。
(4)某個 View 一旦開始處理事件,若它不消耗 ACTION_DOWN 事件(onTouchEvent 返回了 false),那麼同一事件序列中的其他事件都不會再交給它來處理,且事件將重新交給它的父元素去處理,即父元素的 onTouchEvent 會被調用。
(5)若 View 不消耗除 ACTION_DOWN 以外的其他事件,那麼這個點擊事件會消失,此時父元素的 onTouchEvent 不會被調用,且當前 View 可以持續收到後續的事件,最終這些消失的點擊事件會傳遞給 Activity 處理。
(6)ViewGroup 默認不攔截任何事件。
(7)View 沒有 onInterceptTouchEvent 方法,一旦有點擊事件傳遞給它,則會調用 onTouchEvent。
(8)View 的onTouchEvent 默認都會消耗事件(返回 true),除非它是不可點擊的(clickable 和 longClickable 同時爲 false)。
(9)View 的 enable 屬性不影響 onTouchEvent 的默認返回值,只要它的 clickable 或 longClickable 有一個爲 true,則默認返回 true。
(10)onClick 會發生的前提是當前 View 是可點擊的,且收到了 down 和 up 事件。
(11)事件傳遞過程是由外向內的,即父元素 → 子元素。通過 requestDisallowInterceptTouchEvent 方法可以在子元素中干預父元素的事件分發過程,但是 down 事件除外。

5. View 滑動衝突
在界面中只要內外兩層同時可以滑動,就會產生滑動衝突。
(1)外部攔截法(推薦)
符合點擊事件的分發機制,重寫父容器的 onInterceptTouchEvent 即可。
<span style="font-size:14px;"><span style="font-size:14px;">public boolean onInterceptTouchEvent(MotionEvent event) {
	boolean intercepted = false;
	int x = (int) event.getX();
	int y = (int) event.getY();
	switch (event.getAction()) {
		case MotionEvent.ACTION_DOWN:
			intercepted = false;	//父容器必須返回 false,即不攔截 ACTION_DOWN 事件,否則後續的 MOVE 和 UP 都會直接由父容器處理,而不會傳遞給子元素了。
			break;
		case MotionEvent.ACTION_MOVE:
			if (父容器需要當前點擊事件) {
				intercepted = true;
			} else {
				intercepted = false;
			}
			break;
		case MotionEvent.ACTION_UP:
			intercepted = false;	//UP 事件沒多大意義,必須返回 false。
			break;
		default:
			break;
	}
	mLastXIntercept = x;
	mLastYIntercept = y;
	return intercepted;
}</span></span>

(2)內部攔截法
父容器不攔截任何事件,所有事件都傳遞給子元素,若子元素需要此事件就直接消耗掉,否則就交由父容器處理。
不符合點擊事件的分發機制,需配合 requestDisallowInterceptTouchEvent 方法,重寫子元素的 dispatchTouchEvent 。
<span style="font-size:14px;"><span style="font-size:14px;">public boolean dispatchTouchEvent(MotionEvent event) {
	int x = (int) event.getX();
	int y = (int) event.getY();
	switch (event.getAction()) {
		case MotionEvent.ACTION_DOWN:
			parent.requestDisallowInterceptTouchEvent(true);
			break;
		case MotionEvent.ACTION_MOVE:
			int deltaX = x - mLastX;
			int deltaY = y - mLastY;
			if (父容器需要當前點擊事件) {
				parent.requestDisallowInterceptTouchEvent(false);
			}
			break;
		case MotionEvent.ACTION_UP:
			break;
		default:
			break;
	}
	mLastX = x;
	mLastY = y;
	return super.dispatchTouchEvent(event);
}</span></span>

父元素要默認攔截除了 ACTION_DOWN 以外的其他事件,這樣當子元素調用 parent.requestDisallowInterceptTouchEvent(false) 時,父元素才能繼續攔截所需的事件。因爲 ACTION_DOWN 不受 FLAG_DISALLOW_INTERCEPT 標記位的控制,故一旦父容器攔截 ACTION_DOWN,那麼所有的事件都無法傳到子元素了。父元素修改如下:
<span style="font-size:14px;"><span style="font-size:14px;">public boolean onInterceptTouchEvent(MotionEvent event) {
	int action = event.getAction();
	if (action == MotionEvent.ACTION_DOWN) {
		return false;
	} else {
		return true;
	}
}</span></span>

四、View 的工作原理

1. 初識 ViewRoot 和 DecorView
ViewRoot 對應於 ViewRootImpl 類,它是連接 WindowManager 和 DecorView 的紐帶,View 三大流程均是通過 ViewRoot 來完成的。在 ActivityThread 中,當 Activity 對象被創建完畢後,會將 DecorView 添加到 Window 中,同時會創建 ViewRootImpl 對象,並將 ViewRootImpl 對象和 DecorView 建立關聯。
View 繪製流程是從 ViewRoot 的 performTraversals 方法開始的,它經過 measure、layout、draw 三個過程才能最終將一個 View 繪製出來。
  • measure:測量 View 的寬和高,Measure 完成以後,可以通過 getMeasuredWidth 和 getMeasuredHeight 獲取 View 測量後的寬 / 高,在幾乎所有的情況下它都等同於 View 的最終的寬 / 高。特殊情況除外。
  • layout:確定 View 在父容器中的放置位置,即 View 的四個頂點的座標和實際的 View 寬 / 高,完成後可以通過 getTop、getBottom、getLeft、getRight獲取四個頂點的位置,getWidth 和 getHeight 獲取最終寬 / 高。
  • draw:負責將 View 繪製在屏幕上,只有 draw 完成後 View 的內容才能顯示在屏幕上。
DecorView 作爲頂級 View,其實是一個 FrameLayout,一般包含一個豎直方向的 LinearLayout,這個 LinearLayout 包含上下 2 部分:標題欄和內容欄。在 Activity 中通過 setContentVIew 設置的便是內容欄 FrameLayout 的佈局,內容欄的 id 是 content。View 層的事件都先經過 DecorView,然後才傳遞給我們的 View。
<span style="font-size:14px;"><span style="font-size:14px;"><span style="font-size:14px;">ViewGroup content = (ViewGroup) findViewById(android.id.content);	//獲取 content
content.getChildAt(0);	//獲取我們設置的 View</span></span></span>

2. MeasureSpec
(1)MeasureSpec 很大程度上決定了一個 View 的尺寸規格,這個過程還受父容器的影響。測量過程中,系統會將 View 的 LayoutParams 根據父容器所施加的規則轉換成對應的 MeasureSpec,再根據這個 MeasureSpec 來測量出 View 的寬 / 高。
MeasureSpec 代表一個 32 位 int 值,高 2 位代表 SpecMode 測量模式,低 30 位代表 SpecSize 某種測量模式下的規格大小。MeasureSpec 通過將 SpecMode 和 SpecSize 打包成一個 int 值來避免過多的對象內存分配,且提供了打包和解包方法。SpecMode 和 SpecSize 也是 int 值。
SpecMode 有三類:
  • UNSPECIFIED
父容器不對 VIew 有任何限制,要多大給多大,一般用於系統內部,表示一種測量的狀態。
  • EXACTLY
父容器已經檢測出 View 所需要的精確大小,此時 View 的最終大小就是 SpecSize 所指定的值。它對應於 LayoutParams 中的 match_parent 和具體的數值這兩種模式。
  • AT_MOST
父容器指定了一個可用大小即 SpecSize,View 的大小不能大於這個值,對應於 LayoutParams 的 wrap_content。

(2)MeasureSpec 和 LayoutParams 的對應關係
DecorView:其 MeasureSpec 由窗口的尺寸和其自身的 LayoutParams 共同確定。
普通 View:其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 共同確定。MeasureSpec 一旦確定後,onMeasure 中就可以確定 View 的測量寬 / 高。

DecorView 的 MeasureSpec 的產生過程遵守如下規則:
  • LayoutParams.MATCH_PARENT:精確模式,大小就是窗口的大小。
  • LayoutParams.WRAP_CONTENT:最大模式,大小不定,但不能超過窗口的大小。
  • 固定大小:精確模式,大小爲 LayoutParams 中指定的大小。

子元素可用的大小爲父容器的尺寸減去 padding。

3. View 的工作流程
(1)measure 過程
由 View 的 measure() 方法完成,此是一個 final 類型方法,子類不能重寫,在 measure() 中會調用 View 的 onMeasure() 方法。

關於 getSuggestedMinimumWidth() 方法:
若 View 沒有設置背景,那麼返回 android:minWidth 的值(可以爲 0);若 View 設置了背景,則返回 android:minWidth 和 背景的最小寬度 兩者中的最大值。getSuggestedMinimumWidth 的返回值就是 View 在 UNSPECIFIED 情況下的測量寬。

直接繼承 View 的自定義控件需要重寫 onMeasure 方法並設置 wrap_content 時的自身大小,否則在佈局中使用 wrap_content 就相當於使用 match_parent。解決如下:
<span style="font-size:14px;"><span style="font-size:14px;">/**
 * 只需給 View 指定一個默認的內部 寬/高(mWidth 和 mHeight),並在 wrap_content 時設置此寬/高即可。
 * 對於非 wrap_content 情形,沿用系統的測量值即可。
 */
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	super.onMeasure(widthMeasureSpec, heightMeasureSpec);
	int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
	int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
	int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
	int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
	if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
		setMeasuredDimension(mWidth, mHeight);
	} else if (widthSpecMode == MeasureSpec.AT_MOST) {
		setMeasuredDimension(mWidth, heightSpecSize);
	} else if (heightSpecMode == MeasureSpec.AT_MOST) {
		setMeasuredDimension(widthSpecSize, mHeight);
	}
}</span></span>


問題:若想在 Activity 一啓動的時候就做一件任務,但是這一任務需要獲取某個 View 的寬/高,因爲 View 的 measure 過程和 Activity 的生命週期方法不是同步執行的,因此無法保證 Activity 執行了 onCreate、onStart、onResume 時某個 View 已經測量完畢了,若未測量完畢,則獲得的寬/高就是0。
解決:
①Activity/View#onWindowFocusChanged。
onWindowFocusChanged 的含義:View 已經初始化完畢了,寬/高已經準備好了,這時可以獲取。但是 onWindowFocusChanged  會被調用多次,當 Activity 的窗口得到焦點和失去焦點時均會被調用一次。
<span style="font-size:14px;">public void onWindowFocusChanged(boolean hasFocus) {
	super.onWindowFocusChanged(hasFocus);
	if (hasFocus) {
		int width = view.getMeasuredWidth();
		int height = view.getMeasuredHeight();
	}
}</span>

②view.post(runnable)。
通過 post 可以將一個 runnable 投遞到消息隊列的尾部,然後等待 Looper 調用此 runnable 的時候,View 也已經初始化好了。
<span style="font-size:14px;">protected void onStart() {
	super.onStart();
	view.post(new Runnable() {
		@override
		public void run() {
			int width = view.getMeasuredWidth();
			int height = view.getMeasuredHeight();
		}
	});
}</span>

③ViewTreeObserver。
使用 ViewTreeObserver 的衆多回調可以完成這個功能。
如使用 OnGlobalLayoutListener 這個接口,當 View 樹的狀態發生改變或 View 樹內部的 View 的可見性發生改變時,onGlobalLayout 方法將被回調,這是一個獲取 View 的寬/高的好時機。伴隨着 View 樹的狀態改變等,onGlobalLayout 會被調用多次。
<span style="font-size:14px;">protected void onStart() {
	super.onStart();
	
	ViewTreeObserver observer = view.getViewTreeObserver();
	observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
		@override
		public void onGlobalLayout() {
			view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
			int width = view.getMeasuredWidth();
			int height = view.getMeasuredHeight();
		}
	});
}</span>

④view.measure(int widthMeasureSpec, int heightMeasureSpec)。
手動對 View 進行 measure。

(2)layout 過程
layout 方法確定 View 本身的位置,onLayout 確定所有子元素的位置。

(3)draw 過程
①繪製背景 background.draw(canvas)
②繪製自己 (onDraw)
③繪製 children (dispatchDraw)
④繪製裝飾(onDrawScrollBars)

 
View 有一個特殊方法 setWillNotDraw:若一個 View 不需要繪製任何內容,則設置這個標記位爲 true 以後,系統會進行相應的優化。View 默認沒有啓用這個標記位,但是 ViewGroup 默認啓用此標記位。當我們的自定義控件繼承於 ViewGroup 並且本身不具備繪製功能時,就可以開啓這個標記位便於系統進行後續優化。當明確知道一個 VIewGroup 需要通過 onDraw 來繪製內容時,需要顯式地關閉 WILL_NOT_DRAW 這個標記位。

4.自定義 View
(1)自定義 View 的分類
  •  繼承 View 重寫 onDraw 方法
              主要用於實現一些不方便通過佈局的組合方式達到的不規則的效果。此方法需要自己支持 wrap_content,且 padding 也需要自己處理。
  • 繼承 ViewGroup 派生特殊的 Layout
             用於實現自定義的佈局。需要自己處理 ViewGroup 的測量、佈局過程。
  • 繼承特定的 View (如 TextView)
             用於擴展某種已有的 View 的功能,不需要自己支持 wrap_content 和 padding 等。
  • 繼承特定的 ViewGroup(如 LinearLayout)
             無需自己處理 測量、佈局過程。

(2)自定義 View 須知
①讓 View 支持 wrap_content
直接繼承 View 或 ViewGroup 的控件,若不在 onMeasure 中對 wrap_content 做特殊處理,那麼當外界在佈局中使用 wrap_content 時就無法達到預期效果。
②若有必要,讓 View 支持 wrap_content
因爲直接繼承 View 的控件,若不在 draw 中處理 paddings,則 padding 屬性是無法起作用的。







============================================================================================

《Android 應用程序開發權威指南》

一、基礎

  1. 管理器 功能
    LocationManager 和設備上的基於位置的服務進行交互
    ViewManager,WindowManager 負責顯示界面以及設備相關的用戶界面的基礎
    AccessibilityManager 負責輔助事件,提供對物理損傷的用戶的設備支持
    ClipboardManager 提供了訪問設備全局剪貼板的能力,可以剪切和複製內容
    DownloadManager 作爲系統服務,負責 HTTP 的後臺下載
    FragmentManager 管理一個 Activity 的 Fragment
    AudioManager 提供了音頻和振鈴控制的訪問
  2. 文件 功能
    AndroidManifest.xml 應用的核心配置文件。定義了應用程序的功能和權限,以及如何運行。
    ic_launcher-web.png 一張 32 位的 512*512 大小的高分辨率圖標,用來在 Google Play 商店中顯示。該圖大小不能超過 1024KB。
    proguard-project.txt Android IDE 和 ProGuard 使用的編譯文件。可以通過編輯該文件來配置代碼優化選項,以及發佈版本的混淆設置。
    project.properties Android IDE 中使用的編譯文件。定義了應用程序的構建目標,以及其他編譯系統選項。不要編輯這個文件
    /src 必須的文件夾,包含所有的源代碼
    /gen 必須的文件夾,包含所有的自動生成的文件。
    /gen/.../BuildConfig.java 調試應用程序時,該源文件自動生成。不要編輯
    /gen/.../R.java 自動生成的資源管理的源文件。不要編輯
    /assets 必須的文件夾。包含了項目中未編譯的資源文件,一些你不想作爲應用程序資源管理的應用程序數據(文件、目錄)
  3. 術語 描述
    Context(上下文) 是 Android 應用的中央指揮中心。大部分應用特定的功能可以通過上下文訪問或引用。Context 類是任何 Android 應用的基本構建模塊,提供了訪問應用程序範圍的功能,譬如應用程序的私有文件、設備資源,以及整個系統的服務。應用程序的 Context 對象會被實例化爲一個 Application 對象。
    Activity(活動) 一個 Activity 類是 Context 類的子類,因此它也擁有 Context 類的所有功能。
    Fragment(碎片) 一個活動有一個獨特的任務或目的,但它可以進一步組件化,每一個組件被稱爲碎片。Fragment 類往往被用來組織活動的功能,從而允許在不同的屏幕大小、方向和縱橫比上提供更靈活的用戶體驗。碎片常常用來在由多個 Activity 類組成的不同的屏幕上,使用相同的代碼和屏幕邏輯放置相同的用戶界面。
    Intent(意圖) Android 操作系統使用異步的消息傳遞機制,將任務匹配到合適的 Activity。每一個請求被打包成一個意圖。使用 Intent 類是應用程序組件如活動和服務之間通信的主要方法。
    Service(服務) 不需要用戶交互的任務可以封裝成一個服務。當需要處理耗時任務或需要定時處理時使用服務,用來處理後臺操作。繼承自 Context 類。
  4. 應用程序 Context
    因爲 Activity 類是由 Context 類派生的,故有時可以使用它而不是顯示地獲取應用程序 Context。但不要在任何情況下都使用 Activity Context,因爲可能會導致內存泄漏。

    • 獲取應用程序資源,如字符串、圖形、xml 文件。getResources()
    • 訪問應用程序首選項 getSharedPreferences()
    • 管理私有的應用程序文件和目錄
    • 獲取未編譯的應用程序資產 getAssets()
    • 訪問系統服務
    • 管理自由的應用程序數據庫(SQLite)
    • 以應用程序權限工作
  5. Activity 生命週期

    • onCreate() 初始化靜態 Activity 數據
    • onResume() 初始化及取回 Activity 數據
    • onPause() 停止、保存和釋放 Activity 數據,保存重要數據到用久存儲,使用 onSaveInstanceState() 保存一些可以從當前屏幕快速恢復的數據或某些不重要的信息(如 未提交的表單數據或任何其他減少用戶麻煩的狀態信息)。

      停止任何聲音、視頻、動畫,也必須停用資源如數據庫遊標對象 或 其他 Activity 終止時應該清理的對象。onPause() 可能是 Activity 進入後臺時最後用來清理或釋放不需要的資源的機會,需要在這裏保存任何未提交的數據。
      調用 onPause() 後,系統保留殺死任何一個 Activity 而沒有進一步通知的權利。
      Activity 需要在 onPause() 方法中執行快速的代碼,因爲只有 onPause() 方法返回後,新的前臺 Activity 纔會啓動。
      一般來說,任何在 onResume() 方法中獲取的資源和數據應該在 onPause() 方法中釋放,否則當進程終止後,這些資源可能無法乾淨地釋放。

      避免 Activity 被殺死
      內存不足時 Android 操作系統可以殺死任何暫停、停止或銷燬的 Activity。這基本意味着任何沒有在前臺的 Activity 都會面臨被關閉的可能。
      若 Activity 在 onPause() 後被殺掉,那麼 onStop() 和 onDestroy() 方法將不會被調用。在 onPause() 方法內更多的釋放 Activity 的資源,那麼 Activity 就越不太可能在後臺被直接殺掉(沒有其他的狀態切換方法被調用)。
      殺死一個 Activity 並不會導致它從 Activity 堆棧中的移除。若 Activity 實現並使用了 onSaveInstanceState() 用於自定義數據,Activity 狀態將被保存到 Bundle 對象中(雖然一些 View 數據將會被自動保存)。當用戶返回到 Activity 後,onCreate() 方法會被再次調用,這次會有一個有效的 Bundle 對象作爲方法參數。

    • onDestroy() 銷燬靜態 Activity 數據
      當 Activity 通過正常的操作被銷燬,onDestroy() 方法將會被調用。
      onDestroy() 會在 2 種情況下被調用:Activity 自己完成了它的生命週期,或因爲資源問題,Activity 被 Android 操作系統殺掉,但仍有足夠的時間從容銷燬 Activity(與不調用 onDestroy() 方法直接終止 Activity 不同)。

      若 Activity 是被 Android 操作系統殺掉的,isFinishing() 方法會返回 false。該方法在 onPause() 中十分有用,可以知道 Activity 是否能夠恢復。

  6. AndroidManifest.xml 清單文件
    作用:

    • Android 操作系統使用清單文件來安裝、更新和運行應用程序包
    • 顯示應用程序的詳細信息,如名稱、描述、圖標
    • 指定應用的系統需求,包括對 Android SDK 的支持、設備配置的需求(如方向鍵),以及應用依賴的平臺功能(如多點觸摸)
    • 以市場過濾爲目的,指定應用的哪些功能是必須的
    • 註冊應用的 Activity,並指定如何啓動
    • 管理應用程序的權限
    • 配置其他高級的應用組件配置詳細信息,包括定義 Service、Broadcast Receiver 及 Content Provider
    • 爲你的 Activity、Service 及 Broadcast Receiver 指定 Intent 過濾器
    • 爲應用測試開啓應用設置,如調試和配置儀器

    設置應用程序的系統需求:

    • <uses-feature> 標籤用於指定應用需要哪些 Android 功能才能正常運行。這些設置只供參考不會強制使用。當使用 <uses-feature> 標籤時,可以指定 android:required 的可選屬性,並設置爲 true 或 false,這個可以用於配置 Google Play 商店中的過濾。若該值爲 true,Google Play 將只會在具有特定硬件或軟件功能的設備上顯示你的應用程序(如攝像頭)。若應用程序需要多個功能,則必須爲每個功能創建一個 <uses-feature> 標籤。

      若應用正常運行時並不需要一個特定的功能,與其在應用商店內過濾並限制特定的設備,還可以使用 getPackageManager().hasSystemFeature() 在運行時檢查特定的設備功能,並在用戶的設備上支持該功能時才允許特定的功能,這樣可以最大化安裝和使用你應用的人羣。

    • <uses-sdk> 標籤用於指定應用程序支持的 Android 平臺版本。應用商店會根據應用的清單文件的<uses-sdk> 標籤等的設置來爲給定的用戶過濾應用。忽略使用該標籤將會在編譯環境中產生一個警告信息。
    • <uses-configuration> 標籤用於指定應用程序支持的硬件或軟件的輸入方法。有 5 個方向的配置屬性:硬件鍵盤和鍵盤類型;方向設備如方向鍵、軌跡球和滾輪;觸摸屏的設置。若應用程序支持多種輸入配置,則必須有多個 <uses-configuration> 標籤。
    • <supports-screens> 標籤用於指定應用支持的 Android 屏幕類型。
    • <application> 內的 <uses-library> 標籤用於註冊應用中鏈接到的外部庫。
    • <supports-gl-texture> 用於指定應用支持的 GL 材質的壓縮格式。使用圖形庫的應用使用該標籤,並用於兼容可以支持指定壓縮格式的設備。
    • <application> 標籤屬性中設置應用程序範圍的主題。
    • <instrumentation> 設置單元測試功能。
    • <activity-alias> 爲 Activity 起別名。
    • <receiver> 註冊 Broadcast Receivers。
    • <provider> 註冊 Content Provider,使用 <grant-uri-permission> 和 <path-permission> 管理 Content Provider 的權限。
    • <meta-data> 包含應用的 Activity、Service、Receiver 組件註冊的其他數據。
  7. 管理資源

    • 所有 Android 應用程序由 2 部分組成:
      功能部分——代碼指令(程序運行的任何算法)
      數據部分——資源(文本字符串、樣式主題、尺寸、圖片圖標、音視頻文件等)

    • 默認 Android 資源目錄,所有資源必須存放在項目的 /res 目錄下的指定子目錄,且目錄名必須小寫。

      資源子目錄 內容
      /res/drawable-*/ 圖形資源
      /res/layout/ 用戶界面資源
      /res/menu/ 菜單資源,用於顯示 Activity 中的選項或操作
      /res/values/ 簡單的數據,如字符串、樣式主題、尺寸資源
      /res/values-sw*/ 覆蓋默認的尺寸資源
      /res/values-v*/ 較新 API 自定義的樣式和主題資源
    • 常見的資源類型及存儲結構

      資源類型 所需目錄 建議文件名 XML 標籤
      字符串 /values/ strings.xml <string>
      字符串複數形式 /values/ strings.xml <plurals>, <item>
      字符串數組 /values/ strings.xml 或 arrays.xml <string-array>, <item>
      布爾類型 /values/ bools.xml <bool>
      顏色 /values/ colors.xml <color>
      顏色狀態列表 /color/ 包括 buttonstates.xml, indicators.xml <selector>, <item>
      尺寸 /values/ dimens.xml <dimen>
      ID /values/ ids.xml <item>
      整型 /values/ integers.xml <integer>
      整型數組 /values/ integers.xml <integer-array>
      混合類型數組 /values/ arrays.xml <array>, <item>
      簡單可繪製圖形(可打印) /values/ drawables.xml <drawable>
      XML 文件定義的圖形(如形狀) /drawable/ 包括 icon.png, logo.jpg 支持的圖形文件或可繪製圖形
      補間動畫 /anim/ 包括 fadesequence.xml, spinsequence.xml <set>, <alpha>, <scale>,<translate>, <rotate>
      屬性動畫 /animator/ mypropanims.xml <set>, <objectAnimator>, <valueAnimator>
      幀動畫 /drawable/ 包括 sequence1.xml, sequence2.xml <animation-list>, <item>
      菜單 /menu/ 包括 mainmenu.xml, helpmenu.xml <menu>
      XML 文件 /xml/ 包括 data.xml, data2.xml 開發者定義
      原始文件 /raw/ 包括 jingle.mp3, video.mp4, text.txt 開發者定義
      佈局 /layout/ 包括 main.xml, help.xml 多樣,但必須是佈局類型
      樣式和主題 /values/ styles.xml, themes.xml <style>
    • 使用字符串資源

      • 所有包含撇號及單引號的字符串需要被轉義 "\" 或被雙引號包裹
      • 可使用 HTML 樣式的屬性(粗體<b>、斜體<i>、下劃線<u>標籤)
      • 忽略格式:String s = getResources().getString(R.string.hello);
      • 保留字符串格式: ① CharSequence cs = getResourece().getText(R.string.hello); ②使用 TextUtils 的 htmlEncode()
  8. TexdView 文本中創建上下文鏈接:autoLink 屬性

    描述
    note 禁用所有鏈接
    web 允許 web 網頁的 URL 鏈接
    email 允許電子郵件地址鏈接,並在郵件客戶端填寫收件人
    phone 允許電話號碼鏈接,可以在撥號器應用中填寫電話號碼來撥打
    map 允許街道地址的鏈接,可以在地圖應用中顯示位置
    all 允許所有類型的鏈接

    開啓 autoLink 功能依賴於 Android SDK 中的各種類型的檢測。有時候也可能鏈接不正確或產生誤導。

  9. EditText

    • 輸入過濾器限制輸入
      使用 setFilters() 來設置 InputFilter 限制用戶輸入,InputFilter 接口包含一個 filter() 方法。可以實現 InputFilter 接口來創建自定義的過濾器。setFilters() 的參數是 InputFilter 對象的數組,這對於組合多個過濾器是非常有用的。

    • 自動完成

      • AutoCompleteTextView :基於用戶輸入的內容來填寫整個文本。
      • 允許用戶輸入字符串列表,每一個都具有自動完成功能。這些字符串都要以某種方式分隔,提供給 MultiAutoCompleteTextView 對象的Tokenizer 來處理。這對於指定通用標籤等的列表有所幫助。也可以自己實現 MultiAutoCompleteTextView.Tokenizer 接口,內置逗號分隔符CommaTokenizer()
<span style="font-size:14px;"><span style="font-size:14px;"><span style="font-size:14px;">   <span>	</span>     android:pathPattern="string"</span></span></span>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章