Android悬浮窗的一种实现

使用这个接口显示窗口的模版代码如下:

//‘解析布局文件为视图’

val windowView = LayoutInflater.from(context).inflate(R.id.window_view, null)

//‘获取WindowManager系统服务’

val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager

//‘构建窗口布局参数’

WindowManager.LayoutParams().apply {

type = WindowManager.LayoutParams.TYPE_APPLICATION

width = WindowManager.LayoutParams.WRAP_CONTENT

height = WindowManager.LayoutParams.WRAP_CONTENT

gravity = Gravity.START or Gravity.TOP

x = 0

y = 0

}.let { layoutParams->

//‘将视图添加到窗口’

windowManager.addView(windowView, layoutParams)

}

上述代码在当前界面的左上角显示R.id.window_view.xml中定义的布局。

为避免重复,将这段代码抽象成一个函数,其中窗口视图内容和展示位置会随着需求而变,遂将其参数化:

object FloatWindow{

private var context: Context? = null

//‘当前窗口参数’

var windowInfo: WindowInfo? = null

//‘把和Window布局有关的参数打包成一个内部类’

class WindowInfo(var view: View?) {

var layoutParams: WindowManager.LayoutParams? = null

//‘窗口宽’

var width: Int = 0

//‘窗口高’

var height: Int = 0

//‘窗口中是否有视图’

fun hasView() = view != null && layoutParams != null

//‘窗口中视图是否有父亲’

fun hasParent() = hasView() && view?.parent != null

}

//‘显示窗口’

fun show(

context: Context,

windowInfo: WindowInfo?,

x: Int = windowInfo?.layoutParams?.x.value(),

y: Int = windowInfo?.layoutParams?.y.value(),

) {

if (windowInfo == null) { return }

if (windowInfo.view == null) { return }

this.windowInfo = windowInfo

this.context = context

//‘创建窗口布局参数’

windowInfo.layoutParams = createLayoutParam(x, y)

//‘显示窗口’

if (!windowInfo.hasParent().value()) {

val windowManager = this.context?.getSystemService(Context.WINDOW_SERVICE) as WindowManager

windowManager.addView(windowInfo.view, windowInfo.layoutParams)

}

}

//‘创建窗口布局参数’

private fun createLayoutParam(x: Int, y: Int): WindowManager.LayoutParams {

if (context == null) { return WindowManager.LayoutParams() }

return WindowManager.LayoutParams().apply {

//‘该类型不需要申请权限’

type = WindowManager.LayoutParams.TYPE_APPLICATION

format = PixelFormat.TRANSLUCENT

flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS

gravity = Gravity.START or Gravity.TOP

width = windowInfo?.width.value()

height = windowInfo?.height.value()

this.x = x

this.y = y

}

}

//‘为空Int提供默认值’

fun Int?.value() = this ?: 0

}

FloatWindow声明成了单例,目的是在 app 整个生命周期,任何界面都可以方便地显示浮窗。

为了方便统一管理窗口的参数,抽象了内部类WindowInfo

现在就可以像这样在屏幕左上角显示一个浮窗了:

val windowView = LayoutInflater.from(context).inflate(R.id.window_view, null)

WindowInfo(windowView).apply{

width = 100

height = 100

}.let{ windowInfo ->

FloatWindow.show(context, windowInfo, 0, 0)

}

浮窗背景色


产品要求当浮窗显示时,屏幕变暗。设置WindowManager.LayoutParams.FLAG_DIM_BEHIND标签配合dimAmount就能轻松实现:

object FloatWindow{

//当前窗口参数

var windowInfo: WindowInfo? = null

private fun createLayoutParam(x: Int, y: Int): WindowManager.LayoutParams {

if (context == null) { return WindowManager.LayoutParams() }

return WindowManager.LayoutParams().apply {

type = WindowManager.LayoutParams.TYPE_APPLICATION

format = PixelFormat.TRANSLUCENT

flags =

WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or

WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or

//‘设置浮窗背景变暗’

WindowManager.LayoutParams.FLAG_DIM_BEHIND

//‘设置默认变暗程度为0,即不变暗,1表示全黑’

dimAmount = 0f

gravity = Gravity.START or Gravity.TOP

width = windowInfo?.width.value()

height = windowInfo?.height.value()

this.x = x

this.y = y

}

}

//‘供业务界面在需要的时候调整浮窗背景亮暗’

fun setDimAmount(amount:Float){

windowInfo?.layoutParams?.let { it.dimAmount = amount }

}

}

