阅读ViewGroup源码解决SurfaceView叠加切换TouchEvent事件冲突问题

先说说故事背景吧,要开发类似微信视频聊天功能,就是两个SurfaceView叠加,然后有按钮可以切换大小屏(对方画面和自己预览画面)(两个SurfaceView层级切换,见另一篇文章《Android两个大小SurfaceView切换》),这个时候有个功能是,需要小的屏幕可以拖拽,悬停。故事背景大概是这样。本文有点长,是讲解原理的,伸手党,可能要有点耐心看完本文才有答案。

描述:


这是图一

页面大概是这样,A和B是两个SurfaceView,点击小的画面可以AB切换,拖拽小画面,可以任意移动

这是图二


这是图三(是图一点击大小屏切换变化得来的)


这是图四(假装和图一不一样,图四是图三点击大小屏切换得来的)

上面图,分别示意小画面拖拽和画面切换。

就是为了实现这么一个功能,具体功能点有:

1.小画面的touch事件监听(消费本次事件,touch事件return true)

2.小画面的click事件监听

3.大画面的touch事件监听(有其他用处,(消费本次事件,touch事件return true))

布局文件大概是这么写的:

<RelativeLayout
    android:id="@+id/rl_root"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <SurfaceView
        android:id="@+id/surface_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
    <SurfaceView
        android:id="@+id/surface_small"
        android:layout_width="50dp"
        android:layout_height="100dp"/>
</RelativeLayout>

然后大小屏切换的代码大概是怎么写的:

private void changeScreen() {
        if (null == mSurfaceView) {
            return;
        }
        mIsChangeScreen = !mIsChangeScreen;
        RelativeLayout.LayoutParams lpSmall = getSmallLayoutParams();
        RelativeLayout.LayoutParams lpNormal = getNormalLayoutParams();
        mRlRoot.removeView(mSurfaceViewSmall);
        mRlRoot.removeView(mSurfaceView);
        mRlRoot.removeView(mRlFun);
        mRlRoot.removeView(mIvPlayState);

        if (mIsChangeScreen) {
            mSurfaceView.setZOrderOnTop(true);
            mSurfaceView.setZOrderMediaOverlay(true);

            mSurfaceViewSmall.setZOrderOnTop(false);
            mSurfaceViewSmall.setZOrderMediaOverlay(false);

            mSurfaceView.setLayoutParams(lpSmall);
            mRlRoot.addView(mSurfaceView);

            mSurfaceViewSmall.setLayoutParams(lpNormal);
            mRlRoot.addView(mSurfaceViewSmall);
    }
}

因为两个SurfaceView要改变层级,只能先remove在add,所以要这么写。


问题:

图一一切正常,小画面可以收到touch事件,可以正常拖拽。大画面也可以收到touch事件。但是进过两次切换,进入图四状态,小画面的touch事件无法收到,只能收到大画面的touch事件(小画面区域触摸也是收到大画面的touch事件),问题到底在哪里呢,图一和图四的差别在哪里呢?

带着这些问题,就要翻阅点击事件的源码才行了。

废话不多说,直接进入最关键的源码ViewGroup#dispatchTouchEvent(),因为这里解决的是ASurfaceView的touch事件无法收到的问题。所以就要从touch事件的分发开始看起

for (int i = childrenCount - 1; i >= 0; i—) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);
		…
		if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
		…

}

运气比较好,看到这个好像有点像,再继续往下看

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;
        }
	…
}
运气还真的比较好,dispatchTransformedTouchEvent()调用了子View的child.dispatchTouchEvent(),我们知道子View的TouchEvent是由dispatchTouchEvent()调用的。

所以之前问题的答案就在上面的源码里面了。

最关键的就是这个

for (int i = childrenCount - 1; i >= 0; i—) {

没错,它是倒序的,图一,因为是layout布局,大布局比小布局先写,所以大布局顺序在小布局前面,遍历的时候,也就是小布局先去执行touch事件。然后切换先后顺序之后,到了图四,先add小布局,再add大布局,这个时候,大布局的touch事件被执行,return true直接被消费,当然就没有小布局的touch什么事了。

解决方案:

好了,现在知道了问题的根源,改一下也是很简单了。只要改变一下布局add的先后顺序就可以了,问题迎刃而解。

小结:

解决本问题的关键就在于,要知道统一层级的touch事件分发是倒序分发的,怎么知道它是倒序的呢,就是看源码喽,(网上没有多少资料会告诉你那么细的)。所以以后碰到问题,还是多看源码,多看api,这样不仅可以从根源上解决问题,自己还可以熟悉原码,一举多得。



评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值