Android | 您所在的位置:网站首页 › ios视频悬浮窗功能实现 › Android |
// 获取服务的操作对象 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之后,就再次回到了第一个任务栈。 如果有其他好的解决方案 欢迎留言。 原文地址:https://huanglinqing.blog.csdn.net/article/details/95372212 自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。 深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前! 因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。 既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化! 由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新! 如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android) ![]() 我花了将近半个月时间将:深入 Java 泛型.、注解深入浅出、并发编程.、数据传输与序列化、Java 虚拟机原理、反射与类加载、高效 IO、Kotlin项目实战等等Android架构师筑基必备技能整合成了一套系统知识笔记PDF,相信看完这份文档,你将会对这些Android架构师筑基必备技能有着更深入、更系统的理解。 由于文档内容过多,为了避免影响到大家的阅读体验,在此只以截图展示部分内容 注:资料与上面思维导图一起看会更容易学习哦!每个点每个细节分支,都有对应的目录内容与知识点!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取! 统知识笔记PDF,相信看完这份文档,你将会对这些Android架构师筑基必备技能有着更深入、更系统的理解。 由于文档内容过多,为了避免影响到大家的阅读体验,在此只以截图展示部分内容 注:资料与上面思维导图一起看会更容易学习哦!每个点每个细节分支,都有对应的目录内容与知识点![外链图片转存中…(img-rNoLbmuK-1713214318198)] [外链图片转存中…(img-vl4arhil-1713214318199)] 这份资料就包含了所有Android初级架构师所需的所有知识! 《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取! |
CopyRight 2018-2019 实验室设备网 版权所有 |