Android悬浮窗

您所在的位置:网站首页 安卓悬浮窗是干什么的 Android悬浮窗

Android悬浮窗

2024-06-04 16:24| 来源: 网络整理| 查看: 265

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录 前言一、实现方案思路:步骤:1.如何获取系统实时日志?2.如何实现悬浮窗?3.因为悬浮窗需要长期运行,不依赖于界面,所以放在服务里。 二、实现过程1.获取系统日志2.实现Android悬浮窗3.悬浮窗完整代码4.通过Service控制悬浮窗显隐5.实现效果截图 总结

前言

提示:这里可以添加本文要记录的大概内容:

在做车载地图导航的时候,项目新增了U盘更新离线地图数据的功能。因为车机的特殊性,U盘插入车机的时候,电脑端不能查看车机的实时日志,代不方便代码调试。因此就想出把车机日志实时打印到车机上,便于观察。

提示:以下是本篇文章正文内容,下面案例可供参考

一、实现方案 思路:

此功能是基于Android悬浮窗实现,在Android系统中,每个窗口都对应一个Window对象,而悬浮窗就是一种特殊的Window。可以在其他应用程序的上层显示,可以随意拖动、缩放、关闭等操作,常用于提醒、通知、广告等。

步骤: 1.如何获取系统实时日志? 2.如何实现悬浮窗? 3.因为悬浮窗需要长期运行,不依赖于界面,所以放在服务里。 二、实现过程 1.获取系统日志

一般开发过程中可以使用cmd:

adb logcat

同样的方式也可以在代码中实现:

// 使用 adb 命令获取所有应用的 log 日志 var bufferedReader: BufferedReader? = null try { // 此处cmd就是我们平时常用的command命令 val cmd = "logcat -s ${tag}" // val cmd = "logcat com.kkw.floatlogger.*:V" val process = Runtime.getRuntime().exec(cmd) bufferedReader = BufferedReader(InputStreamReader(process.inputStream)) // line是每一条日志记录 var line: String? do { line = bufferedReader.readLine() line?.let { mHandler.sendMessage(Message.obtain(mHandler, 0, it)) } } while (line != null) } catch (e: IOException) { e.printStackTrace() } finally { bufferedReader?.close() }

使用方式:

