添加到 WindowManager 的自定义视图,根据状态将事件传递给底层 window

Custom view added to WindowManager, pass events to underlying window depending on state

我遇到了一个相当复杂的情况,我需要在通过 WindowManager 添加的自定义视图中处理事件,或者将它们传递给基础 window(如果它在通缉区。想要的区域是 containerView,它可以比根视图本身更小,或者可以具有相等的宽度/高度。

视图的大小为 28x28,但可以增大到 60x60。增长部分使用 ValueAnimator 完成,其中当前宽度和目标宽度由 ValueAnimator.getAnimatedValue() 确定(在本例中,介于 28 和 60 之间)。如果 window 被点击,或者点击了比 window 本身更小的目标视图,则需要消耗该事件。

布局示例如下所示:

<FrameLayout android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <FrameLayout android:id="@+id/containerView"
         android:layout_width="28dp"
         android:layout_height="28dp"
         android:layout_gravity="center">

         <!-- rest of the view, not important -->

         <!-- the containerView can have 28x28 size or
                   60x60 size -->

    </FrameLayout>

</FrameLayout>

动画视图是用android:id="@+id/containerView"定义的视图。

我试过使用常规布局参数附加视图,就像这样,使 window 布局动态化:

WindowManager manager = context.getSystemService(WindowManager.class);
View rootView = LayoutInflater.from(context).inflate(resId, null, false);

WindowManager.LayoutParams params = new WindowManager.LayoutParams();

params.width = WindowManager.LayoutParams.WRAP_CONTENT;
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
params.flags = FLAG_NOT_FOCUSABLE | FLAG_WATCH_OUTSIDE_TOUCH;

manager.addView(rootView, params);

并且这个类似的代码块添加了 28x28 大小的视图,这不是问题。然而,当根据状态变化(在 containerView 上)将动画设置为 60x60 大小时,动画会闪烁很多。我猜这是因为视图本身和 window 都需要重新调整大小。我试过使用 setLayerType(HARDWARE, null) 但似乎没有用。然后我找到了另一种解决方法,它直接增加 window 的大小,在开始动画之前,通过给它固定的宽度 - 高度值,像这样:

params.width = dpToPx(60);
params.height = dpToPx(60);

manager.updateViewLayout(rootView, params);

然后,我开始生长动画,逐渐改变 containerView 宽度和高度。通过这种方式,动画很流畅,即使在低端设备上也是如此,所以我认为这是一个很好的优化。

问题从 window 大小变化开始。 你看,containerView 必须有属性 android:layout_gravity="center" 来定位视图到 window 的中心。但是,增加 window 宽度和高度会改变视图的位置。为了克服这个问题,我决定通过执行以下操作来编写另一种方法:

// This method is inside the root view, which contains
// the WindowManager.LayoutParams as its layout params.
private void setWindowSize(int widthPx, int heightPx)
{
    WindowManager.LayoutParams params = getLayoutParams(); // ignore cast

    int oldWidth = params.width;
    int oldHeight = params.height;

    int differenceWidth = widthPx - oldWidth;
    int differenceHeight = heightPx - oldHeight;

    // Position the view relatively to the window so 
    // it should look like its position is not changed
    // due to containerView's center layout_gravity.

    params.x -= differenceWidth / 2;
    params.y -= differenceHeight / 2;
    params.width = widthPx;
    params.height = heightPx;

    // Update itself since this is already the root view.
    manager.updateViewLayout(this, params);
}

上面的代码导致动画发生位置变化。因此,我搜索了是否可以禁用此动画,发现 似乎可以与 Android 10 模拟器一起使用。但是,我不认为这是一种可靠的方法,因为大多数制造商更改框架 类 的源代码以实现他们自己的主题等,所以我正在寻找一种更可靠的方法。由于 containerView.onLayout() 操作,该更改还会导致闪烁,大概发生在执行 manager.updateViewLayout() 之后,它出现在左上角的一帧和第二帧的中心,眼睛可见。

至此,我只能想到一些方法来防止这些bug:

1) 只处理特定状态下的触摸事件(比如截取containerView的坐标)

2) 在收到 MotionEvent.ACTION_OUTSIDE 后使视图不可触摸,这将指示触摸事件发生在视图边界之外。

第一个有缺陷:如果视图在所有情况下都是可点击的,则从根视图开始变为可点击,并且一旦从该视图接收到触摸事件,就不会传递给其他视图windows(a.k.a 基础应用程序)导致问题。

第二个对我来说似乎是个好方法,但事件 MotionEvent.ACTION_OUTSIDE 不包含任何特定的 x 或 y 坐标,因此无法判断事件是否发生在 window 的边界内.如果可能的话,我会将 FLAG_NOT_TOUCHABLE 添加到布局参数并更新视图,并在要处理触摸时删除该标志。

那么,我的问题是:

添加了 WindowManager 的自定义视图是否可以根据(我不知道)从 dispatchTouchEvent() 返回 false 或其他什么来选择进一步传递事件?或者,有没有一种方法可以接收所有触摸事件,即使是在我们的应用程序之外,也可以使用特定的屏幕坐标,这样我就可以根据它更改 window 标志?

感谢任何帮助,非常感谢。

我能够通过应用一个丑陋的 hack 来解决这个问题。我使用了第二个 window,其中 window 本身是全屏并包含标志 FLAG_NOT_FOCUSABLEFLAG_NOT_TOUCHABLE,并且由于触摸被禁用,事件在 window.

window-根据动画调整大小闪烁是原因,所以我考虑过使用临时视图,通过获取缓存将视图添加到第二个 window视图本身使用位图和 canvas(顺便缓存和回收状态),并使图像视图可见,将原始 window 上的视图设置为 INVISIBLE 和在确保它变得不可见之后(通过使用 ViewTreeObserver.addOnDrawListener 因为绘制函数被调用)改变 window 大小。

使用这种方法,当 window 大小发生变化并相应地进行翻译时,视图已经变得不可见,从而消除了出现错误视图的可能性。

然后,在布局完成后(我也通过使用 ViewTreeObserver.addOnGlobalLayoutListener() 确保并等待视图放置在相对于父对象的目标坐标上),切换视图。由于额外添加了 window 和图像视图和位图,因此使用了额外的内存,但问题似乎已解决。

唯一剩下的事情是如何通过调用 windowManager.updateViewLayout() 禁用 window 动画,因为另一个问题提到的标志显然添加在 API 18 中,而此应用程序针对到 API 16. 在我测试过的其余模拟器和设备上似乎始终有此标志,并且 window 翻译动画似乎已成功禁用。