如何处理Android悬浮弹窗双击返回事件?

目录

1 前言

1.1 准备知识

1.2 问题概述

2 解决方案

3 代码部分

3.1 动态更新窗口焦点

3.2 窗口监听返回事件

3.3 判断焦点是否在窗口内部

3.4 窗口监听焦点移入/移出

4 注意事项

4.1 窗口范围

4.2 空隙处的返回事件处理


1 前言

1.1 准备知识

1)开发环境

  • 2D开发环境:所有界面或窗口都在主界面显示;
  • 3D开发环境:保留原生Android的主界面,在主界面之外绘制各种窗口,配合3D渲染以实现3D效果。

2)焦点:就是Hover点、中央注视点、可与用户交互的点。

3)窗口:就是系统窗口、悬浮弹窗,内部通过addView方法去添加View,本文窗口监听指的就是View监听。

4)事件分发:Android设备一般会使用如下3种,本文采用的第3种setOnHoverListener获取事件。

  • setOnTouchListener(MotionEvent::InputEvent):手机、平板、车载等屏幕可触控的2D设备;
  • setOnKeyListener(KeyEvent::InputEvent):电视、投影仪等屏幕不可触控的2D设备;
  • setOnHoverListener(MotionEvent::InputEvent):AR眼镜等增强现实设备。

5)Hover事件分发:当前View在焦点移出(不再是Hover状态)时,不会立即发送ACTION_HOVER_EXIT退出事件,需要等到下一个View获取到ACTION_HOVER_ENTER状态时才会发送上一个View的ACTION_HOVER_EXIT退出事件。

6)窗口内部View的Hover事件分发过程

  • RootView会先获取到ACTION_HOVER_ENTER事件;
  • 当进入ChildView时,ChildView会先获取到ACTION_HOVER_ENTER事件,然后RootView会获取到ACTION_HOVER_EXIT事件;
  • 当从ChildView退出时,ChildView会先获取到ACTION_HOVER_EXIT事件,然后RootView会获取到ACTION_HOVER_ENTER事件。

1.2 问题概述

        问题描述:在Android悬浮弹窗上双击返回,主界面响应返回事件。

        问题原因:悬浮弹窗设置了flag为窗口不可获取焦点即:WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE。

        问题分析

  • 悬浮弹窗设置flag为窗口不可获取焦点,是为了不影响主界面的焦点响应(Android默认主界面的窗口是获取焦点的);
  • 如果悬浮弹窗设置flag可获取焦点,那么Android的事件分发是无法发送到主界面的,会将事件分发给当前可获取焦点的悬浮弹窗;
  • 如下图,左侧图1为悬浮弹窗,右侧图2为主界面某应用打开一个Activity。图1悬浮弹窗是常驻于图2主界面的左侧,且默认不可获取焦点,但在特殊情况时可获取焦点(如展开键盘、焦点在此悬浮弹窗内部等情况)。

        解决方案:当焦点在悬浮弹窗内部时,设置窗口flag可获取焦点;当焦点不在悬浮弹窗内部时,设置窗口flag不可获取焦点。

2 解决方案

        方案主要分为如下几步:

  1. 窗口默认不可获取焦点;
  2. 窗口监听焦点的移入/移出事件;
  3. 窗口监听到焦点移入,判断窗口是否可获取焦点,否——设置窗口可获取焦点,是——不做任何操作;
  4. 窗口监听到焦点移出,判断焦点是否在窗口内部,否——设置窗口不可获取焦点,是——不做任何操作;

        读者可思考如下2个问题,

1)问题1:为什么在窗口监听到焦点移入后,要再判断窗口是否可获取焦点?

2)问题2:为什么在窗口监听到焦点移出后,要再判断焦点是否在窗口内部?

        相信本文《1.1 准备知识的Hover事件分发部分》可以给你一些灵感。   

     

3 代码部分

