現在很多流行的軟件都加入了狀態欄提示的功能,比如手機qq,微信等,今天我們就一起來看看狀態欄提示控件的原理與實現。
一、狀態欄提示的實現原理
不知道大家看到狀態欄提示控件,第一感覺它是怎么實現的呢?
我們知道即使平時寫的view是充滿全屏的,也始終不會顯示到statusBar的上層的。也就是說statusBar應該是一個特殊的view,始終位於程序的topLevel,這就容易聯想到UIKit中一個特殊的view-----UIWindow。UIWindow有一個windowLevel的屬性剛好能實現一直保持在上層的功能,於是方向就比較明確了。我較早寫的兩篇博客UIWindowLevel詳解以及關於UIWindow的一點兒思考中對windowLevel有過詳細的介紹和驗證。
確定了使用UIWindow來實現狀態欄提示控件,好像問題就全部解決了,真的是這樣嗎?
如果你的程序僅僅支持portrait方向的話,那么最主要的功能就結束了,剩下的事情就是文本框布局和簡單動畫的實現了。但如果你的控件要支持其他三個方向的話,就還需要處理window的旋轉。
IOS中完整的旋轉流程如下: 設備檢測到方向旋轉->UIApplication收到旋轉事件->通知window設備發生了旋轉->window通知它的rootViewController進行旋轉->viewController會調整自身view的transform。觀察發現自始至終window本身的位置和方向是沒有發生變化的,也就是說如果自己創建一個window用於展示提示,我們需要自己處理該window的旋轉,根據不同的方向調整window的位置和transform。
綜上要實現狀態欄提示控件我們需要做以下兩件事:
1、創建一個UIWindow,指定它的frame為statusBar的frame,並且設置該window的windowLevel級別略高於statusBar的windowLevel。
2、注冊系統的旋轉通知,監測設備方向變化,根據當前設備的方向做出相應的調整。
整個過程中主要用到了UIWindow和transfrom的知識,這兩部分知識我前面寫的博客都有涉及,難點主要集中在自己旋轉window這一塊。
二、Window的旋轉
UIKit通過UIWindow和UIViewContoller為我們提供了一套旋轉支持的框架,在方向變化以后viewController中view的坐標系統就已經被轉到正確的方向了,我們只需要簡單的重新布局就可以了。我們現在是直接通過UIWindow實現狀態欄提示控件,因此需要自己完成對該window進行旋轉的操作。
我們知道對當前view設置的transform是針對它的父view的,window本身就是一種特殊的view。你可能會疑問window不就是最底層的view,它還有父view嗎?
答案是YES,不信的話你可以打印一下window的superView看看。window默認方向是portrait方向,向下y坐標增加,向右x坐標增加。因此Portrait方向我們只需要向普通的view那樣布局就可以了,其它幾個方向我們就需要用到transform和設置位置來搞定了。
下面我們看一看從Portrait方向轉到landscapeRight方向的圖示:
上圖中從左到右展示了如何將初始位置(Portrait方向),旋轉到目標位置(landscapeRight方向)的過程。
1、原始window位置位於屏幕最上方(與statusBar的位置一樣)。
2、首先我們對這個window做順時針90°旋轉,變化后到達綠色繪制位置。
3、接着我們再修改window的center到屏幕最右邊並且上下居中,達到紅色虛線所示的位置。
4、最后對該window的bound進行設置,使該window充滿屏幕最右邊的區域。注意這個時候由於window的transform已經轉了90°,所以設置時width代表着高度,height代表這寬度。
下面是完整的處理旋轉到四個方向的代碼:
- (void)updateOrientation:(NSNotification*)noti { UIInterfaceOrientation newOrientation = [[noti.userInfo valueForKey:UIApplicationStatusBarOrientationUserInfoKey] integerValue]; NSLog(@"new orientation: %d", newOrientation); switch (newOrientation) { case UIInterfaceOrientationPortrait: { self.transform = CGAffineTransformIdentity; self.frame = CGRectMake(0, 0, SCREEN_WIDTH, HEIGHT); break; } case UIInterfaceOrientationPortraitUpsideDown: { // 先轉矩陣,坐標系統落在屏幕有右下角,朝上是y,朝左是x self.transform = CGAffineTransformMakeRotation(M_PI); self.center = CGPointMake(SCREEN_WIDTH / 2, SCREEN_HEIGHT - HEIGHT / 2); self.bounds = CGRectMake(0, 0, SCREEN_WIDTH, HEIGHT); break; } case UIInterfaceOrientationLandscapeLeft: { self.transform = CGAffineTransformMakeRotation(-M_PI_2); // 這個時候坐標軸已經轉了90°,調整x相當於調節豎向調節,y相當於橫向調節 self.center = CGPointMake(HEIGHT / 2, [UIScreen mainScreen].bounds.size.height / 2); self.bounds = CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.height, HEIGHT); break; } case UIInterfaceOrientationLandscapeRight: { // 先設置transform,在設置位置和大小 self.transform = CGAffineTransformMakeRotation(M_PI_2); self.center = CGPointMake(SCREEN_WIDTH - HEIGHT / 2, SCREEN_HEIGHT / 2); self.bounds = CGRectMake(0, 0, SCREEN_HEIGHT, HEIGHT); break; } default: break; } }
三、狀態欄提示控件源碼
講了那么多,說好的控件呢?
下面是完整的控件代碼:

// // SvStatusBarTipsWindow.h // SvStatusBarTips // // Created by maple on 4/21/13. // Copyright (c) 2013 maple. All rights reserved. // #import <UIKit/UIKit.h> @interface SvStatusBarTipsWindow : UIWindow /* * @brief get the singleton tips window */ + (SvStatusBarTipsWindow*)shareTipsWindow; /* * @brief show tips message on statusBar */ - (void)showTips:(NSString*)tips; /* * @brief show tips message on statusBar */ - (void)showTips:(NSString*)tips hideAfterDelay:(NSInteger)seconds; /* * @brief show tips icon and message on statusBar */ - (void)showTipsWithImage:(UIImage*)tipsIcon message:(NSString*)message; /* * @brief show tips icon and message on statusBar */ - (void)showTipsWithImage:(UIImage*)tipsIcon message:(NSString*)message hideAfterDelay:(NSInteger)seconds; /* * @brief hide tips window */ - (void)hideTips; @end

// // SvStatusBarTipsWindow.m // SvStatusBarTips // // Created by maple on 4/21/13. // Copyright (c) 2013 maple. All rights reserved. // #import "SvStatusBarTipsWindow.h" #define SCREEN_WIDTH ([UIScreen mainScreen].bounds.size.width) #define SCREEN_HEIGHT ([UIScreen mainScreen].bounds.size.height) #define HEIGHT 20 #define ICON_WIDTH 20 #define TIPMESSAGE_RIGHT_MARGIN 20 #define ICON_RIGHT_MARGIN 5 @interface SvStatusBarTipsWindow () { UILabel *_tipsLbl; UIImageView *_tipsIcon; NSTimer *_hideTimer; } @property (nonatomic, copy) NSString *tipsMessage; @end @implementation SvStatusBarTipsWindow @synthesize tipsMessage; static SvStatusBarTipsWindow *tipsWindow = nil; + (SvStatusBarTipsWindow*)shareTipsWindow { if (!tipsWindow) { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ tipsWindow = [[super allocWithZone:NULL] init]; }); } return tipsWindow; } + (id)copyWithZone:(NSZone *)zone { return [[self shareTipsWindow] retain]; } + (id)allocWithZone:(NSZone *)zone { return [[self shareTipsWindow] retain]; } - (id)retain { return tipsWindow; } - (oneway void)release { return; } - (id)autorelease { return tipsWindow; } - (id)init { CGRect frame = [UIApplication sharedApplication].statusBarFrame; self = [super initWithFrame:frame]; if (self) { self.autoresizingMask = UIViewAutoresizingFlexibleWidth; self.windowLevel = UIWindowLevelStatusBar + 10; self.backgroundColor = [UIColor clearColor]; _tipsIcon = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, ICON_WIDTH, ICON_WIDTH)]; _tipsIcon.autoresizingMask = UIViewAutoresizingFlexibleHeight; _tipsIcon.backgroundColor = [UIColor redColor]; [self addSubview:_tipsIcon]; [_tipsIcon release]; _tipsLbl = [[UILabel alloc] initWithFrame:self.bounds]; #ifdef NSTextAlignmentRight _tipsLbl.textAlignment = NSTextAlignmentLeft; _tipsLbl.lineBreakMode = NSLineBreakByTruncatingTail; #else _tipsLbl.textAlignment = 0; // means UITextAlignmentLeft _tipsLbl.lineBreakMode = 4; //UILineBreakModeTailTruncation; #endif _tipsLbl.textColor = [UIColor whiteColor]; _tipsLbl.font = [UIFont systemFontOfSize:12]; _tipsLbl.backgroundColor = [UIColor blackColor]; _tipsLbl.autoresizingMask = UIViewAutoresizingFlexibleHeight; [self addSubview:_tipsLbl]; [_tipsLbl release]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateOrientation:) name:UIApplicationWillChangeStatusBarOrientationNotification object:nil]; } return self; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; [tipsMessage release]; [super dealloc]; } #pragma mark - #pragma mark Notification Handle - (void)updateOrientation:(NSNotification*)noti { UIInterfaceOrientation newOrientation = [[noti.userInfo valueForKey:UIApplicationStatusBarOrientationUserInfoKey] integerValue]; NSLog(@"new orientation: %d", newOrientation); switch (newOrientation) { case UIInterfaceOrientationPortrait: { self.transform = CGAffineTransformIdentity; self.frame = CGRectMake(0, 0, SCREEN_WIDTH, HEIGHT); break; } case UIInterfaceOrientationPortraitUpsideDown: { // 先轉矩陣,坐標系統落在屏幕有右下角,朝上是y,朝左是x self.transform = CGAffineTransformMakeRotation(M_PI); self.center = CGPointMake(SCREEN_WIDTH / 2, SCREEN_HEIGHT - HEIGHT / 2); self.bounds = CGRectMake(0, 0, SCREEN_WIDTH, HEIGHT); break; } case UIInterfaceOrientationLandscapeLeft: { self.transform = CGAffineTransformMakeRotation(-M_PI_2); // 這個時候坐標軸已經轉了90°,調整x相當於調節豎向調節,y相當於橫向調節 self.center = CGPointMake(HEIGHT / 2, [UIScreen mainScreen].bounds.size.height / 2); self.bounds = CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.height, HEIGHT); break; } case UIInterfaceOrientationLandscapeRight: { // 先設置transform,在設置位置和大小 self.transform = CGAffineTransformMakeRotation(M_PI_2); self.center = CGPointMake(SCREEN_WIDTH - HEIGHT / 2, SCREEN_HEIGHT / 2); self.bounds = CGRectMake(0, 0, SCREEN_HEIGHT, HEIGHT); break; } default: break; } } #pragma mark - #pragma mark Tips Method /* * @brief show tips message on statusBar */ - (void)showTips:(NSString*)tips { if (_hideTimer) { [_hideTimer invalidate]; [_hideTimer release]; } _tipsIcon.image = nil; _tipsIcon.hidden = YES; CGSize size = [tips sizeWithFont:_tipsLbl.font constrainedToSize:CGSizeMake(320, 30)]; size.width += TIPMESSAGE_RIGHT_MARGIN; if (size.width > self.bounds.size.width - ICON_WIDTH) { size.width = self.bounds.size.width - ICON_WIDTH; } _tipsLbl.frame = CGRectMake(self.bounds.size.width - size.width, 0, size.width, self.bounds.size.height); _tipsLbl.text = tips; [self makeKeyAndVisible]; } - (void)showTips:(NSString*)tips hideAfterDelay:(NSInteger)seconds { [self showTips:tips]; _hideTimer = [NSTimer scheduledTimerWithTimeInterval:seconds target:self selector:@selector(hideTips) userInfo:nil repeats:NO]; } /* * @brief show tips icon and message on statusBar */ - (void)showTipsWithImage:(UIImage*)tipsIconImage message:(NSString*)message { if (_hideTimer) { [_hideTimer invalidate]; [_hideTimer release]; } CGSize size = [message sizeWithFont:_tipsLbl.font constrainedToSize:self.bounds.size]; size.width += TIPMESSAGE_RIGHT_MARGIN; if (size.width > self.bounds.size.width - ICON_WIDTH) { size.width = self.bounds.size.width - ICON_WIDTH; } _tipsLbl.frame = CGRectMake(self.bounds.size.width - size.width, 0, size.width, self.bounds.size.height); _tipsLbl.text = message; _tipsIcon.center = CGPointMake(self.bounds.size.width - _tipsLbl.bounds.size.width - ICON_WIDTH / 2 - ICON_RIGHT_MARGIN, self.bounds.size.height / 2); _tipsIcon.image = tipsIconImage; _tipsIcon.hidden = NO; [self makeKeyAndVisible]; } - (void)showTipsWithImage:(UIImage*)tipsIconImage message:(NSString*)message hideAfterDelay:(NSInteger)seconds { [self showTipsWithImage:tipsIconImage message:message]; _hideTimer = [NSTimer scheduledTimerWithTimeInterval:seconds target:self selector:@selector(hideTips) userInfo:nil repeats:NO]; } /* * @brief hide tips window */ - (void)hideTips { self.hidden = YES; } @end
該狀態欄提示控件實現了添加提示圖標和提示文字,以及自動隱藏等功能。顯示和隱藏動畫實現起來比較簡單,控件中就沒有實現,大家可以根據需要隨意發揮。該控件使用單例模式,接口非常簡單,使用起來很方便。上面代碼相信大家都能看得懂,這里就不展開講了,有什么問題歡迎討論。
注: 轉載請注明出去,有什么不對的地方,歡迎指正。