Android 悬浮窗功能的实现

1.悬浮窗可以显示在其他应用或launchers之上,这个肯定需要悬浮窗权限,而悬浮窗权限属于特殊权限,所以只能通过引导用户去打开无法像危险权限那样直接申请。可以做到后台显示则说明悬浮窗是一个Service。

2.通话页面隐藏时悬浮窗显示,通话页面显示时悬浮窗隐藏,可以看出悬浮窗和Activity的生命周期相关联,所以悬浮窗的Service和通话页面的Activity是通过bind去绑定的。

3.既然Service和Activity是通过bind去绑定的,说明当悬浮窗显示的时候,通话Activity虽然不可见但仍在运行。

结合上述技术问题分析,我们倒叙一一通过编码实现

悬浮窗实现方案

  • 实现效果

  • 准备工作

首先我们新建一个项目,项目中有两个Activity,我们在第二个Activity编写通话模拟页面。在第二个页面的原因我们后面会讲到。

  • 如何将acitivity置于后台

其实很简单,我们调用一个方法即可

moveTaskToBack(true);

这个方法的含义就是将当前的任务战置于后台,so,为什么我要在第二个Activity中实现的原因之一,因为默认的Activity的启动模式是标准模式,而上面方法会将任务栈置于后台而不是一个单独的Activity,所以我们为了显示悬浮窗时不影响操作软件的其他功能,我们要将通话页面的Activity设置为singleInstance,这样当调用上面方法的时候只是将通话页面所在的Activity栈置于后台,如果你还不了解启动模式可以移步至上一篇文章:Activity的启动模式

我们现在在右上方的点击事件中添加上述代码,可以看到通话页面的Activity的已经在后台运行了。

  • 判断是否有悬浮窗权限

点击左上角图标时,我们要先判断当前app是否有悬浮窗权限,首先我们在配置文件中添加,悬浮窗的权限。

(很多文章标题都是悬浮窗如何绕过权限,什么设置类型为TOAST或者PHONE,我想说不可能的事,TOAST类型的虽然部分机型可以显示但是就是一个普通的TOSAT会自动消失)

那么我们如何判断是否有悬浮窗权限呢,这一块不同厂商处理方案可能不一样,这里我们用一种通用的处理方案,测试表明除了(vivo部分)无效,其他多数机型都ok。并且vivo部分机型微信通话也不会弹出提示(这我就放心了~)

fun zoom(v: View) {

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {

if (!Settings.canDrawOverlays(this)) {

Toast.makeText(this, “当前无权限,请授权”, Toast.LENGTH_SHORT)

GlobalDialogSingle(this, “”, “当前未获取悬浮窗权限”, “去开启”, DialogInterface.OnClickListener { dialog, which ->

dialog.dismiss()

startActivityForResult(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse(“package:” + packageName)), 0)

}).show()

} else {

moveTaskToBack(true)

val intent = Intent(this@Main2Activity, FloatWinfowServices::class.java)

hasBind = bindService(intent, mVideoServiceConnection, Context.BIND_AUTO_CREATE)

}

}

}

我们通过Settings.canDrawOverlays(this)来判断当前应用是否有悬浮窗权限,如果没有,我们弹窗提示,通过

startActivityForResult(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse(“package:” + packageName)), 0)

跳转到开启悬浮窗权限页面。如果悬浮窗权限已开启,直接将当前任务栈置于后台,开启服务即可。

其实回调方法,并没有直接告诉我们是否授权成功,所以我们需要在回调中再次判断

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {

if (requestCode == 0) {

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {

if (!Settings.canDrawOverlays(this)) {

Toast.makeText(this, “授权失败”, Toast.LENGTH_SHORT).show()

} else {

Handler().postDelayed({

val intent = Intent(this@Main2Activity, FloatWinfowServices::class.java)

intent.putExtra(“rangeTime”, rangeTime)

hasBind = bindService(intent, mVideoServiceConnection, Context.BIND_AUTO_CREATE)

moveTaskToBack(true)

}, 1000)

}

}

}

}

这里我们可以看到回调中延迟了1秒,因为测试发现某些机型反应“过快”,收到回调的时候还以为没有授权成功,其实已经成功了。

