最全面的-Android-悬浮窗功能实现--[转]

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())
    }

//开始触控的坐标,移动时的坐标(相对于屏幕左上角的坐标)
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
}
}

/**

  • 初始化窗口
    */
    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)
    chronometer = mFloatingLayout!!.findViewById(R.id.chronometer)
    chronometer!!.start()
    // 添加悬浮窗的视图
    winManager!!.addView(mFloatingLayout, wmParams)
    }

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
}

override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
return super.onStartCommand(intent, flags, startId)
}

override fun onDestroy() {
super.onDestroy()
winManager!!.removeView(mFloatingLayout)
}
}

  • 实际应用中需要考虑的一些其他问题

在使用使用的过程中,我们肯定会遇到其他问题:

1.用户使用过程中,可能会直接按Home键,这个时候如何提示呢?

产生问题原因:因为用户按Home键之后,开发者无法重写Home键逻辑,此时应用不在前台运行,无法弹窗提醒,此时用户点击APP图标进入的是第一个栈,这个时候用户就没有进入通话页面的入口了。

解决方案:

第一种解决方案 我们可以仿照微信那样去做,就是在整个通话过程中开启一个前台通知,用户点击通知时进入通话页面。

第二种解决方案 就是检测应用是否在前台,当通话页面在运行的时候,并且应用重新回到前台,我们广播到其他页面,提示权限引导即可。

2.用户在通话页面(singleInstance模式),点击Home键

应用在后台运行的时候,通话结束,Activity被finish,此时从任务程序中切回应用你会发现打开的竟然是通话页面!

这个问题简单的说就是,如果你在通话页面呼叫某人,通话过程中按Home键,然后电话挂断,此时你从任务程序中切回应用,会再次呼叫这个人,也就是这种状态下重新回到了onCreate方法。

问题产生原因:

1.因为通话页面是singleInstance模式,此时有两个任务栈,按Home键后再从任务程序中切回,此时应用只保留了第二个任务栈,已经失去了和第一个任务栈的关系,finish之后无法在回到第一个任务栈。

解决方案:

1.(不推荐)通话页面不使用singleInstance模式,这种情况下,在通话过程中无法操作软件的其他功能,一般都不采取。

2.(我目前的解决方案)设置一个标记位,标记当前是否在通话,在onCreate中如果通话已经结束了,跳转到一个过渡页面(标准模式),过渡页面中finish,就可以了,添加过渡页面的原因是我们不知道上一个页面是哪里,因为我们收到来电可能是任意页面,我们我们在过渡页面finsh之后,就再次回到了第一个任务栈。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

感觉现在好多人都在说什么安卓快凉了,工作越来越难找了。又是说什么程序员中年危机啥的,为啥我这年近30的老农根本没有这种感觉,反倒觉得那些贩卖焦虑的都是瞎j8扯谈。当然,职业危机意识确实是要有的,但根本没到那种草木皆兵的地步好吗?

Android凉了都是弱者的借口和说辞。虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。

所以,最后这里放上我耗时两个月,将自己8年Android开发的知识笔记整理成的Android开发者必知必会系统学习资料笔记,上述知识点在笔记中都有详细的解读,里面还包含了腾讯、字节跳动、阿里、百度2019-2021面试真题解析,并且把每个技术点整理成了视频和PDF(知识脉络 + 诸多细节)。

以上全套学习笔记面试宝典,吃透一半保你可以吊打面试官,只有自己真正强大了,有核心竞争力,你才有拒绝offer的权力,所以,奋斗吧!骚年们!千里之行,始于足下。种下一颗树最好的时间是十年前,其次,就是现在。

最后,赠与大家一句诗,共勉!

不驰于空想,不骛于虚声。不忘初心,方得始终。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

droid开发者必知必会系统学习资料笔记,上述知识点在笔记中都有详细的解读,里面还包含了腾讯、字节跳动、阿里、百度2019-2021面试真题解析,并且把每个技术点整理成了视频和PDF(知识脉络 + 诸多细节)。

[外链图片转存中…(img-3zVahr0i-1713638965064)]

以上全套学习笔记面试宝典,吃透一半保你可以吊打面试官,只有自己真正强大了,有核心竞争力,你才有拒绝offer的权力,所以,奋斗吧!骚年们!千里之行,始于足下。种下一颗树最好的时间是十年前,其次,就是现在。

最后,赠与大家一句诗,共勉!

不驰于空想,不骛于虚声。不忘初心,方得始终。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

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

余额充值