当您告诉导航控制器隐藏导航栏时,它会将其内容视图(您ReadingViewController
的视图)调整为全屏,并且内容视图将其子视图布置为新的全屏尺寸。默认情况下,它会在任何动画块之外执行此布局,因此新布局会立即生效。
要修复它,您需要使视图在动画块内执行布局。幸运的是,SDK 包含一个用于隐藏导航栏的动画持续时间的常量,并且动画使用线性曲线。hideControls:
将您的方法更改为:
- (void)hideControls:(BOOL)visible {
[UIView animateWithDuration:UINavigationControllerHideShowBarDuration animations:^{
[self.navigationController setNavigationBarHidden:visible animated:YES];
self.backFiftyWordsButton.hidden = visible;
self.forwardFiftyWordsButton.hidden = visible;
self.WPMLabel.hidden = visible;
self.timeRemainingLabel.hidden = visible;
[self.view layoutIfNeeded];
}];
}
这里有两个变化。一个是我使用UINavigationControllerHideShowBarDuration
常量将方法主体包装在动画块中,因此动画具有正确的持续时间。另一个变化是我发送layoutIfNeeded
到动画块内的视图,因此视图将动画到它们的新帧。
结果如下:
alpha
您还可以使用此动画块通过更改它们的属性而不是它们的属性来淡入和淡出标签hidden
。
更新
针对您评论中的问题:
首先,您需要了解运行循环的各个阶段。您的应用程序始终在其主线程上运行一个循环。极其简化的循环如下所示:
while (1) {
wait for an event (touch, timer, local or push notification, etc.)
Event phase: dispatch the event as appropriate (this often ends up
calling into your code, for example calling your tap recognizer's action)
Layout phase: send `layoutSubviews` to every view in the on-screen
view hierarchy that has been marked as needing layout
Draw phase: send `drawRect:` to any view that has been marked as needing
display (because it's a new view or it received `setNeedsDisplay` or
it has `UIViewContentModeRedraw`)
}
例如,如果您在 中放置断点hideControls:
,点击屏幕,然后查看调试器中的堆栈跟踪,您会PurpleEventCallback
在跟踪中看到向下(右上方__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
)。这告诉您您正处于事件处理阶段。(紫色是苹果内部 iPhone 项目的代号。)
如果您看到CA::Transaction::observer_callback
,则说明您处于布局阶段或绘制阶段。再往上看,你会看到CA::Layer::layout_if_needed
或者CA::Layer::display_if_needed
取决于你所处的阶段。
这就是运行循环及其阶段。现在,视图何时被标记为需要布局?它在收到时被标记为需要布局setNeedsLayout
。例如,如果您更改了视图应显示的内容并且需要相应地移动或调整它们的大小,则可以发送此信息。但是视图会setNeedsLayout
在两种情况下自动发送自己:当它的大小bounds
发生变化(或它的大小frame
)时,以及当它的subviews
数组发生变化时。
请注意,更改视图的大小或其子视图不会使视图立即布置其子视图!它只是计划在运行循环的布局阶段稍后布局其子视图。
那么……这一切与你有什么关系?
在你的hideControls:
方法中,你这样做了[self.navigationController setNavigationBarHidden:visible animated:YES]
。假设visible
是NO
。下面是导航控制器的响应:
- 它开始一个动画块。
- 它将导航栏的位置设置为屏幕顶部的上方。
- 它将内容视图的高度增加了 44 个点(导航栏的高度)。
- 它将内容视图的 Y 坐标减少 44 个点。
- 它结束了动画块。
内容视图框架的更改导致内容视图发送自身setNeedsLayout
。
请注意,导航栏框架和内容视图框架的更改是动画的。但是内容视图的子视图的框架还没有改变。这些更改发生在稍后的布局阶段。
因此,导航控制器会为您的顶级内容视图的更改设置动画,但不会为您的内容视图的子视图设置动画。您必须强制将这些更改设置为动画。
您可以通过两个步骤强制使这些更改变为动画:
- 您创建一个动画块,其参数与导航控制器使用的参数匹配。
- 在该动画块内,您通过发送到内容视图来强制布局阶段立即发生。
layoutIfNeeded
layoutIfNeeded
文档是这样说的:
使用此方法在绘制之前强制子视图的布局。从接收者开始,只要超级视图需要布局,此方法就会向上遍历视图层次结构。然后它把整棵树放在那个祖先的下面。
它通过向树中的视图发送layoutSubviews
消息来布置整个树,按从根到叶的顺序。如果您不使用自动布局,它还会在发送layoutSubviews
到视图之前应用每个视图的子视图的自动调整大小掩码。
因此,通过发送到您的内容视图,您将强制自动布局在返回layoutIfNeeded
之前立即更新内容视图的子视图的框架。layoutIfNeeded
这意味着这些更改发生在您的动画块内,因此它们使用与导航栏和内容视图更改相同的参数(持续时间和曲线)进行动画处理。
在动画块中布置子视图非常重要,Apple 定义了一个动画选项,UIViewAnimationOptionLayoutSubviews
. 如果你指定了这个选项,那么在动画块的最后,它会自动发送layoutIfNeeded
. 但是使用该选项需要使用消息的长版本animateWithDuration:delay:options:animations:completion:
,所以通常[self.view layoutIfNeeded]
在块的末尾自己做会更容易。