绑定Service我们需要一个ServiceConnection对象

internal var mVideoServiceConnection: ServiceConnection = object : ServiceConnection { override fun onServiceConnected(name: ComponentName, service: IBinder) { // 获取服务的操作对象 val binder = service as FloatWinfowServices.MyBinder binder.service } override fun onServiceDisconnected(name: ComponentName) {} }

Main2Activity的完整代码如下所示:

/**

  • @author Huanglinqing

*/

class Main2Activity : AppCompatActivity() {

private val chronometer: Chronometer? = null

private var hasBind = false

private val rangeTime: Long = 0

override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

setContentView(R.layout.activity_main2)

}

fun zoom(v: View) {

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {

if (!Settings.canDrawOverlays(this)) {

Toast.makeText(this, “当前无权限,请授权”, Toast.LENGTH_SHORT)

GlobalDialogSingle(this, “”, “当前未获取悬浮窗权限”, “去开启”, DialogInterface.OnClickListener { dialog, which ->

dialog.dismiss()

startActivityForResult(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse(“package:” + packageName)), 0)

}).show()

} else {

moveTaskToBack(true)

val intent = Intent(this@Main2Activity, FloatWinfowServices::class.java)

hasBind = bindService(intent, mVideoServiceConnection, Context.BIND_AUTO_CREATE)

}

}

}

internal var mVideoServiceConnection: ServiceConnection = object : ServiceConnection {

override fun onServiceConnected(name: ComponentName, service: IBinder) {

// 获取服务的操作对象

val binder = service as FloatWinfowServices.MyBinder

binder.service

}

override fun onServiceDisconnected(name: ComponentName) {}

}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {

if (requestCode == 0) {

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {

if (!Settings.canDrawOverlays(this)) {

Toast.makeText(this, “授权失败”, Toast.LENGTH_SHORT).show()

} else {

Handler().postDelayed({

val intent = Intent(this@Main2Activity, FloatWinfowServices::class.java)

intent.putExtra(“rangeTime”, rangeTime)

hasBind = bindService(intent, mVideoServiceConnection, Context.BIND_AUTO_CREATE)

moveTaskToBack(true)

}, 1000)

}

}

}

}

override fun onRestart() {

super.onRestart()

Log.d(“RemoteView”, “重新显示了”)

//不显示悬浮框

if (hasBind) {

unbindService(mVideoServiceConnection)

hasBind = false

}

}

override fun onNewIntent(intent: Intent) {

super.onNewIntent(intent)

}

override fun onDestroy() {

super.onDestroy()

}

}

  • 新建悬浮窗Service

新建悬浮窗Service FloatWinfowServices,因为我们使用的BindService,我们在onBind方法中初始化service中的布局

override fun onBind(intent: Intent): IBinder? {

initWindow()

//悬浮框点击事件的处理

initFloating()

return MyBinder()

}

service中我们通过WindowManager来添加一个布局显示。

/**

  • 初始化窗口

*/

private fun initWindow() {

winManager = application.getSystemService(Context.WINDOW_SERVICE) as WindowManager

//设置好悬浮窗的参数

wmParams = params

// 悬浮窗默认显示以左上角为起始坐标

wmParams!!.gravity = Gravity.LEFT or Gravity.TOP

//悬浮窗的开始位置,因为设置的是从左上角开始,所以屏幕左上角是x=0;y=0

wmParams!!.x = winManager!!.defaultDisplay.width

wmParams!!.y = 210

//得到容器,通过这个inflater来获得悬浮窗控件

inflater = LayoutInflater.from(applicationContext)

// 获取浮动窗口视图所在布局

mFloatingLayout = inflater!!.inflate(R.layout.remoteview, null)

// 添加悬浮窗的视图

winManager!!.addView(mFloatingLayout, wmParams)

}

悬浮窗的参数主要设置悬浮窗的类型为

WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY

8.0 以下可设置为:

wmParams!!.type = WindowManager.LayoutParams.TYPE_PHONE

代码如下所示:

private //设置window type 下面变量2002是在屏幕区域显示,2003则可以显示在状态栏之上

