您的位置:首页 > 移动开发 > Android开发

Android View事件传递与源码分析

2017-08-06 17:07 351 查看

1、MotionEvent

当手指点击屏幕后会产生一系列事件:
ACTION_DOWN——手指首次触碰屏到幕
ACTION_MOVE——手指在屏幕上滑动
ACTION_UP——手指离开屏幕


一般情况下,手指从按下到离开屏幕会发生一系列事件:

DOWN->MOVE(MOVE事件可能有很多次,也可能没有,取决于有没有滑动和滑动距离)->UP。

Android系统这这些事件封装成MotionEvent类,在这个类里有点击事件类型、点击时间以及点击坐标等信息。

当我们手指首次按下时,会产生一个ACTION_DOWN类型的MotionEvent对象,当手指不断移动时,又会不断产生ACTION_MOVE类型的MotionEvent对象,当手指离开屏幕时,产生一个ACTION_UP类型的MotionEvent对象。这就是我们一次触摸屏幕将会生成的一系列事件,我们称为一个事件序列,以ACTION_DOWN开始,中间有数量不定的ACTION_MOVE,以ACTION_UP结束。

View分发传递的点击事件就是这些系统封装好的MotionEvent对象,并通过获取MotionEvent对象封装的信息对触摸事件进行处理。

2、事件传递

Android的UI结构如下图所示



当点击事件生成,首先拿到事件的是Activity,Activity又将事件传递给PhoneWindow,PhoneWindow将事件传递给DecorView。DecorView是顶级View,我们在Activity中使用setContentView()就是将布局放在DecorView下的ContentView,而View对事件的分发也是从DecorView开始的。

1、Activity和PhoneWindow的事件传递

Activity和PhoneWindow的事件传递比较简单,通过看几行源码就明白了

Activity#dispatchTouchEvent(MotionEvent ev)

public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}


PhoneWindow#superDispatchTouchEvent(MotionEvent event)
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}


当系统将事件传递给Activity后会调用Activity#dispatchTouchEvent(MotionEvent ev)对事件进行分发,getWindow().superDispatchTouchEvent(ev)将事件传递给PhoneWindow,并调用PhoneWindow的superDispatchTouchEvent(ev)将事件往下传递,这个传递过程层层嵌套,直到对底层的View,

如果没有任何View处理这个事件,则返回false,那么Activity的onTouchEvent()会被调用。也就是说,当最终没人处理某个事件时,Activity会处理该事件。

再看看PhoneWindow#superDispatchTouchEvent(MotionEvent event),其实它并没有做什么,单纯将事件传递非DecorView,PhoneWindow也没有处理事件的能力,即使DecorView包括它的下级View都没有处理事件,PhoneWindow也无法处理该事件,而是直接上传给Activity处理。

2、View事件传递的三个方法

前面我们介绍了Activity和PhoneWindow的事件传递,PhoneWindow将事件传递给DecorView,就开始了最重要也是最常用的事件传递过程,即事件在View的传递过程。

点击事件在View的传递由三个很重要的方法来共同完成:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent方法。

public boolean dispatchTouchEvent(MotionEvent ev)

用来进行事件的分发。如果事件能够传递给当前的View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEevent方法的影响,true表示消耗当前事件,false表示不消耗。

public boolean onInterceptTouchEvent(MotionEvent ev)

在dispatchTouchEvent方法中被调用,用来判断当前View是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。此方法只在ViewGroup中存在,View没有此方法,很好理解,作为最低层的View,已经没有下级View可以往下传递了,所以不必判断是否拦截。

public boolean onTouchEvent(MotionEvent event)

在dispatchTouchEvent中被调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接受到事件。

这三个方法的关系可以用如下伪代码表示

以下代码来自《Android开发艺术探究》
public boolean dispatchTouchEvent(MotionEvent event) {
boolean consume = false;
if( onInterceptToucnEvent(ev) ){
consume = onTouchEvent(ev)
}else{
consume = child.dispatchTouchEvent(ev)
}
return consume;
}


在dispatchTouchEvent中调用onInterceptTouchEvent判断是否拦截事件,若拦截,则调用当前View的onTouchEvent处理事件,onTouchEvent返回的值作为dispatchTouchEvent的返回值,表示是否消耗该事件;若不拦截,则调用子View的dispatchTouchEvent,将事件往下传递,如此递归将点击事件传递下去。

