先说说故事背景吧,要开发类似微信视频聊天功能,就是两个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,这样不仅可以从根源上解决问题,自己还可以熟悉原码,一举多得。