稍后添加子视图以与父视图一起动画

11
我有一个容器视图 - 让我们称其为套接字视图 - 它有一个单独的子视图,它的内容视图 - 让我们称其为插头视图。这个插头视图可以为空,也就是说套接字视图当前为空。如果它包含了一个插头视图,那么它占据整个套接字的空间,也就是说它的框架是套接字的边界。从外部来看,你甚至无法看出实际上有两个视图,因为插头视图总是在套接字所在的位置。
我正在努力使动画正常工作:如果插头视图存在并且在动画之前已经布局好,那么一切都按预期工作。然而,如果我在动画已经运行时才设置套接字的插头视图,我会得到一个不想要的效果:
插头视图被布局到动画结束时它需要在的位置,并且不随着它的套接字一起动画。我希望它看起来一直在那里,但现在才变得可见,也就是说插头视图(和它的子视图)应该随着套接字一起动画,即使我在动画进行时添加它。
我该如何实现这种行为?
我的想法是:显然,插头视图必须被布局两次:一次是为了最终位置,另一次是在套接字视图开始动画或添加到其中的位置。我可以计算这个框架,在没有动画的情况下应用它,并在新的动画块中对最终框架进行动画。为了保持动画时间的一致性,我需要相同的曲线和持续时间,但是需要在过去开始动画或以某种方式推进动画。这可行吗?是否有其他方法让插头视图在任何时候都是全宽度和高度?
作为对Rob答案的后续说明,以下是我正在寻找的一些更多细节:
- 套接字视图正在进行动画,因为它的所有者的边界尺寸已更改。您可以将其视为表视图中的全宽度单元格。
  • 插头视图可能包含自己的子视图,如图像视图、标签等。这些视图也应加入到插座视图的动画中,就好像它们自始至终一直存在。

  • 理论上,虽然可以在运行一个动画的同时启动新的动画,但我并不介意这种情况的行为。

  • 用户在动画运行时不需要与插头视图交互;这最有可能发生在界面方向改变时。

  • 插头视图可能由于异步模型更新而在动画过程中决定更改其内容,但这仍属于边缘情况。即使这种情况下动画不完美,我也不介意。但是,它的大小不会改变 - 它始终与插头视图的大小相同。


  • 你能在这里发布你的动画代码吗? - Kusal Shrestha
    @KusalShrestha 我只是在动画块中使用不同大小设置套接字视图的框架,没有什么特别的。 - Christian Schnorr
    界面方向变化的动画持续时间为0.3秒。你为什么如此在意在该动画中间插入一个视图? - rob mayoff
    顺便说一句,我并不是想用这个问题来表现自己的傲慢。即使是苹果也不会在旋转过程中尝试为每个UI元素添加动画效果。试着在Safari中打开位置/搜索框并旋转屏幕,你会发现键盘会隐藏一些东西,然后旋转,最后再重新显示那些被隐藏的内容。 - rob mayoff
    @robmayoff 我是一个追求完美的人。确实,这并不是什么大问题,但我仍然希望它看起来很棒。我认为,在旋转过程中出现的单元格大小与现有单元格不一致会显得奇怪。 - Christian Schnorr
    3个回答

    10

    您可以在这里找到我的测试项目。

    您说您的插头视图应该完全、准确地覆盖插座视图。我们需要考虑两个问题:插头的层位置 (layer.position) 和层大小 (layer.bounds.size)。

    默认情况下,position 控制层(和视图)的中心,因为默认的 anchorPoint 是 (0.5, 0.5)。如果我们将 anchorPoint 设置为 (0, 0),那么 position 就控制了层的左上角,在插座的坐标系中我们始终希望它在 (0, 0)。所以通过将 anchorPointposition 都设置为 CGPointZero,我们可以避免担心动画化 layer.position

    这留下了我们要动画化的 layer.bounds.size

    当您使用 UIKit 动画来动画化一个视图时,它会在后台创建 CABasicAnimation 的实例。 CABasicAnimationCAAnimation 子类,添加了(除其他之外)fromValuetoValue 属性,指定动画的起始和结束值。

    CAAnimation 符合 CAMediaTiming 协议,这意味着它有一个 beginTime 属性。当您创建一个 CAAnimation 时,它具有默认的 beginTime 值为零。Core Animation 在提交当前事务时将其更改为当前时间(请参阅 CACurrentMediaTime)。

    但是,如果动画已经具有非零的 beginTime,则 Core Animation 使用它本身。如果该 beginTime 在过去,那么当它首次出现在屏幕上时,动画已经部分地(甚至完全)完成,并且已经以适当的进度绘制出来。我们可以基本上“追溯”一个动画。

    因此,如果我们挖掘控制插座 bounds.sizeCABasicAnimation,并将其添加到插头上,那么插头应该以我们想要的方式进行动画。当我们将动画附加到插头时,它的 beginTime 是它开始动画化插座的时间。因此,即使我们稍后将其附加到插头上,它也将与插座完美同步。而且,由于我们希望插头和插座具有相同的大小,fromValuetoValue 也已经是正确的。

    在我的测试应用程序中,我将插座设置为粉色,插头设置为蓝色。它们都是UIImageView,显示带有边缘线的图像,以便我们始终能确保视图具有正确的大小。以下是实际运行时的效果:

    simple demo

    还有另一个变化。如果你对一个已经在动画的视图进行动画处理,UIKit不会停止先前的动画。它添加第二个动画,并且两个动画同时运行。
    这是因为UIKit使用了加性动画。当你将一个视图的宽度从320动画到160时,UIKit立即将其宽度设置为160,然后添加一个加性动画,从160到0。在动画开始时,表面上的宽度为160+160=320,在结束时,表面上的宽度为160+0=160。
    当UIKit在第一个动画正在运行时添加第二个动画时,两个动画的值将被添加到用于在屏幕上绘制视图的表面值上。以下是效果:

    multiple animations

    对我们来说,这意味着我们必须查找所有关键路径为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];
            }
        }
    }
    

    这是多个同时动画的结果:

    带有多个动画的演示

    更新

    获取插头视图的完整视图层次结构,使其像一开始就存在一样进行动画处理实在是太麻烦了。

    以下是一种替代方案:

    1. 以插座的原始大小布置插头视图,并创建其图像(“起始图像”)。
    2. 以插座的最终大小布置插头视图并创建其图像(“结束图像”)。
    3. 将一个占位符图像视图放入插座中。
    4. 将大小动画从插座复制到占位符。
    5. 使用大小动画在占位符上创建内容动画,将其内容从起始图像淡出到结束图像。
    6. 当动画结束时,用插头视图替换占位符。

    效果如下:

    交叉淡化演示

    除了交叉淡化的不完美之外,这个版本无法处理同时运行的多个动画。您可以在我的存储库中找到这个标签(链接在顶部)下的内容。

    另一种方法是仅将插头视图呈现为其最终大小,并将其放入占位符中而不进行交叉淡化。效果如下:

    拉伸结束图像演示

    我认为这样看起来更好,这个版本处理了堆叠动画。您可以在我的存储库中找到此版本的“stretch-end-image”标签。

    最后,还有一种完全不同的方法,我没有实现它。这仅适用于您自己创建的调整大小动画,无法处理系统在方向更改时创建的旋转动画。您可以使用定时器自己动画处理套接字视图的边界,而不是使用UIKit动画。 WWDC 2012 Session 228:精通自动布局的最佳实践 在最后讨论了这个问题。他建议使用NSTimer,但我认为使用CADisplayLink会更好。如果采用这种方法,则插头视图的所有子视图都将完美地进行动画处理。这比使用UIKit动画要麻烦得多,但应该很容易实现。


    感谢您抽出时间制作如此出色的演示项目!动画行为正是我为插头视图本身所设想的,但对于其子视图则不然。如果这一点不明显,我深表歉意,但我希望给插头视图一个机会来布局其子视图并对其进行动画处理。目前,在将插头视图插入层次结构时,任何子视图都将在动画已经运行时无动画地布局。请参见我提交的pull request。有什么想法可以让这个工作起来吗? - Christian Schnorr
    编辑您的问题,详细说明视图层次结构的相关信息。为什么插座视图在动画?能否在现有动画正在运行时启动新的动画?用户能否在动画期间与插头视图进行交互或更改其内容? - rob mayoff
    我回答了你的问题并添加了一些额外的信息。感谢你的时间。 - Christian Schnorr
    这是一个很棒的答案! - Ciacco Davide

    1

    我这样修改了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
    

    0
    为什么不将插头视图始终作为套接字视图的子视图存在,但设置hidden = YES?或者您可以使用alpha = 0。然后,当您想要显示它时,只需将其设置为hidden = NOalpha = 1
    这样,当您动画套接字视图时,您的插头视图将始终“随行”。
    顺便说一下,对于那些与TCP套接字一起工作的人来说,您的术语可能会让人感到迷惑。(“套接字?什么?”)

    2
    这是不可能的,我正在尝试像UITableView一样重用插头视图,只有插座视图应该始终保持活动状态。 - Christian Schnorr

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接