购物车吸顶7.0无法点击问题排查

问题表现:

购物车吸顶效果在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,所以不会有问题。