溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Android如何實現(xiàn)懸浮窗

發(fā)布時間:2021-09-24 14:43:03 來源:億速云 閱讀:251 作者:小新 欄目:開發(fā)技術(shù)

小編給大家分享一下Android如何實現(xiàn)懸浮窗,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!

    1. 前言

    現(xiàn)在很多應(yīng)用都有小懸浮窗的功能,比如看直播的時候,通過Home鍵返回桌面,直播的小窗口仍可以在屏幕上顯示。

    2.原理

    Window我們應(yīng)該很熟悉,它是一個接口類,具體的實現(xiàn)類為PhoneWindow,它可以對View進(jìn)行管理。WindowManager是一個接口類,繼承自ViewManager,從名稱就知道它是用來管理Window的,它的實現(xiàn)類是WindowManagerImpl。如果我們想要對Window(View)進(jìn)行添加、更新和刪除操作就可以使用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也繼承了這些方法,而這些方法傳入的參數(shù)都是View類型,說明了Window是以View的形式存在的。

    3.具體實現(xiàn)

    3.1浮窗布局

    懸浮窗的簡易布局如下的可參考下面的layout_floating_window.xml文件。頂層深色部分的FrameLayout布局是用來實現(xiàn)懸浮窗的拖拽功能的,點擊右上角ImageView可以實現(xiàn)關(guān)閉懸浮窗,剩下區(qū)域顯示內(nèi)容,這里只是簡單地顯示文本內(nèi)容,不做復(fù)雜的東西,故只設(shè)置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 懸浮窗的實現(xiàn)

    1. 使用服務(wù)Service

    Service 是一種可在后臺執(zhí)行長時間運行操作而不提供界面的應(yīng)用組件,可由其他應(yīng)用組件啟動,而且即使用戶切換到其他應(yīng)用,仍將在后臺繼續(xù)運行。要保證應(yīng)用在后臺時,懸浮窗仍然可以正常顯示,所以這里可以使用Service。

    2. 獲取WindowManager并設(shè)置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 {
            // 實現(xiàn)在其他應(yīng)用和窗口上方顯示浮窗
            type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
            } else {
                WindowManager.LayoutParams.TYPE_PHONE
            }
            format = PixelFormat.RGBA_8888
            // 設(shè)置浮窗的大小和位置
            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. 創(chuàng)建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. 實現(xiàn)懸浮窗的拖拽和關(guān)閉功能
    // 浮窗的坐標(biāo)
    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)
    
        // 點擊浮窗的右上角關(guān)閉按鈕可以關(guān)閉浮窗
        floatingView.findViewById<AppCompatImageView>(R.id.iv_close).setOnClickListener {
         windowManager.removeView(floatingView)
        }
        // 實現(xiàn)浮窗的拖動功能, 通過改變layoutParams來實現(xiàn)
        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. 利用廣播進(jìn)行通信
    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發(fā)送信息

    fun sendMessage(view: View?) {
        Intent("android.intent.action.MyReceiver").apply {
            putExtra("content", "Hello, World!")
            sendBroadcast(this)
        }
    }
    6. 設(shè)置權(quán)限

    懸浮窗的顯示需要權(quán)限,在AndroidManefest.xml中添加:

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

    此外,還要通過Settings.ACTION_MANAGE_OVERLAY_PERMISSION來讓動態(tài)設(shè)置權(quán)限,在Activity中設(shè)置。

    // 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, "懸浮窗權(quán)限授權(quán)成功", 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導(dǎo)致崩潰
        private var attached = false
    
        override fun onCreate() {
            super.onCreate()
            // 注冊廣播
            receiver = MyReceiver()
            val filter = IntentFilter()
            filter.addAction("android.intent.action.MyReceiver")
            registerReceiver(receiver, filter);
    
            // 獲取windowManager并設(shè)置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
                // 當(dāng)文本超出屏幕自動滾動,保證文本處于最底部
                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
                }
                // 設(shè)置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如何實現(xiàn)懸浮窗”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對大家有所幫助,如果還想學(xué)習(xí)更多知識,歡迎關(guān)注億速云行業(yè)資訊頻道!

    向AI問一下細(xì)節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI