微博iOS的護眼模式

夜間模式的探討

與其他App切換夜間模式不同:


知乎的夜間模式

微博采取了護眼模式:


微博的護眼模式

兩種方案各有利弊:

  • 夜間模式優點
    可以對每一個原生控件元素進行定制(背景色、字體顏色、分隔線顏色等)UI整體上更加精致;
    夜間模式缺點
    夜間主題的配色比較難掌握,對設計有一定的要求。一旦沒有選擇好配色,不僅起不到夜間的效果,反而晃瞎眼;
    另一個嚴重的問題是web頁面以及三方的url無法控制,如下圖很明顯navigationbartabbar還處于夜間狀態,中間的web卻亮瞎眼
    知乎活動廣場頁面
  • 護眼模式的優點
    Mac OS 10.14推出黑色主題不同的是 ,iOS設備一直對于夜間沒有做特殊的處理,僅僅通過感光元件的檢測來實時調整手機的亮度;護眼模式的出發點也一樣,App自己通過一定的方式再一次降低亮度以達到保護眼睛的作用
    全局生效,只要還停留在App內不論是原生還是web,所有的頁面都會被護眼的陰影籠罩;
    護眼模式的缺點
    感官上沒有夜間模式那么討喜,那么酷炫

選擇適合的方案

如果App內有很多web外鏈,首推微博的護眼方案;如果有自己設計團隊全力配合且大部分為原生的頁面和控件夜間模式可以讓App看起來更高級
我個人更喜歡護眼方案,不僅僅是因為App內web較多,護眼這種模式實施起來起來也更加的簡單

護眼模式是何如做到的?

前面這么多廢話,終于講到正題。憑借多年開發經驗第一次看到微博的護眼模式我果斷的認為微博是通過降低手機屏幕的亮度來護眼的(就像微信打開二維碼時提高了亮度一樣)。于是我自信滿滿的檢查手機的亮度調節開關,驚訝的發現亮度并沒有發生變化!這一下子吊起了我的胃口,飛快的打開百度、google進行搜索,無果!
與此同時開始萌生一個新的想法:難道在keywindow上覆蓋一層灰色半透明的mask??急于驗證的我只能耍流氓的使用逆向工具對微博進行視圖調試!
幸好有牛逼的 MonkeyDev讓逆向App變得如此簡單。不會的同學可以看我的另一篇《MonkeyDev的安裝以及與Reveal配合使用》

配置完MonkeyDev和Reveal后查看,果然!?。。?/p>

image.png

微博在最上層加了一個windowLevel2099UIWindow,這個window層級高于keywindow0,UITextEffectsWindow10,UIStatusWindow1000以及UIWindowLevelAlert2000;但是遠低于鍵盤所在window,這意味著:除了鍵盤之外所有的視圖都會被這個WBSkinCoverWindow所覆蓋。

如此簡單粗暴卻行之有效的方案!

另外通過Reveal可以發現:
WBSkinCoverWindow上添加了一個Opacity0.5的黑色背景layer

image.png

既然原理有了,接下來就開始模仿微博實現一個自己的護眼模式吧!

護眼模式的開發

為了降低耦合性,創建一個工具類 ,我這里起名WEEyeCareModeUtil
這個類暴露三個接口方法:

/**
 * 單例創建方法
 * @return 單例對象
 */
+ (instancetype)sharedUtil;

/**
 * 護眼模式是否已經打開
 * @return 是否已經打開
 */
- (BOOL)queryEyeCareModeStatus;

/**
 * 切換護眼模式
 * @param on 是否打開
 */
- (void)switchEyeCareMode:(BOOL)on;

分別是創建方法查詢狀態方法以及切換模式方法

/// NSUserDefaults存的key
static NSString * const kEyeCareModeStatus = @"kEyeCareModeStatus";
- (BOOL)queryEyeCareModeStatus
{
    return [[NSUserDefaults standardUserDefaults] boolForKey:kEyeCareModeStatus];
}
  • 切換狀態
- (void)switchEyeCareMode:(BOOL)on
{
    // 切換的具體實現
    ...

    // 將狀態寫入設置
    [[NSUserDefaults standardUserDefaults] setBool:on forKey:kEyeCareModeStatus];
    [[NSUserDefaults standardUserDefaults] synchronize];
}