3、View事件传递过程

前面介绍了负责View事件传递的三个重要方法,这里将介绍这三个方法是怎样将一个点击事件层层传递的,这里单纯从业务流程描述,不涉及三个方法的源码分析。

先看下面这张图(网上找的)



在上图中,可以把每一个虚线框视为一个dispatchTouchEvent方法。

这张图有些不完整,在确定拦截事件后,会先判断是否有设置onTouchListener,有的话onTouch()方法会优先执行,关于onTouch、onTouchEvent和onClick这三个处理事件的方法在后面会详细介绍,这里先不管。

还有就是,图中onTouchEvent部分可能会有误导,让读者以为是先返回再处理事件,事实上处理事件就在onTouchEvent中实现,然后再返回,且返回值跟是否处理事件无关,只表示是否消耗事件,这里的处理事件指的是在onTouchEvent中进行的逻辑操作。例如我们可以在onTouchEvent中根据拿到的MotionEvent处理一些事务,也可以不处理事务,返回值可以返回false,也可以返回true,但要注意的是若是返回false,那么同一事件序列的其他事件都不会传递给此View,并且交给它的父元素去处理,即调用父元素的onTouchEvent。一般情况下,我们都需要一个完整的事件序列,包括down、move及up事件,因此,若想处理一个完整的事件序列,要返回true,表示消耗此事件。关于这点,后面也会详细解释。

这张图已经基本将View事件传递的过程表现清楚,但是有一些地方还是要说明一下。

我们知道,Android中所有的控件、继承ViewGroup的各种布局都继承自View,图中,顶级父View可视为DecorView,DecorView继承自FrameLayout,父View可视为任何一个layout布局,如LinearLayout等等,子View则是我们常用的控件如Button、TextView
4000
等,内部没有onInterceptTouchEvent方法。图中的三层结构基本上可以表示出绝大多数情况了,就算嵌套再多的布局,也一样,不过是多重复几次而已。

当PhoneWindow调用DecorView的dispatchTouchEvent(MotionEvent ev),便将点击事件MotionEvent传递给了DecorView并启动DecorView的事件传递过程,而DecorView的dispatchTouchEvent又会调用子View的dispatchTouchEvent,就这样递归调用,将事件传递下去。

从图中可以看到,当一个事件一直不被拦截直至传递到最下层的View时,View的dispatchTouchEvent会调用onTouchEvent方法(先假设没有注册onTouchListener),onTouchEvent的返回值就是此View dispatchTouchEvent的返回值,若是返回false表示不消耗此事件,那么此事件就要开始往上传递,上层View会调用自己的onTouchEvent,若是上层View的onTouchEvent也返回false,又继续往上传递,重复刚才步骤。若是都没有消耗此事件,那么此事件最终会上传到Activity。

以上便是View事件传递的过程,此外还有几个结论,也是View事件传递的关键细节。

1、当某个View决定拦截事件,那么这个事件序列内的所有事件都会交给此View处理,并且它的onInterceptTouchEvent不会再被调用。

2、某个View一旦开始处理时间,如果它不消耗ACTION_DOWN事件,即onTouchEvent返回false,那么后续同一事件序列的其他事件都不会交给它处理,并且事件重新交给父元素去处理,即父元素的onTouchEvent会被调用。

这样说有些抽象,举个栗子。一个序列事件中ACTION_DOWN首先被拿到,并开始传递,传递到中间的一个View时,被拦截了,也就停止了往下传递,因为这个序列事件是这个View想要的,这个序列事件找到归宿了,接着这个View的onTouchEvent被调用(还是先不考虑onTouchListener),但是在onTouchEvent中处理完这个ACTION_DOWN事件后若是返回false,那就糟糕了,因为,这个false告诉这个View的父元素:这个不是我想要的,我包括我的下级没人想要这个事件!这表示已经往下遍历一遍所有点击区域的View了,都没人消耗这个事件,就开始往上传递了,调用父元素的onTouchEvent,就是上面流程图往上传递的部分。接着往上传递的过程中,要么有View的onTouchEvent返回true,消耗这个ACTION_DOWN事件,然后这个事件序列后面的事件都将交由消耗事件的这个View处理;要么所有View的onTouchEvent返回false,那么这个ACTION_DOWN事件将传递到Activity中,然后同一事件序列的后续事件,则在传递到DecorView后直接被作为顶级View的DecorView拦截,若DecorView不消耗该事件,那么将传递回Activity。

