您的当前位置:首页正文

Android悬浮窗功能实现

2024-10-23 来源:个人技术集锦

一.使用悬浮窗功能的原因:

二.悬浮窗的具体实现步骤:

一.添加权限:

二.动态申请权限:

 三.WindowManager.LayoutParams的TYPE类型:

四.编写代码:

1.XML代码:

2.Activity相关代码:

3.WindowManager相关代码 


一.使用悬浮窗功能的原因:

      在Android开发中我们想要做到提醒用户关键信息的作用的时候.例如App更新信息之类的,有很多种方式去实现,主要的话还是三种方式Dialog,AlertDialog,PopupWindow,但是他们都有一个共同的缺点那就是依赖于Activity,而悬浮窗是不依赖Activity的,甚至,App在后台运行,悬浮窗依旧会弹出来,只要App进程不被杀死,但是悬浮窗也有缺点,那就是权限问题

二.悬浮窗的具体实现步骤:

一.添加权限:

<!-- 悬浮窗需要添加该权限-->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

二.动态申请权限:

 private fun initFloatWindow() {
        // 权限判断
        if (!Settings.canDrawOverlays(applicationContext)) {
            // 启动Activity让用户授权
            val mIntent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
            Uri.parse("package:${packageName}"))
            startActivityForResult(mIntent, 10)
        } else {
            // 已经有权限了,就去初始化对应的视图或者悬浮窗弹窗的初始化
            initView()
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (resultCode == 10) {
            if (Settings.canDrawOverlays(applicationContext)) {
                initView()
            } else {
                ToastUtils.showToast(this, "请设置对应权限")
            }
        }
    }

       这段代码是用于在Activity中动态申请悬浮窗权限的,上面那个 ToastUtils是我自己封装的吐司类,这个很简单,相信大家都会,最终我们进入对应的Activity当中我们会跳转到这样一个界面来获取该应用的悬浮窗权限,像电视或者车载开发之类的对于权限已经做了静默处理了,总不能我们在使用车载导航的时候,弹出一个是否开启定位吧哈哈
 

 三.WindowManager.LayoutParams的TYPE类型:

     为什么要将这个TYPE类型放在第三位呢,因为这个就是导致各种问题出现的罪魁祸首

type = when { 
    Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY 
    Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 -> WindowManager.LayoutParams.TYPE_PHONE 
    else -> WindowManager.LayoutParams.TYPE_TOAST 
} 

我举一个例子,如下: 

这是因为我的手机为Android7我用的是被淘汰的TYPE类型,这个情况下我们需要更换TYPE类型就能解决对应的问题 

四.编写代码:

1.XML代码:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >
    <TextView
        android:background="#f46d43"
        android:paddingVertical="10dp"
        android:gravity="center"
        android:textSize="28sp"
        android:textColor="#ffffff"
        android:text="WindowManager与悬浮框"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

    <Button
        android:textAllCaps="false"
        android:id="@+id/activityshow_btn"
        android:text="Activity显示悬浮框"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

    <Button
        android:textAllCaps="false"
        android:id="@+id/serviceshow_btn"
        android:text="Service显示悬浮框"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

    <Button
        android:textAllCaps="false"
        android:id="@+id/activityshow_btn_cancle"
        android:text="Activity隐藏悬浮框"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>


</LinearLayout>
2.Activity相关代码:
/**
 *  悬浮窗: 不依赖Activity, 当Activity销毁的时候悬浮窗仍然可以显示,注意悬浮窗需要手动获取权限以及声明权限
 */
class FloatingWindowActivity : AppCompatActivity(), View.OnClickListener {
    private lateinit var mActivityFloatingWindow: ActivityFloatingWindow
    private val mServiceshowBtn by lazy { findViewById<Button>(R.id.serviceshow_btn) }
    private val mActivityshowBtn by lazy { findViewById<Button>(R.id.activityshow_btn) }
    private val m_activityshow_btn_cancle by lazy { findViewById<Button>(R.id.activityshow_btn_cancle) }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_floating_window)
        initFloatWindow()
        initListener()
    }

    private fun initFloatWindow() {
        // 权限判断
        if (!Settings.canDrawOverlays(applicationContext)) {
            // 启动Activity让用户授权
            val mIntent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
            Uri.parse("package:${packageName}"))
            startActivityForResult(mIntent, 10)
        } else {
            // 已经有权限了
            initView()
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (resultCode == 10) {
            if (Settings.canDrawOverlays(applicationContext)) {
                initView()
            } else {
                ToastUtils.showToast(this, "请设置对应权限")
            }
        }
    }

    private fun initView() {
        mActivityFloatingWindow = ActivityFloatingWindow(this)
    }

    private fun initListener() {
        mServiceshowBtn.setOnClickListener(this)
        mActivityshowBtn.setOnClickListener(this)
        m_activityshow_btn_cancle.setOnClickListener(this)
    }

    override fun onClick(v: View?) {
        when(v?.id) {
            R.id.serviceshow_btn -> {

            }
            R.id.activityshow_btn -> {
                activityShowFloatingWindow()
            }
            R.id.activityshow_btn_cancle -> {
                mActivityFloatingWindow.remove()
            }
        }
    }

    private fun activityShowFloatingWindow() {
        mActivityFloatingWindow.showFloatWindow()
    }
}
3.WindowManager相关代码 
class ActivityFloatingWindow(context: Context) : View.OnTouchListener {
    private var mContext: Context
    private lateinit var mWindowParams: WindowManager.LayoutParams
    private lateinit var mWindowManager: WindowManager

