感谢 Android 开发艺术探索
点击事件的传递
Android 事件分发的三个方法: dispatchTouchEvent, onInterceptTouchEvent, onTouchEvent
dispatchTouchEvent
用来进行事件的分发, 如果事件能够传递到当前 view , 那么该方法一定会被调用, 返回结果受当前 View 的 onTouchEvent 和下级 view 的 dispatchTouchEvent 影响 , 表示是否消耗当前事件。
onInterceptTouchEvent
在 dispatchTouchEvent 方法内部调用, 用来判断是否拦截某个事件,如果当前 View 拦截了某个事件,那么在同一个事件序列当中, 此方法不会被调用, 返回结果表示是否拦截该事件。
onTouchEvent
也是在 dispatchTouchEvent 方法中调用,用来处理点击事件, 如果不消耗那么在当前 view 无法再接收到事件
点击事件传递规则:
对于一个根 ViewGroup 来说, 当一个点击事件产生后, 首先会传递给它的 dispatchTouchEvent 来分发, 接着会调用它的 onInterceptTouchEvent 判断是否拦截这个事件, 如果拦截该事件, 那么当前事件将又这个 ViewGroup 自己处理, 如果 onInterceptTouchEvent 返回 false, 那么将交由它的子 View 的 dispatchTouchEvent 方法去处理,注意, View 中并不包含 onInterceptTouchEvent 方法, 如果传递到了 view, 那么它的 onTouchEvent 方法将会被调用 ,如此反复。
当一个 View 需要处理事件时, 如果设置了 onTouchListener, 那么 onTouchListener 中的 onTouch 方法会被调用, 此时如果 onTouch 方法返回 false, 那么它的 onTouchEvent 将会被调用, 如果返回 true , onTouchEvent 方法将不会被调用,如果 view 设置了 onClickListener 那么它的 onClick 事件将会在 onTouchEvent 的 MotionEvent.ACTION_UP 里调用。 如果 onTouchEvent 里返回了 false, 那么它的父类的 onTouchEvent 将会被调用, 因为它自己处理不了, Android开发艺术探索中描述了一个例子:假如有一个难题领导交给了一个程序员去处理, 结果这个程序员搞不定(onTouchEvent 返回false), 那么这个难题将返回给更高级的上级去处理。
关于事件机制有这样一些结论(后面分析源码会更清晰)
- 同一个事件从手指接触屏幕的那一刻起, 到手指抬起,事件从 down 事件开始, 经历多个 move 事件, 最终 up 事件结束
- 正常情况下, 一个事件序列只能被一个 view 消耗(要么是子 view, 要么是父 view)
- 某一个 view 一旦决定拦截事件, 那么这个事件序列都将由它来处理
- 某个 view 一旦开始处理事件,如果它不消耗 ACTION_DOWN 事件,那么同一事件序列中的其他事件都不会交给它来处理(后续分下源码理解这个), 并且事件将重新交由它的父元素去处理, 即父元素的 onTouchEvent 会被调用, 意思就是事件一旦交给一个 view 去处理, 那么它就必须消耗掉, 否则同一个事件序列中剩下的事件就不再交给它来处理了
- 如果 view 不消耗 除 ACTION_DOWN 以外的其他事件, 那么这个点击事件会消失, 此时父元素的 onTouchEvent 并不会被调用, 并且当前 view 可以持续的收到后续的事件, 最终这些消失的点击事件将会传递给 Activity 处理
- ViewGroup 默认不拦截任何事件
- View 中没有 onInterceptTouchEvent 方法, 一旦有点击事件传递给它, 那么它的 onTouchEvent 会被调用
- View 的 onTouchEvent 会默认消耗掉事件,除非它是不可点击的(clickable 和 : 都是false),View 的 longClickable 默认都为 false, clickable 要分情况, 必须 Button 的 clickable 属性默认为 true, TextView 的 clickable 属性默认为 false。
- View 的 enable 属性不影响 onTouchEvent 的默认返回值, 哪怕一个 View 是 disable 状态的, 只要它的 clickable 或者 longClickable 有一个是true, 那么它的 onTouchEvent 就会返回true。
- Onclick 会发生的前提是 View 是可点击的, 并且收到了 down 和 up 事件
事件传递过程是由外向内的, 即事件总是先传递到父元素,然后由父元素分发给子 view, 通过 requestDisallowInterceptTouchEvent 方法可以在子元素中干预父元素的事件分发过程, 但是 ACTION_DOWN 事件除外。
源码解析
点击事件用 MotionEvent 表示, 事件最开始由 Activity 开始传递, 看下面代码, 刚开始有 Activity 的 diapatchTouchEvent 开始分发,由 Activity 传递给 Window, Window 传递给 DecorView , DecorView 其实是 setContentView 的 view 的父容器。
/**
* 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);
}
看上面的代码, 首先会交给 Window 来分发, 如果返回 true 那么整个循环就结束了, 如果返回了 false, 那么意味着子 view 都没有处理这个事件, 那么 Activity 会调用自己的 onTouchEvent 自己进行处理。
接下来 Window 会将事件分发到 ViewGroup:
/**
* Used by custom windows, such as Dialog, to pass the touch screen event
* further down the view hierarchy. Application developers should
* not need to implement or call this.
*
*/
public abstract boolean superDispatchTouchEvent(MotionEvent event);
通过查看 window 的 superDispatchTouchEvent 方法发现它是个抽象方法, 那么必定有它的实现类, 从 window 的源码可知,它的实现类是 PhoneWindow
/**
* Abstract base class for a top-level window look and behavior policy. An
* instance of this class should be used as the top-level view added to the
* window manager. It provides standard UI policies such as a background, title
* area, default key processing, etc.
*
* <p>The only existing implementation of this abstract class is
* android.view.PhoneWindow, which you should instantiate when needing a
* Window.
*/
那么查看一下 PhoneWindow 是怎么进行分发的,
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
很明显这里传递给了 DecorView 去继续分发,查看 DecorView 源码可知
public class DecorView extends FrameLayout
DecorView 继承了 FrameLayout,是一个 ViewGroup , 这就清楚了, 那么 DecorView 的子 view 不就是 setContentView 里面的 view 么, 这个 view 一般也都是一个 ViewGroup, 这样才能在上面添加各种布局啊。
ViewGroup 怎么进行事件传递上面已经说了, 那么看一下它的 diapatchTouchEvnet 方法的拦截情况,
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
从源码中可以发现, ViewGroup 会在2种情况下会判断是否拦截当前事件, 事件类型为 actionMasked ==MotionEvent.ACTION_DOWN || mFirstTouchTarget != null, 那么这个 mFirstTouchTarget 是什么, 后面代码会详细分析, 当事件由 ViewGroup 的子元素成功处理时, mFirstTouchTarget 会被赋值并指向子元素, 换句话说, 当 ViewGroup 不拦截事件把事件交给子元素处理时 mFirstTouchTarget != null , 反过来, 一旦事件交由 ViewGroup 拦截, mFirstTouchTarget != null 就不成立, 那么当 ViewGroup 的 move 和 up 事件到来的时候, actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null 判断为 false , 那么这个事件序列中的其他事件也将交给它处理。
当然从源码中发现还有一个标志位可以影响 ViewGroup 的拦截, 那就是 FLAG_DISALLOW_INTERCEPT 这个标志位,
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
从源码中可以看出, requestDisallowInterceptTouchEvent 方法可以改变 mGroupFlags 这个标记位,
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
如果子 view 通过这个方法改变了标记位 , 那么将影响 ViewGroup 的拦截, 当然拦截的事件是除了 ACTION_DOWN 事件以外的, 因为如果是 ACTION_DOWN,
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
从源码中发现, 会执行 resetTouchState(); 重置标记位, 因此子 view 调用改变标记位这个方法并不能影响 ViewGroup ACTION_DOWN 事件的执行, 从上面的分析可以得出结论, 当 ViewGroup 决定拦截事件后, 那么后续的点击事件会默认交给它处理并且不再调用它的 onInterceptTouchEvent 方法, 证实了上面的 3 结论,这个标记位的作用是让 ViewGroup 不拦截事件, 前提是 ViewGroup 不拦截 ACTION_DOWN, 证实了 11 结论 ,这段分析可以总结以下几点:
- onInterceptTouchEvent 不会每次都会被调用, 如果要想处理所有点击事件, 那么要选择 dispatchTouchEvent 方法
既然这个标记位可以影响 ViewGroup 是否拦截事件, 那么我们是否可以用这个标记位来处理滑动冲突
如果 ViewGroup 不拦截事件, 那么事件将继续向下传递给它的子 View,主要源码如下,
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
首先遍历所有子 View, 然后判断子 view 是否能够接收点击事件, 主要看:
if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
/**
* Returns true if a child view can receive pointer events.
* @hide
*/
private static boolean canViewReceivePointerEvents(@NonNull View child) {
return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null;
}
/**
* Returns true if a child view contains the specified point when transformed
* into its coordinate space.
* Child must not be null.
* @hide
*/
protected boolean isTransformedTouchPointInView(float x, float y, View child,
PointF outLocalPoint) {
final float[] point = getTempPoint();
point[0] = x;
point[1] = y;
transformPointToViewLocal(point, child);
final boolean isInView = child.pointInView(point[0], point[1]);
if (isInView && outLocalPoint != null) {
outLocalPoint.set(point[0], point[1]);
}
return isInView;
}
分别判断了子元素是否在执行动画和点击事件的坐标是否落在了子元素的区域内, 如果满足了这2个条件之一, 那么事件会传递给它处理, 可以发现 dispatchTransformedTouchEvent 方法实际调用了子元素的 dispatchTouchEvent 方法, 这样完成了一次事件分发,
/**
* Transforms a motion event into the coordinate space of a particular child view,
* filters out irrelevant pointer ids, and overrides its action if necessary.
* If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
*/
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
}
如果子元素的 dispatchTouchEvent 返回 true, 那么
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
newTouchTarget 被赋值, 终止对子元素的遍历,如果返回 false , 那么继续遍历其他子元素, 实际上看下面代码 mFirstTouchTarget 是在 addTouchTarget 被赋值的,
/**
* Adds a touch target for specified child to the beginning of the list.
* Assumes the target child is not already present.
*/
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
mFirstTouchTarget 是一种单链表结构, 它是否被赋值将直接影响 ViewGroup 的拦截策略, 如果 mFirstTouchTarget 是 null, intercepted 将是 true, 那么 ViewGroup 将会拦截事件序列中的所有点击事件, 如果遍历所有子元素后事件都没有被合适的处理, 那么会有2种情况, 一个是 ViewGroup 没有子元素, 还有一种情况是子元素虽然处理了事件, 但是还是在 dispatchTouchEvent 返回了 false , 在这俩种情况下 ViewGroup 都会自己处理点击事件, 证实了 4 结论, 代码如下:
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
}
此时 child 传入了 null, 那么 super.dispatchTouchEvent(event); 它将事件交由 View 类来处理。
View 对点击事件的处理
看 View 中的 dispatchTouchEvent 方法, 注意 这里的 View 不包含 ViewGroup 了, View 是一个单独的元素, 它没有子元素所以事件无法向下传递, 所以只能自己处理
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
首先会判断是否有 onTouchListener 如果有那么返回true, 那么看上面代码 onTouchEvent 就不会被调用, 所以 onTouchListener 优先级高于 onTouchEvent, 接着分析 onTouchEvent:
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
当 view 处于 DISABLED 状态时, 它仍然会消耗掉点击事件, 尽管它看起来不可用,但是并不代表它的 onclick 事件可以执行。
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
如果 view 设置了代理, 那么还会执行代理的 onTouchEvent。
完整 代码:
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();
}
if (!clickable) {
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
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:
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
}
mHasPerformedLongPress = false;
if (!clickable) {
checkForLongClick(0, x, y);
break;
}
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, x, y);
}
break;
case MotionEvent.ACTION_CANCEL:
if (clickable) {
setPressed(false);
}
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
break;
case MotionEvent.ACTION_MOVE:
if (clickable) {
drawableHotspotChanged(x, y);
}
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
// Remove any future long press/tap checks
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
break;
}
return true;
}
return false;
}
从上面的代码可以看出, 只要 CLICKABLE 或者 LONG_CLICKABLE 有一个是true , 那么就会消耗掉事件, 不管它是不是 DISABLED 的状态, 证实了上面得 8 , 9 , 10 结论, 那么当 ACTION_UP 发生的时候会调用 performClick , 如果设置了 onClickListener , 就会执行点击事件, 看 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);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
View 的 LONG_CLICKABLE 默认都为 false, 而 CLICKABLE 要分具体的 view, Button 的 CLICKABLE 是 true, TextView 默认为 false, 可以通过 setClickable 和 setLongClickable 来改变它们的属性,另外通过 setOnClickListener 或者通过 setOnLongClickListener 会将 setClickable 或者 setLongClickable 属性设置为 true。
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}