3、enable属性不影响View对事件的拦截消耗,将一个View设置为disable状态,该拦截消耗的还是会拦截消耗。

4、如果View不消耗ACTION_DOWN以外的其他时间,这个View仍然会收到后续同一个事件序列的事件,但因为这个View不消耗这些事件,所以这些会在这个View中消失,而不是往上传递调用父元素的onTouchEvent。

4、View对点击事件分发过程源码分析

View对点击事件的分发主要是各个继承自ViewGroup的layout来进行的,而子View已经是UI框架的最底层了,不需要也无法往下分发了。View对事件的分发就在dispatchTouchEvent中,我们接下来就分析下ViewGroup的dispatchTouchEvent方法。

顶级View(一般都是ViewGroup)拿到点击事件,并且dispatchTouchEvent被调用,开始对事件分发,这这个方法里首先判断是否拦截该事件,下面是方法中关于事件拦截的代码。

ViewGroup#dispatchTouchEvent(MotionEvent ev)部分代码

if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
//若有该View的子元素的dispatchTouchEvent返回true,则mFirstTouchTarget指向该子元素,若View将事件拦截或没有子元素消耗事件,则mFirstTouchTarget为null
//并且当新的序列事件到来时,mFirstTouchTarget会被清空
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;
}


从上面的源码,我们发现onInterceptTouchEvent并不会每次都被调用,只有当事件类型为ACTION_DOWN或者mFirstTouchTarget不为空的时候才会调用,也就是说,如果当前View将ACTION_DOWN事件拦截(此时mFirstTouchTarget为null),那么当同一事件序列的ACTION_MOVE、ACTION_UP到来时,由于if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)不成立,将不会调用onInterceptTouchEvent,并且都交给该View处理。这验证了前面的第一条结论。

另外,子View可以通过requestDisallowInterceptTouchEvent方法设置FLAG_DISALLOW_INTERCEPT,使父View放弃拦截除ACTION_DOWN外的事件,之所以不能使父View放弃拦截ACTION_DOWN的原因是,当ACTION_DOWN到来时,父View会重置FLAG_DISALLOW_INTERCEPT

ViewGroup#dispatchTouchEvent(MotionEvent ev)部分代码
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);//清空mFirstTouchTarget
resetTouchState();//重置各种标志
}


当View不拦截事件,事件将会下发给它的子View处理,下面这段源码是dispatchTouchEvent中关于寻找并下发事件给子View的部分:

ViewGroup#dispatchTouchEvent(MotionEvent ev)部分代码

for (int i = childrenCount - 1; i >= 0; i--) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
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);
//如果dispatchTransformedTouchEvent返回true,说明下级子元素消耗该事件
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();
//对mFirstTarget赋值
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);
}
if (preorderedList != null) preorderedList.clear();
}

······
//mFirstTouchTarget==null有两种情况
//1、没有合适的子元素可以接收事件
//2、子元素接收事件,但dispatchTouchEvent返回false
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
//ViewGroup自己处理事件
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}


上面这段代码,先遍历ViewGroup的所有子元素,然后判断子元素是否能够接受到点击事件。能否接收点击事件的标准主要由两点来决定:点击事件的坐标是否在子元素的区域内和子元素是否在播放动画,只有这两点同时满足的子元素,事件才会传递给它处理,通过调用dispatchTransformedTouchEvent方法来把事件传递的符合条件的子元素的

ViewGroup#dispatchTransformedTouchEvent部分代码
private boolean dispatchTransformedTouchEvent(MotionEvent event,
boolean cancel, View child, int desiredPointerIdBits)

if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
<
e806
/pre>

上面这段代码是dispatchTransformedTouchEvent的核心代码,若找到符合条件的子元素,则将子元素作为child参数传入,然后调用子元素的dispatchTouchEvent,完成一轮事件分发,若是没有找到符合,或接收事件的子元素返回false,则传入null,此时会调用super.dispatchTouchEvent(event),也就是调用所继承的父类View的dispatchTouchEvent来处理点击事件,也就是说ViewGroup自己处理了。

