美文网首页UI
利用DecorView实现悬浮窗的效果

利用DecorView实现悬浮窗的效果

作者: 超级绿茶 | 来源:发表于2020-08-09 10:43 被阅读0次

    由于众所周知的原因,Android系统虽然提供了悬浮窗的功能,但使用之前需要用记授权,有些手机对这个授权还要再次确认,以至于很多用户出于谨慎的目地就不去打开了。但我们在实际开发当中却又需要这个功能是该怎么?

    既然直接使用是不行了,那只能考虑折中的办法了。首先能想到的是把悬浮窗作在XML布局里面,不用时隐藏,需要时显示。但如果我们遇到一批需要悬浮窗的界面时要怎么办?有一个比较稳妥的方法;从DecorView下手!

    我们知道Activity对于界面的加载是交给自己的手下PhoneWindow去处理的,PhoneWindow再交给自己的手下DecorView。DecorView虽然是一个FrameLayout的自定义类,理论上由它来加载就可以了,但DecorView还挺能整活,它不直接加载我们定义的XML布局,而是先加载了一个系统内置的布局,这个内置的布局是LinearLayout结构的,上面是一个标题区,下面是内容区,内容区就是一个FrameLayout,并且有一个系统级的ID作为标记。这个内置布局结构如下:

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true"
        android:orientation="vertical">
        <ViewStub android:id="@+id/action_mode_bar_stub"
                  android:inflatedId="@+id/action_mode_bar"
                  android:layout="@layout/action_mode_bar"
                  android:layout_width="match_parent"
                  android:layout_height="wrap_content"
                  android:theme="?attr/actionBarTheme" />
        <FrameLayout
             android:id="@android:id/content"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:foregroundInsidePadding="false"
             android:foregroundGravity="fill_horizontal|top"
             android:foreground="?android:attr/windowContentOverlay" />
    </LinearLayout>
    

    我们自定义的XML布局就是被加载到这个ID为content的FrameLayout里的。由于这是一个系统内置的布局,每一个Activity都有,所以我们可以充分利用这一便利来给实现我们的悬浮窗效果。

    大致的思路是这样的:

    1. 自定义一个Activity子类专门用于处理悬浮窗的操作,在项目中需要用到悬浮窗的地方就继承这个类。
    2. 在自定义的Activity子类中通过findViewById<ViewGroup>(android.R.id.content)获取到系统内置的内容布局(ContentFrameLayout)。
    3. 自定义一个View作为我们的悬浮窗,然后将这个View添加到内容布局。

    在上述步骤之前ID为content的内容布局之下原本只有一个子布局,即我们在setContentView里指定的XML布局资源,执行了上述步骤之后内容布局又会多出一个子布局,由于内容布局本身就是一个FrameLayout,所以后加的布局自然放在最上层,这就是悬浮窗的效果了。

    下面就来实例个带拖动效果的悬浮窗


    悬浮窗带拖动效果

    先自定义一个FloatActivity,封装一下悬浮窗的功能。由于我们的悬浮窗是带有拖动功能的,所以还需要把窗体的坐标保存在配置文件里,以便于Activity在跳转的时候可以读取得到:

    
    import android.annotation.SuppressLint
    import android.content.Context
    import android.os.Bundle
    import android.view.LayoutInflater
    import android.view.MotionEvent
    import android.view.View
    import android.view.ViewGroup
    import android.widget.FrameLayout
    import androidx.appcompat.app.AppCompatActivity
    import androidx.core.content.edit
    import androidx.lifecycle.Lifecycle
    import androidx.lifecycle.LifecycleObserver
    import androidx.lifecycle.OnLifecycleEvent
    import kotlinx.android.synthetic.main.float_window.view.*
    
    /**
     * 带悬浮窗操作的Activity
     */
    open class FloatActivity : AppCompatActivity() {
        private var offX = 0
        private var offY = 0
        private var isPressing = false
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            // 根据Activity的生命周期显示或移除悬浮窗
            this.lifecycle.addObserver(object : LifecycleObserver {
                @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
                fun onResume() {
                    // 在Activity可见时加载悬浮窗
                    showFloatWindow(isShowing(applicationContext))
                }
    
                @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
                fun onStop() {
                    // 由于悬浮窗是每个Activity都有的,所以在暂停时移除以释放资源
                    removeFloatWindow()
                }
            })
        }
    
        /**
         * 定义一个悬浮窗,这个悬浮窗只是一个普通的View对象
         * 可以根据需求定义不同的窗体
         */
        @SuppressLint("ClickableViewAccessibility")
        private fun buildFloatWindow(): View {
            val view = LayoutInflater.from(this).inflate(R.layout.float_window, null, false)
            view.layoutParams = ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT
            )
            view.btnClose.setOnClickListener { showFloatWindow(false) }
            view.ivIcon.setOnTouchListener { v, event ->
                when (event.action) {
                    MotionEvent.ACTION_DOWN -> {
                        offX = event.rawX.toInt() - view.x.toInt()
                        offY = event.rawY.toInt() - view.y.toInt()
                        isPressing = true
                        return@setOnTouchListener true
                    }
                    MotionEvent.ACTION_UP -> {
                        isPressing = false
                        saveLocation(this, view.x.toInt(), view.y.toInt())
                        return@setOnTouchListener true
                    }
                    MotionEvent.ACTION_MOVE -> {
                        if (isPressing) {
                            view.x = event.rawX - offX
                            view.y = event.rawY - offY
                        }
                        return@setOnTouchListener true
                    }
                }
                false
            }
            return view
        }
    
        /**
         * 设置悬浮窗的显示或隐藏
         */
        protected fun showFloatWindow(isShow: Boolean) {
            val vgContent = findViewById<ViewGroup>(android.R.id.content)
            if (vgContent.childCount == 1) {
                vgContent.addView(FrameLayout(this))
            }
            val vgFloatContainer = vgContent.getChildAt(1) as ViewGroup
            if (isShow) {
                if (vgFloatContainer.childCount == 0) {
                    val viewFloat = buildFloatWindow()
                    viewFloat.x = loadLocationX(this).toFloat()
                    viewFloat.y = loadLocationY(this).toFloat()
                    vgFloatContainer.addView(viewFloat)
                }
            } else {
                vgFloatContainer.removeAllViews()
            }
            setShowing(this, isShow)
        }
    
        /**
         * 删除悬浮窗
         */
        protected fun removeFloatWindow() {
            val vgContent = findViewById<ViewGroup>(android.R.id.content)
            if (vgContent.childCount == 2)
                vgContent.removeViewAt(1)
        }
    
        /**
         * 保存和读取悬浮窗的参数到配置文件
         */
        companion object {
            private const val FILE = "floatWindow"
            fun isShowing(ctx: Context): Boolean {
                return ctx.getSharedPreferences(FILE, Context.MODE_PRIVATE)
                    .getBoolean("show", false)
            }
    
            private fun setShowing(ctx: Context, isShowing: Boolean) {
                ctx.getSharedPreferences(FILE, Context.MODE_PRIVATE)
                    .edit { putBoolean("show", isShowing) }
            }
    
            private fun loadLocationX(ctx: Context): Int {
                return ctx.getSharedPreferences(FILE, Context.MODE_PRIVATE)
                    .getInt("x", 0)
            }
    
            private fun loadLocationY(ctx: Context): Int {
                return ctx.getSharedPreferences(FILE, Context.MODE_PRIVATE)
                    .getInt("y", 0)
            }
    
            private fun saveLocation(ctx: Context, x: Int, y: Int) {
                ctx.getSharedPreferences(FILE, Context.MODE_PRIVATE)
                    .edit {
                        putInt("x", x)
                        putInt("y", y)
                    }
            }
        }
    }
    

    我们自定义了一个View作为悬浮窗,然后把这个View添加到一个全屏的FrameLayout里面,悬浮窗的移动都是在这个FrameLayout里实现的,最后再把这个FrameLayout添加到内容布局之中。

    float_window.xml

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@android:color/white"
        android:elevation="6dp"
        android:gravity="center_vertical"
        android:orientation="horizontal"
        android:padding="12dp">
    
        <ImageView
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:id="@+id/ivIcon"
            android:src="@mipmap/ic_launcher_round" />
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:padding="12dp"
            android:text="点击图标拖拽" />
    
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/btnClose"
            android:text="关闭" />
    </LinearLayout>
    

    在项目中只需简单的调用即可,MainActivity.kt

    import android.content.Intent
    import android.os.Bundle
    import kotlinx.android.synthetic.main.activity_main.*
    
    class MainActivity : FloatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            btnSecond.setOnClickListener { startActivity(Intent(this, SecondActivity::class.java)) }
    
            chkShowFloatWindow.setOnCheckedChangeListener { _, isChecked ->
                showFloatWindow(isChecked) //根据勾选值显示或隐藏悬浮窗
            }
        }
    
        override fun onResume() {
            super.onResume()
            chkShowFloatWindow.isChecked = FloatActivity.isShowing(this)
        }
    }
    

    activity_main.xml

    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <Button
            android:id="@+id/btnSecond"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="跳转SecondActivity"
            android:textAllCaps="false"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    
        <CheckBox
            android:id="@+id/chkShowFloatWindow"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="20dp"
            android:text="显示悬浮窗"
            app:layout_constraintLeft_toLeftOf="@+id/btnSecond"
            app:layout_constraintRight_toRightOf="@id/btnSecond"
            app:layout_constraintTop_toBottomOf="@id/btnSecond" />
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    

    SecondActivity.kt

    import android.os.Bundle
    import kotlinx.android.synthetic.main.activity_second.*
    
    class SecondActivity : FloatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_second)
    
            btnBack.setOnClickListener { onBackPressed() }
    
            chkShowFloatWindow.setOnCheckedChangeListener { _, isChecked -> showFloatWindow(isChecked) }
        }
    
        override fun onResume() {
            super.onResume()
            chkShowFloatWindow.isChecked = FloatActivity.isShowing(this)
        }
    }
    

    activity_second.xml

    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".SecondActivity">
    
        <Button
            android:id="@+id/btnBack"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="返回"
            android:textAllCaps="false"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    
        <CheckBox
            android:id="@+id/chkShowFloatWindow"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="20dp"
            android:text="显示悬浮窗"
            app:layout_constraintLeft_toLeftOf="@+id/btnBack"
            app:layout_constraintRight_toRightOf="@id/btnBack"
            app:layout_constraintTop_toBottomOf="@id/btnBack" />
    </androidx.constraintlayout.widget.ConstraintLayout>
    

    点击链接加入QQ群聊:https://jq.qq.com/?_wv=1027&k=5z4fzdT
    或关注微信公众号:口袋里的安卓

    相关文章

      网友评论

        本文标题:利用DecorView实现悬浮窗的效果

        本文链接:https://www.haomeiwen.com/subject/bhbzrktx.html