悬浮球?只要这个就够了
上次优化项目性能,最开始是放了一个 FPS 检测(一种卡顿的检测方式)通过 Label 的形式添加到了程序的主窗口上,后面一直没有移除掉,组长说太难看让我移除 (? ? ?) ,感觉 FPS 在界面上显示直观,对优化项目还是有很大帮助,能够快速定位到什么操作导致剧烈跳 FPS。为了不移除这个 FPS 检测,于是就顺便弄了个悬浮球,悬浮球源码和本文 Demo 地址见 MISFloatingBall 。
前言
因为悬浮球比较简单,但是博客还是要写的(讲道理感觉写博客真的很难写。。。),总结一下这次制作悬浮球主要遇到的一些问题和比较重要的东西。
正文
一个 App 只会有且只会存在一个 keyWindow!默认创建并且启动的 App ,就会有个默认的 keyWindow,就是我们最频繁操作的那个窗口。其实我们是可以创建很多个 window 的,一个 window 显示的过程为,先 visable 然后成为 keyWindow。当一个 window 成为 keyWindow。上一个 keyWindow 就会 resign keyWindow,系统提供了 window 的三种 Level。
windowLevel不添加任何自定义 window,系统默认的窗口为 UIWindowLevelNormal 级别,而状态栏为 UIWindowLevelStatusBar 级别,以及系统弹窗为 UIWindowLevelAlert 级别。所以设计一个全局的悬浮球,我便采用和系统状态栏以及 alertView 同样的思路,创建一个 window,将其 visable 并且成为 keywindow,所以全局悬浮球比较简单,直接将悬浮球添加到自定义的一个窗口上,这里我将自定义窗口的Level设成了下面所示,个人感觉如果是在app中使用的全局悬浮球,还是不要覆盖住系统状态栏好(其实感觉这个还得看具体需求,你也完全弄个 normal 级别的,然后成为 keyWindow 显示)。
self.window.windowLevel = UIWindowLevelStatusBar - 1;
使用比较简单,直接初始化尺寸并调用 visible 显示出来
MISFloatingBall *globallyBall = [[MISFloatingBall alloc] initWithFrame:CGRectMake(100, 100, 60, 60)];
globallyBall.backgroundColor = [UIColor redColor];
[globallyBall visible];
如果想要设置悬浮球内容的话,我提供了 setContent 接口,并且有个 contentType,可供选择设置图片文字或者上面两个不满足,直接传入自定义 customView,在悬浮球内部是自动居中的。这里我的 content 是 id 类型,但我在内部做了限制,如果使用 image 场景但是却传入 title 类型的话就会崩溃,在 Debug 模式下,如果意外传参错误会自动定位到 NSAssert 处。
[globallyBall setContent:[UIImage imageNamed:@"apple"] contentType:MISFloatingBallContentTypeImage];
悬浮球提供了一个点击的 block 回调,可以在 block 中处理点击悬浮球需要处理的事件(或者代理方法中处理,此处省略)
__weak typeof(globallyBall) weakBall = globallyBall;
[globallyBall setClickHander:^{
[weakBall disVisible];
}];
如果需要自动靠边则开启属性
globallyBall.autoCloseEdge = YES;
如果想要设置靠边之后停靠多少秒缩进我提供了一个快速设置接口 MISEdgeRetractConfig 是一个 stuct 类型,提供一个快速设值的内联方法 MISEdgeOffsetConfigMake,内部通过请求 block 返回的 config 进行了设置悬浮球缩进后的参数,MISEdgeOffsetConfigMake(CGPointMake(20, 30), 0.7f) 表示,当靠边后 3s 之后会像当前停靠边缘缩进,如果是左右边则缩进 offse.x 即为 20,如果上下的话那就是缩进 30 啦,并且缩进之后悬浮球透明度变为 0.7f,如果 edgeRetractConfigHander 传入 NULL ,外界不传值,则使用默认值。
// 3s后缩进
[floating autoEdgeRetractDuration:3.0f edgeRetractConfigHander:^MISEdgeRetractConfig{
return MISEdgeOffsetConfigMake(CGPointMake(20, 30), 0.7f);
}];
但是有时候可能并不想使用全局的一个悬浮球,毕竟可以全屏幕跑跨各种 VC。有时候需要限制住只在当前的某一个页面使用生效,点击 push 或者跳转,都不会影响下一个页面,所以除了全局悬浮球,我提供了一个快速生成的指定 view 生效的悬浮球,接口使用也比较简单。
MISFloatingBall *floatingBall = [[MISFloatingBall alloc] initWithFrame:CGRectMake(100, 100, 100, 100) inSpecifiedView:self.view];
floatingBall.backgroundColor = [UIColor orangeColor];
其他接口的调用同上,主要说下,因为指定 view 我是在接口内部 addSubview 当前的 floatingBall。所以这里会有个问题,在不影响外界操作的情况下,我内部的细节是对外部隐藏的,所以会出现一个问题如果 visable 之后,在同样的 view上添加了另一个子 view 如果位置有重合,就会覆盖我的悬浮球的 bug,因为现在悬浮球和所有的 view 都是在同一个 window 上,不像上一个全局的悬浮球那样层级高的 window 内的 view 是不会被 noremal 层级的 window 中的子 view 覆盖住的,于是用 runtime swizzle 掉系统的 addSubview。
- (void)mis_addSubview:(UIView *)subview {
[self mis_addSubview:subview];
[self.subviews enumerateObjectsUsingBlock:^(UIView * obj, NSUInteger idx, BOOL * _Nonnull stop) {
if ([obj isKindOfClass:[MISFloatingBall class]]) {
[self insertSubview:subview belowSubview:(MISFloatingBall *)obj];
}
}
最开始是这样子的,其实有个严重的问题,只要当前类包含了 MISFloatingBall 文件,只有程序装载了我的 MISFloatingBall 文件,所有的类调用 addSubView 都会去遍历 subviews,想必是很可怕的一件事,并不是实现了效果就完全 OK 了,所以如果不频繁的 addSubview 都会走 swizzle 方法去遍历,最后做了下面的限制。
- (void)mis_addSubview:(UIView *)subview {
[self mis_addSubview:subview];
// 限制
if ([MISFloatingBallManager shareManager].canRuntime) {
if ([[MISFloatingBallManager shareManager].superView isEqual:self]) {
[self.subviews enumerateObjectsUsingBlock:^(UIView * obj, NSUInteger idx, BOOL * _Nonnull stop) {
if ([obj isKindOfClass:[MISFloatingBall class]]) {
[self insertSubview:subview belowSubview:(MISFloatingBall *)obj];
}
}];
}
}
}
在这里做了一个限制,只有当前 view 添加了我的 ball,并且添加之后,过滤掉非 ball 父视图的 addSubview 操作,这样就不会影响其他界面的操作了,其实还有其他的方法,比如给分类添加一个ball标识然后内部直接去复赋值 insertSubview:belowSubview: 就不需要遍历了这里需要感谢,川大神的交流指导提供了一波思路。
其实上次刚好也看到有个人写的一个 floatingBall,虽然他打了一个 .a 静态库看不到源码,但是通过测试发现他是用的 hitTest 方法,触摸的 view 直接放到当前父视图最上层,这种方法其实是存在 bug 的,初始化的时候位置刚刚好就被覆盖住了,最后上个效果图。
效果图.gif总结
其实悬浮球比较简单,但是还是有很多细节问题需要注意的,其实还有两个功能没有开发,旋转屏适配
和 弹出扩展 view
,那样的话就更完美了后续再持续更新,其实还有个主要目的是想先弄一个 OC 版本的出来再写个 swift 的版本,因为刚好在学 swift,就当实例操作了,还有最重要的是终于趁着节假日,搞完了一篇博客。