快跑,小蜗牛

就这样一直向前


  • 首页

  • 标签

  • 分类

  • 归档

  • 关于

  • 搜索

【Android-技术干货】Touch事件在Window间穿透

发表于 2019-02-25 | 分类于 技术干货 | | 阅读次数:

1.解决了什么问题?

在某些场景下,希望当前的Window只处理部分区域的Touch事件,而把Window中其他区域的收到的事件,“穿透”到下层的其他Window中。
例如:桌面悬浮窗需要全屏展示,但是只有一部分区域是可以点击的,其他的区域需要将事件交给其他应用去处理,这时候就可以用到本文的方案。

2.解决思路

首先,需要知道Android的Window对事件的处理,是由ViewTreeObserver中的OnComputeInternalInsetsListener来确定的,所以,需要修改OnComputeInternalInsetsListener回调里的InternalInsetsInfo参数来设置事件的点击区域。
接下来,就是怎么修改OnComputeInternalInsetsListener的问题。先来看下ViewTreeObserver的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public final class ViewTreeObserver {
...
private CopyOnWriteArray<OnComputeInternalInsetsListener> mOnComputeInternalInsetsListeners;
...
/**
* Interface definition for a callback to be invoked when layout has
* completed and the client can compute its interior insets.
* ...
* @hide
*/
public interface OnComputeInternalInsetsListener {
/**
* Callback method to be invoked when layout has completed and the
* client can compute its interior insets.
*
* @param inoutInfo Should be filled in by the implementation with
* the information about the insets of the window. This is called
* with whatever values the previous OnComputeInternalInsetsListener
* returned, if there are multiple such listeners in the window.
*/
public void onComputeInternalInsets(InternalInsetsInfo inoutInfo);
}
...

/**
* Register a callback to be invoked when the invoked when it is time to
* compute the window's internal insets.
* ...
* @hide
*/
public void addOnComputeInternalInsetsListener(OnComputeInternalInsetsListener listener) {
checkIsAlive();

if (mOnComputeInternalInsetsListeners == null) {
mOnComputeInternalInsetsListeners =
new CopyOnWriteArray<OnComputeInternalInsetsListener>();
}

mOnComputeInternalInsetsListeners.add(listener);
}

/**
* Remove a previously installed internal insets computation callback
* ...
* @hide
*/
public void removeOnComputeInternalInsetsListener(OnComputeInternalInsetsListener victim) {
checkIsAlive();
if (mOnComputeInternalInsetsListeners == null) {
return;
}
mOnComputeInternalInsetsListeners.remove(victim);
}
...

从源码里可以看到,我们需要创建一个OnComputeInternalInsetsListener,然后将它添加到mOnComputeInternalInsetsListeners中就能实现事件穿透。然而,OnComputeInternalInsetsListener类以及与它相关的方法都是hide的。因此,一般的调用是满足不了的,我们只能使用其他方法,那就是暴力的反射。
确定了反射的方案后,我们分两步:
第一步是创建OnComputeInternalInsetsListener,对于隐藏的类实例,我们可以利用Java的动态代理来实现。

1
2
3
4
5
6
public class OnComputeInternalInsetsListener implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
...
}
}

第二步是把创建的实例添加到mOnComputeInternalInsetsListeners,这一步就比较简单了,只需要对addOnComputeInternalInsetsListener方法进行反射调用就可以了。
以上就是事件在Window间实现穿透的实现思路。