为什么是调用View的dispatchTouchEvent来处理呢?因为ViewGroup的dispatchTouchEvent中并没有具体处理点击事件的内容,而且在处理事件时还要判断是否有注册onTouchListener、onClickListner等事务,这些都会在View的dispatchTouchEvent处理好,所以直接调用View的dispatchTouchEvent,而且我们知道View没有onInterceptTouchEvent方法,调用View的dispatchTouchEvent会直接开始处理点击事件,而不用进行事件分发,关于View的dispatchTouchEvent会在后面介绍。

如果子元素的dispatchTouchEvent返回true,那么mFirstTouchTarget就会被赋值,指向子元素,同时跳出for循环。

mFirstTouchTarget的赋值在addTouchTarget方法中完成

ViewGroup#addTouchTarget
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
//以接收事件并返回true的子元素为参数创建TouchTarget
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
//将创建的target赋给mFirstTouchTarget
mFirstTouchTarget = target;
return target;
}


5、View对点击事件的处理

前面我们说了,无论View还是GroupView,在最终要处理点击事件的时候,都是调用View#dispatchTouchEvent方法来处理,因为在这个方法里有关于onTouchEvent、onTouch、onClick的相关逻辑判断与调用,而且View因为已经没有子元素可以往下分发传递了,当事件传递到它时,直接就进行处理,所以它的dispatchTouchEvent都是关于处理事件的,而没有关于分发传递事件的。

还有一点,在前面我们说如果子元素不处理事件或者说不消耗事件,就交由父元素处理,显然,这里说的交由父元素处理包括了父元素的onTouch、onTouchEvent、onClick,只不过前文为了简洁,忽略onTouch和onClick

源码如下所示

View#dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
// If the event should be handled by accessibility focus first.
if (event.isTargetAccessibilityFocus()) {
// We don't have focus or no virtual descendant has it, do not handle the event.
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
// We have focus and got the event, then use normal event dispatch.
event.setTargetAccessibilityFocus(false);
}

boolean result = false;

if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}

final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}

if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
//判断是否有注册onTouchListner,有的话,执行onTouch
//onTouch的返回值表示是否消耗事件
//下面这个if语句,会从左往右判断,所以当前三个表达式成立时,第四个就会执行
//所以onTouch被执行,且若onTouch返回true,if成立,result为true,表示消耗事件
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//若有执行onTouch,且返回值为false,也就是说onTouch不消耗事件
//执行OnTouchEvent,若onTouch返回true,事件被消耗,则onTouchEvent不能被执行
if (!result && onTouchEvent(event)) {
result = true;
}
}

if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}

// Clean up after nested scrolls if this is the end of a gesture;
// also cancel it if we tried an ACTION_DOWN but we didn't want the rest
// of the gesture.
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}

return result;
}


从上面的代码及注释,我们发现,onTouch的优先级比onTouchEvent高,若onTouch返回True,消耗事件,那么onTouchEvent就不能被执行。

View关于onClick的判断部分在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();

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(); //内部调用onClick方法
}
}
}

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;

······

}
//只要View的CLICKABLE和LONG_CLICKABLE为true,那么这个View就会消耗事件
return true;
}

return false;
}


我们从上面的代码中发现,View的onTouchEvent方法,几乎都是关于CLICK的判断。所以在自定义控件时,继承View后,得重写onTouchEvent,在这方法中根据需求,实现对事件的处理,并且,若想要控件响应click,则要在onTouchEvent 返回的时候调用View的onTouchEvent :return super.onTouchEvent(event)。

而且,当View的CLICKABLE和LONG_CLICKABLE属性,只要有一个是ENABLE(android:clickable = “true” 或android:longClickable=”true”),即使这个View是DISENABLE状态(android:enabled=”false”),那么不管是否有注册onClickListener,View都会消耗事件。

onClick的具体判断调用封装在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;
}


关于click属性,当注册监听器后,setOnClickListener和setOnLongClickListener会分别把View的click属性和long_click属性设置为true,所以想要将click或long_click设置为false,要在注册监听器之后设置。

下面是注册监听器代码

public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}

public void setOnLongClickListener(@Nullable OnLongClickListener l) {
if (!isLongClickable()) {
setLongClickable(true);
}
getListenerInfo().mOnLongClickListener = l;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android 源码