当前位置:   article > 正文

Android悬浮窗的简单实现_android悬浮窗实现

android悬浮窗实现

1. 前言

        现在很多应用都有小悬浮窗的功能,比如看直播的时候,通过Home键返回桌面,直播的小窗口仍可以在屏幕上显示。下面将介绍下悬浮窗的的一种简单实现方式。

2.原理

       Window我们应该很熟悉,它是一个接口类,具体的实现类为PhoneWindow,它可以对View进行管理。WindowManager是一个接口类,继承自ViewManager,从名称就知道它是用来管理Window的,它的实现类是WindowManagerImpl。如果我们想要对Window(View)进行添加、更新和删除操作就可以使用WindowManager,WindowManager会将具体的工作交由WindowManagerService处理。这里我们只需要知道WindowManager能用来管理Window就好。

  WindowManager是一个接口类,继承自ViewManager,ViewManager中定义了3个方法,分布用来添加、更新和删除View,如下所示:

  1. public interface ViewManager {
  2.     public void addView(View view, ViewGroup.LayoutParams params);
  3.     public void updateViewLayout(View view, ViewGroup.LayoutParams params);
  4.     public void removeView(View view);
  5. }

        WindowManager也继承了这些方法,而这些方法传入的参数都是View类型,说明了Window是以View的形式存在的。

3.具体实现

3.1浮窗布局

  悬浮窗的简易布局如下的可参考下面的layout_floating_window.xml文件。顶层深色部分的FrameLayout布局是用来实现悬浮窗的拖拽功能的,点击右上角ImageView可以实现关闭悬浮窗,剩下区域显示内容,这里只是简单地显示文本内容,不做复杂的东西,故只设置TextView。

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <LinearLayout
  3.     xmlns:android="http://schemas.android.com/apk/res/android"
  4.     xmlns:app="http://schemas.android.com/apk/res-auto"
  5.     android:layout_width="match_parent"
  6.     android:layout_height="match_parent"
  7.     android:orientation="vertical">
  8.     
  9.     <FrameLayout
  10.         android:id="@+id/layout_drag"
  11.         android:layout_width="match_parent"
  12.         android:layout_height="15dp"
  13.         android:background="#dddddd">
  14.         <androidx.appcompat.widget.AppCompatImageView
  15.             android:id="@+id/iv_close"
  16.             android:layout_width="15dp"
  17.             android:layout_height="15dp"
  18.             android:layout_gravity="end"
  19.             android:src="@drawable/img_delete"/>
  20.     </FrameLayout>
  21.     
  22.     <androidx.appcompat.widget.AppCompatTextView
  23.         android:id="@+id/tv_content"
  24.         android:layout_width="match_parent"
  25.         android:layout_height="match_parent"
  26.         android:layout_gravity="center_horizontal"
  27.         android:background="#eeeeee"
  28.         android:scrollbars="vertical"/>
  29. </LinearLayout>

3.2 悬浮窗的实现

1. 使用服务Service

  Service 是一种可在后台执行长时间运行操作而不提供界面的应用组件,可由其他应用组件启动,而且即使用户切换到其他应用,仍将在后台继续运行。要保证应用在后台时,悬浮窗仍然可以正常显示,所以这里可以使用Service。

2. 获取WindowManager并设置LayoutParams

  1. private lateinit var windowManager: WindowManager
  2. private lateinit var layoutParams: WindowManager.LayoutParams
  3. override fun onCreate() {
  4.     // 获取WindowManager
  5.     windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
  6.     layoutParams = WindowManager.LayoutParams().apply {
  7.         // 实现在其他应用和窗口上方显示浮窗
  8.         type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
  9.             WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
  10.         } else {
  11.             WindowManager.LayoutParams.TYPE_PHONE
  12.         }
  13.         format = PixelFormat.RGBA_8888
  14.         // 设置浮窗的大小和位置
  15.         gravity = Gravity.START or Gravity.TOP
  16.         flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
  17.         width = 600
  18.         height = 600
  19.         x = 300
  20.         y = 300
  21.     }
  22. }

3. 创建View并添加到WindowManager

  1. private lateinit var floatingView: View
  2. override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
  3.     if (Settings.canDrawOverlays(this)) {
  4.         floatingView = LayoutInflater.from(this).inflate(R.layout.layout_floating_window.xml, null)
  5.         windowManager.addView(floatingView, layoutParams)
  6.     }  
  7.     return super.onStartCommand(intent, flags, startId)
  8. }