设置浮窗点击事件


为浮窗设置点击事件等价于为浮窗视图设置点击事件,但如果直接对浮窗视图使用setOnClickListener()的话,浮窗的触摸事件就不会被响应,那拖拽就无法实现。所以只能从更底层的触摸事件着手:

object FloatWindow : View.OnTouchListener{

//‘显示窗口’

fun show(

context: Context,

windowInfo: WindowInfo?,

x: Int = windowInfo?.layoutParams?.x.value(),

y: Int = windowInfo?.layoutParams?.y.value(),

) {

if (windowInfo == null) { return }

if (windowInfo.view == null) { return }

this.windowInfo = windowInfo

this.context = context

//‘为浮窗视图设置触摸监听器’

windowInfo.view?.setOnTouchListener(this)

windowInfo.layoutParams = createLayoutParam(x, y)

if (!windowInfo.hasParent().value()) {

val windowManager = this.context?.getSystemService(Context.WINDOW_SERVICE) as WindowManager

windowManager.addView(windowInfo.view, windowInfo.layoutParams)

}

}

override fun onTouch(v: View, event: MotionEvent): Boolean {

return false

}

}

onTouch(v: View, event: MotionEvent)中可以拿到更详细的触摸事件,比如ACTION_DOWNACTION_MOVEACTION_UP。这方便了拖拽的实现,但点击事件的捕获变得复杂,因为需要定义上述三个 ACTION 以怎样的序列出现时才判定为点击事件。幸好GestureDetector为我们做了这件事:

public class GestureDetector {

public interface OnGestureListener {

//‘ACTION_DOWN事件’

boolean onDown(MotionEvent e);

//‘单击事件’

boolean onSingleTapUp(MotionEvent e);

//‘拖拽事件’

boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);

}

}

构建GestureDetector实例并将MotionEvent传递给它就能将触摸事件解析成感兴趣的上层事件:

object FloatWindow : View.OnTouchListener{

private var gestureDetector: GestureDetector = GestureDetector(context, GestureListener())

private var clickListener: WindowClickListener? = null

private var lastTouchX: Int = 0

private var lastTouchY: Int = 0

//‘为浮窗设置点击监听器’

fun setClickListener(listener: WindowClickListener) {

clickListener = listener

}

override fun onTouch(v: View, event: MotionEvent): Boolean {

//‘将触摸事件传递给 GestureDetector 解析’

gestureDetector.onTouchEvent(event)

return true

}

//‘记忆起始触摸点坐标’

private fun onActionDown(event: MotionEvent) {

lastTouchX = event.rawX.toInt()

lastTouchY = event.rawY.toInt()

}

private class GestureListener : GestureDetector.OnGestureListener {

//‘记忆起始触摸点坐标’

override fun onDown(e: MotionEvent): Boolean {

onActionDown(e)

return false

}

override fun onSingleTapUp(e: MotionEvent): Boolean {

//‘点击事件发生时,调用监听器’

return clickListener?.onWindowClick(windowInfo) ?: false

}

}

//‘浮窗点击监听器’

interface WindowClickListener {

fun onWindowClick(windowInfo: WindowInfo?): Boolean

}

}

拖拽浮窗


ViewManager提供了updateViewLayout(View view, ViewGroup.LayoutParams params)用于更新浮窗位置,所以只需监听ACTION_MOVE事件并实时更新浮窗视图位置就可实现拖拽。ACTION_MOVE事件被GestureDetector解析成OnGestureListener.onScroll()回调:

