手机共享了热点,另一部手机通过wifi连接连接上这个热点。手机顶部会出现一个蓝色的工具栏,并显示”个人热点:1个连接“,这个工具栏比正常的工具栏告20个像素。
当打开一个应用,若该页面不显示系统工具栏,看不到这个热点工具栏;若该页面显示了系统工具栏,那么就可以看到这个高度增加了一倍的工具栏,并且页面的所有空间下移20像素,导致在底部的按钮显示不全。想修改这个问题,主要是如何发现蓝条的显示和消失,来决定是否上移底部的按钮20个像素。系统会根据蓝条的显示和消失,重新布局页面,所以不要移动所有所有控件,一般只需要适配底层一到二个控件就可以。
判断应用状态栏是否显示蓝条可以通过三种方法:
// 标准系统状态栏高度
#define SYS_STATUSBAR_HEIGHT 20
// 热点栏高度
#define HOTSPOT_STATUSBAR_HEIGHT 20
(1).被动方式,当蓝条显示或消失,会发送状态栏变更通知
(UIApplicationWillChangeStatusBarFrameNotification),通过读取通知中状态栏的高度来判断是否显示了栏条。
- (void)handleUIApplicationWillChangeStatusBarFrameNotification:(NSNotification*)notification { [self logViewTreeForMainWindow]; if(notification != nil) { CGRect newStatusBarFrame = [(NSValue*)[notification.userInfo objectForKey:UIApplicationStatusBarFrameUserInfoKey] CGRectValue]; // 根据系统状态栏高判断热点栏的变动 [SingleObject sharedInstance].bPersonalHotspotConnected = (CGRectGetHeight(newStatusBarFrame)==(SYS_STATUSBAR_HEIGHT+HOTSPOT_STATUSBAR_HEIGHT)?YES:NO); } else { [SingleObject sharedInstance].bPersonalHotspotConnected = ([UIApplication sharedApplication].statusBarFrame.size.height==(SYS_STATUSBAR_HEIGHT+HOTSPOT_STATUSBAR_HEIGHT)?YES:NO); } float hotspotConnectedOffsetY = 0; if(([SingleObject sharedInstance].bPersonalHotspotConnected) && (_listenOrderPanel != nil)) { hotspotConnectedOffsetY = -HOTSPOT_STATUSBAR_HEIGHT; } //调整控件的frame }
(2).主动方式,通过直接读取系统状态栏的高度([UIApplication sharedApplication].statusBarFrame.size.height)来判断是否显示了热点蓝条。
- (void)applicationDidBecomeActive:(UIApplication *)application { BOOL bPersonalHotspotConnected = ([UIApplication sharedApplication].statusBarFrame.size.height==(SYS_STATUSBAR_HEIGHT+HOTSPOT_STATUSBAR_HEIGHT)?YES:NO); [SingleObject sharedInstance].bPersonalHotspotConnected = bPersonalHotspotConnected; if([SingleObject sharedInstance].isInYXCenterViewController) { [[NSNotificationCenter defaultCenter] postNotificationName:CHANGE_STATUS_BAR_FRAME_NOTIFICATION object:nil]; } }
(3).主动获取,通过检查keyWindow的倒数第二个图层的初始y坐标是否为20像素来判断是否显示热点蓝条
+(BOOL)getPersonalHotspotConnected { UIView *keyWindowView = [[UIApplication sharedApplication] keyWindow]; if((keyWindowView != nil) && ([keyWindowView subviews].count > 1)) { UIView *UILayoutContainerView0 = [keyWindowView subviews][1]; if(UILayoutContainerView0.frame.origin.y == HOTSPOT_STATUSBAR_HEIGHT) { [SingleObject sharedInstance].bPersonalHotspotConnected = YES; } else { [SingleObject sharedInstance].bPersonalHotspotConnected = NO; } } return [SingleObject sharedInstance].bPersonalHotspotConnected; }
既然知道了如何判断是否显示了热点蓝条,剩下的是如何捕获热点蓝条显示和消失的时机,进而进行实时调整底部的控件的偏移就可以了。热点蓝条显示和消失的情景如下:
1.应用在前台切换到后台时会收到状态栏变更通知,蓝色状态栏通知(状态栏高度为正常的20像素),就是应用在后台,你的应用是没有热点蓝条的。
2.应用在后台,操作系统热点蓝条消失,若应用非定位服务的应用可能你收不到状态栏变更通知,定位服务的应用会收到状态栏变更通知。
3.应用在后台,操作系统热点蓝条出现,若应用非定位服务的应用可能你收不到状态栏变更通知,定位服务的应用应该会收到状态栏变更通知。
4.当应用从后台切换到前台时,会收到状态栏改变通知(状态栏高度为40像素)。
5.当操作系统已经显示蓝条,应用开始启动,你收不到状态变更通知。你想正确调整底部控件,只能主动获取状态栏的高度进行是否显示了热点蓝条,进而调整底部控件。
6.启动时没有热点蓝条,当应用在前台,有人接入热点,显示热点蓝条,会收到状态栏变更通知。当应用在前台,显示热点蓝条,后蓝条消失,会收到状态栏变更通知。
5.当操作系统已经显示蓝条,应用开始启动后,有人接入热点,显示热点蓝条,会收到状态栏变更通知。当应用在前台,显示热点蓝条,后蓝条消失,会收到状态栏变更通知。
可以看到应用在前台时,都是能够收到系统的状态栏变更,只需要被动的接收系统的状态栏变更通知,再调整控件就可以了。
当应用在后台,经过多次热点蓝条的消失和显示,你可能漏掉正确的调整空间的时机。
真对这种情况,你只需要在应用进入前台时,通知相关需要调整的页面调整一下底部控件就可以了,如:(2)主动式获取状态栏的处理。
你处理了这两类情况,你就解决了绝大多数情况。但是还有一种异常情况,咱们看一下:
当然当应用启动前手机已经显示了蓝条,有可能(我的出租车司机端必现)出现整个页面下移动40像素。当蓝条消失时,显示顶部控件到手机顶部比期望的高20像素,工具栏显示的状态栏背景为黑色。当显示热点蓝条时,通过View UI Hierarchy,观察立体图层发现该种异常情况下,页面的全屏幕布局分了3个阶梯,每一个阶梯相差20像素;当不显示热点蓝条时,通过View UI Hierarchy,观察立体图层发现该种异常情况下,页面的全屏幕布局分了2个阶梯,每一个阶梯相差20像素。
当然当应用启动前手机不显示蓝条,进入首页面显示热点蓝条时,通过View UI Hierarchy,观察立体图层发现该种情况下,页面的全屏幕布局分了2个阶梯,每一个阶梯相差20像素;当热点蓝条消失后,通过View UI Hierarchy,观察立体图层发现该种异常情况下,页面的全屏幕布局都一个高度。
通过分析上面两种情况的全屏幕图层的frame高度和起始高度数据看到第6层及以后的名称为 UILayoutContainerView图层显然出现了明显的差异,如:
进入时有蓝条,及蓝条消失第7层的打印数据:
------------[ 6] UILayoutContainerView, height:548.000000, origin.y:20.000000
------------[ 6] UILayoutContainerView, height:548.000000, origin.y:20.000000
进入时没有蓝条,蓝条显示和消失,再显示时的第7层的打印数据:
------------[ 6] UILayoutContainerView, height:568.000000, origin.y:0.000000
------------[ 6] UILayoutContainerView, height:568.000000, origin.y:0.000000
------------[ 6] UILayoutContainerView, height:568.000000, origin.y:0.000000
可以看到不同,这就是进入时有蓝条,当进入首页时顶部控件距离热点蓝条多20像素,当热点蓝条消失时显示的状态栏有20个像素的黑色区域并且首页的顶部控件到状态栏多20个像素的真正原因。
既然知道了原因,只需要判断出这这种情况,修改大于等于第6层的 UILayoutContainerView图层frame就可以了,看来还是需要修改系统底层的东西啊!应用的图层都是在第8层UINavigationTransitionView以后的图层,并且名称都不叫UILayoutContainerView。只要在加载首页面时判断出这种情况修改好就可以。可以通过遍历keyWindow的所有图层,找到异常的图层把他的frame修改成CGRectMake(0, 0, kScreenWidth, kScreenHeight)就可以。
具体需要修改的代码,在viewDidLoad调整就可以,代码如下:
- (void)dumpView:(UIView *)aView atIndent:(int)indent into:(NSMutableString *)outstring { for (int i = 0; i < indent; i++) [outstring appendString:@"--"]; [outstring appendFormat:@"[%2d] %@, height:%f, origin.y:%f\n", indent, [[aView class] description], aView.frame.size.height, aView.frame.origin.y]; if((indent > 5) && (aView != nil) && ([[[aView class] description] isEqualToString:@"UILayoutContainerView"])) { if(aView.frame.origin.y == HOTSPOT_STATUSBAR_HEIGHT) { aView.frame = CGRectMake(0, 0, kScreenWidth, kScreenHeight); } } for (UIView *view in [aView subviews]) [self dumpView:view atIndent:indent + 1 into:outstring]; } // Start the tree recursion at level 0 with the root view - (NSMutableDictionary *) displayViews: (UIView *) aView { NSMutableString *outstring = [[NSMutableString alloc] init]; UIView *keyWindowView = [[UIApplication sharedApplication] keyWindow]; [self dumpView: aView atIndent:0 into:outstring]; if(outstring.length != 0) { if((keyWindowView != nil) && ([keyWindowView subviews].count > 1)) { UIView *UILayoutContainerView0 = [keyWindowView subviews][1]; if(UILayoutContainerView0.frame.origin.y == HOTSPOT_STATUSBAR_HEIGHT) { [SingleObject sharedInstance].bPersonalHotspotConnected = YES; } else { [SingleObject sharedInstance].bPersonalHotspotConnected = NO; } } NSString *personalHotspotConnectedValue = [SingleObject sharedInstance].bPersonalHotspotConnected ? @"1":@"0"; NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithObjectsAndKeys:outstring, @"outstring", personalHotspotConnectedValue, @"bPersonalHotspotConnected", nil]; return dic; } else { return nil; } } // Show the tree - (void)logViewTreeForMainWindow { NSMutableDictionary *dic = [self displayViews:[[UIApplication sharedApplication] keyWindow]]; if((dic.count == 2) && ([dic[@"outstring"] isKindOfClass:[NSString class]])) { FLDDLogDebug(@"The view tree:\n%@", dic[@"outstring"]); } } - (void)viewDidLoad { [super viewDidLoad]; [self logViewTreeForMainWindow]; }
进入时有蓝条,及蓝条消失全部数据,第1层的数据都相同就不重复了:
[ 0] UIWindow, height:568.000000, origin.y:0.000000
–[ 1] UILayoutContainerView, height:548.000000, origin.y:20.000000
–[ 1] UILayoutContainerView, height:568.000000, origin.y:0.000000
----[ 2] UINavigationTransitionView, height:548.000000, origin.y:0.000000
----[ 2] UINavigationTransitionView, height:568.000000, origin.y:0.000000
------[ 3] UIViewControllerWrapperView, height:548.000000, origin.y:0.000000
------[ 3] UIViewControllerWrapperView, height:568.000000, origin.y:0.000000
--------[ 4] UIView, height:548.000000, origin.y:0.000000
--------[ 4] UIView, height:568.000000, origin.y:0.000000
----------[ 5] UIView, height:548.000000, origin.y:0.000000
----------[ 5] UIView, height:548.000000, origin.y:0.000000
------------[ 6] UILayoutContainerView, height:548.000000, origin.y:20.000000
------------[ 6] UILayoutContainerView, height:548.000000, origin.y:20.000000
--------------[ 7] UINavigationTransitionView, height:548.000000, origin.y:0.000000
--------------[ 7] UINavigationTransitionView, height:548.000000, origin.y:0.000000
----------------[ 8] UIViewControllerWrapperView, height:548.000000, origin.y:0.000000
----------------[ 8] UIViewControllerWrapperView, height:548.000000, origin.y:0.000000
进入时没有蓝条,蓝条显示和消失的全部数据,第1层的数据都相同就不重复了:
[ 0] UIWindow, height:568.000000, origin.y:0.000000
–[ 1] UILayoutContainerView, height:568.000000, origin.y:0.000000
–[ 1] UILayoutContainerView, height:548.000000, origin.y:20.000000
–[ 1] UILayoutContainerView, height:568.000000, origin.y:0.000000
----[ 2] UINavigationTransitionView, height:568.000000, origin.y:0.000000
----[ 2] UINavigationTransitionView, height:548.000000, origin.y:0.000000
----[ 2] UINavigationTransitionView, height:568.000000, origin.y:0.000000
------[ 3] UIViewControllerWrapperView, height:568.000000, origin.y:0.000000
------[ 3] UIViewControllerWrapperView, height:548.000000, origin.y:0.000000
------[ 3] UIViewControllerWrapperView, height:568.000000, origin.y:0.000000
--------[ 4] UIView, height:568.000000, origin.y:0.000000
--------[ 4] UIView, height:548.000000, origin.y:0.000000
--------[ 4] UIView, height:568.000000, origin.y:0.000000
----------[ 5] UIView, height:568.000000, origin.y:0.000000
----------[ 5] UIView, height:568.000000, origin.y:0.000000
----------[ 5] UIView, height:568.000000, origin.y:0.000000
------------[ 6] UILayoutContainerView, height:568.000000, origin.y:0.000000
------------[ 6] UILayoutContainerView, height:568.000000, origin.y:0.000000
------------[ 6] UILayoutContainerView, height:568.000000, origin.y:0.000000
--------------[ 7] UINavigationTransitionView, height:568.000000, origin.y:0.000000
--------------[ 7] UINavigationTransitionView, height:568.000000, origin.y:0.000000
--------------[ 7] UINavigationTransitionView, height:568.000000, origin.y:0.000000
----------------[ 8] UIViewControllerWrapperView, height:568.000000, origin.y:0.000000
----------------[ 8] UIViewControllerWrapperView, height:568.000000, origin.y:0.000000
----------------[ 8] UIViewControllerWrapperView, height:568.000000, origin.y:0.000000
------------------[ 9] UIView, height:568.000000, origin.y:0.000000
------------------[ 9] UIView, height:568.000000, origin.y:0.000000
------------------[ 9] UIView, height:568.000000, origin.y:0.000000
--------------------[10] UIImageView, height:568.000000, origin.y:0.000000
--------------------[10] UIImageView, height:568.000000, origin.y:0.000000
--------------------[10] UIImageView, height:568.000000, origin.y:0.000000
进入时有蓝条的图层层次图(异常场景):
进入时有蓝条,热点共享蓝条消失后的图层层次图(异常场景):
进入时没有有蓝条,热点共享蓝条出现后的图层层次图(正常场景):
进入时没有热点共享蓝条的图层层次图(正常场景):
进入时没有热点蓝条,当热点蓝条消失时,状态栏显示为20个像素的黑色区域,顶部控件到顶部的距离也比预期的高20个像素的截图如下(异常场景):
进入时有热点蓝条,当热点蓝条消失时,修正后的正确截图如下:
进入时有热点蓝条,修正后的正确截图如下:
下面是微信没有完美解决热点共享蓝条下压后导致顶部工具栏异常的图片。可见只是靠接收UIApplicationWillChangeStatusBarFrameNotification通知来处理它引起的状态栏异常是不完美的。我也经过很长时间的修改探索才找到这个完美解决方案。对工作执着、对产品负责的态度,极度注重细节,不断追求完美,没有绝对完美的产品,我们都是在追求路上。
注意:iOS13.51后,苹果抄袭安卓,把那个多出20像素状态栏修改为显示左上角时间背景颜色,并把时间换成对应图标一小段时间,然后图标消失,仍旧显示时间。所以苹果的热点共享(蓝色),后台定位(蓝色)和后台语音(红色)不会出现状态栏下压20像素导致底部页面20像素显示不出来的问题了。这个小功能变更不知道苹果什么时候修改的,苹果也没有给这方面的说明,我估计可能是iOS13时修改的。不过这样我们不用再适配这种页面混乱了。其实原来影响最差的不是普通页面,而是启动页面。若你的启动页面中间有有意义的图像。当热点状态栏出来时,会把中间截取20像素扔掉。
后台定位刚出现时。
热点共享工具栏出现一段时间后。
后台语音刚出现时。