思路很簡單,接下來就開始寫切換的具體實現

護眼模式切換的實現

參考微博的實現:


image.png

我們也創建一個自己的WESkinCoverLayerWESkinCoverWindow

@interface WESkinCoverLayer : CALayer
@end

@implementation WESkinCoverLayer
@end
/// 專用于護眼模式的UIWindow,這樣才能在`[[UIApplication sharedApplication] windows]`里方便地區分出來
@interface WESkinCoverWindow : UIWindow
@end

@implementation WESkinCoverWindow

- (instancetype)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame]) {
        // 移除所有的子layer
        [self.layer.sublayers makeObjectsPerformSelector:@selector(removeFromSuperlayer)];
        // 添加layer
        WESkinCoverLayer *skinCoverLayer = [WESkinCoverLayer layer];
        skinCoverLayer.frame = CGRectMake(0, 0, frame.size.width, frame.size.height);
        skinCoverLayer.backgroundColor = UIColorBlack.CGColor;
        skinCoverLayer.opacity = 0.5;
        [self.layer addSublayer:skinCoverLayer];
    }
    return self;
}

@end

分別創建兩個子類是為了方便的從[UIApplication sharedApplication].windowsself.layer.sublayers從快速找出屬于護眼模式的專用windowlayer;同時這樣操作在Reveal中也能方便的找到他們

回到工具類WEEyeCareModeUtil

懶加載一個WESkinCoverWindow的實例:

/// 覆蓋window的level
static NSInteger const kWeSkinCoverWindowLevel = 2099;
#pragma mark - setter & getter
- (WESkinCoverWindow *)skinCoverWindow
{
   if (!_skinCoverWindow) {
       // 給window賦值上初始的frame,在ios9之前如果不賦值系統默認認為是CGRectZero
       _skinCoverWindow = [[WESkinCoverWindow alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)];
       _skinCoverWindow.windowLevel = kWeSkinCoverWindowLevel;
       _skinCoverWindow.userInteractionEnabled = NO;
       // 添加到UIScreen
       [_skinCoverWindow makeKeyWindow];
   }
   return _skinCoverWindow;
}

需要注意:

  • windowLevel設置大一些,我們參考微博的做法,將其設置為2099
  • 需要將windowuserInteractionEnabled屬性設為NO,為的是將交互事件傳遞到下面的其他window
  • UIWindow如果需要覆蓋到屏幕上,有兩種方式:作為某一個windowsubWindow或者直接makeKeyWindow;這里顯然使用后者更加合理。

上面我們通過[_skinCoverWindow makeKeyWindow]成功將mask顯示在屏幕上,但[UIApplication sharedApplication]只能有一個keywindow,所以當skinCoverWindow加到UIScreen上之后需要將將key還給上一個keywindow

創建一個弱引用的屬性用來記錄上一個keywindow

// 之前的一個window
@property(nonatomic, weak) UIWindow *previousKeyWindow;

顯示代碼:

// 記錄上一個keywindow
self.previousKeyWindow = [UIApplication sharedApplication].keyWindow;
// 將skinCoverWindow顯示出來
self.skinCoverWindow.hidden = NO;
// 顯示之后把key還給之前的window
[self.previousKeyWindow makeKeyWindow];

隱藏代碼:

if ([[UIApplication sharedApplication].windows containsObject:self.skinCoverWindow]) {
    // 隱藏
    self.skinCoverWindow.hidden = YES;
    // 清空
    self.previousKeyWindow = nil;
}

至此,我們已經完成了大部分工作,但是運行項目會發現灰色半透明遮罩出現和消失都非常突兀。而微博明顯加了動畫,我們也依葫蘆畫瓢優化一下:

出現代碼:

// 記錄上一個keywindow
self.previousKeyWindow = [UIApplication sharedApplication].keyWindow;
// 顯示出來
self.skinCoverWindow.hidden = NO;
// 出現動畫
CABasicAnimation *opacityAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"];
opacityAnimation.fromValue = @(0);
opacityAnimation.toValue = @(1);
opacityAnimation.duration = kAnimationDuration;
opacityAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
opacityAnimation.fillMode = kCAFillModeForwards;
opacityAnimation.removedOnCompletion = NO;
opacityAnimation.qmui_animationDidStopBlock = ^(__kindof CAAnimation *aAnimation, BOOL finished) {
    // 把key還給之前的window
    [self.previousKeyWindow makeKeyWindow];
};
[self.skinCoverWindow.layer addAnimation:opacityAnimation forKey:@"showAnimation"];