object FloatWindow : View.OnTouchListener{

private var gestureDetector: GestureDetector = GestureDetector(context, GestureListener())

private var lastTouchX: Int = 0

private var lastTouchY: Int = 0

override fun onTouch(v: View, event: MotionEvent): Boolean {

//‘将触摸事件传递给GestureDetector解析’

gestureDetector.onTouchEvent(event)

return true

}

private class GestureListener : GestureDetector.OnGestureListener {

override fun onDown(e: MotionEvent): Boolean {

onActionDown(e)

return false

}

override fun onScroll(e1: MotionEvent,e2: MotionEvent,distanceX: Float,distanceY:Float): Boolean {

//‘响应手指滚动事件’

onActionMove(e2)

return true

}

}

private fun onActionMove(event: MotionEvent) {

//‘获取当前手指坐标’

val currentX = event.rawX.toInt()

val currentY = event.rawY.toInt()

//‘获取手指移动增量’

val dx = currentX - lastTouchX

val dy = currentY - lastTouchY

//‘将移动增量应用到窗口布局参数上’

windowInfo?.layoutParams!!.x += dx

windowInfo?.layoutParams!!.y += dy

val windowManager = context?.getSystemService(Context.WINDOW_SERVICE) as WindowManager

var rightMost = screenWidth - windowInfo?.layoutParams!!.width

var leftMost = 0

val topMost = 0

val bottomMost = screenHeight - windowInfo?.layoutParams!!.height - getNavigationBarHeight(context)

//‘将浮窗移动区域限制在屏幕内’

if (windowInfo?.layoutParams!!.x < leftMost) {

windowInfo?.layoutParams!!.x = leftMost

}

if (windowInfo?.layoutParams!!.x > rightMost) {

windowInfo?.layoutParams!!.x = rightMost

}

if (windowInfo?.layoutParams!!.y < topMost) {

windowInfo?.layoutParams!!.y = topMost

}

if (windowInfo?.layoutParams!!.y > bottomMost) {

windowInfo?.layoutParams!!.y = bottomMost

}

//‘更新浮窗位置’

windowManager.updateViewLayout(windowInfo?.view, windowInfo?.layoutParams)

lastTouchX = currentX

lastTouchY = currentY

}

}

浮窗自动贴边


新的需求来了,拖拽浮窗松手后,需要自动贴边。

把贴边理解成一个水平位移动画。在松手时求出动画起点和终点横坐标,利用动画值不断更新浮窗位置:

object FloatWindow : View.OnTouchListener{

private var gestureDetector: GestureDetector = GestureDetector(context, GestureListener())

private var lastTouchX: Int = 0

private var lastTouchY: Int = 0

//‘贴边动画’

private var weltAnimator: ValueAnimator? = null

override fun onTouch(v: View, event: MotionEvent): Boolean {

//‘将触摸事件传递给GestureDetector解析’

gestureDetector.onTouchEvent(event)

//‘处理ACTION_UP事件’

val action = event.action

when (action) {

MotionEvent.ACTION_UP -> onActionUp(event, screenWidth, windowInfo?.width ?: 0)

else -> {

}

}

return true

}

private fun onActionUp(event: MotionEvent, screenWidth: Int, width: Int) {

if (!windowInfo?.hasView().value()) { return }

//‘记录抬手横坐标’

val upX = event.rawX.toInt()

//‘贴边动画终点横坐标’

val endX = if (upX > screenWidth / 2) {

screenWidth - width

} else {

0

}

//‘构建贴边动画’

if (weltAnimator == null) {

weltAnimator = ValueAnimator.ofInt(windowInfo?.layoutParams!!.x, endX).apply {

interpolator = LinearInterpolator()

duration = 300

addUpdateListener { animation ->

val x = animation.animatedValue as Int

if (windowInfo?.layoutParams != null) {

windowInfo?.layoutParams!!.x = x

}

val windowManager = context?.getSystemService(Context.WINDOW_SERVICE) as WindowManager

//‘更新窗口位置’

if (windowInfo?.hasParent().value()) {

windowManager.updateViewLayout(windowInfo?.view, windowInfo?.layoutParams)

}

}

}

}

weltAnimator?.setIntValues(windowInfo?.layoutParams!!.x, endX)

weltAnimator?.start()

}

//为空Boolean提供默认值

fun Boolean?.value() = this ?: false

}

  • GestureDetector解析后ACTION_UP事件被吞掉了,所以只能在onTouch()中截获它。

  • 根据抬手横坐标和屏幕中点横坐标的大小关系,来决定浮窗贴向左边还是右边。

管理多个浮窗


