美文网首页iOS开发经验总结iOSiOS记录篇
解决点击状态栏时ScrollView自动滚动到初始位置失效办法

解决点击状态栏时ScrollView自动滚动到初始位置失效办法

作者: Top_熊 | 来源:发表于2015-08-05 02:05 被阅读5215次

    相信细心的开发者都会发现scrollView自带一个功能,当用户点击顶部的状态栏时,scrollView的ContentOffset.y轴会自动滚动到初始位置,效果如图所示:

    单个scrollView单击顶部状态栏系统自带功能展示单个scrollView单击顶部状态栏系统自带功能展示
    这个功能对用户来说非常实用,尤其是在scrollView(TableView, WebView, CollectionView一切继承scrollView的控件)展示的内容很多,当用户翻看很久以后,想回到最顶部时,只需单击一下顶部的状态栏位置就可以轻松返回到顶部(这里吐槽下.貌似很多用户都不知道有这个功能),而不用使劲用手滑动到顶部.

    可是功能在当前控制器有多个scrollView(TableView, WebView, CollectionView一切继承scrollView的控件)的时候就会失效,效果如下图所示:

    当控制器内有多个scrollView时,系统自带的滚动到顶的功能就会失效当控制器内有多个scrollView时,系统自带的滚动到顶的功能就会失效
    • 如图所示,一旦有多个scrollView时,系统自带的方法就失效了

    实际开发中,我们的产品在同一个控制器经常会有多个scrollView组合在一起的情况,这就意味着系统的方法已经失效了,需要开发人员自己来实现这个效果,下面我们就来搞定这个需求

    我们分析下原生的方法为什么会失效,当一个控制器内只有一个scrollView时,点击状态栏,系统会遍历当前keyWindow的子控件,发现子控件中只有一个scrollView会调用这个scrollView的setContentOffset: animated:的这个方法,将scrollView的contentOffset.y值修改为初始值,但是当子控件中又多个scrollView时,系统会不知道掉用哪一个scrollView而失效,知道这点我们就知道该如何搞定这个问题了

    这里就直接将解决思路一一写出来不将代码分段展示了,在代码中我加了详细的注释objective-c的套路和swift基本一样,在最后会将Swift和objective-c的代码一起放上,如果需要直接解决问题的童鞋可以直接将代码拷贝到工程里即可

    • 首先创建一个topWindow继承至NSObject,这里我们考虑将这个功能完全封装起来,所以所有的方法都用的类方法,所以用最基本的类就可以
    • 在initialize方法中初始化topWIndow,将topWIndow的级别改成最高的UIWindowLevelAlert级别,设置topWindow位置,并且添加点击手势
    • 在topWIndow被点击调用的方法中,我们拿出UIApplication的keyWindow,遍历keyWindow的所有子控件,如果满足是scrollView同时又显示在当前keyWindow条件时,将subView的contentOffset的y值回复到原始
    • 然后采用递归的套路在遍历subView内时候有满足条件的子控件,直到没有满足条件时会停止

    Swift的代码

    import UIKit
    
    class TopWindow: UIWindow {
        
        private static let window_: UIWindow = UIWindow()
        
        ///  类初始化方法,保证window_只被创建一次
        override class func initialize() {
            window_.frame = CGRectMake(0, 0, global.appWidth, 20)
            window_.windowLevel = UIWindowLevelAlert
            window_.addGestureRecognizer(UITapGestureRecognizer(target: self, action: "topWindowClick"))
        }
        
        class func topWindowClick() {
            // 遍历当前主窗口所有view,将满足条件的scrollView滚动回原位
            searchAllowScrollViewInView(UIApplication.sharedApplication().keyWindow!)
        }
        
        private class func searchAllowScrollViewInView(superView: UIView) {
            
            for subview: UIView in superView.subviews as! [UIView] {
                
                if subview.isKindOfClass(UIScrollView.self) && superView.viewIsInKeyWindow() {
                    // 拿到scrollView的contentOffset
                    var offest = (subview as! UIScrollView).contentOffset
                    // 将offest的y轴还原成最开始的值
                    offest.y = -(subview as! UIScrollView).contentInset.top
                    // 重新设置scrollView的内容
                    (subview as! UIScrollView).setContentOffset(offest, animated: true)
                }
                // 递归,让子控件再次调用这个方法判断时候还有满足条件的view
                searchAllowScrollViewInView(subview)
            }
        }
        
        ///  添加topWindow,使手势生效
        class func showTopWindow() {
            window_.hidden = false
        }
        
        ///  隐藏topWindow,移除手势
        class func hiddenTopWindow() {
            window_.hidden = true
        }
    }
    
    ///  对UIView的一个扩展
    extension UIView {
        
        ///  判断调用方法的view是否在keyWindow中
        func viewIsInKeyWindow() -> Bool {
            let keyWindow = UIApplication.sharedApplication().keyWindow!
            
            // 将当前view的坐标系转换到window.bounds
            let viewNewFrame = keyWindow.convertRect(self.frame, fromView: self.superview)
            let keyWindowBounds = keyWindow.bounds
            
            // 判断当前view是否在keyWindow的范围内
            let isIntersects = CGRectIntersectsRect(viewNewFrame, keyWindowBounds)
            
            // 判断是否满足所有条件
            return !self.hidden && self.alpha > 0.01 && self.window == keyWindow && isIntersects
        }   
    }
    
    • 在AppDelegate里,程序启动完成方法时添加就OK了
        func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
            
            // 添加顶部的window
            TopWindow.showTopWindow()
            
            return true
        }
    
    • 需要注意添加了自定义的window后,控制器的改变状态栏状态方法会失效,可以在info.plist中将改变状态栏的管理权交给UIApplication解决,或者在需要改变状态栏的控制器中调用TopWindow.hiddenTopWindow()即可,或者直接改info.plist,用UIApplication.sharedApplication().setStatusBarStyle来管理

    objective-c代码

    • .h文件只暴露显示和隐藏方法
     #import <Foundation/Foundation.h>
     @interface WNXTopWindow : NSObject
     + (void)show;
     + (void)hide;
    @end
    
    • .m文件
     #import "WNXTopWindow.h"
    @implementation WNXTopWindow
    static UIWindow *window_;
    //初始化window
     + (void)initialize {
        window_ = [[UIWindow alloc] init];
        window_.frame = CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, 20);
        window_.windowLevel = UIWindowLevelAlert;
        [window_ addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(windowClick)]];
    }  
     + (void)show {
        window_.hidden = NO;
    }
     + (void)hide {
        window_.hidden = YES;
    }
     // 监听窗口点击
     + (void)windowClick {
        UIWindow *window = [UIApplication sharedApplication].keyWindow;
        [self searchScrollViewInView:window];
    }
     + (void)searchScrollViewInView:(UIView *)superview {
        for (UIScrollView *subview in superview.subviews) {
            // 如果是scrollview, 滚动最顶部
            if ([subview isKindOfClass:[UIScrollView class]] && [self isShowingOnKeyWindow: subView]) {
                CGPoint offset = subview.contentOffset;
                offset.y = - subview.contentInset.top;
                [subview setContentOffset:offset animated:YES];
            }
            
            // 递归继续查找子控件
            [self searchScrollViewInView:subview];
        }
    }
     + (BOOL)isShowingOnKeyWindow:(UIView *)view {
        // 主窗口
        UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
        // 以主窗口左上角为坐标原点, 计算self的矩形框
        CGRect newFrame = [keyWindow convertRect:view.frame fromView:view.superview];
        CGRect winBounds = keyWindow.bounds;
        // 主窗口的bounds 和 self的矩形框 是否有重叠
        BOOL intersects = CGRectIntersectsRect(newFrame, winBounds);
        
        return !view.isHidden && view.alpha > 0.01 && view.window == keyWindow && intersects;
    }
    @end
    
    • 同样,也是在程序初始化完成AppDelegate文件中显示topWindow,整个工程这个问题就统统解决了
     - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
        // 添加一个window, 点击这个window, 可以让屏幕上的scrollView滚到最顶部
        [WNXTopWindow show];
        
        return YES;
    }
    

    针对iOS9.0会运行程序崩溃的解决办法

    • 更新了下Xcode7.0,发现运行程序会崩溃,解决方法是给顶部的topWindow添加一个rootViewController,创建一个topVC继承至UIViewController,将topVC设置为顶部自定义的window的跟控制器,将原本点击事件放到topVC的touchBegin方法中即可

    完成的效果

    完成效果完成效果

    欢迎关注我的技术博客

    点击链接我的博客

    我的新浪微博

    我的新浪微博

    欢迎朋友关注,如果有什么意见和问题关注留言即可, 本文随意转载,请注明作者出处

    相关文章

      网友评论

      • 751fc49dcbfd:贴git地址
      • Manba_小洛:哥们,那个QQ消息粘性的demo封装的代码创建,达不到效果啊,后面的小圆没有添加上去。求解啊。:sob:
      • Marshall_Yin:我觉得这样很麻烦,我是这样解决的。几行代码就搞定了。
        - (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView
        {
        // 当前的索引
        NSInteger index = scrollView.contentOffset.x / scrollView.width;
        // 取出子控制器
        UITableViewController *vc = self.childViewControllers[index];

        vc.view.x = scrollView.contentOffset.x;
        vc.view.y = 0; // 设置控制器view的y值为0(默认是20)
        vc.view.height = scrollView.height; // 设置控制器view的height值为整个屏幕的高度(默认是比屏幕高度少个20)

        // 先把所有scrollView上面的tableView禁用scrollToTop
        for (UITableView *subview in scrollView.subviews) {
        subview.scrollsToTop = NO;
        }
        // 再设置当前scrollToTop的属性为YES。
        vc.tableView.scrollsToTop = YES;
        [scrollView addSubview:vc.tableView];
        }
        Marshall_Yin:@Marshall_Yin 就最后几行代码。完美解决问题。
      • b59457960ac9:跟mj上课说的代码一样
        16b6db92e3b6:@Twistar 小白求问 mj是谁啊 看简书里总被提到
      • 我是Python小白:楼主,新建立的window遮住了状态栏 。
      • StoneLeon:
        针对iOS9.0会运行程序崩溃的解决办法, 不是特别理解,请告知一二.
      • SilenceZhou:看见希望回复
      • SilenceZhou:有没Demo 贴出来, 你说说的:“解决方法是给顶部的topWindow添加一个rootViewController,创建一个topVC继承至UIViewController,将topVC设置为顶部自定义的window的跟控制器,将原本点击事件放到topVC的touchBegin方法中即可 ” 怎么进行处理 ????
      • devn8:这个会把ios9系统状态栏事件给遮挡,觉得还是在viewcontroller里面处理比较好!
      • 958645c161f4:楼主,iOS9上会崩溃,Application windows are expected to have a root view controller at the end of application launch,在调用了[TopWindow show];的时候,好像是跟 Window 的 windowLevel 有关系
        hY_Ramos:@我的眼里只有代码 大牛,怎么解决的
        958645c161f4:@维尼的小熊 我试了一下,给 TopWindow 的 rootViewController添加了一个UIViewController,然后设置 backgroundColor 为 clearColor,但是运行之后,原本的 window 下面的 ViewController 上移了20,状态栏也不见了,你知道怎么解决吗
        Top_熊:@我的眼里只有代码 iOS9.0中,window必须有跟控制器,后面修改过针对ios9.0的方法。文章里有提到
      • Zeroxhj:我的qq是1173937600
      • Zeroxhj:最简单的实现方法是,设置scrollView的scrollToTop属性。能加我的qq吗,找你好久了,有问题请教
      • 梦中回音:楼主 什么时候开始做iOS的呀
      • 刺刀:有没有试过iOS9,貌似有闪退
        刺刀:@维尼的小熊 UIWindow还需要清除一下背景色,要不一块黑色 window_.backgroundColor = [UIColor clearColor];
        Top_熊:@shaZhu 更新了下Xcode7.0,发现运行程序会崩溃,解决方法是给顶部的topWindow添加一个rootViewController,创建一个topVC继承至UIViewController,将topVC设置为顶部自定义的window的跟控制器,将原本点击事件放到topVC的touchBegin方法中即可
        18932d421a01:@刺刀 是跟系统的有冲突,你解决了么
      • 693ee20639da:[subview isShowingOnKeyWindow] 写着的这句代码会报错,我改成啦 [subview respondsToSelector:@selector(isShowingOnKeyWindow)]这句代码,我不知道对不对,可是用一个实例去调用类方法,不是不对啊的,希望小熊 给个解释,妹子谢谢啦
        Top_熊:@维尼的小熊 [subview isShowingOnKeyWindow] 修改为 [self isShowingOnKeyWindow:subview]这样,当初给抽取成分类了,方便大家看又直接拷贝过来了,发现忘记修改了 :sweat:
      • 958645c161f4:谢谢!前几天在自己的一个项目里面遇到这个问题了,没去解决,今天看到了你的这篇文章,帮了我一个大忙,再次感谢!
        Top_熊:@我的眼里只有代码 :relaxed:
      • 35415af4d2df:当看到lz用添加window的方法来截取点击,我想到一件事,不知道lz可否解决:现在,你这样设置window_.windowLevel = UIWindowLevelAlert;亲问当你的滚动功能实现之后有一个alertview出现,之后消失了,你的点击返回还正常吗?
        Top_熊:@沙漠浮萍 如果失效的了话,可以通过代理方法监听alertview的消失,消失后在代理方法中重新添加下window试试看可以不
      • GJCode:使用了维尼小熊的这个方法,太棒了,谢谢您,这是我的扣扣号,641102478,希望可以和大神多多交流
      • 刺刀: [subview isShowingOnKeyWindow] 修改为 [self isShowingOnKeyWindow:subview]
        Top_熊:@刺刀 当初给抽取成分类了,方便大家看又直接拷贝过来了,发现忘记修改了 :smiley: 谢谢提醒,已经修改过来了
      • b51d8f5e89cb:@wilson_yuan听过今日头条么?而且以面相对象的思想写也应该封装起来,呵呵
      • 9dfbae8dcfcf:其实没有这么复杂. 如果有多个scrollView, 只让一个scrollView的scrollsToTop=Yes, 其余的不显示的或者不需要响应的全部设为No,问题就解决了.
        ddaa8dae50b0:@维尼的小熊 这么做的话, 如果一个顶层view上有多个scrollView, 点了状态栏, 这些scrollView会同时滚到顶部.
        ddaa8dae50b0:@维尼的小熊 如果需要这个功能, 这是在开始编程的时候就要注意的. 这个方法是在项目已经完成时候的补救.
        Top_熊:@wilson_yuan 我觉得少量scrollView时可以用scrollsToTop=Yes,但是如果很多,每个控制器里都有的话,一个一个写不麻烦么?文章里提到的方法解可以解决整个工程里所有的scrollView
      • Top_熊:@d678e7567a25 scrollview的约束最好用代码来搞定,他在sb里约束子控件需要知道自己的contentSize,子控件需要参照一个别的控件。。估计说的我不太明白,我整理下发个文章吧还是。。
        d678e7567a25:恩,谢谢@维尼的小熊
      • d678e7567a25:scrollview好高深,特别是加约束的适合各种报错,求教或者提供写得好的博文
        d678e7567a25:@从今以后 今天来简书看到回复,太感谢你了
        不知什么人:@d678e7567a25 可以参考下这篇 http://nonomori.farbox.com/post/scrollview-yu-autolayout 博文. scrollView 和内容视图四边的约束决定了 contentSize, 一般会让内容视图参照别的控件来设置约束确定内容视图自身尺寸(总之就是不能依赖和 scrollView 四边的约束确定自身尺寸),然后根据和 scrollView 的四边的约束来确定 contentSize.

      本文标题:解决点击状态栏时ScrollView自动滚动到初始位置失效办法

      本文链接:https://www.haomeiwen.com/subject/lwwvqttx.html