3.1 动态更新窗口焦点

        核心API:

  • WindowManager.updateViewLayout
  • WindowManager.LayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
    private fun initLiveDataBus() {
        LiveDataBus.get().with(Constants.NOTIFICATION_EVENT_BUS_FOCUSABLE, Boolean::class.java)
            .observeForever { focusable: Boolean ->
                Log.d(TAG, "onChanged: $focusable")
                updateNotificationParams(focusable)
            }
    }

    private fun updateNotificationParams(focusable: Boolean) {
        initLayoutParams(focusable)
        mUiHandler.post {
            synchronized(this) {
                if (mIsBarWindowAdded) {
                    try {
                        mWindowManager.updateViewLayout(mNotificationBar, mLayoutParams)
                    } catch (e: Exception) {
                        e.printStackTrace()
                    }
                }
            }
        }
    }

    private fun initLayoutParams(focusable: Boolean) {
        mLayoutParams = WindowManager.LayoutParams().apply {
            type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
            val density = mContext.resources.displayMetrics.density
            width = (640 * density).toInt()
            height = (640 * density).toInt()
            flags =
                WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH or WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
            if (!focusable) {
                flags = flags or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
            }
            format = PixelFormat.RGBA_8888 // 去除默认时有的黑色背景,设置为全透明
            gravity = Gravity.TOP or Gravity.START
            title = SYSUI_NOTIFICATION
            x = -(640 * density).toInt()
            y = 0
        }
    }
    

3.2 窗口监听返回事件

        窗口设置可获得焦点后,内部View会获取到事件分发的事件,在此View中重写dispatchKeyEvent方法,监听keyCode == KeyEvent.KEYCODE_BACK事件,就可对返回事件进行处理。

    override fun dispatchKeyEvent(event: KeyEvent): Boolean {
        if (event.keyCode == KeyEvent.KEYCODE_BACK) {
            Log.i(TAG, "dispatchKeyEvent: KEYCODE_BACK")
            // 窗口设置可获得焦点后,内部View会获取到事件分发的事件,并可对返回事件进行处理
        }
        return super.dispatchKeyEvent(event)
    }

3.3 判断焦点是否在窗口内部

        通过View相对于屏幕位置X/Y、以及View宽高,共同确定View的边界。

    mRootView.post {
        val locationXY = IntArray(2)
        mRootView.getLocationOnScreen(locationXY)
        val locationX = locationXY[0]
        val locationY = locationXY[1]
        val measuredWidth = mRootView.measuredWidth
        val measuredHeight = mRootView.measuredHeight
    }


    /**
     * 焦点:就是Hover点、中央注视点、可与用户交互的点。
     *
     * @param locationX View相对于屏幕位置X
     * @param locationY View相对于屏幕位置Y
     * @param measuredWidth View宽
     * @param measuredHeight View高
     * @param rawX 焦点相对于屏幕位置X
     * @param rawY 焦点相对于屏幕位置Y
     *
     * @return 焦点是否未在View内部
     */
    private fun isViewNotFocus(
        locationX: Int,
        locationY: Int,
        measuredWidth: Int,
        measuredHeight: Int,
        rawX: Float,
        rawY: Float
    ) =
        if (rawX <= locationX || rawX >= locationX + measuredWidth || rawY <= locationY || rawY >= locationY + measuredHeight) {
            // 焦点不在View内部
            Log.i(TAG, "isViewNotFocus: 焦点不在View内部")
            true
        } else {
            // 焦点在View内部
            Log.i(TAG, "isViewNotFocus: 焦点在View内部")
            false
        }

3.4 窗口监听焦点移入/移出

  • 窗口监听到焦点移入,判断窗口是否可获取焦点,否——设置窗口可获取焦点,是——不做任何操作;
  • 窗口监听到焦点移出,判断焦点是否在窗口内部,否——设置窗口不可获取焦点,是——不做任何操作;
  • 最后,通过发送NOTIFICATION_EVENT_BUS_FOCUSABLE事件,进而设置窗口的是否可获取焦点。
    // 注:Focus移出时需要包含边界。
    mRootView.setOnHoverListener { v, event ->
        when (event.action) {
            MotionEvent.ACTION_HOVER_ENTER -> {
                Log.i(
                    TAG,
                    "OnHoverListener: 进入, action =  ${event.action},motionX = ${event.rawX},motionY = ${event.rawY}"
                )
                LiveDataBus.get().with(NOTIFICATION_EVENT_BUS_FOCUSABLE).value?.let {
                    if (!(it as Boolean)) {
                        Log.i(TAG, "OnHoverListener: 进入, focus-true-again")
                        LiveDataBus.get().with(NOTIFICATION_EVENT_BUS_FOCUSABLE).value =
                            true
                    }
                } ?: let {
                    Log.i(TAG, "OnHoverListener: 进入, focus-true-init")
                    LiveDataBus.get().with(NOTIFICATION_EVENT_BUS_FOCUSABLE).value = true
                }
            }
            MotionEvent.ACTION_HOVER_MOVE -> {
            }
            MotionEvent.ACTION_HOVER_EXIT -> {
                Log.i(
                    TAG,
                    "OnHoverListener: 退出, action =  ${event.action},motionX = ${event.rawX},motionY = ${event.rawY}"
                )
                if (isViewNotFocus(
                        locationX,
                        locationY,
                        measuredWidth,
                        measuredHeight,
                        event.rawX,
                        event.rawY
                    )
                ) {
                    Log.i(TAG, "OnHoverListener: 退出, focus-false")
                    LiveDataBus.get().with(NOTIFICATION_EVENT_BUS_FOCUSABLE).value = false
                }
            }
        }
        false
    }

