JJStatusBarExtension 完美实现点击 stat
2016-02-24 本文已影响270人
德蒙_托尔斯泰
背景
- 现在有许多 app 都有这个需求, 点击 statusBar, tableView/collectionview 内容滚动到顶部
- iOS其实已经集成了这种功能, 但是它只能在当前控制器之下只有一个(只能有一个) scrollView 或者其子类的时候才能有用, 如果你 tableView又有一个 scrollView 的标题栏, 那它自带的这个功能你是用不了的
- 本文详细探求了如何扩展该功能, 使其能共通用
- 代码详见本人 GitHub/quickCode/JJStatusBarExtension
动手
-
苹果是怎么实现该功能的, 我们无从得知, 所以我们得自己实现
-
大致功能实现步骤: 点击 statusBar--> 触发手势-->-->找到我们需要操作的内容 view-->滚动它
- 首先我们很容易想到, 在 statusBar 上盖上一个 view, 然后就监听点击就是了, 但我要告诉你这是不可取的
- 你的 view 不可能盖到 statusBar 上, 因为它也是在一个独立的 window 上的, 这个 window 在我们 app.keyWindow之上, 所以你的 view 永远都会在 statusBar 下面(有兴趣的可以去打印看看)
- 也许你会说把 view 加到了 statusBar 下也没关系, 把 statusBar 的点击忽略掉就行, 我只能说天真, 且不说 statusBar 层次结构复杂, 各种控件, 而且还拿不到这些控件, 就算能拿到, 你要在 hitTest 里面一步步递归判断吗, 这样不现实,所以否定这种想法
- 加 view 不行, 那就只能加 window 了, 因为 Window是能加到 statusBar 上的, 只要改变 window 的优先级--windowLevel属性即可(补充一点, 优先级相同的 window, 后加的在上面)
- 光加个 window 还不够, 我们需要一个顶层控制器来统一管理 statusBar 的, 这样才能在控制器里完成对 view 的点击操作进行监听, 光搞个 view 是没用的
- window 多大合适, 直接告诉你, 和屏幕. bounds 相等即可, 为什么, 如果跟 statusBar.bounds 相等的话, 旋转屏幕的时候会有 bug, statusBar 会消失不见
- 控制器 vi的大小就可以设置成 statusBar.bounds 大小了, 在 window 的 hitTest 方法里忽略掉 statusBar 以下的点击事件, 传递给 app.window处理
- 首先我们很容易想到, 在 statusBar 上盖上一个 view, 然后就监听点击就是了, 但我要告诉你这是不可取的
-
主要代码:
- 这里我把 topWindow 设计成单例, 方便后面拿到顶层的 topVc
// JJStatusBarExtension.m
static JJStatusBarExtension *_topWindow;
+ (instancetype)sharedStatusBarExtension
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_topWindow = [[self alloc] init];
});
return _topWindow;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_topWindow = [super allocWithZone:zone];
});
return _topWindow;
}
- (id)copyWithZone:(NSZone *)zone
{
return _topWindow;
}
+ (void)showWithStatusBarClickBlock:(void (^)())block
{
if (_topWindow) return;
[JJStatusBarExtension sharedStatusBarExtension].windowLevel = UIWindowLevelAlert;
[JJStatusBarExtension sharedStatusBarExtension].backgroundColor = [UIColor clearColor];
// 先显示window
[JJStatusBarExtension sharedStatusBarExtension].hidden = NO;
// 设置根控制器
JJTopViewController *topVc = [[JJTopViewController alloc] init];
topVc.view.backgroundColor = [UIColor clearColor];
topVc.view.frame = [UIApplication sharedApplication].statusBarFrame;
topVc.view.autoresizingMask = UIViewAutoresizingFlexibleWidth;
topVc.clickedBlock = block;
[JJStatusBarExtension sharedStatusBarExtension].rootViewController = topVc;
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (point.y > 20) {
return nil;
}
return [super hitTest:point withEvent:event];
}
- 通过 + showWithStatusBarClickBlock:方法来创建出 topWindow, 在 APPDelegate 中调用, 这里我把寻找内容 view 并滚动到最前面的方法独立出来, 并在 + showWithStatusBarClickBlock:后的 block 回调, 如果你其他地方有这个需求, 你可以使用这个方法, 你只要传入你要找的内容view 的父view 或父父view...
- 这里我们把 application.keyWindow传进去, 因为我们需要全局实现
- 实现滚动到 top 的原理, 递归查找子控件, 找到 scrollView 就把它滚到最前面
// AppDelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// NSLog(@"%@", self.window);
[JJStatusBarExtension showWithStatusBarClickBlock:^{
// [self test];
// 如果想要 app的所有界面都有点击 statusBar 滚到最前面, 则调用下面这个方法
[JJStatusBarExtension scrollToTopInsideView:self.window];
}];
return YES;
}
+ (void)scrollToTopInsideView:(UIView *)view
{
CGRect viewRect = [view convertRect:view.bounds toView:nil];
if (!CGRectIntersectsRect([UIApplication sharedApplication].keyWindow.frame, viewRect)) {
return;
}
for (UIView *subview in view.subviews) {
[self scrollToTopInsideView:subview];
}
if (![view isKindOfClass:[UIScrollView class]]) {
return;
}
UIScrollView *scrollView = (UIScrollView *)view;
// CGPoint contentOffset = scrollView.contentOffset;
// contentOffset.y = - scrollView.contentInset.top;
// [scrollView setContentOffset:contentOffset animated:YES];
[scrollView scrollRectToVisible:CGRectMake(0, 0, 1, 1) animated:YES];
}
完成上面的控件部署, 下面就实现功能实现部分
- 首先明白几个条件:
- 以前可以用 application 来修改 statusBarHidden 和 style, 但 iOS9后, application 的方法过期了
- 在控制器中, 控制器可以控制 statusBar 的 hidden 和 style, 首先明白一点, 系统是怎么来控制statusBar的, 有一点可以验证到, 就是每次 view 将要显示的时候, 系统都会调用下面的方法, 以此来控制 statusBar 状态
- 通过setNeedsStatusBarAppearanceUpdate能重新调用下面的三个方法
- statusBar 只能由顶层控制器来控制, 所以我们加了 topVc 后, 其他控制器里对 statusBar 状态的修改是无效的,因为顶层控制器会覆盖它
- 根据上面的分析, 我们也在 view 将要显示的时候来操作, 在 viewWillAppear 方法来想办法
- (BOOL)prefersStatusBarHidden
{
return YES;
}
- (UIStatusBarStyle)preferredStatusBarStyle
{
return UIStatusBarStyleDefault;
}
- (UIStatusBarAnimation)preferredStatusBarUpdateAnimation
{
return UIStatusBarAnimationFade;
}
- 为了保证系统原有的对 statusBar 的控制依然有效, 我们就必须在 topVc 里能
拿到当前显示 Vc对 statusBar 的设置数据 - 这里我们新建一个 viewController 的分类, 在这里面完成相关操作, 这里要弄到运行时的交换方法, 因为我们要拦截 viewController 的 viewWillAppear 方法
- 拦截到后, 将当前显示控制器, 通过 topWindow 传给 topVc, 这样就能在 topVc 里面拿到当前显示控制器里对 statusBar 的舍子数据, 然后 topVc 在调用setNeedsStatusBarAppearanceUpdate方法, 刷新状态栏的显示
- 这里还对如果设置了statusBar 显示动画的情况作了判断
// UIViewController (JJStatusBarExtension) --分类
@implementation UIViewController (JJStatusBarExtension)
+ (void)load
{
// 交换系统 viewWillAppear 和我们自定义的 jj_viewWillAppear
Method method1 = class_getInstanceMethod(self, @selector(jj_viewWillAppear:));
Method method2 = class_getInstanceMethod(self, @selector(viewWillAppear:));
method_exchangeImplementations(method1, method2);
}
- (void)jj_viewWillAppear:(BOOL)animated
{
[self jj_viewWillAppear:animated];
// 如果非当前窗口显示的控制器, 我只测试到UINavigationController下,"UIInputWindowController"会产生影响, 把它屏蔽掉
// NSLog(@"%@", self.class);
if ([NSStringFromClass(self.class) isEqualToString:@"UIInputWindowController"]) return;
if ([self respondsToSelector:@selector(jj_ignoreStatusBar)]) {
if ([self jj_ignoreStatusBar]) return;
}
JJTopViewController *statusBarVc = (JJTopViewController *)[JJStatusBarExtension sharedStatusBarExtension].rootViewController;
if (statusBarVc == self) return;
statusBarVc.showingVc = self;
// 判断是否设置动画
if (statusBarVc.showingVc.preferredStatusBarUpdateAnimation == UIStatusBarAnimationNone) {
[statusBarVc setNeedsStatusBarAppearanceUpdate];
}else{
[UIView animateWithDuration:[JJStatusBarExtension sharedStatusBarExtension].statusBarAnimationDuration animations:^{
[statusBarVc setNeedsStatusBarAppearanceUpdate];
}];
}
}
@end
- topVc拿到数据刷新,
- topVc 的 touchsBegin中监听点击, 回调 scrollToTop 的方法
@interface JJTopViewController : UIViewController
@property(nonatomic, strong) void(^clickedBlock)();
@property(nonatomic, strong) UIViewController * showingVc;
@end
@implementation JJTopViewController
- (BOOL)prefersStatusBarHidden
{
return self.showingVc.prefersStatusBarHidden;
}
- (UIStatusBarStyle)preferredStatusBarStyle
{
return self.showingVc.preferredStatusBarStyle;
}
- (UIStatusBarAnimation)preferredStatusBarUpdateAnimation
{
return self.showingVc.preferredStatusBarUpdateAnimation;
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
if (self.clickedBlock) {
self.clickedBlock();
}
}
@end
以上就是所有实现的主要思路以及代码
- 在此基础上, 还加了个jj_ignoreStatusBar的方法, 主要是防止当前显示 vc上还有多个小 vc的情况, 这时就在小 vc实现这个方法, 屏蔽小 vc 的影响
- 解惑:因为jj_ignoreStatusBar这个方法我是不需要在我分类中实现的, 我只是需要在需要屏蔽的时候可选实现, 所以设计成这种协议模式, 这种设计模式, 大家可以借鉴借鉴, 其实我也不是理解的很深, 互相学习吧!