Android悬浮窗功能实现

目录

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

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

一.添加权限:

二.动态申请权限:

 三.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 
} 

上述这段代码是用来根据不同的Android版本设置窗口类型的,使用的Kotlin的when表达式,来判断设备的Android版本(Build.VERSION.SDK_INT),并根据版本返回不同的窗口类型(TYPE)

1. 如果 Android 版本大于或等于 Oreo(API 级别 26),则窗口类型为TYPE_APPLICATION_OVERLAY ,这通常用于应用程序的悬浮窗。  
2. 如果 Android 版本大于或等于 Nougat MR1(API 级别 25),则窗口类型为  TYPE_PHONE ,这通常用于电话应用的窗口。 
3. 如果以上两个条件都不满足,则使用  TYPE_TOAST ,这通常用于显示短暂的 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相关的代码我推荐大家使用单例,我这里没有使用单例,运行效果如下:

 

1. 在 AndroidManifest.xml 文件中添加权限: ```xml <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> ``` 2. 创建一个服务类,继承自 Service: ```java public class FloatingService extends Service { @Nullable @Override public IBinder onBind(Intent intent) { return null; } @Override public int onStartCommand(Intent intent, int flags, int startId) { createFloatingWindow(); return super.onStartCommand(intent, flags, startId); } private void createFloatingWindow() { // 创建悬浮窗 } @Override public void onDestroy() { super.onDestroy(); // 关闭悬浮窗 } } ``` 3. 在 onCreate() 方法中启动服务: ```java public void onCreate() { super.onCreate(); Intent intent = new Intent(this, FloatingService.class); startService(intent); } ``` 4. 在 createFloatingWindow() 方法中创建悬浮窗: ```java private void createFloatingWindow() { // 初始化 WindowManager WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE); // 创建悬浮窗布局 View floatingView = LayoutInflater.from(this).inflate(R.layout.floating_window, null); // 设置悬浮窗布局参数 WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(); layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT; layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT; layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; layoutParams.format = PixelFormat.TRANSLUCENT; layoutParams.gravity = Gravity.CENTER; // 添加悬浮窗布局到 WindowManager 中 windowManager.addView(floatingView, layoutParams); } ``` 5. 在 onDestroy() 方法中关闭悬浮窗: ```java @Override public void onDestroy() { super.onDestroy(); WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE); windowManager.removeView(floatingView); } ``` 6. 创建悬浮窗布局文件 floating_window.xml: ```xml <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:background="#FF0000" android:layout_width="wrap_content" android:layout_height="wrap_content"> <TextView android:text="悬浮窗" android:textColor="#FFFFFF" android:textSize="20sp" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </LinearLayout> ``` 7. 运行程序即可看到悬浮窗
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值