mHandler = object : Handler(Looper.getMainLooper()) { override fun handleMessage(msg: Message) { super.handleMessage(msg) mLogAdapter?.add(LogEntity(msg.obj as String?)) // 自动滚动到底部 mBinding.logList.smoothScrollToPosition(mLogAdapter?.itemCount?.minus(1) ?: 0) } } 2.实现Android悬浮窗

首先需要在AndroidManifest.xml中声明悬浮窗权限:

其次实现悬浮窗的方案有很多种,可以使用系统封装好的PopupWindow,也可以自定义配置WindowManager。 这里采用第二种方式:

/** * 初始化悬浮窗 */ private fun initWindow() { // 获取WindowManager windowManager = mContext.getSystemService(Context.WINDOW_SERVICE) as WindowManager // 创建布局参数 layoutParams = WindowManager.LayoutParams() //这里需要进行不同的设置 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { layoutParams?.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY } else { layoutParams?.type = WindowManager.LayoutParams.TYPE_PHONE } layoutParams?.apply { // 设置内部视图对齐方式 gravity = Gravity.START or Gravity.TOP // 设置窗口的宽高,这里为自动 width = WindowManager.LayoutParams.MATCH_PARENT height = WindowManager.LayoutParams.WRAP_CONTENT // 是指定窗口的像素格式为 RGBA_8888。 // 使用 RGBA_8888 像素格式的窗口可以在保持高质量图像的同时实现透明度效果。 format = PixelFormat.RGBA_8888 // 设置透明度 alpha = 0.5f // 窗口相对坐标 x = 900 y = 300 // 这段非常重要,是后续是否穿透点击的关键 // FLAG_NOT_TOUCH_MODAL表示悬浮窗口不会阻塞事件传递,即用户点击悬浮窗口以外的区域时,事件会传递给后面的窗口处理。 // FLAG_NOT_FOCUSABLE表示悬浮窗口不需要获取焦点,这样用户点击悬浮窗口以外的区域,就不需要关闭悬浮窗口。 flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE } }

使用方式:

windowManager?.addView(view, layoutParams)

如果需要悬浮窗移动可添加触摸事件监听:

/** * 触摸移动监听事件 */ private inner class FloatingOnTouchListener : View.OnTouchListener { private var lastX = 0 private var lastY = 0 // 视图是否有移动 private var hasMoved = false override fun onTouch(view: View, event: MotionEvent): Boolean { when (event.action) { MotionEvent.ACTION_DOWN -> { lastX = event.rawX.toInt() lastY = event.rawY.toInt() hasMoved = false } MotionEvent.ACTION_MOVE -> { val nowX = event.rawX.toInt() val nowY = event.rawY.toInt() val movedX = nowX - lastX val movedY = nowY - lastY lastX = nowX lastY = nowY // 更新视图位置 layoutParams?.let { it.x = it.x + movedX it.y = it.y + movedY } windowManager?.updateViewLayout(view, layoutParams) // 点击防抖 if (abs(movedX) > 6 || abs(movedY) > 6) { hasMoved = true } } MotionEvent.ACTION_UP -> { // 返回true消费此次事件,后续不会触发click事件 // 返回false不消费,触发click事件 return hasMoved } else -> {} } return false } }

使用方式:

view.setOnTouchListener(FloatingOnTouchListener()) 3.悬浮窗完整代码 /** * 承载日志的悬浮窗 * @author kkw * @date 2023/11/14 */ class FloatView(private val mContext: Context) { private val mBinding: ViewFloatBinding by lazy { ViewFloatBinding.inflate(LayoutInflater.from(mContext), null, false) } private var windowManager: WindowManager? = null private var layoutParams: WindowManager.LayoutParams? = null // 日志适配器 private var mLogAdapter: LogAdapter? = null private var mHandler: Handler private val pools = Executors.newSingleThreadExecutor() init { initWindow() initView() initAdapter() mHandler = object : Handler(Looper.getMainLooper()) { override fun handleMessage(msg: Message) { super.handleMessage(msg) mLogAdapter?.add(LogEntity(msg.obj as String?)) // 自动滚动到底部 mBinding.logList.smoothScrollToPosition(mLogAdapter?.itemCount?.minus(1) ?: 0) } } } /** * 初始化悬浮窗 */ private fun initWindow() { // 获取WindowManager windowManager = mContext.getSystemService(Context.WINDOW_SERVICE) as WindowManager // 创建布局参数 layoutParams = WindowManager.LayoutParams() //这里需要进行不同的设置 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { layoutParams?.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY } else { layoutParams?.type = WindowManager.LayoutParams.TYPE_PHONE } layoutParams?.apply { // 设置内部视图对齐方式 gravity = Gravity.START or Gravity.TOP // 设置窗口的宽高,这里为自动 width = WindowManager.LayoutParams.MATCH_PARENT height = WindowManager.LayoutParams.WRAP_CONTENT // 是指定窗口的像素格式为 RGBA_8888。 // 使用 RGBA_8888 像素格式的窗口可以在保持高质量图像的同时实现透明度效果。 format = PixelFormat.RGBA_8888 // 设置透明度 alpha = 0.5f // 窗口相对坐标 x = 900 y = 300 // 这段非常重要,是后续是否穿透点击的关键 // FLAG_NOT_TOUCH_MODAL表示悬浮窗口不会阻塞事件传递,即用户点击悬浮窗口以外的区域时,事件会传递给后面的窗口处理。 // FLAG_NOT_FOCUSABLE表示悬浮窗口不需要获取焦点,这样用户点击悬浮窗口以外的区域,就不需要关闭悬浮窗口。 flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE } } /** * 初始化EditText布局 */ private fun initView() { mBinding.logTag.setOnClickListener { showSoftInput() } mBinding.logTag.addTextChangedListener { mBinding.logTag.setText(it?.toString()) } } /** * 初始化日志适配器 */ private fun initAdapter() { mLogAdapter = LogAdapter() mBinding.logList.apply { adapter = mLogAdapter layoutManager = LinearLayoutManager(mContext) } } /** * 显示浮窗 */ fun show() { initData("FloatService") if (Settings.canDrawOverlays(mContext)) { mBinding.root.setOnTouchListener(FloatingOnTouchListener()) windowManager?.addView(mBinding.root, layoutParams) } else { Toast.makeText(mContext, "需要开启应用悬浮窗权限", Toast.LENGTH_SHORT).show() } } /** * 关闭浮窗 */ fun dismiss() { pools.shutdownNow() if (Settings.canDrawOverlays(mContext)) { windowManager?.removeView(mBinding.root) } } /** * 获取系统logcat日志 */ private fun initData(tag: String?) { pools.execute { // 使用 adb 命令获取所有应用的 log 日志 var bufferedReader: BufferedReader? = null try { val cmd = "logcat -s ${tag}" // val cmd = "logcat com.kkw.floatlogger.*:V" val process = Runtime.getRuntime().exec(cmd) bufferedReader = BufferedReader(InputStreamReader(process.inputStream)) // line是每一条日志记录 var line: String? do { line = bufferedReader.readLine() line?.let { mHandler.sendMessage(Message.obtain(mHandler, 0, it)) } } while (line != null) } catch (e: IOException) { e.printStackTrace() } finally { bufferedReader?.close() } } } /** * 显示软键盘 */ private fun showSoftInput() { mBinding.logTag.isEnabled = true //设置可获得焦点 mBinding.logTag.isFocusable = true; mBinding.logTag.isFocusableInTouchMode = true; //请求获得焦点 mBinding.logTag.requestFocus(); KeyboardUtil.toggleSoftInput(mBinding.logTag) } /** * 触摸移动监听事件 */ private inner class FloatingOnTouchListener : View.OnTouchListener { private var lastX = 0 private var lastY = 0 // 视图是否有移动 private var hasMoved = false override fun onTouch(view: View, event: MotionEvent): Boolean { when (event.action) { MotionEvent.ACTION_DOWN -> { lastX = event.rawX.toInt() lastY = event.rawY.toInt() hasMoved = false } MotionEvent.ACTION_MOVE -> { val nowX = event.rawX.toInt() val nowY = event.rawY.toInt() val movedX = nowX - lastX val movedY = nowY - lastY lastX = nowX lastY = nowY // 更新视图位置 layoutParams?.let { it.x = it.x + movedX it.y = it.y + movedY } windowManager?.updateViewLayout(view, layoutParams) // 点击防抖 if (abs(movedX) > 6 || abs(movedY) > 6) { hasMoved = true } } MotionEvent.ACTION_UP -> { // 返回true消费此次事件,后续不会触发click事件 // 返回false不消费,触发click事件 return hasMoved } else -> {} } return false } } } 4.通过Service控制悬浮窗显隐

这里只是简单实现一个服务,没有进行service保活处理,感兴趣的小伙伴可以自行实现。

/** * 开启悬浮窗的服务 * @author kkw * @date 2023/11/14 */ class FloatService : Service() { private val mFloatView: FloatView by lazy { FloatView(this) } override fun onCreate() { super.onCreate() Log.d(TAG, "onCreate: ") mFloatView.show() } override fun onBind(intent: Intent?): IBinder? { TODO("Not yet implemented") } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { Log.d(TAG, "onStartCommand: ") return super.onStartCommand(intent, flags, startId) } override fun onDestroy() { super.onDestroy() Log.d(TAG, "onDestroy: ") mFloatView.dismiss() } companion object { private const val TAG = "FloatService" } } 5.实现效果截图

在这里插入图片描述

总结

以上就是本次功能的实现思想,已经基本满足开发需要,有兴趣的小伙伴可以自己试试看。 附上完整项目链接:

https://github.com/kkingso/FloatLogger



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3