温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

Android如何实现悬浮窗

发布时间:2021-09-24 14:43:03 来源:亿速云 阅读:252 作者:小新 栏目:开发技术

小编给大家分享一下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)
            }
        }
    }

    以上是“Android如何实现悬浮窗”这篇文章的所有内容,感谢各位的阅读!相信大家都有了一定的了解,希望分享的内容对大家有所帮助,如果还想学习更多知识,欢迎关注亿速云行业资讯频道!

    向AI问一下细节

    免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

    AI