微博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后查看,果然!!!!


image.png

微博在最上层加了一个windowLevel2099UIWindow,这个window层级高于keywindow0UITextEffectsWindow10UIStatusWindow1000以及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阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,516评论 25 707
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,693评论 2 59
  • 多年的好同学胡淑,突然问我一句:“你说,我要找个什么对象,才能过好余生呢?” 我听后立马挥手阻止道:“别,你千万别...
    陈奕蓉阅读 381评论 0 1
  • 报名了拓思PCP课程之后,课程还没开始,我却还是摆脱不了内心的担忧和害怕。自己跟从内心找到了教练课程选择了拓思,本...
    zbx1224阅读 327评论 1 1
  • 18.问 既然下定决心,那就说做就做,阿亮也没有含糊,收拾下,换身舒爽的衣服就出了门。 路上一次又一次的回忆着小美...
    小和尚讲大道阅读 265评论 0 0