Android事件分发笔记

前言

以前在学习Android的时候接触过一次Android事件分发,时间久远就忘记了,前段时间项目碰到一个需求,一个ListviewItem里面嵌套了一个Edittext,然后碰到了各种各样的问题,最后还是用事件分发的知识处理了,记录是最好的学习方式,所以就写了一篇关于时间分发的笔记,以备过后忘记的时候可以看看。

1.布局没有别的控件,只有一个activity的时候

<span style="font-size:14px;">public class Activity140 extends Activity{
private static final String TAG = "print";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_do);
}

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
                Log.d(TAG, "Activity-->dispatchTouchEvent-->ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
                Log.d(TAG, "Activity-->dispatchTouchEvent-->ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
                Log.d(TAG, "Activity-->dispatchTouchEvent-->ACTION_UP");
break;
        }
return super.dispatchTouchEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
                Log.d(TAG, "Activity-->onTouchEvent-->ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
                Log.d(TAG, "Activity-->onTouchEvent-->ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
                Log.d(TAG, "Activity-->onTouchEvent-->ACTION_UP");
break;
        }
return super.onTouchEvent(event);
}
}</span>


Log如下:


可以看到,事件是从dispatchTouchEvent开始,一个完整的手势是从ACTION_DOWN开始,ACTION_UP结束,不难理解,dispatch英文就是分发的意思,返回super的源码为:

<span style="font-size:14px;">/**
 * Called to process touch screen events.  You can override this to
 * intercept all touch screen events before they are dispatched to the
 * window.  Be sure to call this implementation for touch screen events
 * that should be handled normally.
 *
 * @param ev The touch screen event.
 *
 * @return boolean Return true if this event was consumed.
 */
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
    }
return onTouchEvent(ev);
}</span>

其中onUserInteraction();是一个空函数,然后下面有个,下面这个方法分析起来很复杂,涉及很多东西,如果有兴趣可以研究一下,我就直 结论 接说好了,如果这个顶层的window下的子view消耗了这个事件,就直接返回true,不然就把事件传递给Activity的onTouchevent去处理,所以这也就是为什么当只有一个Activity时dispatchTouchEvent返回super时是默认往本类的onTouchEvent里传的原因

1.2.把Activity的dispatchTouchEvent返回值改成true

log为:

从log可以看出,事件并没有被分发,而是由dispatchTouchEvent进行消费。

1.3把Activity的dispatchTouchEvent返回值改成false

log为:

从log可以看出事件也没有进行分发,似乎感觉返回true和false都是一样的


2.增加一个子控件到布局中:

public class CustomBtnView extends Button {
private static final String TAG = "print";
public CustomBtnView(Context context) {
super(context);
    }

public CustomBtnView(Context context, AttributeSet attrs) {
super(context, attrs);
    }

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
                Log.d(TAG, "Button-->dispatchTouchEvent-->ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
                Log.d(TAG, "Button-->dispatchTouchEvent-->ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
                Log.d(TAG, "Button-->dispatchTouchEvent-->ACTION_UP");
break;
        }
return super.dispatchTouchEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
                Log.d(TAG, "Button-->onTouchEvent-->ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
                Log.d(TAG, "Button-->onTouchEvent-->ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
                Log.d(TAG, "Button-->onTouchEvent-->ACTION_UP");
break;
        }
return super.onTouchEvent(event);
}
}

log为:

从log可以看出,Activity的dispatchTouchEvent最先接到用户的点击事件,先接到的是按下这个动作,然后事件并没有由Activity的onTouchEvent进行处理而是往下传递到了Button的dispatchTouchEvent,然后在让button的onTouchEvent对这个点击事件进行处理

2.1.把Button的dispatchTouchEvent返回值改为false


log为:

从log可以看出,down事件传递到button的dispatchTouchEvent方法时,返回为false的情况下,down事件并没有被传递给button的onTouchEvent方法,而是向上传递给Activity的onTouchEvent方法,随后的UP事件就不会传递到Button而是直接交由Activity的onTouchEvent方法进行消费,这也比较符合我们平常的逻辑。

2.2.把Button的dispatchTouchEvent返回值改为true


log为:

从log可以看出,事件传递到Button的dispatchTouchEvent方法时并没有往下传了,它自己消费了这个点击事件

2.3.把Button的onTouchEvent返回值改成false

log为:

从log可以看出,Down事件传递到Button的onTouchEvent方法时由于返回值false,所以事件又被传递到Activity的onTouchEvent方法中进行处理,接下来的Up事件直接交由Activity的onTouchEvent处理

3.用一个LinearLayout嵌套Button

public class CustomBtnLinearLayout extends LinearLayout {
private static final String TAG = "print";
public CustomBtnLinearLayout(Context context) {
super(context);
    }

public CustomBtnLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
    }

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
                Log.d(TAG, "LinearLayout-->dispatchTouchEvent-->ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
                Log.d(TAG, "LinearLayout-->dispatchTouchEvent-->ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
                Log.d(TAG, "LinearLayout-->dispatchTouchEvent-->ACTION_UP");
break;
        }
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
                Log.d(TAG, "LinearLayout-->onTouchEvent-->ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
                Log.d(TAG, "LinearLayout-->onTouchEvent-->ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
                Log.d(TAG, "LinearLayout-->onTouchEvent-->ACTION_UP");
break;
        }
return super.onTouchEvent(event);
}
}

布局为:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