4. 实现悬浮窗的拖拽和关闭功能

  1. // 浮窗的坐标
  2. private var x = 0
  3. private var y = 0
  4.  
  5. override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {   
  6.     if (Settings.canDrawOverlays(this)) {
  7.     floatingView = LayoutInflater.from(this).inflate(R.layout.layout_floating_window.xml, null)
  8.     windowManager.addView(floatingView, layoutParams)
  9.     
  10.     // 点击浮窗的右上角关闭按钮可以关闭浮窗
  11.     floatingView.findViewById<AppCompatImageView>(R.id.iv_close).setOnClickListener {
  12.         windowManager.removeView(floatingView)
  13.     }
  14.     // 实现浮窗的拖动功能, 通过改变layoutParams来实现
  15.     floatingView.findViewById<AppCompatImageView>(R.id.layout_drag).setOnTouchListener { v, event ->
  16.         when (event.action) {
  17.             MotionEvent.ACTION_DOWN -> {
  18.                 x = event.rawX.toInt()
  19.                 y = event.rawY.toInt()
  20.             }
  21.             MotionEvent.ACTION_MOVE -> {
  22.                 val currentX = event.rawX.toInt()
  23.                 val currentY = event.rawY.toInt()
  24.                 val offsetX = currentX - x
  25.                 val offsetY = currentY - y
  26.                 x = currentX
  27.                 y = currentY
  28.                 layoutParams.x = layoutParams.x + offsetX
  29.                 layoutParams.y = layoutParams.y + offsetY
  30.                 // 更新floatingView
  31.                 windowManager.updateViewLayout(floatingView, layoutParams)
  32.             }
  33.         }
  34.         true
  35.     }
  36.     return super.onStartCommand(intent, flags, startId)
  37. }

5. 利用广播进行通信

  1. private var receiver: MyReceiver? = null
  2. override fun onCreate() {
  3.     // 注册广播
  4.     receiver = MyReceiver()
  5.     val filter = IntentFilter()
  6.     filter.addAction("android.intent.action.MyReceiver")
  7.     registerReceiver(receiver, filter)
  8. }
  9.  
  10. inner class MyReceiver : BroadcastReceiver() {
  11.     override fun onReceive(context: Context, intent: Intent) {
  12.         val content = intent.getStringExtra("content") ?: ""
  13.         
  14.         // 通过Handler更新UI
  15.         val message = Message.obtain()
  16.         message.what = 0
  17.         message.obj = content
  18.         handler.sendMessage(message)
  19.     }
  20. }
  21.  
  22. val handler = Handler(this.mainLooper) { msg ->
  23.     tvContent.text = msg.obj as String
  24.     false
  25. }

可以在Activity中通过广播给Service发送信息

  1. fun sendMessage(view: View?) {
  2.     Intent("android.intent.action.MyReceiver").apply {
  3.         putExtra("content", "Hello, World!")
  4.         sendBroadcast(this)
  5.     }
  6. }

6. 设置权限

悬浮窗的显示需要权限,在AndroidManefest.xml中添加:

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
此外,还要通过Settings.ACTION_MANAGE_OVERLAY_PERMISSION来让动态设置权限,在Activity中设置。

  1. // MainActivity.kt
  2. fun startWindow(view: View?) {
  3.     if (!Settings.canDrawOverlays(this)) {
  4.         startActivityForResult(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName")), 0)
  5.     } else {
  6.         startService(Intent(this@MainActivity, FloatingWindowService::class.java))
  7.     }
  8. }
  9. override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
  10.     super.onActivityResult(requestCode, resultCode, data)
  11.     if (requestCode == 0) {
  12.         if (Settings.canDrawOverlays(this)) {
  13.             Toast.makeText(this, "悬浮窗权限授权成功", Toast.LENGTH_SHORT).show()
  14.             startService(Intent(this@MainActivity, FloatingWindowService::class.java))
  15.         }
  16.     }
  17. }

3.3 完整代码

  1. class FloatingWindowService : Service() {
  2.     private lateinit var windowManager: WindowManager
  3.     private lateinit var layoutParams: WindowManager.LayoutParams
  4.     private lateinit var tvContent: AppCompatTextView
  5.     private lateinit var handler: Handler
  6.  
  7.     private var receiver: MyReceiver? = null
  8.     private var floatingView: View? = null
  9.     private val stringBuilder = StringBuilder()
  10.  
  11.     private var x = 0
  12.     private var y = 0
  13.     
  14.     // 用来判断floatingView是否attached 到 window manager,防止二次removeView导致崩溃
  15.     private var attached = false
  16.  
  17.     override fun onCreate() {
  18.         super.onCreate()
  19.         // 注册广播
  20.         receiver = MyReceiver()
  21.         val filter = IntentFilter()
  22.         filter.addAction("android.intent.action.MyReceiver")
  23.         registerReceiver(receiver, filter);
  24.         
  25.         // 获取windowManager并设置layoutParams
  26.         windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
  27.         layoutParams = WindowManager.LayoutParams().apply {
  28.             type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
  29.                 WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
  30.             } else {
  31.                 WindowManager.LayoutParams.TYPE_PHONE
  32.             }
  33.             format = PixelFormat.RGBA_8888
  34. //            format = PixelFormat.TRANSPARENT
  35.             gravity = Gravity.START or Gravity.TOP
  36.             flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
  37.             width = 600
  38.             height = 600
  39.             x = 300
  40.             y = 300
  41.         }
  42.         handler = Handler(this.mainLooper) { msg ->
  43.             tvContent.text = msg.obj as String
  44.             // 当文本超出屏幕自动滚动,保证文本处于最底部
  45.             val offset = tvContent.lineCount * tvContent.lineHeight
  46.             floatingView?.apply {
  47.                 if (offset > height) {
  48.                     tvContent.scrollTo(0, offset - height)
  49.                 }
  50.             }
  51.             false
  52.         }
  53.     }
  54.  
  55.     override fun onBind(intent: Intent?): IBinder? {
  56.         return null
  57.     }
  58.  
  59.     @SuppressLint("ClickableViewAccessibility")
  60.     override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
  61.         if (Settings.canDrawOverlays(this)) {
  62.             floatingView = LayoutInflater.from(this).inflate(R.layout.layout_show_log, null)
  63.             tvContent = floatingView!!.findViewById(R.id.tv_log)
  64.             floatingView!!.findViewById<AppCompatImageView>(R.id.iv_close).setOnClickListener {
  65.                 stringBuilder.clear()
  66.                 windowManager.removeView(floatingView)
  67.                 attached = false
  68.             }
  69.             // 设置TextView滚动
  70.             tvContent.movementMethod = ScrollingMovementMethod.getInstance()
  71.  
  72.             floatingView!!.findViewById<FrameLayout>(R.id.layout_drag).setOnTouchListener { v, event ->
  73.                 when (event.action) {
  74.                     MotionEvent.ACTION_DOWN -> {
  75.                         x = event.rawX.toInt()
  76.                         y = event.rawY.toInt()
  77.                     }
  78.                     MotionEvent.ACTION_MOVE -> {
  79.                         val currentX = event.rawX.toInt()
  80.                         val currentY = event.rawY.toInt()
  81.                         val offsetX = currentX - x
  82.                         val offsetY = currentY - y
  83.                         x = currentX
  84.                         y = currentY
  85.                         layoutParams.x = layoutParams.x + offsetX
  86.                         layoutParams.y = layoutParams.y + offsetY
  87.                         windowManager.updateViewLayout(floatingView, layoutParams)
  88.                     }
  89.                 }
  90.                 true
  91.             }
  92.  
  93.             windowManager.addView(floatingView, layoutParams)
  94.             attached = true
  95.         }
  96.         return super.onStartCommand(intent, flags, startId)
  97.     }
  98.  
  99.     override fun onDestroy() {
  100.         // 注销广播并删除浮窗
  101.         unregisterReceiver(receiver)
  102.         receiver = null
  103.         if (attached) {
  104.             windowManager.removeView(floatingView)
  105.         }
  106.     }
  107.  
  108.     inner class MyReceiver : BroadcastReceiver() {
  109.         override fun onReceive(context: Context, intent: Intent) {
  110.             val content = intent.getStringExtra("content") ?: ""
  111.             stringBuilder.append(content).append("\n")
  112.             val message = Message.obtain()
  113.             message.what = 0
  114.             message.obj = stringBuilder.toString()
  115.             handler.sendMessage(message)
  116.         }
  117.     }
  118. }

4. 总结

        以上就是Android悬浮窗的一个简单实现方式。如果需要实现其他复杂一点的功能,比如播放视频,也可以在此基础上完成。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/500744
推荐阅读
相关标签
  

闽ICP备14008679号