//设置可以显示在状态栏上

//设置悬浮窗口长宽数据

val params: WindowManager.LayoutParams

get() {

wmParams = WindowManager.LayoutParams()

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

wmParams!!.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY

} else {

wmParams!!.type = WindowManager.LayoutParams.TYPE_PHONE

}

wmParams!!.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or

WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR or

WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH

wmParams!!.width = WindowManager.LayoutParams.WRAP_CONTENT

wmParams!!.height = WindowManager.LayoutParams.WRAP_CONTENT

return wmParams

}

当点击悬浮窗的时候回到Activity2页面,并且悬浮窗消失,所以我们只需要给悬浮窗添加点击事件

linearLayout!!.setOnClickListener { startActivity(Intent(this@FloatWinfowServices, Main2Activity::class.java)) }

当Service走到onDestory的时候将view移除,对于Activity2页面来说 当onResume的时候 解绑Service,当onstop的时候 绑定Service。

从效果图中我们可以看到悬浮窗可以拖拽的,所以还要设置触摸事件,当移动距离超过某个值的时候让onTouch消费事件,这样就不会触发点击事件了。这个算是view比较基础的知识,相信大家都明白了。

//开始触控的坐标,移动时的坐标(相对于屏幕左上角的坐标)

private var mTouchStartX: Int = 0

private var mTouchStartY: Int = 0

private var mTouchCurrentX: Int = 0

private var mTouchCurrentY: Int = 0

//开始时的坐标和结束时的坐标(相对于自身控件的坐标)

private var mStartX: Int = 0

private var mStartY: Int = 0

private var mStopX: Int = 0

private var mStopY: Int = 0

//判断悬浮窗口是否移动,这里做个标记,防止移动后松手触发了点击事件

private var isMove: Boolean = false

private inner class FloatingListener : View.OnTouchListener {

override fun onTouch(v: View, event: MotionEvent): Boolean {

val action = event.action

when (action) {

MotionEvent.ACTION_DOWN -> {

isMove = false

mTouchStartX = event.rawX.toInt()

mTouchStartY = event.rawY.toInt()

mStartX = event.x.toInt()

mStartY = event.y.toInt()

}

MotionEvent.ACTION_MOVE -> {

mTouchCurrentX = event.rawX.toInt()

mTouchCurrentY = event.rawY.toInt()

wmParams!!.x += mTouchCurrentX - mTouchStartX

wmParams!!.y += mTouchCurrentY - mTouchStartY

winManager!!.updateViewLayout(mFloatingLayout, wmParams)

mTouchStartX = mTouchCurrentX

mTouchStartY = mTouchCurrentY

}

MotionEvent.ACTION_UP -> {

mStopX = event.x.toInt()

mStopY = event.y.toInt()

if (Math.abs(mStartX - mStopX) >= 1 || Math.abs(mStartY - mStopY) >= 1) {

isMove = true

}

}

else -> {

}

}

//如果是移动事件不触发OnClick事件,防止移动的时候一放手形成点击事件

return isMove

}

}

FloatWinfowServices所有代码如下所示:

class FloatWinfowServices : Service() {

private var winManager: WindowManager? = null

private var wmParams: WindowManager.LayoutParams? = null

private var inflater: LayoutInflater? = null

//浮动布局

private var mFloatingLayout: View? = null

private var linearLayout: LinearLayout? = null

private var chronometer: Chronometer? = null

override fun onBind(intent: Intent): IBinder? {

initWindow()

//悬浮框点击事件的处理

initFloating()

return MyBinder()

}

inner class MyBinder : Binder() {

val service: FloatWinfowServices

get() = this@FloatWinfowServices

}

override fun onCreate() {

super.onCreate()

}

/**

  • 悬浮窗点击事件

*/

private fun initFloating() {

linearLayout = mFloatingLayout!!.findViewById(R.id.line1)

linearLayout!!.setOnClickListener { startActivity(Intent(this@FloatWinfowServices, Main2Activity::class.java)) }

//悬浮框触摸事件,设置悬浮框可拖动

linearLayout!!.setOnTouchListener(FloatingListener())

  • 27
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
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、付费专栏及课程。

余额充值