原来今天才是周一,昨天那篇不算哈,本来周日就不应该给大家推送的,好像是我自己日子过糊涂了。
今天的这篇文章来自 码农小阿飞 ,虽说他写博客的时间并不久,但今天投稿的这篇悬浮窗的文章真是6得飞起。我之前也写过一些悬浮窗相关的文章,但都偏向于使用基础知识来完成一些简单的效果,而码家小阿飞的这篇文章算是将悬浮窗的效果玩到了极致。文章代码比较长,想下载源码的可以点击最下方的 阅读原文 链接。
码农小阿飞的博客地址:http://blog.csdn.net/mario_0824
基础部分
今天我要讲的是如何用 WindowManager 去实现一个悬浮窗迷你音乐盒。首先介绍 WindowManager 和它的一些属性,可能会有些枯燥。
在Android应用开发中,其实整个Android的窗口机制是基于一个叫做 WindowManager 的一个系统服务接口,WindowManager 可以添加view到屏幕,也可以从屏幕删除view。它面向的对象一端是屏幕,另一端就是View,其实就连我们常用的Activity和Diolog的底层实现都是通过 WindowManager, WindowManager 是全局的,整个系统就只用一个 Windowmanager服务,我们需要向系统获取服务才能调用它,而它就是显示View的最底层。
其实WindowManager用起来非常方便,就三个方法:
1. 添加View
addView(View view, WindowManager.LayoutParams params);
从方法中我们可以看到,addView需要两个参数,view 简单,就是我们要向窗口中去添加的对象,至于 params,就是给窗口设置的显示策略,包括窗口的大小、透明度等等,在后文会有所介绍。
2. 移除View
removeView(View view);
既然能够向窗口去添加View,当然也就能够从窗口上移除View,这个很简单 view 就是你要从窗口中移除的对象。
3. 刷新View
updateViewLayout(View view, ViewGroup.LayoutParams params)
同样窗口刷新也需要两个参数,和添加View一样 view 是需要更新的对象,而 params 就是更新后的策略属性。
相比于 WindowManager,WindowManager.LayoutParams 可就要复杂好多了。WindowManager.LayoutParams 是 WindowManager 接口的嵌套类,在窗口管理中扮演着重要的角色。它继承于 ViewGroup.LayoutParams,它用于向 WindowManager 描述窗口的管理策略。
WindowManager.LayoutParams 可以直接 new WindowManager.LayoutParams() 新建,也可以从对窗口的 getAttributes() 得到其 WindowManager.LayoutParams 对象。WindowManager.LayoutParams 常用的有以下主要常量成员:
flag:
WindowManager.LayoutParams.FLAG_SECURE
不允许截屏;设置了这个属性的窗口,在窗口可见的情况下,是会禁用系统的截图功能的。那么问题来了:假如有一天,你的公司要求写一个类似于‘阅后即焚’功能的页面的话,不妨在activity中获得WindowManager.LayoutParams并添加该属性,轻轻松松搞定。
WindowManager.LayoutParams.FLAG_BLUR_BEHIND
背景模糊;假如你的窗口设置了这个属性,并且这个窗口可见,在这窗口之后的所有背景都会被模糊化,但我还没有发现一个属性是可以控制模糊程度的。
WindowManager.LayoutParams.FLAG_DIM_BEHIND
背景变暗;设置这个效果的窗口,在窗口可见的情况下,窗口后方的背景会相应的变暗,这个属性需要配合参数dimAmount一起使用,dimAmount会在后文中介绍。
WindowManager.LayoutParams.FLAG_FULLSCREEN
设置全屏;这个属性也许是大家接触的最多的一个属性,很多应用开发过程中会要求有些页面需要动态设置Activity为全屏,而我们只需要获得Activity的WindowManager.LayoutParams并设置WindowManager.LayoutParams.FLAG_FULLSCREEN属性就行。
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
设备常亮;设置这个属性的窗口,在窗口可见的情况下,整个屏幕会处于常亮并且高亮度的状态,并且不受待机时间的约束。
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
布局不受限制;设置这个属性的窗口,将不再受设备显示范围边界 的约束,通俗点讲,就是窗口可以出设备之外,然后移除部分不可见。具体会在坐标参数中讲到。
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
不设置聚焦;关于焦点获得我有必要说明一下,如果窗口获得焦点的话,只要窗口处于可视化状态,当前设备的物理按键点击事件都会被这个窗口接收,但是如果不设置窗口的焦点的话,直接传递到之后窗口进行接收。这就导致一个问题,如果你的需求要求你写的悬浮窗点击返回键能够关闭或是进行其他操作的话,你就必须让你的窗口获得焦点,并为当前View设置按键监听事件。
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
取消触摸事件; 设置这个属性的窗口将不再处理任何Touch事件,就算显示的View设置了onTouch事件,那么这个窗口就会是一个僵尸窗口。
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
不知道怎么去归纳,这个属性还是比较有意思的,设置这个属性的窗口,在窗口可见的情况下,就算窗口没有设置属性FLAG_NOT_FOCUSABLE,也就是在窗口获得焦点的情况下,当触摸事件是在窗口之外区域的时候,窗口不在拦截触摸事件,而是将事件往下传递,也算是解决聚焦后的事件拦截问题吧。
WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER
显示壁纸;官方文档说明是在窗口之后显示系统壁纸,但是我亲测,似乎并没有这个想效果,还是这个属性需要配合其他的属性设置一起使用,希望有设置成功的小伙伴能够在评论区分享你的结果。
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
锁屏显示;关于这个属性官方文档给出的说明是在锁屏的时候显示的窗口,但是,实在惭愧,在下还是没有能够有一个实验结果,不知道是需要给权限呢还是需要同时进行其他设置。同样,还是很希望有知道的小伙伴能够在评论区向大家分享。
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
点亮屏幕;设置这个属性的窗口,当窗口显示的时候,如果设备处于待机状态,会点亮设备。这个应该在很多锁屏窗口中用的比较多,比如收到消息点亮屏幕。
WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
这个也不知道怎么去归纳,也是一个比较有意思的属性,之前我们说到FLAG_NOT_TOUCH_MODAL,在窗口获得焦点的情况下,当触摸事件是在窗口之外区域的时候,窗口不在拦截触摸事件,而是将事件往下传递,而如果再设置这个属性,窗口能在MotionEvent.ACTION_OUTSIDE中收获窗口之外的点击事件,遗憾的是不能进行屏蔽,也就是说事件依然会向下传递。
以上的也是最常用到的几个flag属性了吧,其他还有很多,也希望大家空闲之余能够去研究研究。
type:
WindowManager.LayoutParams.TYPE_APPLICATION_PANEL
我翻阅过 PopupWindow 的源码,PopupWindow用的就是TYPE_APPLICATION_PANEL这个属性类型。这种类型的窗口在显示寄生于宿主窗口,并显示与宿主窗口之上,因此这种类型的窗口会随着宿主窗口的关闭而关闭,显然不能满足我们悬浮窗的要求。
WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
系统提示窗口,常见的比如内存不够的警告、低电量警告。它总是出现在应用程序窗口之上,而这一点,正合我们做一个能够显示在任何应用之上的悬浮迷你音乐盒的要求。
screenBrightness、buttonBrightness:
其中 screenBrightness 表示屏幕的亮度,而 buttonBrightness 表示一般按键和键盘按键的亮度。它们都拥有以下三个系统属性:
WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_OFF 最低屏幕亮度。
WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE 默认屏幕亮度。
WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL 最高屏幕亮度。
dimAmount:
讲flag属性的时候有提到过,这个参数是要和 WindowManager.LayoutParams.FLAG_DIM_BEHIND 这个flag属性一起使用,dimAmount 的取值在0.0f~1.0f之间,取值越大背景的变暗程度越高,默认取值1.0f。
width、height:
这里的width、height其实和View中的width、height一样的理解,就是控制窗口视图的大小,可以具体取值,也可以使用系统属性:
WindowManager.LayoutParams.WRAP_CONTENT 自适应大小
WindowManager.LayoutParams.MATCH_PARENT 填满整个布局
gravity:
窗口的对齐方式,一般在创建窗口的时候,都会设置gravity为左上角对齐,也就是 Gravity.LEFT | Gravity.TOP,因为窗口的坐标设置,是基于gravity来进行计算的,设置gravity左上角,刚好是和系统的坐标相对应,方便计算。
x、y:
x和y用于控制窗口的坐标位置,如果有设置gravity的话,x和y设置的就是在gravity这个基础上的一个偏移量。不设置gravity的话,x和y就是一个绝对坐标。因此,将gravity设置为 Gravity.LEFT | Gravity.TOP 是最易于开发的。需要注意的一点是:设置y的时候常常需要考虑状态栏的高度。
正常情况下,就算x和y的坐标已经在设备之外,也会贴边显示。而如果设置属性 FLAG_LAYOUT_NO_LIMITS 则相对于系统的坐标如果x和y超出设备,那么超出部分将无法显示。
windowAnimations
windowAnimations控制的是窗口出现和消失的动画效果,设置的是要系统自带的动画效果(android.R.style之下的动画效果),因为窗口管理器是不能访问应用资源的。
format
format可以理解为最后窗口生成的位图是什么格式,默认背景是黑色的。一般我们都设置为PixelFormat.RGBA_8888,这样我们的窗口就会有一个透明的背景。
alpha
这个不难理解,设置窗口的透明度
其实WindowManager.LayoutParams的属性有很多,全介绍一遍恐怕要讲到天亮,而且还有一些我本人也没有试过,就先讲到这里吧。
实战项目
上面粗略的向大家介绍了 WindowManager 和 WindowManager.LayoutParams,讲的都是理论知识,现在我们就要动起手来,着手开发炫酷的悬浮迷你音乐盒了。先上效果图:
其实实现这么一个悬浮迷你音乐盒的功能也并不难,首先需要两个布局,一个就是像效果图中展示的可以四处拖动的View的布局我们暂且称它为 floatView,它的实现很简单其实就是我之前文章的《用RotateDrawable实现网易云音乐唱片机效果》的迷你版。另一个就是点击 floatView 后跳出的歌曲控制菜单的布局,我们可以叫它 playerView。
接着就是写一个类来控制 floatView 与 playerView 这两个View之间的转换以及一些属性的设置,代码确实有点长,全贴出来的话,相信不少人都会看厌、看烦,我还是选择其中比较有代表性的代码,再进行一番说明,可能更有助于大家理解我写这个Demo时思考的角度。
public void setFloatView(View floatView) {
if(floatView != null) {
this.mFloatView = floatView;
setContentView(mFloatView);
}
}
首先,floatView的设置非常简单。至于 setContentView(mFloatView),是因为悬浮窗默认首先显示的是小窗口,所以在设置 MenuView 的时候,就将窗口的布局设置为floatView。
menuView 的设置相对于 floatView 的设置复杂一些,大家可以看到,我为添加进来的 MenuView 包裹了一层 BackgroundView,而这一点正是从 PopupWindow 的源码中借鉴而来,BackgroundView 是一个自定义内部类,继承至 RelativeLayout,并且重写按键点击监听事件和触摸事件,主要是用于PlayerView 的返回键关闭功能和外部点击关闭功能。
由于我们要实现的悬浮迷你音乐盒,是需要在两个视图之间进行切换的,所以在每次视图切换之前,我都会对先要加载的视图进行一个准备,由代码 contentView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED) 可以看到,主动计算出当前View的宽高信息,还有就是 getStatusBarHeight(getContext()) 计算设备状态栏高度的方法,因为在窗口移动的时候,默认x、y指定的是窗口左上角的坐标,这样就涉及到一个偏移量的问题,在排除偏移量问题后,我们的坐标就会精确指定窗口的正中心。而且在代码中能够明显看出,我对 contentView 设置了一个onTouch 触摸事件,主要是实现窗口的滑动,具体实现在后面会有介绍。
由基础部分介绍,WindowManager.LayoutParams 控制着窗口显示的策略,因此在窗口显示之前,我们要完成对 WindowManager.LayoutParams 的配置。由上面的代码看出,窗口类型是系统提示窗口,也就上篇文章中说到的可以显示在任何视图之上的那种窗口类型,窗口的宽高是自适应。对齐方式是左上角对齐。窗口的透明度为1.0,也就是不透明。虽然窗口并没有设置属性 FLAG_NOT_FOCUSABLE,也就是说我们的窗口默认是设置焦点的,但是由于我设置 FLAG_WATCH_OUTSIDE_TOUCH 属性的缘故,窗口外的区域,还是能够自由接收点击触摸事件的。
从代码中可以看出,视图切换用到的基础部分介绍到的视图的移除和视图的添加,是的,想要进行视图新的切换,首先要移除原有的视图,并将需要显示的视图添加进来。
在视图切换中用到了窗口的移除和窗口的添加,而在位置更新中用到的是WindowManger 三个方法中的第三个方法窗口更新,通过传入我们需要重新设置 WindowManager.LayoutParams 中的x、y位置坐标,然后更新窗口,就能实现窗口位置的更新操作。
floatView的触摸监听事件非常简单,就是根据手指触摸的坐标位置实时刷新窗口的坐标位置,当up坐标和dowm坐标的区域在预先设置好的合理范围内,触摸事件超过1秒钟则视为长按事件,小于1秒钟则视为点击事件,移除floatView,添加playerView。注意区别MotionEvent的getX、getY和getRawX、getRawY方法,getX、getY点击位置在视图内的坐标,getRawX、getRawY则是点击位置相对于整个屏幕的坐标。
打开playerView后,我设置的的窗口大小是占满整个屏幕,这样就能完全捕捉到 BackgroundView 中设置的触摸事件,实现点击窗口外部关闭的效果。oldX、oldY则用于记录floatView移除前的坐标,方便在重新显示的时候定位。
关闭PlayerView的方法也很简单,就是重新将试图切换为floatView,设置窗口大小为自适应大小,并定位在之前视图记录的原有坐标。
到这里炫酷的悬浮音乐盒的实现也就实现的差不多了,其实几个在效果图中能够看到的样子没有介绍,例如:自定义SeekBar样式的使用,悬浮窗移动时,手指离开屏幕,悬浮穿自动依附到屏幕两侧的过渡动画,还有就是在3秒钟左右悬浮窗没有任何操作是变得透明的效果的实现。想要了解这些是怎么实现的小伙伴,可以点击下方 阅读原文 来下载源码进行研究。
作者付出不易,如果觉得本文对你有帮助,请赞赏任意金额以示支持,所有赞赏的钱都将支付给作者。
如果你有好的技术文章想和大家分享,欢迎向我的公众号投稿,投稿具体细节请在公众号主页点击“投稿”菜单查看。
欢迎长按下图 -> 识别图中二维码或者扫一扫关注我的公众号: