Добавить subview позже, чтобы анимировать вместе с superview

У меня есть вид контейнера-назовем его гнездо вид - это имеет один подпанели, это вид контента - назовем это вид штепсельной вилки. Этот вид штепселя может быть равен нулю, т. е. вид сокета в настоящее время пуст. Если он содержит представление plug, он занимает все пространство сокета, т. е. его рамка является границами сокета. С внешней точки зрения вы даже не должны быть в состоянии сказать, что на самом деле есть два вида, так как представление plug всегда находится именно там, где сокет.

Я изо всех сил пытаюсь заставить анимацию работать правильно: если представление plug существует и выложено до анимации, все работает так, как ожидалось. Однако, если я устанавливаю вид разъема сокета только тогда, когда анимация уже запущена, я получаю нежелательный эффект:

вид штепселя выложен туда, где он будет в конце анимации, и не анимируется рядом с его сокетом. Я хотел бы, чтобы это выглядело так, как будто оно было там все время, но только стало видимый только сейчас, т. е. вид плагина (и его подвиды) должен анимироваться рядом с сокетом, даже если я добавляю его во время анимации.

Как я могу добиться этого поведения?

мои идеи: очевидно, вид штепселя должен быть выложен дважды: один раз для его окончательной позиции и еще раз для того, где вид сокета начал анимацию, или где он был добавлен. Я мог бы вычислить этот кадр, применить его без анимации и анимировать к конечному кадру в новом анимационный блок. Для того, чтобы время анимации было согласованным, мне нужно было бы иметь ту же кривую и продолжительность, но запустить анимацию в прошлом или каким-то образом переслать ее. Возможно ли это? Есть ли другие подходы к тому, чтобы вид вилки был полной шириной и высотой во все времена?


в дополнение к ответу Роба, вот еще несколько деталей того, что именно я ищу:

  • вид сокета анимируется, потому что изменился размер привязки владельца. Вы можете думать об этом как полную ширину ячейки в таблице.

  • представление plug может содержать собственные подвиды вдоль подобных представлений изображений, меток и так далее. Они также должны присоединиться к анимации представления сокета, как если бы они всегда были там с тех пор, как анимация началась.

  • хотя теоретически можно запустить новую анимацию, пока она уже запущена, я не действительно, обратите внимание на поведение в этом случае edge.

  • пользователю не обязательно иметь возможность взаимодействовать с представлением plug во время работы анимации; это, скорее всего, произойдет во время изменения ориентации интерфейса.

  • представление plug может решить изменить свое содержимое из-за асинхронного обновления модели во время анимации, но опять же это крайний случай, и я не возражаю, если анимация не выглядит идеально в этом случае. Его размер, однако,не меняется - он всегда совпадает с размером представления plug.

3 ответов


вы можете найти здесь мой тестовый проект.

вы говорите, что ваш вид вилки должен полностью, точно покрывать вид сокета. Нам нужно беспокоиться о двух вещах: положение слоя вилки (layer.position) и размер слоя (layer.bounds.size).

по умолчанию position управляет центром слоя (и видом), потому что по умолчанию anchorPoint is (0.5, 0.5). Если мы установим anchorPoint to (0, 0), затем position управляет верхним левым углом слоя, и мы всегда хотим, чтобы это было в (0, 0) в системе координат сокета. Поэтому, установив оба anchorPoint и position to CGPointZero, мы можем избежать беспокоиться о анимации layer.position.

это просто оставляет нас, чтобы оживить layer.bounds.size.

когда вы анимируете представление с помощью анимации UIKit, он создает экземпляры CABasicAnimation под капотом. CABasicAnimation является наследником CAAnimation это добавляет (среди прочего) fromValue и toValue свойства, указывающие начальные и конечные значения анимация.

CAAnimation соответствует CAMediaTiming протокол, что означает, что есть beginTime собственность. При создании CAAnimation, он имеет значение по умолчанию beginTime ноль. Основная анимация изменяет это на текущее время (см. CACurrentMediaTime) когда он фиксирует текущую транзакцию.

но если анимация уже имеет ненулевое beginTime, Core Animation использует его как есть. Если это beginTime находится в прошлом, то анимация уже частично (или даже полностью) завершена, когда он впервые появляется на экране, и обращается с соответствующим количеством уже достигнутого прогресса. Мы можем по существу "задним числом" анимацию.