消失代碼:

[self.previousKeyWindow makeKeyWindow];
if ([[UIApplication sharedApplication].windows containsObject:self.skinCoverWindow]) {
    // 隱藏skinCoverWindow
    // 消失動畫
    CABasicAnimation *opacityAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"];
    opacityAnimation.fromValue = @(1);
    opacityAnimation.toValue = @(0);
    opacityAnimation.duration = kAnimationDuration;
    opacityAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    opacityAnimation.fillMode = kCAFillModeForwards;
    opacityAnimation.removedOnCompletion = NO;
    opacityAnimation.qmui_animationDidStopBlock = ^(__kindof CAAnimation *aAnimation, BOOL finished) {
        self.skinCoverWindow.hidden = YES;
        self.previousKeyWindow = nil;
    };
    [self.skinCoverWindow.layer addAnimation:opacityAnimation forKey:@"hideAnimation"];
} else {
    NSAssert(NO, @"Error:關閉護眼模式的時windows沒有找到WESkinCoverWindow?。?);
}

其中qmui_animationDidStopBlockCAAnimation的一個分類方法(來自于QMUI中的CAAnimation+QMUI);當然也可以自己實現CAAnimationDelegate用代理方法拿到動畫完成回調

使用注意

最后在項目中使用的時候需要注意:

  • 每次進入App時候在didFinishLaunchingWithOptions:中需要根據設置里保存的狀態判斷是否開啟護眼模式:
// 護眼模式配置
if ([[WEEyeCareModeUtil sharedUtil] queryEyeCareModeStatus]) {
    [[WEEyeCareModeUtil sharedUtil] switchEyeCareMode:YES];
}

運行項目會發現系統報錯

*** Assertion failure in -[UIApplication _runWithMainScene:transitionContext:completion:], 
/BuildRoot/Library/Caches/[com.apple.xbs/Sources/UIKitCore/UIKit-3698.93.8/UIApplication.m:3855](com.apple.xbs/Sources/UIKitCore/UIKit-3698.93.8/UIApplication.m:3855)
(lldb)

這是因為在didFinishLaunchingWithOptions:方法中我們通常會像下面這樣創建視圖界面:

// 界面
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
WEHomeViewController *homeVC = [[WEHomeViewController alloc] init];
WENavigationController *navi = [[WENavigationController alloc] initWithRootViewController:homeVC];
self.window.rootViewController = navi;
[self.window makeKeyAndVisible];

可以發現我們創建了一個window并將其makeKeyAndVisible,隨后我們在switchEyeCareMode:里又創建我們自己的護眼模式window并將其makeKeyWindow,而之前的keywindowrootViewController還沒有完成transitionContext:
因此需要對switchEyeCareMode:進行延遲操作,或者將護眼模式的配置推遲到rootViewControllerviewWillAppear:中。

總結

  • 善用觀察學習大廠的App
  • 適當了解一些逆向的知識很有益處
  • 自己動手擼代碼比看再多的技術文章都管用
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,497評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,305評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,962評論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,727評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,193評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,411評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,945評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,777評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,978評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,216評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,657評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,960評論 2 373

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,643評論 25 708
  • 用兩張圖告訴你,為什么你的 App 會卡頓? - Android - 掘金 Cover 有什么料? 從這篇文章中你...
    hw1212閱讀 12,777評論 2 59
  • 多年的好同學胡淑,突然問我一句:“你說,我要找個什么對象,才能過好余生呢?” 我聽后立馬揮手阻止道:“別,你千萬別...
    陳奕蓉閱讀 397評論 0 1
  • 報名了拓思PCP課程之后,課程還沒開始,我卻還是擺脫不了內心的擔憂和害怕。自己跟從內心找到了教練課程選擇了拓思,本...
    zbx1224閱讀 336評論 1 1
  • 18.問 既然下定決心,那就說做就做,阿亮也沒有含糊,收拾下,換身舒爽的衣服就出了門。 路上一次又一次的回憶著小美...
    小和尚講大道閱讀 285評論 0 0