Android Touch事件
在Android中,事件主要包括點按、長按、拖拽、滑動等,點按又包括單擊和雙擊,另外還包括單指操作和多指操作。所有這些都構成了Android中得事件響應。總的來說,所有的事件都由如下三個部分作為基礎:
- 按下(ACTION_DOWN)
- 移動(ACTION_MOVE)
- 抬起(ACTION_UP)
所有的事件首先必須執行ACTION_DOWN操作,之后可以觸發ACTION_MOVE接著ACTION_UP,或者直接ACTION_UP。
應用通過視圖組件和用戶交互,所以所有的事件都是通過視圖組件觸發的。在Android中所有的視圖都繼承于View,對視圖組件進行布局的布局組件(ViewGroup)也繼承于View。所以事件的觸發和傳遞主要在View和ViewGroup之間。
事件的傳遞
View.java
public boolean dispatchTouchEvent(MotionEvent event)
public boolean onTouchEvent(MotionEvent event)
ViewGroup.java
public boolean dispatchTouchEvent(MotionEvent event)
public boolean onTouchEvent(MotionEvent event)
public boolean onInterceptTouchEvent(MotionEvent ev)
Android事件的傳遞主要用到了以上的方法。事件傳遞的方法有兩個共同特點,它們的方法返回值都是boolean,參數都是MotionEvent。在Android中,所有的事件都是從開始經過傳遞到完成事件的消費,這些方法的返回值就決定了某一事件是否是繼續往下傳,還是被攔截了,或是被消費了。(true代表事件被消費了,false代表事件繼續往下傳遞)
MotionEvent繼承于InputEvent,用于標記各種動作事件。ACTION_DOWN、ACTION_MOVE、ACTION_UP都是MotinEvent中定義的常量。我們通過MotionEvent傳進來的事件類型來判斷接收的是哪一種類型的事件。
View事件傳遞方法的調用
dispatchTouchEvent
觸摸任何一個控件時,首先都會調用這個方法。dispatchTouchEvent方法代碼如下
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
在這個方法內,首先判斷if語句里面的條件:mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event),如果這個條件為true那么直接返回true,否則繼續執行onTouchEvent()。
分析下if語句里的條件:
- mTouchListener變量是否被賦值,該變量是通過以下方法賦值的
public void setOnTouchListener(OnTouchListener l) {
mOnTouchListener = l;
}
也就是說我們給控件注冊了觸摸事件監聽器,就是給mTouchListenter賦值。
- (mViewFlags & ENABLED_MASK) == ENABLED是判斷當前點擊的控件是否是enable的,按鈕默認都是enable的,因此這個條件恒定為true。
- mOnTouchListener.onTouch(this, event),其實也就是去回調控件注冊touch事件監聽器時的onTouch方法。也就是說如果我們在onTouch方法里返回true,就會讓這三個條件全部成立,從而整個方法直接返回true。如果我們在onTouch方法里返回false,就會再去執行onTouchEvent(event)方法。
綜上所述,在事件分發時首先調用dispatchTouchEvent(),執行onTouch()方法。如果onTouch()返回true后,不會再去調用onTouchEvent()方法。代表事件已經被onTouch()消費掉了。
onTouchEvent
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
// 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) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
//判斷控件是否可以點擊
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
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 (!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();
}
if (!post(mPerformClick)) {
//調用該方法,去處理點擊事件
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
mPrivateFlags |= PRESSED;
refreshDrawableState();
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
break;
case MotionEvent.ACTION_DOWN:
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPrivateFlags |= PREPRESSED;
mHasPerformedLongPress = false;
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
break;
case MotionEvent.ACTION_CANCEL:
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
removeTapCallback();
break;
case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();
// Be lenient about moving outside of buttons
int slop = mTouchSlop;
if ((x < 0 - slop) || (x >= getWidth() + slop) ||
(y < 0 - slop) || (y >= getHeight() + slop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
// Need to switch from pressed to not pressed
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
}
}
break;
}
return true;
}
return false;
}
performClick()方法來觸發點擊事件
public boolean performClick() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
if (mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
}
return false;
}
由上面代碼可以看到只要點擊事件的監聽器不為null,那么就去回調onClick()方法。同上面mTouchListener類似,通過以下方法注冊監聽器:
public void setOnClickListener(OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
mOnClickListener = l;
}
這里其實就是給控件注冊點擊事件監聽器。
綜上所述,事件從ACTION_DOWN開始,調用dispatchTouchEvent方法。當ACTION_UP觸發,并且onTouch()方法返回false,那么就回去執行onTouchEvent方法。最后會回調onClick,執行點擊事件響應的處理。
實踐
Button事件傳遞:touch事件傳遞至觸發點擊事件
public class ButtonActivity extends AppCompatActivity{
private static final String TAG = "ButtonActivity";
private Button mButton;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_button);
mButton = (Button) findViewById(R.id.button);
mButton.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch(event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "button ontouch execute, action " + event.getAction());
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, "button ontouch execute, action " + event.getAction());
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, "button ontouch execute, action " + event.getAction());
break;
}
return false;
}
});
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d(TAG, "button onClick execute");
}
});
}
}
log打印如下:
onTouch方法返回true,查看是否觸發點擊事件(調用onTouchEvent方法):
public class ButtonActivity extends AppCompatActivity{
private static final String TAG = "ButtonActivity";
private Button mButton;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_button);
mButton = (Button) findViewById(R.id.button);
mButton.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch(event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "button ontouch execute, action " + event.getAction());
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, "button ontouch execute, action " + event.getAction());
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, "button ontouch execute, action " + event.getAction());
break;
}
return true;
}
});
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d(TAG, "button onClick execute");
}
});
}
}
log日志打印如下:
這里有個重要的知識點,touch事件的層級傳遞。我們點擊控件時,都會觸發一系列的ACTION_DOWN,ACTION_MOVE,ACTION_UP等事件。這里需要注意,如果你在執行ACTION_DOWN的時候返回了false,后面一系列其它的action就不會再得到執行了。簡單的說,就是當dispatchTouchEvent在進行事件分發的時候,只有前一個action返回true,才會觸發后一個action。原理分析:
首先在onTouch事件里返回了false,就一定會進入到onTouchEvent方法中,然后我們來看一下onTouchEvent方法的細節。由于我們點擊了按鈕,就會進入到事件類型判斷switch中,然后你會發現,不管當前的action是什么,最終都一定會返回一個true。那么我可以這樣理解,touch事件都是從ACTION_DOWN開始的,如果ACTION_DOWN事件在dispatchTouchEvent()后返回false,那么就代表這個事件沒有消費掉。然而view(如Button)沒有子view,所以這個事件相當于沒有處理,touch事件也就無法傳遞下去,也就沒有ACTION_UP和ACTION_MOVE事件觸發。
ImageView事件傳遞:
public class ImageViewActivity extends AppCompatActivity{
private static final String TAG = "ImageViewActivity--->";
private ImageView mImageView;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_image_view);
mImageView = (ImageView) findViewById(R.id.img);
mImageView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch(event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "imageView onTouch execute, action " + event.getAction());
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, "imageView onTouch execute, action " + event.getAction());
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, "imageView onTouch execute, action " + event.getAction());
break;
}
return false; //這里我們可以直接返回true,實現觸發三種action.或者在ImageView屬性中設置可點擊
}
});
}
}
log日志打印如下:
在ACTION_DOWN執行完后,后面的一系列action都不會得到執行了。這是因為ImageView和按鈕不同,它是默認不可點擊的,因此在onTouchEvent的可否點擊判斷時無法進入到事件類型判斷,直接跳到最后返回了false,也就導致后面其它的action都無法執行了。