3.代码

  • OnComputeInternalInsetsListener动态代理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class OnComputeInternalInsetsListener implements InvocationHandler {

private Region touchRegion = null;
public Object getListener() {
Object target = null;
try {
Class class1 = Class.forName("android.view.ViewTreeObserver$OnComputeInternalInsetsListener");
target = Proxy.newProxyInstance(OnComputeInternalInsetsListener.class.getClassLoader(),
new Class[]{class1}, this);
} catch (Exception e) {
e.printStackTrace();
}
return target;
}

public Region getTouchRegion() {
return touchRegion;
}

public void setTouchRegion(Region touchRegion) {
this.touchRegion = touchRegion;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) {
try {
Field regionField = args[0].getClass()
.getDeclaredField("touchableRegion");
regionField.setAccessible(true);
Field insetField = args[0].getClass()
.getDeclaredField("mTouchableInsets");
insetField.setAccessible(true);
if (touchRegion != null) {
Region region = (Region) regionField.get(args[0]);
region.set(touchRegion);
insetField.set(args[0], InputMethodService.Insets.TOUCHABLE_INSETS_REGION);
} else {
insetField.set(args[0], InputMethodService.Insets.TOUCHABLE_INSETS_FRAME);
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
  • addOnComputeInternalInsetsListener和removeOnComputeInternalInsetsListener方法反射:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public static void removeOnComputeInternalInsetsListener(ViewTreeObserver viewTree) {
if (viewTree == null) {
return;
}
try {
Class<?> clazz = Class.forName("android.view.ViewTreeObserver");
Field field = viewTree.getClass().getDeclaredField("mOnComputeInternalInsetsListeners");
field.setAccessible(true);
Object listenerList = field.get(viewTree);
Method method = listenerList.getClass().getDeclaredMethod("getArray");
method.setAccessible(true);
ArrayList<Object> list = (ArrayList<Object>) method.invoke(listenerList);
Class<?> classes[] = {Class.forName("android.view.ViewTreeObserver$OnComputeInternalInsetsListener")};
if (list != null && list.size() > 0) {
clazz.getDeclaredMethod("removeOnComputeInternalInsetsListener", classes).invoke(viewTree,
list.get(0));
}
} catch (Exception e) {
e.printStackTrace();
}
}

public static void addOnComputeInternalInsetsListener(ViewTreeObserver viewTree, Object object) {
if (viewTree == null) {
return;
}
try {
Class<?> classes[] = {Class.forName("android.view.ViewTreeObserver$OnComputeInternalInsetsListener")};
Class<?> clazz = Class.forName("android.view.ViewTreeObserver");
clazz.getDeclaredMethod("addOnComputeInternalInsetsListener", classes).invoke(viewTree,
object);
} catch (Exception e) {
e.printStackTrace();
}
}
  • 给RootView设置指定的点击区域
1
2
3
4
5
6
private void configTouch() {
mInvocationHandler = new OnComputeInternalInsetsListener();
ReflectionUtils.removeOnComputeInternalInsetsListener(mRootView.getViewTreeObserver());
ReflectionUtils.addOnComputeInternalInsetsListener(mRootView.getViewTreeObserver(), mInvocationHandler.getListener());
mInvocationHandler.setTouchRegion(mTouchRegion);
}

4.Demo下载

下载地址

5.Window可接受Touch事件区域的系统源码

可以查阅源码中的ViewRootImpl类,下面是关键源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public final class ViewRootImpl implements ViewParent,
View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {
private void performTraversals() {
...
if (computesInternalInsets) {
// Clear the original insets.
final ViewTreeObserver.InternalInsetsInfo insets = mAttachInfo.mGivenInternalInsets;
insets.reset();

// Compute new insets in place.
mAttachInfo.mTreeObserver.dispatchOnComputeInternalInsets(insets);
mAttachInfo.mHasNonEmptyGivenInternalInsets = !insets.isEmpty();

// Tell the window manager.
if (insetsPending || !mLastGivenInsets.equals(insets)) {
mLastGivenInsets.set(insets);

// Translate insets to screen coordinates if needed.
final Rect contentInsets;
final Rect visibleInsets;
final Region touchableRegion;
if (mTranslator != null) {
contentInsets = mTranslator.getTranslatedContentInsets(insets.contentInsets);
visibleInsets = mTranslator.getTranslatedVisibleInsets(insets.visibleInsets);
touchableRegion = mTranslator.getTranslatedTouchableArea(insets.touchableRegion);
} else {
contentInsets = insets.contentInsets;
visibleInsets = insets.visibleInsets;
touchableRegion = insets.touchableRegion;
}

try {
mWindowSession.setInsets(mWindow, insets.mTouchableInsets,
contentInsets, visibleInsets, touchableRegion);
} catch (RemoteException e) {
}
}
}
...
}
...
}

暴走蜗牛

1 日志
2 分类
2 标签
GitHub
© 2020 暴走蜗牛
由 Hexo 强力驱动
|
主题 — NexT.Mist v5.1.4