    <com.example.sjr.all.customview.CustomBtnLinearLayout
android:layout_width="400dp"
android:layout_height="400dp"
android:background="#9c6d6d">

        <com.example.sjr.all.customview.CustomBtnView
android:id="@+id/btn_cus"
android:layout_width="300dp"
android:layout_height="300dp"
android:text="点我" />
    </com.example.sjr.all.customview.CustomBtnLinearLayout>
</LinearLayout>

log为:

从log可以看出,事件是从Activity的dispatchTouchEvent传递给LinearLayout的dispatchTouchEvent然后不是传递给LinearLayout的onTouchEvent而是往下传递给Button的dispatchTouchEvent然后由Button的onTouchEvent对事件进行处理

3.1.把Button的onTouchEvent方法返回值改成false

log为:

从log可以看出,DOWN事件往上传递由Button的父控件LinearLayout的onTouchEvent方法进行处理,此时onTouchEvent没有重写,返回的是super,所以事件又往上传递到LinearLayout的父窗体也就是Activity的onTouchEvent进行处理,之后的Up事件就直接由Activity的dispatchTouchEvent传递本类的onTouchEvent方法进行处理
然后我们把LinearLayout的onTouchevent的返回值也改成false,log为:

从log可以看出, 事件同super一样也是往上进行传递
super.onTOuchEvent的源码为
/**
 * Implement this method to handle touch screen motion events.
 * <p>
 * If this method is used to detect click actions, it is recommended that
 * the actions be performed by implementing and calling
 * {@link #performClick()}. This will ensure consistent system behavior,
 * including:
 * <ul>
 * <li>obeying click sound preferences
 * <li>dispatching OnClickListener calls
 * <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
 * accessibility features are enabled
 * </ul>
 *
 * @param event The motion event.
 * @return True if the event was handled, false otherwise.
 */
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();

if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
// 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)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
    }

if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
        }
    }

if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
            (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_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.
setPressed(true, x, y);
                   }

if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// 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) {
                        postDelayed(mUnsetPressedState,
                                ViewConfiguration.getPressedStateDuration());
                    } else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
                    }

                    removeTapCallback();
                }
mIgnoreNextUpEvent = false;
break;

case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;

if (performButtonActionOnTouchDown(event)) {
break;
                }

// 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 |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
                    }
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                } else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
                    checkForLongClick(0);
                }
break;

case MotionEvent.ACTION_CANCEL:
                setPressed(false);
                removeTapCallback();
                removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;

case MotionEvent.ACTION_MOVE:
                drawableHotspotChanged(x, y);

// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();

                        setPressed(false);
                    }
                }
break;
        }

return true;
    }

return false;
}

截取源码分析:
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
// 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)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
    }

如果view被禁用了,虽然对事件不做响应,但还是要消费这个事件
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
        }
    }
