1、位置
1.1 坐標系
下面是 Android 中的 View 坐標系的基本圖。要獲得一個 View 的位置,我們可以借助兩個對象,一個是 View ,一個是 MotionEvent。以下是它們的一些方法的位置的含義:
在 View 中共有 mLeft
, mRight
, mTop
和 mBottom
四個變量包含 View 的坐標信息,你可以在源碼中獲取它們的含義:
-
mLeft
:指定控件的左邊緣距離其父控件左邊緣的位置,單位:像素; -
mRight
:指定控件的右邊緣距離其父控件左邊緣的位置,單位:像素; -
mTop
:指定控件的上邊緣距離其父控件上邊緣的位置,單位:像素; -
mBottom
:指定控件的下邊緣距離其父控件上邊緣的位置,單位:像素。
此外,View 中還有幾個方法用來獲取控件的位置等信息,實際上就是上面四個變量的 getter 方法:
-
getLeft()
:即mLeft
; -
getRight()
:即mRight
; -
getTop()
:即mTop
; -
getBottom()
:即mBottom
;
所以,我們可以得到兩個獲取 View 高度和寬度信息的方法:
-
getHeight()
:即mBottom - mTop
; -
getWidth()
:即mRight - mLeft
;
另外,就是 View 中的 getX()
和 getY()
兩個方法,你需要注意將其與 MotionEvent 中的同名方法進行區分。在沒有對控件進行平移的時候,getX()
與 getLeft()
返回結果相同,只是前者會在后者的基礎上加上平移的距離:
-
getX()
:即mLeft + getTranslationX()
,即控件的左邊緣加上 X 方向平移的距離; -
getY()
:即mTop + getTranslationY()
,即控件的上邊緣加上 Y 方向平移的距離;
以上是我們對 View 中獲取控件位置的方法的梳理,你可以到源碼中查看它們更加相詳盡的定義,那更有助于自己的理解。
1.2 MotionEvent
通常當你對控件進行觸摸監聽的時候會用到 MotionEvent ,它封住了觸摸的位置等信息。下面我們對 MotionEvent 中的獲取點擊事件的位置的方法進行梳理,它主要涉及下面四個方法:
-
MotionEvent.getX()
:獲取點擊事件距離控件左邊緣的距離,單位:像素; -
MotionEvent.getY()
:獲取點擊事件距離控件上邊緣的距離,單位:像素; -
MotionEvent.getRawX()
:獲取點擊事件距離屏幕左邊緣的距離,單位:像素; -
MotionEvent.getRawY()
:獲取點擊事件距離屏幕上邊緣的距離,單位:像素。
另外是觸摸事件中的三種典型的行為,按下、移動和抬起。接下來的代碼示例中我們會用到它們來判斷手指的行為,并對其做響應的處理:
-
MotionEvent.ACTION_DOWN
:按下的行為; -
MotionEvent.ACTION_MOVE
:手指在屏幕上移動的行為; -
MotionEvent.ACTION_UP
:手指抬起的行為。
2、滑動
我們有幾種方式實現 View 的滑動:
2.1 layout() 方法
調用控件的 layout()
方法進行滑動,下面是該方法的定義:
public void layout(int l, int t, int r, int b) { /*...*/ }
其中的四個參數 l
, t
, r
, b
分別表示控件相對于父控件的左、上、右、下的距離,分別對應于上面的 mLeft
, mTop
, mRight
和 mBottom
。所以,調用該方法同時可以改變控件的高度和寬度,但有時候我們不需要改變控件的高度和寬度,只要移動其位置即可。所以,我們又有方法 offsetLeftAndRight()
和 offsetTopAndBottom()
可以使用,后者只會對控件的位置進行平移。因此,我們可以進行如下的代碼測試:
private int lastX, lastY;
private void layoutMove(MotionEvent event) {
int x = (int) event.getX(), y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
int offsetX = x - lastX, offsetY = y - lastY;
getBinding().v.layout(getBinding().v.getLeft() + offsetX,
getBinding().v.getTop() + offsetY,
getBinding().v.getRight() + offsetX,
getBinding().v.getBottom() + offsetY);
break;
case MotionEvent.ACTION_UP:
break;
}
}
上面的代碼的效果是指定的控件會隨著手指的移動而移動。這里我們先記錄下按下的位置,然后手指移動的時候記錄下平移的位置,最后調用 layout()
即可。
2.2 offsetLeftAndRight() 和 offsetTopAndBottom()
上面已經提到過這兩個方法,它們只改變控件的位置,無法改變大小。我們只需要對上述代碼做少量修改就可以實現同樣的效果:
getBinding().v.offsetLeftAndRight(offsetX);
getBinding().v.offsetTopAndBottom(offsetY);
2.3 改變布局參數
通過獲取并修改控件的 LayoutParams
,我們一樣可以達到修改控件的位置的目的。畢竟,本身這個對象就代表著控件的布局:
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getBinding().v.getLayoutParams();
lp.leftMargin = getBinding().v.getLeft() + offsetX;
lp.topMargin = getBinding().v.getTop() + offsetY;
getBinding().v.setLayoutParams(lp);
2.4 動畫
使用動畫我們也可以實現控件移動的效果,這里所謂的動畫主要是操作 View 的 transitionX
和 transitionY
屬性:
getBinding().v.animate().translationX(5f);
getBinding().v.animate().translationY(5f);
關于動畫的內容,我們會在后面詳細介紹。
2.5 scrollTo() 和 scrollBy()
scrollBy()
方法內部調用了 scrollTo()
,以下是這部分的源碼。scrollBy()
表示在當前的位置上面進行平移,而 scrollTo()
表示平移到指定的位置:
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
同樣對上述代碼進行修改,我們也可以實現之前的效果:
((View) getBinding().v.getParent()).scrollBy(-offsetX, -offsetY);
或者
View parent = ((View) getBinding().v.getParent());
parent.scrollTo(parent.getScrollX()-offsetX, parent.getScrollY()-offsetY);
此外,還有一個需要注意的地方是:與上面的 offsetLeftAndRight()
和 offsetTopAndBottom()
不同的是,這里我們用了平移的值的相反數。原因很簡單,因為我們要使用這兩個方法的時候需要對指定的控件所在的父容器進行調用(正如上面是先獲取父控件)。當我們希望控件相對于之前的位置向右下方向移動,就應該讓父容器相對于之前的位置向左上方向移動。因為實際上該控件相對于父控件的位置沒有發生變化,變化的是父控件的位置。(參考的坐標系不同)
2.6 Scroller
上面,我們的測試代碼是讓指定的控件隨著手指移動,但是假如我們希望控件從一個位置移動到另一個位置呢?當然,它們也可以實現,但是這幾乎就是在瞬間完成了整個操作,實際的UI效果肯定不會好。所以,為了讓滑動的過程看起來更加流暢,我們可以借助 Scroller
來實現。
在使用 Scroller
之前,我們需要先實例化一個 Scroller
:
private Scroller scroller = new Scroller(getContext());
然后,我們需要覆寫自定義控件的 computeScroll()
方法,這個方法會在繪制 View 的時候被調用。所以,這里的含義就是,當 View 重繪的時候會調用 computeScroll()
方法,而 computeScroll()
方法會判斷是否需要繼續滾動,如果需要繼續滾動的時候就調用 invalidate()
方法,該方法會導致 View 進一步重繪。所以,也就是靠著這種不斷進行重繪的方式實現了滾動的效果。
滑動效果最終結束的判斷是通過 Scroller
的 computeScrollOffset()
方法實現的,當滾動停止的時候,該方法就會返回 false
,這樣不會繼續調用 invalidate()
方法,因而也就不會繼續繪制了。下面是該方法典型的覆寫方式:
@Override
public void computeScroll() {
super.computeScroll();
if (scroller.computeScrollOffset()) {
((View) getParent()).scrollTo(scroller.getCurrX(), scroller.getCurrY());
invalidate();
}
}
然后,我們再加入一個滾動到指定位置的方法,在該方法內部我們使用了 2000ms 來指定完成整個滑動所需要的時間:
public void smoothScrollTo(int descX, int descY) {
scroller.startScroll(getScrollX(), getScrollY(), descX - getScrollX(), descY - getScrollY(), 2000);
invalidate();
}
這樣定義了之后,我們只需要在需要滾動的時候調用自定義 View 的 smoothScrollTo()
方法即可。
3、手勢
3.1 ViewConfiguration
在類 ViewConfiguration
中定義了一些列的常量用來標志指定的行為,比如,TouchSlop
就是滑動的最小的距離。你可以通過 ViewConfiguration.get(context)
來獲取 ViewConfiguration
實例,然后通過它的 getter 方法來獲取這些常量的定義。
3.2 VelocityTracker
VelocityTracker
用來檢測手指滑動的速率,它的使用非常簡單。在使用之前,我們先使用它的靜態方法 obtain()
獲取一個實例,然后在 onTouch()
方法中調用它的 addMovement(MotionEvent)
方法:
velocityTracker = VelocityTracker.obtain();
隨后,當我們想要獲得速率的時候,先調用 computeCurrentVelocity(int)
傳入一個時間片段,單位是毫秒,然后調用 getXVelocity()
和 getYVelocity()
分別獲得在水平和豎直方向上的速率即可:
velocityTracker.computeCurrentVelocity((int) duration);
getBinding().tvVelocity.setText("X:" + velocityTracker.getXVelocity() + "\n"
+ "Y:" + velocityTracker.getYVelocity());
本質上,計算速率的時候是用指定時間的長度變化除以我們傳入的時間片。當我們使用完了 VelocityTracker
之后,需要回收資源:
velocityTracker.clear();
velocityTracker.recycle();
3.3 GestureDectector
GestureDectector
用來檢測手指的手勢。在使用它之前我們需要先獲取一個 GestureDetector
的實例:
mGestureDetector = new GestureDetector(getContext(), new MyOnGestureListener());
這里我們用了 GestureDetector
的構造方法,需要傳入一個 OnGestureListener
對象。這里我們用了 MyOnGestureListener
實例。 MyOnGestureListener
是一個自定義的類,實現了 OnGestureListener
接口:
private class MyOnGestureListener extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onSingleTapUp(MotionEvent e) {
ToastUtils.makeToast("Click detected");
return false;
}
@Override
public void onLongPress(MotionEvent e) {
LogUtils.d("Long press detected");
}
@Override
public boolean onDoubleTap(MotionEvent e) {
LogUtils.d("Double tab detected");
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
LogUtils.d("Fling detected");
return true;
}
}
在 MyOnGestureListener
中,我們覆寫了它的一些方法。比如,單擊、雙擊和長按等等,當檢測到相應的手勢的時候這些方法就會被調用。
然后,我們可以這樣使用 GestureDetector
,只要在控件的觸摸事件回調中調用即可:
getBinding().vg.setOnTouchListener((v, event) -> {
mGestureDetector.onTouchEvent(event);
return true;
});
4、事件分發機制
4.1 事件傳遞的過程
當討論事件分發機制的時候,我們首先要了解 Android 中 View
的組成結構。在 Android 中,一個 Activity 包含一個 PhoneWindow
,當我們在 Activity 中調用 setContentView()
方法的時候,會調用該 PhoneWindow
的 setContentView()
方法,并在這個方法中生成一個 DecorView
作為 Activity 的跟 View
。
根據上面的分析,當一個點擊事件被觸發的時候,首先接收到該事件的是 Activity
。因為,Activity
覆蓋了整個屏幕,我們需要先讓它接收事件,然后它把事件傳遞給根 View
之后,再由根 View
向下繼續傳遞。這樣不斷縮小搜索的范圍,直到最頂層的 View
。當然,任何的父容器都可以決定這個事件是不是要繼續向下傳遞,因此,我們可以大致得到下面這個事件傳遞的圖:
左邊的圖是一個 Activity 內部的 View
和 Window
的組織結構。右面的圖可以看作它的切面圖,其中的黑色箭頭表示事件的傳遞過程。這里事件傳遞的過程是先從下到上,然后再從上到下。也就是從大到小,不斷定位到觸摸的控件,其中每個父容器可以決定是否將事件傳遞下去。(需要注意的地方是,如果一個父容器有多個子元素的話,那么在這些子元素中進行遍歷的時候,順序是從上往下的,也就是按照展示的順序)。
上面我們分析了 Android 事件傳遞的過程,相信你有了一個大致的了解。但是,想要了解整個事件傳遞過程具體涉及了哪些方法、如何作用等,還需要我們對源碼進行分析。
4.2 事件傳遞的原理
當觸摸事件發生的時候,首先會被 Activity 接收到,然后該 Activity 會通過其內部的 dispatchTouchEvent(MotionEvent)
將事件傳遞給內部的 PhoneWindow
;接著 PhoneWindow
會把事件交給 DecorView
,再由 DecorView
交給根 ViewGroup
。剩下的事件傳遞就只在 ViewGroup
和 View
之間進行。我們可以通過覆寫 Activity 的 dispatchTouchEvent(MotionEvent)
來阻止把事件傳遞給 PhoneWindow
。實際上,在我們開發的時候不會對 Window
的事件傳遞方法進行重寫,一般是對 ViewGroup
或者 View
。所以,下面我們的分析只在這兩種控件之間進行。
當討論 View 的事件分發機制的時候,無外乎下面三個方法:
-
boolean onInterceptTouchEvent(MotionEvent ev)
:用來對事件進行攔截,該方法只存在于 ViewGroup 中。一般我們會通過覆寫該方法來攔截觸摸事件,使其不再繼續傳遞給子 View。 -
boolean dispatchTouchEvent(MotionEvent event)
:用來分發觸摸事件,一般我們不覆寫該方法,返回true
則表示事件被處理了。在 View 中,它負責根據手勢的類型和控件的狀態對事件進行處理,會回調我們的OnTouchListener
或者OnClickListener
;在 ViewGroup 中,該方法被覆寫,它的責任是對事件進行分發,會對所有的子 View 進行遍歷,決定是否將事件分發給指定的 View。 -
boolean onTouchEvent(MotionEvent event)
:用于處理觸摸事件,返回true
表示觸摸事件被處理了。ViewGroup 沒有覆寫該方法,故在 ViewGroup 中與 View 中的功能是一樣的。需要注意的是,如果我們為控件設置了OnTouchListener
并且在或者中返回了true
,那么這個方法不會被調用,也就是OnTouchListener
比該方法的優先級較高。對我們開發來說,就是OnTouchListener
比OnClickListener
和OnLongClickListener
的優先級要高。
于是,我們可以得到如下的偽代碼。這段代碼是存在于 ViewGroup 中的,也就是事件分發機制的核心代碼:
boolean dispatchTouchEvent(MotionEvent e) {
boolean result;
if (onInterceptTouchEvent(e)) {
result = super.dispatchTouchEvent(e);
} else {
result = child.dispatchTouchEvent(e);
}
return result;
}
按照上述分析,觸摸事件經過 Activity 傳遞給根 ViewGroup 之后:
如果 ViewGourp 覆寫了 onInterceptTouchEvent()
并且返回了 true
就表示希望攔截該方法,于是就把觸摸事件交給當前 ViewGroup 進行處理(觸發 OnTouchListener
或者 OnClickListener
等);否則,會交給子元素的繼續分發。如果該子元素是 ViewGroup 的話,就會在該子 View 中執行一遍上述邏輯,否則會在當前的子元素中對事件進行處理(觸發 OnTouchListener
或者 OnClickListener
等)……就這樣一層層地遍歷下去,本質上是一個深度優先的搜索算法。
這里我們對整個事件分發機制的整體做了一個素描,在接下來的文章中我們會對各個方法的細節進行源碼分析,為了防止您在接下來的行文中迷路,我們先把這個整體邏輯按下圖進行描述:
4.3 事件傳遞的源碼分析
上述我們分析了事件分發機制的原理,下面我們通過源代碼來更具體地了解這塊是如何設計的。同樣,我們的焦點也只在那三個需要重點關注的方法。
4.3.1 決定是否攔截事件
首先,我們來看 ViewGroup 中的 dispatchTouchEvent(MotionEvent)
方法,我們節選了其一部分:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// ...
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
if (actionMasked == MotionEvent.ACTION_DOWN) { // 1
// 這里表示如果是一個新的觸摸事件就要重置所有的狀態,其中包括將 mFirstTouchTarget 置為 null
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// 在這里檢查是否攔截了事件,mFirstTouchTarget 是之前處理觸摸事件的 View 的封裝
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
// 這里判斷該 ViewGroup 是否禁用了攔截,由 requestDisallowInterceptTouchEvent 設置
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
// 非按下事件并且 mFirstTouchTarget 為 null,說明判斷過攔截的邏輯并且啟用了攔截
intercepted = true;
}
// ...
}
// ...
return handled;
}
上面代碼是我們節選的 ViewGroup 攔截事件的部分代碼,這里的邏輯顯然比偽代碼復雜的多。不過,盡管如此,這些代碼確實必不可少的。因為,當我們要去判斷是否攔截一個觸摸事件的時候,此時觸摸的事件仍然在繼續,這意味著這個方法會被持續調用;抬起的時候再按下,又是另一次調用。考慮到這個連續性,我們需要多做一些邏輯。
這里我們首先在 1 處通過行為是否是“按下”的來判斷是否是一次新的觸摸事件,如果是的話我們需要重置當前的觸摸狀態。其次,我們需要根據事件的類型來決定是否應該調用 onInterceptTouchEvent()
,因為對一次觸摸事件,我們只需要在“按下”的時候判斷一次就夠了。所以,顯然我們需要將 MotionEvent.ACTION_DOWN
作為一個判斷條件。然后,我們使用 mFirstTouchTarget
這個全局的變量來記錄上次攔截的結果——如果之前的事件交給過子元素處理,那么它就不為空。
除了 mFirstTouchTarget
,我們還需要用 mGroupFlags
的 FLAG_DISALLOW_INTERCEPT
標志位來判斷該 ViewGroup 是否禁用了攔截。這個標志位可以通過 ViewGroup 的 requestDisallowInterceptTouchEvent(boolean)
來設置。只有沒有禁用攔截事件的時候我們才需要調用 onInterceptTouchEvent()
判斷是否開啟了攔截。
4.3.2 分發事件給子元素
如果在上面的操作中事件沒有被攔截并且沒有被取消,那么就會進入下面的邏輯。這部分代碼處在 dispatchTouchEvent()
中。在下面的邏輯中會根據子元素的狀態將事件傳遞給子元素:
// 對子元素進行倒序遍歷,即從上到下進行遍歷
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
// ...
// 判斷子元素是否能接收觸摸事件:能接收事件并且不是正在進行動畫的狀態
if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
// ...
// 在這里調用了 dispatchTransformedTouchEvent() 方法將事件傳遞給子元素
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// ... 記錄一些狀態信息
// 在這里完成對 mFirstTouchTarget 的賦值,表示觸摸事件被子元素處理
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
// 結束循環,完成子元素的遍歷
break;
}
// 顯然,如果到了這一步,那么子元素的遍歷仍將繼續
}
當判斷了指定的 View 可以接收觸摸事件之后會調用 dispatchTransformedTouchEvent()
方法分發事件。其定義的節選如下:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
final boolean handled;
// ...
if (child == null) {
// 本質上邏輯與 View 的 dispatchTouchEvent() 一致
handled = super.dispatchTouchEvent(transformedEvent);
} else {
// ...
// 交給子元素繼續分發事件
handled = child.dispatchTouchEvent(transformedEvent);
}
return handled;
}
dispatchTransformedTouchEvent()
會根據傳入的 child
是否為 null
分成兩種調用的情形:事件沒有被攔截的時候,讓子元素繼續分發事件;另一種是當事件被攔截的時候,調用當前的 ViewGroup 的 super.dispatchTouchEvent(transformedEvent)
處理事件。
4.3.3 View 中的 dispatchTouchEvent
上面我們分析的 dispatchTouchEvent(MotionEvent)
是 ViewGroup 中重寫之后的方法。但是,正如我們上面的分析,重寫之前的方法總是會被調用,只是對象不同。這里我們就來分析以下這個方法的作用。
public boolean dispatchTouchEvent(MotionEvent event) {
// ...
boolean result = false;
// ....
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
// 這里回調了 setOnTouchListener() 方法傳入的 OnTouchListener
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// 如果 OnTouchListener 沒有被回調過或者返回了 false,就會調用 onTouchEvent() 進行處理
if (!result && onTouchEvent(event)) {
result = true;
}
}
// ...
return result;
}
根據上面的源碼分析,我們知道,如果當前的 View 設置過 OnTouchListener
, 并且在 onTouch()
回調方法中返回了 true
,那么 onTouchEvent(MotionEvent)
將不會得到調用。那么,我們再來看一下 onTouchEvent()
方法:
public boolean onTouchEvent(MotionEvent event) {
// ...
// 判斷當前控件是否是可以點擊的:實現了點擊、長按或者設置了可點擊屬性
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
// ...
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
// ...
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
// ...
break;
case MotionEvent.ACTION_DOWN:
// ...
if (!clickable) {
checkForLongClick(0, x, y);
break;
}
// ...
break;
// ...
}
return true;
}
return false;
}
這里先判斷指定的控件是否是可點擊的,即是否設置過點擊或者長按的事件。然后會在手勢抬起的時候調用 performClick()
方法,并會在這個方法中嘗試從 ListenerInfo
取 OnClickListener
進行回調;會在長按的時候進行監聽以調用相應長按事件;其他的事件與之類似,可以自行分析。所以,我們可以得出結論:當為控件的觸摸事件進行了賦值并且在其中返回了 true
就代表該事件被消費了,即使設置過單擊和長按事件也不會被回調,觸摸事件的優先級比后面兩者要高。
經過上述分析,我們可以知道 View 中的 dispatchTouchEvent(MotionEvent)
方法就是用來對手勢進行處理的,所以回到 4.3.2
,那里的意思就是:如果 ViewGroup 攔截了觸摸事件,那么它就自己來對事件進行處理;否則就把觸摸事件傳遞給子元素,讓它來進行處理。
4.4.4 總結
以上就是我們對 Android 中事件分發機制的詳解,你可以通過圖片和代碼結合來更透徹得了解這方面的內容。雖然這部分代碼比較多、比較長,但是每個地方的設計都是合情合理的。
源代碼
你可以在Github獲取以上程序的源代碼: Android-references。
Hello, 我是 WngShhng. 如果您喜歡我的文章,可以在以下平臺關注我: