Подтвердить что ты не робот

Добавить subview позже для анимации рядом с надписью

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

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

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

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

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


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

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

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

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

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

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

4b9b3361

Ответ 1

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

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

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

Это просто оставляет нас анимировать layer.bounds.size.

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

CAAnimation соответствует протоколу CAMediaTiming, что означает, что он имеет свойство beginTime. Когда вы создаете CAAnimation, он имеет значение по умолчанию beginTime, равное нулю. Core Animation изменит это на текущее время (см. CACurrentMediaTime), когда он совершает текущую транзакцию.

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

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

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

простая демонстрация

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

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

Когда 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];
        }
    }
}

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

демо с несколькими анимациями

UPDATE

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

Здесь одна альтернатива:

  • Разделите вид вилки на исходный размер сокета и создайте его изображение ( "начать изображение" ).
  • Разделите вид вилки на конечный размер сокета и создайте его изображение ( "конечное изображение" ).
  • Поместите изображение образцового изображения в сокет.
  • Скопируйте анимацию размера из сокета в местозаполнитель.
  • Используйте анимацию размера для создания анимации содержимого на заполнителе, которая пересекает его содержимое с начала изображения до конечного изображения.
  • Когда анимация заканчивается, замените заполнитель на вилку.

Вот как это выглядит:

crossfade demo

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

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

демонстрация окончательного образа конечности

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

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

Ответ 2

Я изменил код Rob mayoff таким образом. Помогает ли вам это?

Важными изменениями являются фактическое использование 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

Ответ 3

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

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

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