Android 无需权限的悬浮窗

259 阅读3分钟

最近接到一个需求,产品需要在App的每个页面显示一个可以拖动的View,并且这个View是在当前页面的最上层,大家第一时间想到的应该就是悬浮窗这个东西,但是悬浮窗这个东西是需要权限的,用户一旦拒绝权限就用不了了,所以咱们得另辟蹊径,想想别的方式,可以在当前页面的添加一个View 用来充当悬浮窗,需要完成以下步骤

  1. 添加View 显示在最上层

首先 需要显示在界面的最上层,想想咱们Android的界面布局,是不是它最底层是用一个FrameLayout装载着我们的界面的,那我们是不是可以去找到这个FrameLayout 然后把这个View添加到FrameLayout里面,因为是在setContentView之后添加的,按层次关系这个View一定是在最上面的

val view = LayoutInflater.from(this).inflate(R.layout.float_test,null)
//不设置拖动不了View
view.isClickable = true
//设置View宽高
var layout = view.layoutParams
if (layout == null){
    layout = ViewGroup.LayoutParams(300,300)
}else {
    layout.width = 300
    layout.height = 300
}
view.layoutParams = layout
//把View添加到当前视图 android.R.id.content这个是固定不变的,Android系统帮我们把它Id值固定了
val frameLayout = window.decorView.findViewById<FrameLayout>(android.R.id.content)
frameLayout.addView(view)
  1. 让View可拖动,且让悬浮窗不会被用户移除界面外,超过界面自动弹回界面内

要让View变的可拖动,给View设置setOnTouchListener 事件,监听用户对当前View的手势操作事件,当用户手指移动时,改变当前View的X 坐标或者Y坐标,让其跟着一起移动,并且判断当前的坐标有没有超出屏幕范围,超出去等手指抬起的时候把坐标设定回边界即可

val screenWidth = windowManager.defaultDisplay.width
val viewWidth = 300
val screenHeight = windowManager.defaultDisplay.height
val viewHeight= 300
view.setOnTouchListener { v, event ->
    when(event.action){
        MotionEvent.ACTION_DOWN ->{
            downX = event.rawX
            currentX = v.x
            downY = event.rawY
            currentY = v.y
        }
        MotionEvent.ACTION_MOVE ->{
            moveX = currentX + (event.rawX - downX)
            moveY = currentY + (event.rawY - downY)
            v.x = moveX
            v.y = moveY
        }
        else ->{
            if (v.x > (screenWidth /2 - viewWidth/2)){
                ObjectAnimator.ofFloat(v, "X", v.x, (screenWidth.toFloat() - viewWidth)).setDuration(300).start()
            }else {
                ObjectAnimator.ofFloat(v, "X", v.x, 0f).setDuration(300).start()
            }
            if (v.y < 0){
                ObjectAnimator.ofFloat(v, "Y", v.y, 0f).setDuration(300).start()
            }else if ((v.y + viewHeight) > screenHeight){
                ObjectAnimator.ofFloat(v, "Y", v.y, (screenHeight - viewHeight).toFloat()).setDuration(300).start()
            }

        }
    }
    false
}
  1. 记录坐标,使其进入下一个界面或者返回上一个界面 坐标跟之前移动的一致,后续代码跟上面代码不是同一个项目,我是在demo中实现拖动效果,然后在项目中实际添加移除操作

实现坐标统一,可以新建一个单例,让其每次初始化的View都是同一个View,这样每次设置完坐标,等到下一个界面添加View的时候这个View的对象还是原来那一个,里面的坐标也都是之前设置的坐标,这一步好像也没有代码贴,就是新建一个静态单例,用单例返回View对象,静态对象生命周期是App的生命周期,所以View一旦创建,它是会一直存在的,后续取的View就都是同一个对象了

  1. 界面创建 添加View 界面销毁 移除View

需要做到每进入一个新界面就添加View,离开就移除View,这样就需要用到ActivityLifecycleCallbacks这个东西了,用它在Application中注册,这样就能监听每个Activity的生命周期,在onResume中添加,然后在onPause中移除

// onResume
FrameLayout frameLayout = activity.getWindow().getDecorView().findViewById(android.R.id.content);
View view = AppFloatViewHelper.getInstance().getFloatView(activity);
if (view != null){
    frameLayout.removeView(view);
    frameLayout.addView(view);
}
// onPause
FrameLayout frameLayout = activity.getWindow().getDecorView().findViewById(android.R.id.content);
View view = getFloatView(activity);
if (view != null){
    frameLayout.removeView(view);
}

注意,以上Activity的使用请慎用,我传入Activity只是为了做跳转,不是用于做创建View的Context,且注意内存泄漏问题,尽量使用WeakReference,以上,一个不需要权限的悬浮窗就有了,相比系统的悬浮窗,只能说没有那么丝滑,切换界面的时候悬浮窗会隐藏,下个界面出来才会出现,效果上没系统的那么好,下面贴个效果图

dy.gif