記得大家剛開(kāi)始接觸安卓的時(shí)候固翰,一個(gè)setOnClickListener就能實(shí)現(xiàn)一個(gè)View的點(diǎn)擊,當(dāng)時(shí)是如此的激動(dòng)~彤委。這大概就是大家對(duì)Android觸摸事件最初的接觸吧剂习。今天我們來(lái)聊下Android重要的觸摸事件分發(fā)機(jī)制揪垄。
例子
我們來(lái)舉一個(gè)栗子吧~
MyButton.java
public class MyButton extends Button
{
@Override
public boolean onTouchEvent(MotionEvent event)
{
int action = event.getAction();
switch (action)
{
case MotionEvent.ACTION_DOWN:
Log.e("w", "onTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e("w", "onTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e("w", "onTouchEvent ACTION_UP");
break;
default:
break;
}
return super.onTouchEvent(event);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event)
{
int action = event.getAction();
switch (action)
{
case MotionEvent.ACTION_DOWN:
Log.e("w", "dispatchTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e("w", "dispatchTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e("w", "dispatchTouchEvent ACTION_UP");
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}
}
在onOutchEvent和disatchTouchEvent中打印日志
main_activity.xml
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<io.weimu.caoyang.MyButton
android:id="@+id/id_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="click me" />
</LinearLayout>
MainActivity.java
public class MainActivity extends Activity
{
private Button mButton ;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mButton = (Button) findViewById(R.id.id_btn);
mButton.setOnTouchListener(new OnTouchListener()
{
@Override
public boolean onTouch(View v, MotionEvent event)
{
int action = event.getAction();
switch (action)
{
case MotionEvent.ACTION_DOWN:
Log.e(“w”, "onTouch ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(“w”, "onTouch ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(“w”, "onTouch ACTION_UP");
break;
default:
break;
}
return false;
}
});
}
}
我們?cè)贛yButton設(shè)置了OnTouchListener監(jiān)聽(tīng)杀糯,
我們看一下Log的打印
標(biāo)志 | 消息 |
---|---|
E/MyButton(879): | dispatchTouchEvent ACTION_DOWN |
E/MyButton(879): | onTouch ACTION_DOWN |
E/MyButton(879): | onTouchEvent ACTION_DOWN |
E/MyButton(879): | dispatchTouchEvent ACTION_MOVE |
E/MyButton(879): | onTouch ACTION_MOVE |
E/MyButton(879): | onTouchEvent ACTION_MOVE |
E/MyButton(879): | dispatchTouchEvent ACTION_UP |
E/MyButton(879): | onTouch ACTION_UP |
E/MyButton(879): | onTouchEvent ACTION_UP` |
按照上面的簡(jiǎn)單實(shí)例我們可以簡(jiǎn)單得出一個(gè)結(jié)論:View的事件分發(fā)無(wú)論DOWN济竹、MOVE、UP都會(huì)經(jīng)過(guò)dispatchTouchEvent
、onTouch
(如果設(shè)置的話)盈简、onTouchEvent
源碼解讀:
Step1 View
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
...
//重點(diǎn)判斷onTouch
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//重點(diǎn)判斷onTouchEvent
if (!result && onTouchEvent(event)) {
result = true;
}
...
return result;
}
先判斷mOnTouchListener是否為空艰额?改View是否為Enable陶因?onTouch是否返回true堰汉?若三個(gè)同時(shí)成立,返回true,且onTouchEvent不會(huì)執(zhí)行费什。
mOnTouchListener從哪里來(lái)呢钾恢?
public void setOnTouchListener(OnTouchListener l) {
mOnTouchListener = l;
}
可以看到這就是栗子中Activity.java里mButton.setOnTouchListener
設(shè)置的。
如果我們?cè)O(shè)置了onTouchListener鸳址,且設(shè)置返回為true瘩蚪,那么View的onTouchEvent就不會(huì)執(zhí)行!
Step2 View
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
//情況1:如果view為disenable且可點(diǎn)擊,返回true
//此情況還是會(huì)消費(fèi)此觸摸事件稿黍,只是不做反應(yīng)罷了
if ((viewFlags & ENABLED_MASK) == DISABLED) {
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
...
//情況2:如果View為enable且可點(diǎn)擊疹瘦,返回ture
//大部分的觸摸操縱都在這里面
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
//這是Part02
break;
case MotionEvent.ACTION_DOWN:
//這是Part01
break;
case MotionEvent.ACTION_CANCEL:
//這是Part04
break;
case MotionEvent.ACTION_MOVE:
//這是Part03
break;
}
return true;
}
//情況3:如果View為enable但不能點(diǎn)擊,直接返回false
return false;
}
在onTouchEvent工有3個(gè)主要情況:
- 情況1:如果view為disEnable且clickable,返回true巡球。此情況還是會(huì)消費(fèi)此觸摸事件言沐,只是不做反應(yīng)
- 情況2:如果View為enable且clickable,返回ture酣栈。大部分的觸摸操縱都在這里面
- 情況3:如果View為enable但unClickable险胰,直接返回false。其實(shí)就是View為unClickable矿筝,基本就是返回false了起便。
view的enable和clickable都可以在java和xml設(shè)置。
以上代碼可以看出窖维,onToucheEvent里的重點(diǎn)操作都在switch里了榆综,這里我們分幾個(gè)步驟進(jìn)行分析
Part01 ACTION_DOWN
case MotionEvent.ACTION_DOWN:
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPrivateFlags |= PREPRESSED;
mHasPerformedLongPress = false;
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
break;
- 初始化CheckForTap,此類為Runnable
- 給mPrivateFlags設(shè)置一個(gè)PREPRESSED的標(biāo)識(shí)
- 設(shè)置mHasPerformedLongPress=false;表示長(zhǎng)按事件還未觸發(fā)铸史;
- 發(fā)送一個(gè)延遲為ViewConfiguration.getTapTimeout()=115的延遲消息鼻疮,到達(dá)延時(shí)時(shí)間后會(huì)執(zhí)行CheckForTap()里面的run方法:
CheckForTap
private final class CheckForTap implements Runnable {
public void run() {
mPrivateFlags &= ~PREPRESSED;
mPrivateFlags |= PRESSED;
refreshDrawableState();
//檢測(cè)長(zhǎng)按
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
postCheckForLongClick(ViewConfiguration.getTapTimeout());
}
}
}
- 取消mPrivateFlags的PREPRESSED
- 設(shè)置PRESSED標(biāo)識(shí)
- 刷新背景
- 如果View支持長(zhǎng)按事件,則再發(fā)一個(gè)延時(shí)消息琳轿,檢測(cè)長(zhǎng)按陋守;
具體如何檢測(cè)長(zhǎng)按呢?
private void postCheckForLongClick(int delayOffset) {
mHasPerformedLongPress = false;
if (mPendingCheckForLongPress == null) {
mPendingCheckForLongPress = new CheckForLongPress();
}
mPendingCheckForLongPress.rememberWindowAttachCount();
postDelayed(mPendingCheckForLongPress,
ViewConfiguration.getLongPressTimeout() - delayOffset);
}
- 初始化CheckForLongPress利赋,此類為Runnable
- 發(fā)送一個(gè)延遲為ViewConfiguration.getLongPressTimeout() - delayOffset=(500-115=385)的延遲消息,到達(dá)延時(shí)時(shí)間后會(huì)執(zhí)行CheckForLongPress()里面的run方法:
CheckForLongPress
class CheckForLongPress implements Runnable {
private int mOriginalWindowAttachCount;
public void run() {
if (isPressed() && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
if (performLongClick()) {
mHasPerformedLongPress = true;
}
}
}
經(jīng)過(guò)一系列判斷猩系,最終調(diào)用performLongClick()即長(zhǎng)按的接口調(diào)用媚送。
這里我們可以得出一個(gè)小結(jié)論:
當(dāng)用戶點(diǎn)擊視圖時(shí),超過(guò)500ms后且設(shè)置了長(zhǎng)按監(jiān)聽(tīng)的話寇甸,會(huì)觸發(fā)長(zhǎng)按監(jiān)聽(tīng)接口塘偎!
Wonder疑問(wèn)
- 那當(dāng)用戶在500ms內(nèi)將手抬起會(huì)是什么情況呢疗涉?
- LongClick已經(jīng)有了,那我們平時(shí)使用的的Click呢吟秩?
Part02 ACTION_UP
case MotionEvent.ACTION_UP:
//判斷mPrivateFlags是否包含PREPRESSED
boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
//如果包含PRESSED或者PREPRESSED則進(jìn)入執(zhí)行體咱扣,在115ms的前后抬起都會(huì)進(jìn)入執(zhí)行體。
if ((mPrivateFlags & PRESSED) != 0 || prepressed) {
//如果該視圖還未獲取焦點(diǎn)涵防,則獲之
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
//判斷是否為長(zhǎng)按狀態(tài)闹伪,不是的話,進(jìn)入執(zhí)行體
if (!mHasPerformedLongPress) {
//這是一個(gè)輕點(diǎn)擊操作壮池,所以要移除長(zhǎng)按檢測(cè)操作
removeLongPressCallback();
//只有在按下?tīng)顟B(tài)時(shí)才執(zhí)行點(diǎn)擊動(dòng)作
if (!focusTaken) {
//使用Runnable進(jìn)行發(fā)送消息偏瓤,而不是直接執(zhí)行performClick。
//這樣視圖可以在點(diǎn)擊操作前更新其可視化狀態(tài)
if (mPerformClick == null) {
mPerformClick = new PerformClick();//*重點(diǎn)01*
}
//重點(diǎn) * 調(diào)用平時(shí)用的click
if (!post(mPerformClick)) {
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();//*重點(diǎn)02*
}
//根據(jù)視圖的mPrivateFlags的狀態(tài)進(jìn)行操作
if (prepressed) {
mPrivateFlags |= PRESSED;
refreshDrawableState();
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
//移除點(diǎn)擊事件的檢測(cè)操作
removeTapCallback();
}
break;
mPrivateFlags的狀態(tài):125ms前為prepressed(點(diǎn)擊前),125ms后位pressed(點(diǎn)擊后)椰憋。以上代碼已經(jīng)做了注釋厅克。這些操作就是500ms內(nèi)的點(diǎn)擊操作處理。
以上有兩個(gè)比較重要的點(diǎn)橙依,這里分析一下:
PerformClick
private final class PerformClick implements Runnable {
@Override
public void run() {
performClick();
}
}
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);//<----------這里
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}
//設(shè)置點(diǎn)擊事件回調(diào)
public void setOnClickListener(OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
以上代碼可以很清楚的看到onClick的調(diào)用证舟!
UnsetPressedState
private final class UnsetPressedState implements Runnable {
public void run() {
setPressed(false);
}
}
public void setPressed(boolean pressed) {
if (pressed) {
mPrivateFlags |= PRESSED;
} else {
mPrivateFlags &= ~PRESSED;
}
refreshDrawableState();
dispatchSetPressed(pressed);
}
我們可以看到無(wú)論如何這個(gè)Runnable都會(huì)執(zhí)行,只是對(duì)不同的狀態(tài)(prePressed,pressed)進(jìn)行處理窗骑。修改mPrivateFlags的狀態(tài)女责,刷新背景,分發(fā)SetPress等慧域。
這里我們可以得出一個(gè)小結(jié)論:
當(dāng)用戶點(diǎn)擊視圖時(shí)鲤竹,低于500ms設(shè)置onClick的接口,就會(huì)觸發(fā)onClick的接口昔榴。且這個(gè)過(guò)程是在OnTouchEvent的ACTION_UP完成辛藻。
Part03 ACTION_MOVE
case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();
//判斷該觸摸事件是否已經(jīng)移出控件外
int slop = mTouchSlop;
if ((x < 0 - slop) || (x >= getWidth() + slop) ||
(y < 0 - slop) || (y >= getHeight() + slop)) {
//當(dāng)觸摸移出當(dāng)前視圖
//移除點(diǎn)擊回調(diào)
removeTapCallback();
if ((mPrivateFlags & PRESSED) != 0) {
//移除長(zhǎng)按檢測(cè)
removeLongPressCallback();
//mPrivateFlags去除PRESSED標(biāo)志
mPrivateFlags &= ~PRESSED;
//刷新背景
refreshDrawableState();
}
}
break;
ACTION_MOVE的工作相對(duì)簡(jiǎn)單一點(diǎn):不斷的記錄x,y。判斷當(dāng)前觸摸事件是否已經(jīng)移除當(dāng)前控件之外互订?如果移除了吱肌,移除相對(duì)應(yīng)的檢測(cè)回調(diào),以及刷新相對(duì)應(yīng)的變量和背景仰禽。
Part04 ACTION_CANCLE
case MotionEvent.ACTION_CANCEL:
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
removeTapCallback();
break;
ACTION_CANCEL的工作主要是:刷新相對(duì)應(yīng)的變量和背景氮墨,移除響度應(yīng)的檢測(cè)回調(diào)。一般遇到的比較少吐葵。有一種情況是:當(dāng)用戶保持按下操作规揪,并從你的控件轉(zhuǎn)移到外層控件時(shí),會(huì)觸發(fā)ACTION_CANCEL温峭。
總結(jié) Summary
- View的事件轉(zhuǎn)發(fā)流程為:View.dispatchEvent->View.setOnTouchListener->View.onTouchEvent
- 在dispatchTouchEvent中會(huì)進(jìn)行OnTouchListener的判斷猛铅,如果OnTouchListener不為null且返回true,則表示事件被消費(fèi)凤藏,onTouchEvent不會(huì)被執(zhí)行奸忽;否則執(zhí)行onTouchEvent堕伪。
- 長(zhǎng)按點(diǎn)擊的回調(diào)是在ACTION_DOWN調(diào)用的。
- 輕按點(diǎn)擊的回調(diào)是在ACTION_UP調(diào)用的栗菜。
- 判斷觸摸事件是否移除了當(dāng)前控件是在ACTION_MOVE監(jiān)聽(tīng)的欠雌。
額外 Extra
我們?cè)趤?lái)舉一個(gè)栗子:
public class MainActivity extends Activity
{
private Button mButton ;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mButton = (Button) findViewById(R.id.id_btn);
mButton.setOnClickListener(new OnClickListener()
{
@Override
public void onClick(View v)
{
Log.e("e", "輕觸點(diǎn)擊");
}
});
mButton.setOnLongClickListener(new OnLongClickListener()
{
@Override
public boolean onLongClick(View v)
{
Log.e("e", "長(zhǎng)按點(diǎn)擊");
return false;
}
});
}
}
如果onLongClick返回的是ture(表示消費(fèi)了),則onClick無(wú)法觸發(fā)。如果返回false,就可以疙筹。大家可以結(jié)合下上面的代碼解析看看為什么會(huì)這樣~
Android觸摸事件分發(fā)機(jī)制(2)之ViewGroup
PS:本文
整理
自以下文章富俄,若有發(fā)現(xiàn)問(wèn)題請(qǐng)致郵 caoyanglee92@gmail.com
工匠若水 Android觸摸屏事件派發(fā)機(jī)制詳解與源碼分析一(View篇)
Hohohong Android View 事件分發(fā)機(jī)制 源碼解析 (上)