4 注意事项

4.1 窗口范围

        在判断焦点是否在窗口内部时,需要确认窗口范围,如果窗口内部的View有设置Padding或Margin,应该将其去掉。

        如:本文的窗口大小是640*640,但View大小是540*580,所以计算时需要去掉相应Padding或Margin,重写isViewNotFocus()方法如下:

    private fun isViewNotFocus(
        locationX: Int,
        locationY: Int,
        measuredWidth: Int,
        measuredHeight: Int,
        rawX: Float,
        rawY: Float
    ): Boolean {
        val density = context.resources.displayMetrics.density
        return rawX <= locationX + 50 * density || rawX >= locationX + measuredWidth - 100 * density || rawY <= locationY + 15 * density || rawY >= locationY + measuredHeight - 60 * density
    }

4.2 空隙处的返回事件处理

        1)从窗口移出到空隙处

        通过本文1.1准备知识的第5部分《Hover事件分发》,我们知道,从窗口移出但还未有下一个View获取焦点时,此时窗口还是会接收到返回事件。

        2)从View移出到空隙处

        从当前View移出但还未有下一个View获取焦点时,此时当前View还是会接收到返回事件。

那么,如何处理这种空隙处的返回事件呢?

       核心:从系统层拦截此种情况下的返回事件 。

  1. 渲染层:提供接口,返回焦点移入移出时当前layer的名称,是否有碰撞窗口等信息;
  2. 系统层:当没有碰撞窗口时,从系统层拦截掉返回事件的分发;
  3. 应用层:监听焦点移入移出,改变窗口focus属性,并处理返回事件;

解决方案:

        当空隙处有返回事件产生时,系统层通过渲染层的接口,获取到当前焦点所在位置的layer名称,如果layer名称为空则断定为空隙处,直接做拦截处理,不再往应用层分发。

注:每个窗口、Activity在其Window中,都有设置其title属性,layer名称就是此title属性的值。


目录

1 前言

1.1 准备知识

1.2 问题概述

2 解决方案

3 代码部分

3.1 动态更新窗口焦点

3.2 窗口监听返回事件

3.3 判断焦点是否在窗口内部

3.4 窗口监听焦点移入/移出

4 注意事项

4.1 窗口范围

4.2 空隙处的返回事件处理

  • 29
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
您可以使用 WindowManager 来动态改变 Android 弹窗悬浮窗)的大小。以下是一种实现方法: 1. 首先,在您的代码中实例化一个 WindowManager 对象: ```java WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE); ``` 2. 创建一个布局文件作为您的弹窗的内容,例如 `popup_layout.xml`: ```xml <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical"> <!-- 弹窗内容 --> </LinearLayout> ``` 3. 在您的代码中添加方法来显示和更新弹窗的大小: ```java private void showPopup(int width, int height) { // 创建弹窗视图 View popupView = LayoutInflater.from(this).inflate(R.layout.popup_layout, null); // 设置弹窗的宽度和高度 WindowManager.LayoutParams params = new WindowManager.LayoutParams( width, height, WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, // 悬浮窗类型,注意需要适配 Android 8.0 及以上版本 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, // 弹窗不获取焦点 PixelFormat.TRANSLUCENT); // 弹窗背景透明 // 显示弹窗 windowManager.addView(popupView, params); } private void updatePopupSize(int width, int height) { View popupView = /* 获取已经显示的弹窗视图 */; WindowManager.LayoutParams params = (WindowManager.LayoutParams) popupView.getLayoutParams(); params.width = width; params.height = height; windowManager.updateViewLayout(popupView, params); } ``` 通过调用 `showPopup()` 方法,您可以创建并显示一个具有指定宽度和高度的弹窗。如果您之后需要更新弹窗的大小,可以调用 `updatePopupSize()` 方法,将新的宽度和高度传递给它。 请注意,显示悬浮窗需要适配 Android 8.0 及以上版本,且需要您的应用拥有悬浮窗权限。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值