软件编程
位置:首页>> 软件编程>> Android编程>> Android实现悬浮窗的简单方法实例

Android实现悬浮窗的简单方法实例

作者:Android开发中文站  发布时间:2023-06-17 18:11:02 

标签:android,悬浮窗

    1. 前言

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

    2.原理

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

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


    public interface ViewManager {
       public void addView(View view, ViewGroup.LayoutParams params);
       public void updateViewLayout(View view, ViewGroup.LayoutParams params);
       public void removeView(View view);
    }

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

    3.具体实现

    3.1浮窗布局

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


    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
       xmlns:android="http://schemas.android.com/apk/res/android"
       xmlns:app="http://schemas.android.com/apk/res-auto"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:orientation="vertical">
    <FrameLayout
           android:id="@+id/layout_drag"
           android:layout_width="match_parent"
           android:layout_height="15dp"
           android:background="#dddddd">
           <androidx.appcompat.widget.AppCompatImageView
               android:id="@+id/iv_close"
               android:layout_width="15dp"
               android:layout_height="15dp"
               android:layout_gravity="end"
               android:src="@drawable/img_delete"/>
       </FrameLayout>
    <androidx.appcompat.widget.AppCompatTextView
           android:id="@+id/tv_content"
           android:layout_width="match_parent"
           android:layout_height="match_parent"
           android:layout_gravity="center_horizontal"
           android:background="#eeeeee"
           android:scrollbars="vertical"/>
    </LinearLayout>

    3.2 悬浮窗的实现

    1. 使用服务Service

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

    2. 获取WindowManager并设置LayoutParams


    private lateinit var windowManager: WindowManager
    private lateinit var layoutParams: WindowManager.LayoutParams
    override fun onCreate() {
       // 获取WindowManager
       windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
       layoutParams = WindowManager.LayoutParams().apply {
           // 实现在其他应用和窗口上方显示浮窗
           type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
               WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
           } else {
               WindowManager.LayoutParams.TYPE_PHONE
           }
           format = PixelFormat.RGBA_8888
           // 设置浮窗的大小和位置
           gravity = Gravity.START or Gravity.TOP
           flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
           width = 600
           height = 600
           x = 300
           y = 300
       }
    }

    3. 创建View并添加到WindowManager


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

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


    // 浮窗的坐标
    private var x = 0
    private var y = 0
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {  
       if (Settings.canDrawOverlays(this)) {
       floatingView = LayoutInflater.from(this).inflate(R.layout.layout_floating_window.xml, null)
       windowManager.addView(floatingView, layoutParams)
    // 点击浮窗的右上角关闭按钮可以关闭浮窗
       floatingView.findViewById<AppCompatImageView>(R.id.iv_close).setOnClickListener {
        windowManager.removeView(floatingView)
       }
       // 实现浮窗的拖动功能, 通过改变layoutParams来实现
       floatingView.findViewById<AppCompatImageView>(R.id.layout_drag).setOnTouchListener { v, event ->
        when (event.action) {
               MotionEvent.ACTION_DOWN -> {
                   x = event.rawX.toInt()
                   y = event.rawY.toInt()
               }
               MotionEvent.ACTION_MOVE -> {
                   val currentX = event.rawX.toInt()
                   val currentY = event.rawY.toInt()
                   val offsetX = currentX - x
                   val offsetY = currentY - y
                   x = currentX
                   y = currentY
                   layoutParams.x = layoutParams.x + offsetX
                   layoutParams.y = layoutParams.y + offsetY
                   // 更新floatingView
                   windowManager.updateViewLayout(floatingView, layoutParams)
               }
           }
           true
       }
       return super.onStartCommand(intent, flags, startId)
    }

    5. 利用广播进行通信


    private var receiver: MyReceiver? = null
    override fun onCreate() {
       // 注册广播
       receiver = MyReceiver()
       val filter = IntentFilter()
       filter.addAction("android.intent.action.MyReceiver")
       registerReceiver(receiver, filter)
    }
    inner class MyReceiver : BroadcastReceiver() {
       override fun onReceive(context: Context, intent: Intent) {
           val content = intent.getStringExtra("content") ?: ""
    // 通过Handler更新UI
           val message = Message.obtain()
           message.what = 0
           message.obj = content
           handler.sendMessage(message)
       }
    }
    val handler = Handler(this.mainLooper) { msg ->
       tvContent.text = msg.obj as String
       false
    }

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


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

    6. 设置权限

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


    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

    此外,还要通过Settings.ACTION_MANAGE_OVERLAY_PERMISSION来让动态设置权限,在Activity中设置。


    // MainActivity.kt
    fun startWindow(view: View?) {
       if (!Settings.canDrawOverlays(this)) {
           startActivityForResult(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName")), 0)
       } else {
           startService(Intent(this@MainActivity, FloatingWindowService::class.java))
       }
    }
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
       super.onActivityResult(requestCode, resultCode, data)
       if (requestCode == 0) {
           if (Settings.canDrawOverlays(this)) {
               Toast.makeText(this, "悬浮窗权限授权成功", Toast.LENGTH_SHORT).show()
               startService(Intent(this@MainActivity, FloatingWindowService::class.java))
           }
       }
    }

    3.3 完整代码


    class FloatingWindowService : Service() {
       private lateinit var windowManager: WindowManager
       private lateinit var layoutParams: WindowManager.LayoutParams
       private lateinit var tvContent: AppCompatTextView
       private lateinit var handler: Handler
    private var receiver: MyReceiver? = null
       private var floatingView: View? = null
       private val stringBuilder = StringBuilder()
    private var x = 0
       private var y = 0
    // 用来判断floatingView是否attached 到 window manager,防止二次removeView导致崩溃
       private var attached = false
    override fun onCreate() {
           super.onCreate()
           // 注册广播
           receiver = MyReceiver()
           val filter = IntentFilter()
           filter.addAction("android.intent.action.MyReceiver")
           registerReceiver(receiver, filter);
    // 获取windowManager并设置layoutParams
           windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
           layoutParams = WindowManager.LayoutParams().apply {
               type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                   WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
               } else {
                   WindowManager.LayoutParams.TYPE_PHONE
               }
               format = PixelFormat.RGBA_8888
    //            format = PixelFormat.TRANSPARENT
               gravity = Gravity.START or Gravity.TOP
               flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
               width = 600
               height = 600
               x = 300
               y = 300
           }
           handler = Handler(this.mainLooper) { msg ->
               tvContent.text = msg.obj as String
               // 当文本超出屏幕自动滚动,保证文本处于最底部
               val offset = tvContent.lineCount * tvContent.lineHeight
               floatingView?.apply {
                   if (offset > height) {
                       tvContent.scrollTo(0, offset - height)
                   }
               }
               false
           }
       }
    override fun onBind(intent: Intent?): IBinder? {
           return null
       }
    @SuppressLint("ClickableViewAccessibility")
       override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
           if (Settings.canDrawOverlays(this)) {
               floatingView = LayoutInflater.from(this).inflate(R.layout.layout_show_log, null)
               tvContent = floatingView!!.findViewById(R.id.tv_log)
               floatingView!!.findViewById<AppCompatImageView>(R.id.iv_close).setOnClickListener {
                   stringBuilder.clear()
                   windowManager.removeView(floatingView)
                   attached = false
               }
               // 设置TextView滚动
               tvContent.movementMethod = ScrollingMovementMethod.getInstance()
    floatingView!!.findViewById<FrameLayout>(R.id.layout_drag).setOnTouchListener { v, event ->
                   when (event.action) {
                       MotionEvent.ACTION_DOWN -> {
                           x = event.rawX.toInt()
                           y = event.rawY.toInt()
                       }
                       MotionEvent.ACTION_MOVE -> {
                           val currentX = event.rawX.toInt()
                           val currentY = event.rawY.toInt()
                           val offsetX = currentX - x
                           val offsetY = currentY - y
                           x = currentX
                           y = currentY
                           layoutParams.x = layoutParams.x + offsetX
                           layoutParams.y = layoutParams.y + offsetY
                           windowManager.updateViewLayout(floatingView, layoutParams)
                       }
                   }
                   true
               }
    windowManager.addView(floatingView, layoutParams)
               attached = true
           }
           return super.onStartCommand(intent, flags, startId)
       }
    override fun onDestroy() {
           // 注销广播并删除浮窗
           unregisterReceiver(receiver)
           receiver = null
           if (attached) {
               windowManager.removeView(floatingView)
           }
       }
    inner class MyReceiver : BroadcastReceiver() {
           override fun onReceive(context: Context, intent: Intent) {
               val content = intent.getStringExtra("content") ?: ""
               stringBuilder.append(content).append("\n")
               val message = Message.obtain()
               message.what = 0
               message.obj = stringBuilder.toString()
               handler.sendMessage(message)
           }
       }
    }

    来源:http://www.androidchina.net/11922.html

    0
    投稿

    猜你喜欢

    手机版 软件编程 asp之家 www.aspxhome.com