问题表现:
购物车吸顶效果在7.0的手机上无法点击选中仓库;
问题排查过程:
首先因为点击效果不能展示,根据效果猜测,可能是事件没有传递到对应的子view,经过断点调试和打log发现。DOWN,MOVE和UP事件都传递给了子view。
此时我把click和checkchanged事件都换成了setOnToucnListener,此时就可以相应到事件的效果了。根据源码分析知道,onclick事件是在onTouchEvent的内部执行的。也就是说事件其实是分发给了对应的子view,并且他能够通过dispatchTouchEvent来分发事件,但是当传递到自己的onTouchEvent里面的时候出现了问题。
在看一下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();
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, x, y);
}
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;
}
首先看一下ACTION_DOWN的处理,能够看出来其中有个isInScrollingContainer的判断,如果在滑动控件中就将这个事件延迟一些执行,因为此时无法判断是tap点击还是想滑动,所以源码中在这里只是切换了标志位,mPrivateFlags |= PFLAG_PREPRESSED,表示我在这里有过一次预点击,但是这个有什么作用呢,一会看一下UP事件就知道了。反之,如果不在滑动控件中,则意味着他是单独的view,此时就会立刻执行setPressed(true,x,y)。进去看一下setPressed的源码:
public void setPressed(boolean pressed) {
final boolean needsRefresh = pressed != ((mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED);
if (pressed) {
mPrivateFlags |= PFLAG_PRESSED;
} else {
mPrivateFlags &= ~PFLAG_PRESSED;
}
if (needsRefresh) {
refreshDrawableState();
}
dispatchSetPressed(pressed);
}
重点就是if和else中更新了标志位,表示是否有过点击。
接着在看一下UP事件中的代码,第一句就判断了是否之前有过预点击,就是刚才遗留的问题。接着看下if判断,如果是有过点击或者是预点击就会执行up时间的逻辑,click就在这里操作的。进入if判断之后如果是预点击过,这时候UP了表示就是真的点击而不是滑动了,此时就会执行setPressed方法。再回到吸顶这个问题上来看,正常情况下,如果view是在scroll布局中,则down事件不会执行setpressed,在up事件的时候才会执行。再往下看具体的点击代码:
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
private final class PerformClick implements Runnable {
@Override
public void run() {
performClick();
}
}
决定能否响应点击效果的重点就是if判断中的post方法了,在看他里面的源码之前,要先分析一下他是否真的走到了这里,还是在前面就断掉了,因为我在分析setpressed的时候发现异常情况下只在DOWN有执行setpressed,UP的时候未执行。但是正常情况下,DOWN和UP都会执行。因为这里有很多的私有方法,在外部是拿不到的,所以在这里通过反射的方法来拿到对应的成员变量,看下方法:
我自定义了一个view继承textview来模拟这个吸顶的一个子view,通过复写他的onTouchEvent方法在UP事件的时候获取view中对应的私有成员变量。
Field field= null;
field = this.getClass().getSuperclass().getSuperclass().getDeclaredField("mPrivateFlags");
field.setAccessible(true);
DebugLog.d("mPrivateFlags:" + field.get(this));
int mPrivateFlags = (int)field.get(this);
int pflagPrepressed = 0x02000000;
int pflagPressed = 0x00004000;
int a = mPrivateFlags & pflagPrepressed;
int b = mPrivateFlags & pflagPressed;
DebugLog.d("pflagPrepressed&:" + a + "---pflagpressed&:" + b);
DebugLog.d("isfocus:" + (isFocusable() && isFocusableInTouchMode() && !isFocused()));
field = this.getClass().getSuperclass().getSuperclass().getDeclaredField("mHasPerformedLongPress");
field.setAccessible(true);
DebugLog.d("mHasPerformedLongPress:" + field.get(this));
field = this.getClass().getSuperclass().getSuperclass().getDeclaredField("mIgnoreNextUpEvent");
field.setAccessible(true);
DebugLog.d("mIgnoreNextUpEvent:" + field.get(this));
field = this.getClass().getSuperclass().getSuperclass().getDeclaredField("mAttachInfo");
field.setAccessible(true);
DebugLog.d("attachInfo:" + field.get(this));
在这里我打出来了UP事件路径上的每一个成员变量,分析出来确实走到了post的if判断中,因此接着看下他的源码:
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
getRunQueue().post(action);
return true;
}
其中的AttachInfo就是视图树的一些相关信息,正常情况下的view都是加在视图树中的,如果此时attachInfo不为空的话,正常情况下会通过attachInfo.mHandler发送一个msg给ui线程,将事件添加到ui队列中,此时就会等待action的执行了,也就是点击效果。但是,如果此时attachInfo为空的话,就说明视图树还没有创建或者类似于我所遇到的这种特殊情况,view并没有加入到视图树中。第一种情况,视图树还没有创建,这时候直接走到getRunQueue().post(action);中,经过分析发现,如果是正常逻辑,这个runQuene会在view的dispatchAttachedToWindow的时候执行相应的action,也就是相当于预先保存,等到视图树创建完成之后才会执行这个方法。。
if (mRunQueue != null) {
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
}
现在说第二种情况,也就是我所遇到的问题,因为这个吸顶的view在listview滚动的时候会被回收掉,也就是说他是一个没有parentview的悬空view,并不在视图树中存在,因此dispatchAttachedToWindow方法并不会执行,因此就没有执行到这个方法,所以执行点击事件的两次机会都没有调用到。
但是,为什么在7.0以下是正常的呢,再分析一下23版本的view源码,发现在post的方法里有一句是不一样的:
ViewRootImpl.getRunQueue().post(action); //23版
getRunQueue().post(action); //24版
为什么23版的通过ViewRootImpl的runQueue就可以了,同样的原理,只是23版使用的不是view自己的attachInfo,在6.0上具体的逻辑就是,事件传递引起了一次post,post会将handler的msg事件交给主looper去执行,当looper执行到了就会更新界面执行requestLayout(),接着会在ViewRootImpl类的performTraversals()方法中执行到这些action,
getRunQueue().executeActions(mAttachInfo.mHandler);
而这里的attachInfo是ViewRootImpl的attachInfo,所以不会有问题。