    private lateinit var rootLayout: View

    init {
        mContext = context
        initFloatWindow()
    }

    private var mInViewX = 0f
    private var mInViewY = 0f
    private var mDownInScreenX = 0f
    private var mDownInScreenY = 0f
    private var mInScreenX = 0f
    private var mInScreenY = 0f
    var isMoving = false

    /**
     *  初始化布局
     */
    private fun initFloatWindow() {
        rootLayout = LayoutInflater.from(mContext)
            .inflate(R.layout.floatingwidow_in_activity, null)
        rootLayout.setOnTouchListener(this)
        mWindowParams = WindowManager.LayoutParams()
        mWindowManager = MyApp.mApplicationContext?.getSystemService(Context.WINDOW_SERVICE) as WindowManager
        mWindowParams.type = WindowManager.LayoutParams.TYPE_PHONE
        mWindowParams.format = PixelFormat.RGBA_8888
        // 设置悬浮窗不获取焦点的原因就是为了传递事件
        mWindowParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
        mWindowParams.gravity = Gravity.START or Gravity.TOP
        mWindowParams.height = WindowManager.LayoutParams.WRAP_CONTENT
        mWindowParams.width = WindowManager.LayoutParams.WRAP_CONTENT
        defaultPosition()
    }

    fun showFloatWindow() {
        if (null == rootLayout.parent) {
            mWindowManager.addView(rootLayout, mWindowParams)
        }
    }

    fun remove() {
        if (null != rootLayout.parent) {
            mWindowManager.removeView(rootLayout)
        }
    }

    override fun onTouch(v: View?, event: MotionEvent?): Boolean {
       return floatLayoutTouch(event!!)
    }

    private fun floatLayoutTouch(motionEvent: MotionEvent): Boolean {
        when (motionEvent.action) {
            MotionEvent.ACTION_DOWN -> {
                // 获取相对View的坐标,即以此View左上角为原点
                mInViewX = motionEvent.x
                mInViewY = motionEvent.y
                // 获取相对屏幕的坐标,即以屏幕左上角为原点
                mDownInScreenX = motionEvent.rawX
                mDownInScreenY = motionEvent.rawY
                mInScreenX = motionEvent.rawX
                mInScreenY = motionEvent.rawY
                isMoving = true
            }
            MotionEvent.ACTION_MOVE -> {
                // 更新浮动窗口位置参数
                mInScreenX = motionEvent.rawX
                mInScreenY = motionEvent.rawY
                mWindowParams.x = (mInScreenX - mInViewX).toInt()
                mWindowParams.y = (mInScreenY - mInViewY).toInt()
                // 手指移动的时候更新小悬浮窗的位置
                mWindowManager.updateViewLayout(rootLayout, mWindowParams)
                isMoving = true
            }
            MotionEvent.ACTION_UP -> {
                isMoving = false
                // 如果手指离开屏幕时,xDownInScreen和xInScreen相等,且yDownInScreen和yInScreen相等,则视为触发了单击事件。
                if (mDownInScreenX == mInScreenX && mDownInScreenY == mInScreenY) {
                    Toast.makeText(mContext, "Click", Toast.LENGTH_SHORT).show()
                }
            }
        }
        return true
    }

    private fun defaultPosition() {
        val metrics = DisplayMetrics()
        // 默认固定位置,靠屏幕左边缘的中间
        mWindowManager.defaultDisplay.getMetrics(metrics)
        mWindowParams.x = 0
        mWindowParams.y = metrics.heightPixels/2
    }
}

 !!!这里的WIndowManager相关的代码我推荐大家使用单例,我这里没有使用单例,运行效果如下:

显示全文