Итак, если мы выкопаем CABasicAnimation управление сокетом bounds.size и добавьте его в вилку, вилка должна анимироваться точно так, как мы хотим. Когда мы прикрепляем анимацию к вилке, ее beginTime это время, когда он начал анимировать сокет. Поэтому, даже если мы прикрепим его к вилке позже, он будет идеально синхронизировано с сокетом. И поскольку мы хотим, чтобы вилка и розетка имеют одинаковый размер,fromValue и toValue также уже правильно.

в моем тестовом приложении я сделал розетку розовой, а вилку синей. Каждый UIImageView отображение изображения с линиями по краям, поэтому мы можем быть уверены, что представления всегда имеют правильные размеры. Вот как это выглядит в действии:

simple demo

есть еще один поворот. Если вы анимация представления, которое уже анимируется, UIKit не останавливает предыдущую анимацию. Он добавляет вторую анимацию, и обе анимации выполняются одновременно.

это работает, потому что UIKit использует добавка анимация. Когда вы анимируете ширину представления от 320 до 160, UIKit немедленно устанавливает ширину в 160 и добавляет аддитивную анимацию, которая идет от 160 до 0. В начале анимации, видимая ширина-160+160=320, а в конце, видимая ширина 160+0=160.

когда UIKit добавляет вторую анимацию во время работы первой, значения обеих анимаций добавляются к кажущемуся значению, используемому для рисования вида на экране. Вот это:

multiple animations

что это означает для нас, мы должны искать все анимации сокетов с keyPath of 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];
        }
    }
}

вот результат с несколько одновременных анимаций:

demo with multiple animations

обновление

получение иерархии полного представления представления плагина для анимации, как если бы он был там в первую очередь, откровенно слишком много работы.

вот один из вариантов:

  1. выложите вид вилки в исходном размере сокета и создайте его изображение ("начать изображение").
  2. положите вне взгляд штепсельной вилки на окончательный размер сокет и создайте его образ ("end image").
  3. поставить изображение заполнитель в розетку.
  4. скопируйте анимацию размера из сокета в заполнитель.
  5. используйте анимацию размера для создания анимации содержимого на заполнителе, который пересекает его содержимое от начального изображения до конечного изображения.
  6. когда анимация закончится, замените заполнитель на представление plug.

вот как это выглядит например:

crossfade demo

в дополнение к несовершенству crossfade, эта версия не обрабатывает несколько анимаций, работающих одновременно. Вы можете найти это под crossfade тег в моем репозитории (ссылка вверху).

другой подход заключается в том, чтобы просто отобразить представление plug в его окончательном размере и поместить его в заполнитель без кроссфейда. Похоже это:

stretch end image demo

я думаю, что это выглядит лучше, и эта версия обрабатывает сложенные анимации. Вы можете найти это под stretch-end-image тег в моем репозитории.

наконец, есть совершенно другой подход, который я не реализовал. Это будет применяться только к изменению размера анимации, которую вы создаете сами-это не будет работать для анимации вращения, созданной системой при изменении ориентации. Вы можете просто анимируйте границы своего сокета самостоятельно, с помощью таймера, вместо использования анимации UIKit. WWDC 2012 сессия 228: лучшие практики для освоения Auto Layout обсуждает это ближе к концу. Он предлагает использовать NSTimer но я думаю, что вам лучше использовать CADisplayLink. Если вы примете этот подход, подвиды plug view будут полностью анимироваться. Это намного больше работы, чем использование анимации UIKit, но должно быть простым в реализации.


Я изменил код Роба майоффа таким образом. Это тебе поможет?

важные изменения заключаются в фактическом использовании CADisplayLink для обновления кадра plugView, и я добавил subview с ограничениями для 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

Почему бы вам не сделать вид штепселя всегда существовать как дочерний вид вида сокета, но установить hidden = YES? Или вы можете использовать alpha = 0. Затем, когда вы хотите показать его, просто установите его в Hidden = hidden = NO или alpha = 1.

таким образом, ваш вид штепселя всегда будет "ездить", когда вы анимируете вид сокета.

кстати, ваша терминология дезориентирует тех из нас, кто работает с сокетами TCP. ("Розетки? Что?")