稍后添加子视图以与超级视图一起制作动画
Add subview later to animate alongside superview
我有一个容器视图 - 我们称之为 套接字视图 - 它有一个子视图,它是内容视图 - 我们称之为 插头视图 .此插头视图可以为零,即套接字视图当前为空。如果它确实包含一个插头视图,它会占据整个套接字的 space,即它的框架是套接字的边界。从外在的角度来看,你甚至不应该知道实际上有两个视图,因为插头视图总是恰好在插座所在的位置。
我正在努力让动画正常工作:如果插件视图存在并且在动画之前布局,一切都会按预期工作。但是,如果我仅在动画已经 运行 时设置套接字的插头视图,我会得到一个不希望的效果:
插头视图布局到动画结束时的位置,并且不会与其插座一起设置动画。我希望它看起来一直都在那里,但只是现在才可见,即插头视图(及其子视图)应该与插座一起动画,即使我在动画进行时添加它也是如此。
我怎样才能实现这种行为?
我的想法:显然,插头视图必须布局两次:一次用于其最终位置,另一次用于插座视图开始动画的位置或添加它的位置。我可以计算这个帧,在没有动画的情况下应用它,并在一个新的动画块中对最后一帧进行动画处理。为了使动画时间保持一致,我需要具有相同的曲线和持续时间,但在过去开始动画或以某种方式向前移动。这可能吗?是否有其他方法可以让插头视图始终保持全宽和全高?
作为 Rob 回答的后续,这里有一些关于我正在寻找的内容的更多详细信息:
套接字视图正在动画,因为它的所有者的绑定大小已更改。您可以将其视为 table 视图中的全角单元格。
插件视图可能包含其自己的子视图,例如图像视图、标签等。这些也应该加入到套接字视图的动画中,就像它们从动画开始就一直存在一样。
虽然理论上可以在已经 运行 的情况下开始新动画,但我并不介意这种极端情况下的行为。
当动画处于 运行 时,用户无需能够与插件视图进行交互;无论如何,这很可能发生在界面方向更改期间。
插件视图可能会由于动画时的异步模型更新而决定更改其内容,但这同样是一种边缘情况,我不介意动画在这个案例。但是,它的大小不会改变 - 它始终与插件视图的大小相同。
为什么不让插头视图始终作为套接字视图的子视图存在,而是设置hidden = YES
?或者您可以使用 alpha = 0
。然后当你想显示它时,只需将它设置为 hidden = hidden = NO
或 alpha = 1
.
这样,当您为套接字视图设置动画时,您的插头视图将始终 "come along for the ride"。
顺便说一句,您的术语让我们这些使用 TCP 套接字的人感到迷惑。 ("Sockets? What?")
You can find my test project here.
你说你的插头视图应该完全、准确地覆盖插座视图。我们需要担心两件事:插头的层位置(layer.position
)和层大小(layer.bounds.size
)。
默认情况下,position
控制图层(和视图)的中心,因为默认的 anchorPoint
是 (0.5, 0.5)。如果我们将 anchorPoint
设置为 (0, 0),那么 position
控制层的 upper-left 角,我们总是希望它位于套接字坐标中的 (0, 0)系统。因此,通过将 anchorPoint
和 position
都设置为 CGPointZero
,我们可以避免担心动画 layer.position
.
剩下的就是让我们制作动画 layer.bounds.size
。
当您使用 UIKit 动画为视图设置动画时,它会在后台创建 CABasicAnimation
的实例。 CABasicAnimation
是 CAAnimation
的子类,它添加了(除其他外)fromValue
和 toValue
属性,指定动画的开始和结束值。
CAAnimation
符合CAMediaTiming
协议,也就是说它有一个beginTime
属性。当您创建 CAAnimation
时,它的默认 beginTime
为零。 Core Animation 在提交当前事务时将其更改为当前时间(请参阅 CACurrentMediaTime
)。
但是如果动画已经有non-zerobeginTime
,Core Animation会使用它as-is。如果那个 beginTime
是过去的,那么当它第一次出现时 on-screen 动画已经部分(甚至完全)完成,并且绘制时已经取得了适当的进度。我们基本上可以“回溯”动画。
因此,如果我们挖掘出控制插座 bounds.size
的 CABasicAnimation
,并将其添加到插头中,插头应该会按照我们想要的方式进行动画处理。当我们将动画附加到插头时,它的 beginTime
是它开始为插座设置动画的时间。所以即使我们稍后将它连接到插头上,它也会与插座完美同步。因为我们希望插头和插座的尺寸相同,所以 fromValue
和 toValue
也已经正确了。
在我的测试应用程序中,我将插座设为粉红色,将插头设为蓝色。每个都是 UIImageView
显示边缘有线条的图像,因此我们可以确保视图始终具有正确的大小。这是它的实际效果:
还有一个转折点。如果您为已经在设置动画的视图设置动画,UIKit 不会停止之前的动画。它添加了第二个动画,并且两个动画同时 运行。
之所以有效,是因为 UIKit 使用了 additive 动画。当您将视图的宽度从 320 设置为 160 时,UIKit 会立即将宽度设置为 160,并添加一个从 160 到 0 的附加动画。在动画开始时,表观宽度为 160+160=320,而在最后,表观宽度为160+0=160.
当第一个动画 运行ning 时 UIKit 添加第二个动画时,两个动画的值都会添加到用于在屏幕上绘制视图的表观值。效果如下:
这对我们来说意味着我们必须查找 keyPath
为 position.size
的所有套接字动画,并将它们全部复制到插头中。这是我的测试应用程序中的代码:
- (IBAction)plugWasTapped:(id)sender {
if (self.plugView.superview) {
[self.plugView removeFromSuperview];
return;
}
self.plugView.frame = self.socketView.bounds;
[self.socketView addSubview:self.plugView];
for (NSString *key in self.socketView.layer.animationKeys) {
CAAnimation *rawAnimation = [self.socketView.layer animationForKey:key];
if (![rawAnimation isKindOfClass:[CABasicAnimation class]]) {
continue;
}
CABasicAnimation *animation = (CABasicAnimation *)rawAnimation;
if ([animation.keyPath isEqualToString:@"bounds.size"]) {
[self.plugView.layer addAnimation:animation forKey:key];
}
}
}
这是多个同步动画的结果:
更新
让插件视图的完整视图层次结构动画化,就好像它一开始就在那里一样,坦率地说,工作量太大了。
这是另一种选择:
- 以插座的原始大小布置插头视图并创建它的图像 ("begin image")。
- 以插座的最终尺寸布置插头视图并创建它的图像 ("end image")。
- 将占位符图像视图放入套接字。
- 将大小动画从套接字复制到占位符。
- 使用大小动画在占位符上创建内容动画,使其内容从开始图像交叉淡化到结束图像。
- 动画结束时,将占位符替换为插件视图。
这是它的样子:
除了交叉淡入淡出的不完善之外,此版本无法同时处理多个动画 运行ning。您可以在我的存储库(链接在顶部)的 crossfade 标签下找到它。
另一种方法是仅以其最终大小呈现插件视图并将其放在占位符中而不使用交叉淡入淡出。它看起来像这样:
我觉得这样看起来更好,而且这个版本可以处理堆叠动画。您可以在我的存储库中的 stretch-end-image 标签下找到它。
最后,还有一个我没有实施的完全不同的方法。这将仅适用于您自己创建的调整大小动画——它不适用于系统在方向更改时创建的旋转动画。您可以使用计时器简单地为套接字视图的边界设置动画,而不是使用 UIKit anima离子。 WWDC 2012 Session 228: Best Practices for Mastering Auto Layout 讨论到最后。他建议使用 NSTimer
,但我认为您最好使用 CADisplayLink
。如果您采用这种方法,插件视图的子视图将全部完美地动画化。这比使用 UIKit 动画要多一些工作,但应该很容易实现。
我以这种方式修改了 Rob mayoff 代码。这对你有帮助吗?
重要的变化是实际使用 CADisplayLink 来更新 plugView 的框架,并且我向 plugView 添加了一个带有约束的子视图。还更改了添加 plugView 及其框架的方式。
只需检查一下这对您是否有效。您应该能够毫无问题地替换项目中的此代码。
#import "ViewController.h"
@interface UIView (recursiveDescription)
- (NSString *)recursiveDescription;
@end
@interface ViewController ()
@property (strong, nonatomic) IBOutlet NSLayoutConstraint *socketWidthConstraint;
@property (strong, nonatomic) IBOutlet NSLayoutConstraint *socketHeightConstraint;
@property (strong, nonatomic) IBOutlet UIView *socketView;
@property (strong, nonatomic) IBOutlet UIImageView *plugView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIView *extraView = [[UIView alloc] init];
extraView.translatesAutoresizingMaskIntoConstraints = NO;
extraView.backgroundColor = [UIColor blackColor];
[self.plugView addSubview:extraView];
NSLayoutConstraint *c1 = [NSLayoutConstraint constraintWithItem:extraView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.plugView attribute:NSLayoutAttributeTop multiplier:1.0 constant:0];
NSLayoutConstraint *c2 = [NSLayoutConstraint constraintWithItem:extraView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.plugView attribute:NSLayoutAttributeLeading multiplier:1.0 constant:0];
NSLayoutConstraint *c3 = [NSLayoutConstraint constraintWithItem:extraView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self.plugView attribute:NSLayoutAttributeWidth multiplier:0.5 constant:0];
NSLayoutConstraint *c4 = [NSLayoutConstraint constraintWithItem:extraView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:self.plugView attribute:NSLayoutAttributeHeight multiplier:0.5 constant:0];
[self.plugView addConstraints:@[c1, c2, c3, c4]];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkDidFire:)];
[link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)displayLinkDidFire:(CADisplayLink *)link {
CGSize size = [self.socketView.layer.presentationLayer frame].size;
self.plugView.frame = CGRectMake(0, 0, size.width, size.height);
[self.plugView layoutIfNeeded];
}
- (IBAction)plugWasTapped:(id)sender {
if (self.plugView.superview) {
[self.plugView removeFromSuperview];
return;
}
CGSize size = [self.socketView.layer.presentationLayer frame].size;
self.plugView.frame = CGRectMake(0, 0, size.width, size.height);
[self.socketView addSubview:self.plugView];
}
- (IBAction)bigButtonWasTapped:(id)sender {
[UIView animateWithDuration:10 animations:^{
self.socketWidthConstraint.constant = 320;
self.socketHeightConstraint.constant = 320;
[self.view layoutIfNeeded];
}];
}
- (IBAction)smallButtonWasTapped:(id)sender {
[UIView animateWithDuration:5 animations:^{
self.socketWidthConstraint.constant = 160;
self.socketHeightConstraint.constant = 160;
[self.view layoutIfNeeded];
}];
}
@end
我有一个容器视图 - 我们称之为 套接字视图 - 它有一个子视图,它是内容视图 - 我们称之为 插头视图 .此插头视图可以为零,即套接字视图当前为空。如果它确实包含一个插头视图,它会占据整个套接字的 space,即它的框架是套接字的边界。从外在的角度来看,你甚至不应该知道实际上有两个视图,因为插头视图总是恰好在插座所在的位置。
我正在努力让动画正常工作:如果插件视图存在并且在动画之前布局,一切都会按预期工作。但是,如果我仅在动画已经 运行 时设置套接字的插头视图,我会得到一个不希望的效果:
插头视图布局到动画结束时的位置,并且不会与其插座一起设置动画。我希望它看起来一直都在那里,但只是现在才可见,即插头视图(及其子视图)应该与插座一起动画,即使我在动画进行时添加它也是如此。
我怎样才能实现这种行为?
我的想法:显然,插头视图必须布局两次:一次用于其最终位置,另一次用于插座视图开始动画的位置或添加它的位置。我可以计算这个帧,在没有动画的情况下应用它,并在一个新的动画块中对最后一帧进行动画处理。为了使动画时间保持一致,我需要具有相同的曲线和持续时间,但在过去开始动画或以某种方式向前移动。这可能吗?是否有其他方法可以让插头视图始终保持全宽和全高?
作为 Rob 回答的后续,这里有一些关于我正在寻找的内容的更多详细信息:
套接字视图正在动画,因为它的所有者的绑定大小已更改。您可以将其视为 table 视图中的全角单元格。
插件视图可能包含其自己的子视图,例如图像视图、标签等。这些也应该加入到套接字视图的动画中,就像它们从动画开始就一直存在一样。
虽然理论上可以在已经 运行 的情况下开始新动画,但我并不介意这种极端情况下的行为。
当动画处于 运行 时,用户无需能够与插件视图进行交互;无论如何,这很可能发生在界面方向更改期间。
插件视图可能会由于动画时的异步模型更新而决定更改其内容,但这同样是一种边缘情况,我不介意动画在这个案例。但是,它的大小不会改变 - 它始终与插件视图的大小相同。
为什么不让插头视图始终作为套接字视图的子视图存在,而是设置hidden = YES
?或者您可以使用 alpha = 0
。然后当你想显示它时,只需将它设置为 hidden = hidden = NO
或 alpha = 1
.
这样,当您为套接字视图设置动画时,您的插头视图将始终 "come along for the ride"。
顺便说一句,您的术语让我们这些使用 TCP 套接字的人感到迷惑。 ("Sockets? What?")
You can find my test project here.
你说你的插头视图应该完全、准确地覆盖插座视图。我们需要担心两件事:插头的层位置(layer.position
)和层大小(layer.bounds.size
)。
默认情况下,position
控制图层(和视图)的中心,因为默认的 anchorPoint
是 (0.5, 0.5)。如果我们将 anchorPoint
设置为 (0, 0),那么 position
控制层的 upper-left 角,我们总是希望它位于套接字坐标中的 (0, 0)系统。因此,通过将 anchorPoint
和 position
都设置为 CGPointZero
,我们可以避免担心动画 layer.position
.
剩下的就是让我们制作动画 layer.bounds.size
。
当您使用 UIKit 动画为视图设置动画时,它会在后台创建 CABasicAnimation
的实例。 CABasicAnimation
是 CAAnimation
的子类,它添加了(除其他外)fromValue
和 toValue
属性,指定动画的开始和结束值。
CAAnimation
符合CAMediaTiming
协议,也就是说它有一个beginTime
属性。当您创建 CAAnimation
时,它的默认 beginTime
为零。 Core Animation 在提交当前事务时将其更改为当前时间(请参阅 CACurrentMediaTime
)。
但是如果动画已经有non-zerobeginTime
,Core Animation会使用它as-is。如果那个 beginTime
是过去的,那么当它第一次出现时 on-screen 动画已经部分(甚至完全)完成,并且绘制时已经取得了适当的进度。我们基本上可以“回溯”动画。
因此,如果我们挖掘出控制插座 bounds.size
的 CABasicAnimation
,并将其添加到插头中,插头应该会按照我们想要的方式进行动画处理。当我们将动画附加到插头时,它的 beginTime
是它开始为插座设置动画的时间。所以即使我们稍后将它连接到插头上,它也会与插座完美同步。因为我们希望插头和插座的尺寸相同,所以 fromValue
和 toValue
也已经正确了。
在我的测试应用程序中,我将插座设为粉红色,将插头设为蓝色。每个都是 UIImageView
显示边缘有线条的图像,因此我们可以确保视图始终具有正确的大小。这是它的实际效果:
还有一个转折点。如果您为已经在设置动画的视图设置动画,UIKit 不会停止之前的动画。它添加了第二个动画,并且两个动画同时 运行。
之所以有效,是因为 UIKit 使用了 additive 动画。当您将视图的宽度从 320 设置为 160 时,UIKit 会立即将宽度设置为 160,并添加一个从 160 到 0 的附加动画。在动画开始时,表观宽度为 160+160=320,而在最后,表观宽度为160+0=160.
当第一个动画 运行ning 时 UIKit 添加第二个动画时,两个动画的值都会添加到用于在屏幕上绘制视图的表观值。效果如下:
这对我们来说意味着我们必须查找 keyPath
为 position.size
的所有套接字动画,并将它们全部复制到插头中。这是我的测试应用程序中的代码:
- (IBAction)plugWasTapped:(id)sender {
if (self.plugView.superview) {
[self.plugView removeFromSuperview];
return;
}
self.plugView.frame = self.socketView.bounds;
[self.socketView addSubview:self.plugView];
for (NSString *key in self.socketView.layer.animationKeys) {
CAAnimation *rawAnimation = [self.socketView.layer animationForKey:key];
if (![rawAnimation isKindOfClass:[CABasicAnimation class]]) {
continue;
}
CABasicAnimation *animation = (CABasicAnimation *)rawAnimation;
if ([animation.keyPath isEqualToString:@"bounds.size"]) {
[self.plugView.layer addAnimation:animation forKey:key];
}
}
}
这是多个同步动画的结果:
更新
让插件视图的完整视图层次结构动画化,就好像它一开始就在那里一样,坦率地说,工作量太大了。
这是另一种选择:
- 以插座的原始大小布置插头视图并创建它的图像 ("begin image")。
- 以插座的最终尺寸布置插头视图并创建它的图像 ("end image")。
- 将占位符图像视图放入套接字。
- 将大小动画从套接字复制到占位符。
- 使用大小动画在占位符上创建内容动画,使其内容从开始图像交叉淡化到结束图像。
- 动画结束时,将占位符替换为插件视图。
这是它的样子:
除了交叉淡入淡出的不完善之外,此版本无法同时处理多个动画 运行ning。您可以在我的存储库(链接在顶部)的 crossfade 标签下找到它。
另一种方法是仅以其最终大小呈现插件视图并将其放在占位符中而不使用交叉淡入淡出。它看起来像这样:
我觉得这样看起来更好,而且这个版本可以处理堆叠动画。您可以在我的存储库中的 stretch-end-image 标签下找到它。
最后,还有一个我没有实施的完全不同的方法。这将仅适用于您自己创建的调整大小动画——它不适用于系统在方向更改时创建的旋转动画。您可以使用计时器简单地为套接字视图的边界设置动画,而不是使用 UIKit anima离子。 WWDC 2012 Session 228: Best Practices for Mastering Auto Layout 讨论到最后。他建议使用 NSTimer
,但我认为您最好使用 CADisplayLink
。如果您采用这种方法,插件视图的子视图将全部完美地动画化。这比使用 UIKit 动画要多一些工作,但应该很容易实现。
我以这种方式修改了 Rob mayoff 代码。这对你有帮助吗?
重要的变化是实际使用 CADisplayLink 来更新 plugView 的框架,并且我向 plugView 添加了一个带有约束的子视图。还更改了添加 plugView 及其框架的方式。
只需检查一下这对您是否有效。您应该能够毫无问题地替换项目中的此代码。
#import "ViewController.h"
@interface UIView (recursiveDescription)
- (NSString *)recursiveDescription;
@end
@interface ViewController ()
@property (strong, nonatomic) IBOutlet NSLayoutConstraint *socketWidthConstraint;
@property (strong, nonatomic) IBOutlet NSLayoutConstraint *socketHeightConstraint;
@property (strong, nonatomic) IBOutlet UIView *socketView;
@property (strong, nonatomic) IBOutlet UIImageView *plugView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIView *extraView = [[UIView alloc] init];
extraView.translatesAutoresizingMaskIntoConstraints = NO;
extraView.backgroundColor = [UIColor blackColor];
[self.plugView addSubview:extraView];
NSLayoutConstraint *c1 = [NSLayoutConstraint constraintWithItem:extraView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.plugView attribute:NSLayoutAttributeTop multiplier:1.0 constant:0];
NSLayoutConstraint *c2 = [NSLayoutConstraint constraintWithItem:extraView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.plugView attribute:NSLayoutAttributeLeading multiplier:1.0 constant:0];
NSLayoutConstraint *c3 = [NSLayoutConstraint constraintWithItem:extraView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self.plugView attribute:NSLayoutAttributeWidth multiplier:0.5 constant:0];
NSLayoutConstraint *c4 = [NSLayoutConstraint constraintWithItem:extraView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:self.plugView attribute:NSLayoutAttributeHeight multiplier:0.5 constant:0];
[self.plugView addConstraints:@[c1, c2, c3, c4]];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkDidFire:)];
[link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)displayLinkDidFire:(CADisplayLink *)link {
CGSize size = [self.socketView.layer.presentationLayer frame].size;
self.plugView.frame = CGRectMake(0, 0, size.width, size.height);
[self.plugView layoutIfNeeded];
}
- (IBAction)plugWasTapped:(id)sender {
if (self.plugView.superview) {
[self.plugView removeFromSuperview];
return;
}
CGSize size = [self.socketView.layer.presentationLayer frame].size;
self.plugView.frame = CGRectMake(0, 0, size.width, size.height);
[self.socketView addSubview:self.plugView];
}
- (IBAction)bigButtonWasTapped:(id)sender {
[UIView animateWithDuration:10 animations:^{
self.socketWidthConstraint.constant = 320;
self.socketHeightConstraint.constant = 320;
[self.view layoutIfNeeded];
}];
}
- (IBAction)smallButtonWasTapped:(id)sender {
[UIView animateWithDuration:5 animations:^{
self.socketWidthConstraint.constant = 160;
self.socketHeightConstraint.constant = 160;
[self.view layoutIfNeeded];
}];
}
@end