一些零碎的知識的。
-
座標系原點預設是螢幕左上角,向右為X軸正方向,向下為Y軸正方向。
-
View的getTop()、getLeft()、getBottom()、getRight()是相對父View來說的。
-
注意區分View的座標系和Canvas的座標系。View座標系的原點是View的左上角;Canvas的座標系預設是與View的重合,但是通過平移、旋轉、縮放可以進行操作。
-
觸控事件MotionEvent的getX()、getY()是相對於View座標系的;getRawX()、getRawY()是相對於螢幕座標系的。
-
0°角與X軸正方向重合,角度沿著順時針增大。
-
A R G B 的取值範圍均為0~255(即16進位制的0x00~0xff),A 從0x00到0xff表示從透明到不透明;RGB 從0x00到0xff表示顏色從淺到深。
-
bitmap大小計算公式,單位為Byte: bitmap.getWidth()*bitmap.getHeight()*(1/inSampleSize)^2*(目標裝置解析度/dpi資料夾解析度)^2*色彩空間
-
merge標籤必須是xml檔案的根標籤,merge標籤與include標籤一起用;通過LayoutInflater填充merge標籤時必須指定父ViewGroup。
-
getMeasureWidth()方法在measure()過程結束後就可以獲取到了,而getWidth()方法要在layout()過程結束後才能獲取到。另外,getMeasureWidth()方法中的值是通過setMeasuredDimension()方法來進行設定的,而getWidth()方法中的值則是通過檢視右邊的座標減去左邊的座標計算出來的。
-
onMeasure()用於使用父View傳過來的widthMeasureSpec,heightMeasureSpec來確定自身能達到的最大尺寸(可以超,但是超過這個尺寸的部分將顯示不出來)。至於View真正的尺寸還需要onLayout()的過程去確定,onLayout()中父View指定的矩形引數有可能使getWidth()比getMeasureWidth()大。最佳實踐:遵守規範,通過數學計算控制引數把內容控制在螢幕之內。
-
View的繪製流程從ViewRootImpl的performTraversals()開始,performTraversals()裡面會依次執行measure()、layout()、draw()的流程。
measure():先會獲取根佈局的MeasureSpec,如果是match_parent則為EXACTLY,如果是wrap_content則為AT_MOST,大小皆為視窗(window)的大小。也就意味著根檢視總是會充滿全屏的。View的onMeasure()接受父View傳來的寬高引數,onMeasure的預設實現是呼叫getDefaultSize()來獲取View的大小,如果MeasureSpec的mode為EXACTLY和AT_MOST則獲取MeasureSpec中的尺寸資訊。覆寫onMesure()函式進行測量的時候記得呼叫setMeasuredDimension()。ViewGroup需要呼叫measureChildren()(最後會呼叫measureChild())去觸發子View進行測量。
layout():接收四個引數,分別代表著左、上、右、下的座標,當然這個座標是相對於當前檢視的父檢視而言的。正如其名字所描述的一樣,這個方法是用於給檢視進行佈局的,也就是確定檢視的位置。ViewGroup中的onLayout()方法竟然是一個抽象方法,這就意味著所有ViewGroup的子類都必須重寫這個方法。View中的onLayout()是空方法。
draw():measure和layout的過程都結束後,接下來就進入到draw的過程了。
-
呼叫順序:onMeasure()-->onSizeChanged()-->onLayout()-->onDraw()。
-
檢視重繪:invalidate()中先呼叫skipInvalidate()方法來判斷當前View是否需要重繪,判斷的邏輯也比較簡單,如果View是不可見的且沒有執行任何動畫,就認為不需要重繪了。接著迴圈請求自己的父View去重繪,直到迴圈到最外層的根View後,呼叫ViewRoot的invalidateChildInParent()去重繪。最後會通過Handler傳送一條DO_TRAVERSAL的訊息給ViewRoot自身,再次呼叫performTraversals()進行繪製。
-
invalidate()方法雖然最終會呼叫到performTraversals()方法中,但這時measure和layout流程是不會重新執行的,因為檢視沒有強制重新測量的標誌位,而且大小也沒有發生過變化,所以這時只有draw流程可以得到執行。而如果你希望檢視的繪製流程可以完完整整地重新走一遍,就不能使用invalidate()方法,而應該呼叫**requestLayout()**了。
-
自定義View有三種:自繪控制元件、組合控制元件、繼承控制元件。
-
onDraw()自繪製圖形的時候,通常會把Canvas座標系移動到中央(或者是旋轉等會使Canvas座標系變化的操作),因為這樣進行引數計算比較簡單,但是這樣會存在一個問題:如果想實現View內區域點選事件監控的話,存在座標不一致的問題。因為MotionEvent的getX()、getY()是相對View的,如果取觸控點的getX()、getY()去跟圓的Region去判斷的話,將會被錯誤的判斷為觸控了圓,而事實上在Canvas座標系中該觸控點為(-x,-y)。解決方法:對Canvas座標系進行了變化的時候記錄下它的逆矩陣,用逆矩陣對MotionEvent的**getRawX()、getRawY()**進行轉化即可得到觸控點相對於該Canvas的座標。原理:在繪製的時候Canvas會把自己的座標轉化為螢幕座標進行繪製,所以想要還原回Canvas繪製狀態時的座標可以用它的逆矩陣進行逆向操作。
注意:如果Canvas座標系與View座標系重合則直接用MotionEvent的getX()、getY()即可。
- 事件分發流程:Activity->ViewGroup->View;對每個接收物件來說:dispatchTouchEvent()->onInterceptTouchEvent()->onTouchEvent()。
型別 | 函式 | Activity | ViewGroup | View | 返回值 |
---|---|---|---|---|---|
事件分發 | dispatchTouchEvent() | √ | √ | √ | true:不繼續向下分發 |
事件攔截 | onInterceptTouchEvent() | x | √ | x | true:攔截事件 |
事件消費 | onTouchEvent() | √ | √ | √ | true:消費掉事件 |
-
ViewGroup事件分發虛擬碼:
public boolean dispatchTouchEvent(MotionEvent ev) { boolean result = false; // 預設狀態為沒有消費過 if (!onInterceptTouchEvent(ev)) { // 如果沒有攔截交給子View result = child.dispatchTouchEvent(ev); } if (!result) { // 如果事件沒有被消費,詢問自身onTouchEvent result = onTouchEvent(ev); } return result; } 複製程式碼
可以看到只有onInterceptTouchEvent()的返回值沒有賦給result,所以onInterceptTouchEvent()返回true只會中斷事件dispatch,還是會繼續從當前的View進行onTouch()反向回撥。而dispatchTouchEvent()返回true中斷掉所有流程;onTouchEvent()返回true則會中斷掉回撥過程,也即是表示事件被消費了。
-
View相關事件呼叫順序:onTouchListener>onTouchEvent>onLongClickListener>onClickListener。
虛擬碼:
public boolean dispatchTouchEvent(MotionEvent event) { if (mOnTouchListener.onTouch(this, event)) { return true; } else if (onTouchEvent(event)) { return true; } return false; } 複製程式碼
onTouchEvent()中處理onClickListener和onLongClickListener。
精簡版原始碼:
public boolean onTouchEvent(MotionEvent event) { ... final int action = event.getAction(); // 檢查各種 clickable if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) { switch (action) { case MotionEvent.ACTION_UP: ... removeLongPressCallback(); // 移除長按 ... performClick(); // 檢查單擊 ... break; case MotionEvent.ACTION_DOWN: ... checkForLongClick(0); // 檢測長按 ... break; ... } return true; // ◀︎表示事件被消費 } return false; } 複製程式碼
上面可以看出:只要 View 可點選onTouchEvent()就返回 true,就表示事件被消費了。
舉例,
<RelativeLayout android:background="#CCC" android:id="@+id/layout" android:onClick="myClick" android:layout_width="200dp" android:layout_height="200dp"> <View android:clickable="true" android:layout_width="200dp" android:layout_height="200dp" /> </RelativeLayout> 複製程式碼
現在你有了一個 RelativeLayout - View 你開開心心的為 RelativeLayout 設定了一個點選事件
myClick
,然而你會發現不論怎麼點都不會接收到資訊,仔細一看,發現內部的 View 有一個屬性android:clickable="true"
正是這個看似不起眼的屬性把事件給消費掉了,由此我們可以得出如下結論: 1. 不論 View 自身是否註冊點選事件,只要 View 是可點選的就會消費事件。 2. 事件是否被消費由返回值決定,true 表示消費,false 表示不消費,與是否使用了事件無關。 -
事件分發核心要點:
- 事件分發原理: 責任鏈模式,事件層層傳遞,直到被消費。
- View 的 dispatchTouchEvent 主要用於排程自身的監聽器和 onTouchEvent。
- View的事件的排程順序是 onTouchListener > onTouchEvent > onLongClickListener > onClickListener 。
- 不論 View 自身是否註冊點選事件,只要 View 是可點選的就會消費事件。
- 事件是否被消費由返回值決定,true 表示消費,false 表示不消費,與是否使用了事件無關。
- ViewGroup 中可能有多個 ChildView 時,將事件分配給包含點選位置的 ChildView。
- ViewGroup 和 ChildView 同時註冊了事件監聽器(onClick等),由 ChildView 消費。
- 一次觸控流程中產生事件應被同一 View 消費,全部接收或者全部拒絕。
- 只要接受 ACTION_DOWN 就意味著接受所有的事件,拒絕 ACTION_DOWN 則不會收到後續內容。
- 如果當前正在處理的事件被上層 View 攔截,會收到一個 ACTION_CANCEL,後續事件不會再傳遞過來。