編輯:關於Android編程
在第一篇文章中又點擊事件的一個例如引入事件的學習,之後第二篇文章查找一下點擊事件最終是在什麼地方觸發的,發現是在onTouchEvent方法中,第三篇和第四篇總結了一下onTouchEvent的參數MotionEvent對象的常用屬性getAction() 與 getX(), getY()。
前幾篇是打下基礎,現在可以基於這些知識分析下View.onTouchEvent也就是之前第二篇文章中查找到的發現點擊事件觸發的地方,View類是所有視圖的基類,也就是如果子類不覆寫此方法的話,觸屏事件都是交由View.onTouchEvent處理。
分析的源碼是Andorid 4.0 (Android 14)原因之前也解釋過,這種通過的功能因為各種版本變化不會太大,View.onTouchEvent有130行肯定不會直接粘貼到blog上然後一行行的解釋是什麼意思,打算按照先總後分的形式整理。這篇文章並不是我想寫這麼長,實在是方法本身源碼就很多,。
/**
* Implement this method to handle touch screen motion events.
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
*/
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
// 當前視圖處於禁用狀態
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PRESSED) != 0) {
// 如果抬起手指清除掉按下狀態
mPrivateFlags &= ~PRESSED;
// 當前顯示還是按下狀態,所以重刷一下
refreshDrawableState();
}
// 應該是當前視圖處理當前觸摸事件的,但是因為指定為禁用狀態,
// 所以還是消耗當前事件,只是不做任何處理。
// 這樣處理符合邏輯,因為用戶當前觸摸的是這個視圖,雖然被設置為不觸發任何處理。
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
// 把當前視圖的事件交由其他視圖處理。
if (mTouchDelegate != null) {
// 事件雖然傳遞給了當前視圖,
// 但是如果其他視圖通過設置mTouchDelegate增大觸摸區域
// 並且當前觸摸點在其他視圖的擴大區域內。
if (mTouchDelegate.onTouchEvent(event)) {
// 交由擴大觸摸區域的那個視圖處理
// 並消耗此次觸摸事件
return true;
}
}
// 如果當前視圖是點擊或者長按狀態
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
// 點擊與長按重要處理的地方,之後著重分析這塊
......
}
// 是點擊或者長按,當前方法反饋與處理用戶操作
return true;
}
// 當前視圖不處理,由事件傳遞機制在找其他匹配的視圖
// 具體如何查找之後在分析
return false;
}
1. 當前視圖是否處於禁用狀態(如果是抬起手指,清理掉按下狀態)
2. 是否在其他視圖的擴大范圍內(通過TouchDelegate實現)
3. 如果以上兩者都不成立,並且當前視圖處於點擊或者長按狀態
4. 如果以上3者都不符合條件,返回false表明當前視圖不消耗此次觸摸事件
以上源碼會涉及到以下知識,可以查看這些變量的注釋,就不一一貼出源碼了:
1. mViewFlags是全局變量,用於存放視圖狀態信息。
2. TouchDelegate 設置視圖的點擊區域(增大或者縮小可點擊區域)
3. DISABLED 當前視圖禁用狀態。通過使用View.setEnabled(false)設置視圖為禁用狀態
4. CLICKABLE 可點擊。通過使用View.setClickable(true)設置
5. LONG_CLICKABLE 可長按。通過使用View.setLongClickable(true)設置
在第三篇文章《Android Touch事件學習 3 區分各種手勢基礎知識》中分析過,Android通過各種ACTION(動作)來區分用戶行為,然後現在需要通過系統提供的這些ACTION來判斷是點擊還是長按,根據之前的經驗ACTION的執行是有先後順序的依次是:ACTION_DOWN, ACTION_MOVE, ACTION_UP, ACTION_CANCEL,且當用戶手指按下時觸發ACTION_DOWN,手指抬起時觸發ACTION_UP,移動時被攔截觸發ACTION_CANCEL,這三個都是僅會觸發依次,手指移動是觸發ACTION_MOVE會根據手指移動事件執行0到多次。 View.onTouchEvent中並不是一這個次序來擺放源碼的,但是下面會依次按照上面的順序分塊進行解釋。
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
// 1. 必須先滿足是鼠標右鍵 或是 手寫筆第一個按鈕,才會返回true
if (performButtonActionOnTouchDown(event)) {
break;
}
// 2. 當前視圖是否可滾動(例如:當前是ScrollView視圖,返回true)
// Walk up the hierarchy to determine if we're inside a scrolling container.
boolean isInScrollingContainer = isInScrollingContainer();
// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
if (isInScrollingContainer) {
// 滾動視圖內,先不設置為按下狀態,因為用戶之後可能是滾動操作
// 不是此次分析的重點,感興趣可以自己了解下
mPrivateFlags |= PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
// 不在滾動視圖內,立即反饋為按下狀態
mPrivateFlags |= PRESSED;
// 刷新為按下狀態
refreshDrawableState();
// 3. 與 4.
checkForLongClick(0);
}
break;
以上標注了1,2,3,4,如果感覺注釋已經看懂了可以直接忽略以下關於這幾點的源碼注釋,直接跳到下一個動作ACTION_MOVE
1. performButtonActionOnTouchDown方法源碼
/**
* Performs button-related actions during a touch down event.
*
* @param event The event.
* @return True if the down was consumed.
*
* @hide
*/
protected boolean performButtonActionOnTouchDown(MotionEvent event) {
// 如果是鼠標右鍵,手寫筆第一個按鈕(詳見BUTTON_SECONDARY常量注釋)
if ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) {
if (showContextMenu(event.getX(), event.getY(), event.getMetaState())) {
return true;
}
}
return false;
}
2. isInScrollingContainer方法源碼
/**
* @hide
*/
public boolean isInScrollingContainer() {
ViewParent p = getParent();
while (p != null && p instanceof ViewGroup) {
if (((ViewGroup) p).shouldDelayChildPressedState()) {
return true;
}
p = p.getParent();
}
return false;
}
3. checkForLongClick方法源碼
private void checkForLongClick(int delayOffset) {
// 當前視圖可以執行長按操作
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
mHasPerformedLongPress = false;
if (mPendingCheckForLongPress == null) {
// 4 與 5
mPendingCheckForLongPress = new CheckForLongPress();
}
// 4.
mPendingCheckForLongPress.rememberWindowAttachCount();
// 延遲一段時間把runnable添加到消息隊列
postDelayed(mPendingCheckForLongPress,
ViewConfiguration.getLongPressTimeout() - delayOffset);
}
}
ViewConfiguration.getLongPressTimeout() Android 4.0 源碼值是500
class CheckForLongPress implements Runnable {
private int mOriginalWindowAttachCount;
public void run() {
if (isPressed() && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
// 5. 觸發執行長按事件
if (performLongClick()) {
mHasPerformedLongPress = true;
}
}
}
public void rememberWindowAttachCount() {
mOriginalWindowAttachCount = mWindowAttachCount;
}
}
這裡很容易搞不清楚,這裡竟然還判斷
mOriginalWindowAttachCount == mWindowAttachCount
明明在CheckForLongPress.rememberWindowAttachCount()中進行賦值的,解釋一下mOriginalWindowAttachCount是CheckForLongPress的內部變量,而mWindowAttachCount是全局變量,CheckForLongPress的對象是延時發送到消息隊列的,也就是說如果在延遲期間mWindowAttachCount改變這個判斷條件還是過不了,那這個變量都在哪裡會發生改變呢?不貼源碼了,會在View.dispatchAttachedToWindow方法中進行累加,而此方法會在ViewGroup.addView時調用,也就是再次期間添加視圖的話,不會滿足條件。
/**
* Call this view's OnLongClickListener, if it is defined. Invokes the context menu if the
* OnLongClickListener did not consume the event.
*
* @return True if one of the above receivers consumed the event, false otherwise.
*/
public boolean performLongClick() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
boolean handled = false;
if (mOnLongClickListener != null) {
// 觸發執行長按事件
handled = mOnLongClickListener.onLongClick(View.this);
}
if (!handled) {
handled = showContextMenu();
}
if (handled) {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
}
return handled;
}
case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();
// Be lenient about moving outside of buttons
// 1. 觸摸點是否在當前視圖內
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
// 如果手指移出視圖區域
// 2. 去除視圖輕觸狀態
removeTapCallback();
// 如果當前是按下狀態
if ((mPrivateFlags & PRESSED) != 0) {
// Remove any future long press/tap checks
// 3. 移除還沒有執行的長按與輕觸檢測
removeLongPressCallback();
// Need to switch from pressed to not pressed
// 移除按下狀態
mPrivateFlags &= ~PRESSED;
// 移除後刷新視圖
refreshDrawableState();
}
}
break;
與之前一樣,以下是上面ACTION_MOVE處理調用的方法,如果已經明白可以直接忽略以下方法與注釋,直接看ACTION_UP的分析
1. pointInView方法源碼
/**
* Utility method to determine whether the given point, in local coordinates,
* is inside the view, where the area of the view is expanded by the slop factor.
* This method is called while processing touch-move events to determine if the event
* is still within the view.
*/
private boolean pointInView(float localX, float localY, float slop) {
// 把觸摸點的x,y 與當前視圖的上下左右進行比較,看是否在視圖區域內
// 視圖區域的上下左右都增加slop長度,在視圖外添加的slop區域內也算點擊到視圖內了
// 目的是為了增加當前視圖的可點擊區域,避免在視圖邊界處,即使移動一丁點就會
// 系統可能就會認為是在兩個視圖間切換
return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) &&
localY < ((mBottom - mTop) + slop);
}
// 在View視圖構造器中對mTouchSlop進行初始化
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
2.removeTapCallback源碼
/**
* Remove the tap detection timer.
*/
private void removeTapCallback() {
// 移除輕觸探測定時器
// 此Runnable是在滾動視圖是才會創建
if (mPendingCheckForTap != null) {
// 去除預按下狀態
mPrivateFlags &= ~PREPRESSED;
// 從消息隊列中刪除mPendingCheckForTap
removeCallbacks(mPendingCheckForTap);
}
}
/**
* Remove the longpress detection timer.
*/
private void removeLongPressCallback() {
// 移除長按探測定時器
if (mPendingCheckForLongPress != null) {
removeCallbacks(mPendingCheckForLongPress);
}
}
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
if ((mPrivateFlags & PRESSED) != 0 || prepressed) {
// 當前視圖處於預按下或者按下狀態
// 如果失去焦點,獲取焦點狀態
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
// 如果是與按下狀態,抬起手指前設置為按下狀態
mPrivateFlags |= PRESSED;
refreshDrawableState();
}
// 如果沒有執行長按
if (!mHasPerformedLongPress) {
// 移除長按探測定時器
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
// 只有按下狀態才會執行點擊事件
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
// 用於執行點擊操作
mPerformClick = new PerformClick();
}
// 使用post到runnable發送到消息隊列的目的是:
// 消息隊列是依次執行,把之前post到隊列的runnable執行完
// 才會執行當前runnable,以保證在之前所有狀態都處理完後執行
if (!post(mPerformClick)) {
// 如果執行不成功,必須保證觸發點擊事件
// 所以直接調用PerformClick類內部調用的觸發事件方法
performClick();
}
}
}
if (mUnsetPressedState == null) {
// 1. 清除按下狀態
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
// 如果是預按下狀態,過段事件後在發送到消息隊列
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
// 執行失敗的話,保證視圖不會永遠處於按下狀態
// 直接執行一次
mUnsetPressedState.run();
}
// 清除輕觸
removeTapCallback();
}
break;
1.UnsetPressedState 類源碼
private final class UnsetPressedState implements Runnable {
public void run() {
setPressed(false);
}
}
case MotionEvent.ACTION_CANCEL: // 清理按下狀態 mPrivateFlags &= ~PRESSED; // 刷新一下 refreshDrawableState(); // 清除輕觸狀態 removeTapCallback(); break;
ViewConfiguration是系統配置,各手機廠商可能會根據自身手機特點修改這些參數。
1. 滿足一些先決條件。例如:當前視圖非禁用狀態、當前視圖允許點擊或者長按(詳見 一、onTouchEvent整體結構)
之後通過系統反饋的動作來進行判斷
2. ACTION_DOWN:當前是否為滾動視圖,如果不是,當前視圖先顯示為按下狀態,且在500毫秒後執行長按操作。(詳見 三、ACTION_DOWN)
3. ACTION_MOVE:如果手指移動出當前視圖范圍內,清理以上設置的所有狀態,並且如果長按還沒有執行不會觸發。(詳見 四、ACTION_MOVE)
4. ACTION_UP:如果MOVE時沒有進行清理,且還沒有執行長按操作,執行點擊操作(詳見 五、ACTION_UP)
5. ACTION_CANCEL:清理所有狀態
重拾Android
從2012年自學Android開始,到現在第4個年頭了,期間一直沒接觸正規的Android項目,加上這幾年一直忙.NET項目,導致去年有兩單Android的私活沒底氣接,
Android攝像頭調試分析
前些天在展訊6825C 上調試gc2115攝像頭,發現後攝顯示效果非常的差,出現很嚴重的整個預覽界面豎條紋現象,但是對光線變化還是有反應的,初步判斷是sens
深入分析Android ViewStub的應用詳解
在開發應用程序的時候,經常會遇到這樣的情況,會在運行時動態根據條件來決定顯示哪個View或某個布局。那麼最通常的想法就是把可能用到的View都寫在上面,先把它們的可見性都
Android apk動態加載機制的研究(二):資源加載和activity生命周期管理
前言 為了更好地閱讀本文,你需要先閱讀Android apk動態加載機制的研究這篇文章,在此文中,博主分析了Android中apk的動態加載機制,並在文章的