若 app 的不同业务界面同时需要显示浮窗:进入 界面A 时显示 浮窗A,然后它被拖拽到右下角,退出 界面A 进入 界面B,显示浮窗B,当再次进入 界面A 时,期望还原上次离开时的浮窗A的位置。

当前FloatWindow中用windowInfo成员存储单个浮窗参数,为了同时管理多个浮窗,需要将所有浮窗参数保存在Map结构中用 tag 区分:

object FloatWindow : View.OnTouchListener {

//‘浮窗参数容器’

private var windowInfoMap: HashMap<String, WindowInfo?> = HashMap()

//‘当前浮窗参数’

var windowInfo: WindowInfo? = null

//‘显示浮窗’

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

总结

其实要轻松掌握很简单,要点就两个:

  1. 找到一套好的视频资料,紧跟大牛梳理好的知识框架进行学习。
  2. 多练。 (视频优势是互动感强,容易集中注意力)

你不需要是天才,也不需要具备强悍的天赋,只要做到这两点,短期内成功的概率是非常高的。

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。

以上就是总结的关于在面试的一些总结,希望对大家能有些帮助,除了这些面试中需要注意的问题,当然最重要的就是刷题了,这里放上我之前整理的一份超全的面试专题PDF

还有 高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

【Android核心高级技术PDF文档,BAT大厂面试真题解析】

这里只是整理出来的部分面试题,后续会持续更新,希望通过这些高级面试题能够降低面试Android岗位的门槛,让更多的Android工程师理解Android系统,掌握Android系统。喜欢的话麻烦点击一个喜欢在关注一下~

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!**

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

总结

其实要轻松掌握很简单,要点就两个:

  1. 找到一套好的视频资料,紧跟大牛梳理好的知识框架进行学习。
  2. 多练。 (视频优势是互动感强,容易集中注意力)

你不需要是天才,也不需要具备强悍的天赋,只要做到这两点,短期内成功的概率是非常高的。

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。

以上就是总结的关于在面试的一些总结,希望对大家能有些帮助,除了这些面试中需要注意的问题,当然最重要的就是刷题了,这里放上我之前整理的一份超全的面试专题PDF

还有 高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

【Android核心高级技术PDF文档,BAT大厂面试真题解析】

[外链图片转存中…(img-AcFwQLZz-1712230398320)]

这里只是整理出来的部分面试题,后续会持续更新,希望通过这些高级面试题能够降低面试Android岗位的门槛,让更多的Android工程师理解Android系统,掌握Android系统。喜欢的话麻烦点击一个喜欢在关注一下~

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
  • 13
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
对于 Android 悬浮窗实现,可以使用 WindowManager实现。但是在实现过程中,可能会遇到 WindowManager 无法传递事件的问题。 这个问题的原因可能是因为 WindowManager 的类型不正确。在创建 WindowManager 的时候,需要指定正确的类型,例如 TYPE_APPLICATION_OVERLAY 类型。 下面是一个基本的悬浮窗实现示例: ```java public class FloatingWindowService extends Service { private WindowManager mWindowManager; private View mFloatingView; public FloatingWindowService() { } @Override public IBinder onBind(Intent intent) { return null; } @Override public void onCreate() { super.onCreate(); // 创建一个 WindowManager mWindowManager = (WindowManager) getSystemService(WINDOW_SERVICE); // 创建一个悬浮窗 View mFloatingView = LayoutInflater.from(this).inflate(R.layout.floating_window, null); // 设置悬浮窗的类型为 TYPE_APPLICATION_OVERLAY WindowManager.LayoutParams params = new WindowManager.LayoutParams( WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY : WindowManager.LayoutParams.TYPE_PHONE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSLUCENT); // 将悬浮窗添加到 WindowManager 中 mWindowManager.addView(mFloatingView, params); } @Override public void onDestroy() { super.onDestroy(); if (mFloatingView != null) { // 从 WindowManager 中移除悬浮窗 mWindowManager.removeView(mFloatingView); } } } ``` 在这个示例中,我们通过创建一个 WindowManager,并将悬浮窗 View 添加到 WindowManager 中来实现悬浮窗。同时,我们还需要设置悬浮窗的类型为 TYPE_APPLICATION_OVERLAY 类型,以确保能够正常接收事件。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值