作者:維尼的小熊 授權本站轉載。
相信細心的開發者都會發現scrollView自帶一個功能,當用戶點擊頂部的狀態欄時,scrollView的ContentOffset.y軸會自動滾動到初始位置,效果如圖所示:
單個scrollView單擊頂部狀態欄系統自帶功能展示
這個功能對用戶來說非常實用,尤其是在scrollView(TableView, WebView, CollectionView一切繼承scrollView的控件)展示的內容很多,當用戶翻看很久以後,想回到最頂部時,只需單擊一下頂部的狀態欄位置就可以輕松返回到頂部(這裡吐槽下.貌似很多用戶都不知道有這個功能),而不用使勁用手滑動到頂部.
可是功能在當前控制器有多個scrollView(TableView, WebView, CollectionView一切繼承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 @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]] && [subview isShowingOnKeyWindow]) { 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; }
完成的效果
歡迎關注我的技術博客
我的新浪微博