如果这个view有一个代理,事件就交由这个代理去处理,如果代理消费了这个事件,则返回true,view不用处理这个事件
if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
            (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
....
如果view不可点击就返回false,不处理这个事件,不然就是一系列处理DOWN、MOVE、UP、CANCEL事件的逻辑.篇幅有限就不详细解释,如果想深入了解得可以自己研究一下

3.2把Button的dispatchTouchEvent改成false

log为:

跟上面有点类似Dow事件传递到Button的dispatchTOuchEvent时没有传递到本类的onTouchEvent而是传递给父控件的onTouchEvent再传递到最顶层Activity的onTouchEvent进行处理


3.3把LinearLayout的dispatchTouchEvent改成false


log为:

从log可以看出DOWN事件从Activity的dispatchTouchEvent传递到了LInearLayout的dispatchTouchEvent然后又传递到Activity的onTouchEvent方法并由这个方法对Down事件进行处理

3.4在LinearLayout中添加onInterceptTouchEvent方法

代码为:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
                Log.d(TAG, "LinearLayout--onInterceptTouchEvent--ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
                Log.d(TAG, "LinearLayout--onInterceptTouchEvent--ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
                Log.d(TAG, "LinearLayout--onInterceptTouchEvent--ACTION_UP");
break;
        }
return super.onInterceptTouchEvent(ev);
}

log为:

从log可以看出,事件传递到LinearLayout的dispatchTouchEvent方法的时候在接着传递到onIterceptTouchEvent方法,然后传递到Buton的dispatchonTouchEvent最后交由Button的onTouchEvent进行处理,即默认是不拦截,super.onTerceptTouchEvent方法的源码为
/**
 * Implement this method to intercept all touch screen motion events.  This
 * allows you to watch events as they are dispatched to your children, and
 * take ownership of the current gesture at any point.
 *
 * <p>Using this function takes some care, as it has a fairly complicated
 * interaction with {@link View#onTouchEvent(MotionEvent)
 * View.onTouchEvent(MotionEvent)}, and using it requires implementing
 * that method as well as this one in the correct way.  Events will be
 * received in the following order:
 *
 * <ol>
 * <li> You will receive the down event here.
 * <li> The down event will be handled either by a child of this view
 * group, or given to your own onTouchEvent() method to handle; this means
 * you should implement onTouchEvent() to return true, so you will
 * continue to see the rest of the gesture (instead of looking for
 * a parent view to handle it).  Also, by returning true from
 * onTouchEvent(), you will not receive any following
 * events in onInterceptTouchEvent() and all touch processing must
 * happen in onTouchEvent() like normal.
 * <li> For as long as you return false from this function, each following
 * event (up to and including the final up) will be delivered first here
 * and then to the target's onTouchEvent().
 * <li> If you return true from here, you will not receive any
 * following events: the target view will receive the same event but
 * with the action {@link MotionEvent#ACTION_CANCEL}, and all further
 * events will be delivered to your onTouchEvent() method and no longer
 * appear here.
 * </ol>
 *
 * @param ev The motion event being dispatched down the hierarchy.
 * @return Return true to steal motion events from the children and have
 * them dispatched to this ViewGroup through onTouchEvent().
 * The current target will receive an ACTION_CANCEL event, and no further
 * messages will be delivered here.
 */
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}

3.5.把onTerceptTouchEvent的返回值改成true

log为:

从log可以看出,DOWN事件被拦截了,然后不会把事件往下传递给button而是传递给本类的onTouchEvent再传递到Activity的onTouchEvent进行处理,而后的UP事件直接由Activity的onTouchEvent进行处理

3.6给Button添加OnClickListener监听


log为:

从log可以看出,onClick是最后才执行的,也就是完成了这一个完整的手势处理以后最后才会响应这个监听
OnclickListener的源码为
/**
 * Interface definition for a callback to be invoked when a view is clicked.
 */
public interface OnClickListener {
/**
     * Called when a view has been clicked.
     *
     * @param v The view that was clicked.
     */
void onClick(View v);
}

从注释可以看出,这个方法是在view已经被点击了才会被回调,所以onClick最后执行,如果你想深入研究Android是怎么让这个接口在一个完整手势之后才会被触发可以研究一下view类的源码,这里不再累赘..

3.7给Button添加OnTouchListener监听且返回值为false


log为:

从log可以看出,当事件传递到Button的dispatchTouchEvent时,会调用oTouche方法,由于返回了false所以事件接着由Button的onTouchEvent进行消费。
OnTouchListener的源码为:
/**
 * Interface definition for a callback to be invoked when a touch event is
 * dispatched to this view. The callback will be invoked before the touch
 * event is given to the view.
 */
public interface OnTouchListener {
/**
     * Called when a touch event is dispatched to a view. This allows listeners to
     * get a chance to respond before the target view.
     *
     * @param v The view the touch event has been dispatched to.
     * @param event The MotionEvent object containing full information about
     *        the event.
     * @return True if the listener has consumed the event, false otherwise.
     */
boolean onTouch(View v, MotionEvent event);
}

可以看到,onTouch被触发在事件分发到一个view之后,然后可以在view响应事件前对这个事件进行响应,所以当onTouch方法返回值改为true时
就可以把事件交由自己处理而不往下传,log也验证了我们的猜测

由于事件在view响应前已经被响应,所以最后的onClick方法也不会被触发.

3.8给Button添加OnLongClickListener,且返回值为false

当长按时log为:

可以看到当一次长按事件的DOWN结束时会回调这个方法,因为返回false而长按也算一次点击事件,所以最后还是回调了onClick方法

3.9给Button添加OnLongClickListener,且返回值为true

当长按时,log为:

OnLongClickListener源码为:
/**
 * Interface definition for a callback to be invoked when a view has been clicked and held.
 */
public interface OnLongClickListener {
/**
     * Called when a view has been clicked and held.
     *
     * @param v The view that was clicked and held.
     *
     * @return true if the callback consumed the long click, false otherwise.
     */
boolean onLongClick(View v);
}
可以看到这是在view已经被点击而且保持这个状态时会回调这个方法。

总结

Android事件分发可以总结为这句话,隧道式传递,冒泡式处理,即事件从最顶层的view传递到最底层的view, 当最底层不处理时依次往上一层进行处理,当不进行处理时,即默认super时,事件传递时默认是往下传也就是不进行消费,而底层view不处理时默认往上也是不进行消费。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值