Анимация изменения кадра UICollectionView при вставке ячеек

Я хочу изменить размер кадра UICollectionView в анимации, которая выполняется вместе с анимированной вставкой ячейки, в тот же вид коллекции внутри блока performBatchUpdates:completion:.

Это код, который запускает вставку ячейки:

[collectionView performBatchUpdates:^{
    indexPathOfAddedCell = ...;
    [collectionView insertItemsAtIndexPaths:@[ indexPathOfAddedCell ]];
} completion:nil];

Поскольку вставка ячейки приводит к изменению contentSize представления коллекции, я попробовал KVO-регистрацию изменений этого свойства, а затем инициировал обновление фрейма представления коллекции из обработчика KVO.

Проблема с этим подходом заключается в том, что триггер KVO для contentSize срабатывает слишком поздно: анимация вставки ячейки к этому моменту уже завершилась (на самом деле KVO срабатывает прямо перед вызовом обработчика завершения performBatchUpdates:completion:, но после того, как анимация отыграла в пользовательском интерфейсе). ).

Я не использую автоматическую раскладку.

Изменить: я разместил пример проекта для демонстрации моей проблемы на GitHub.

Редактировать 2: я должен упомянуть, что мне это нужно для компонента, который я пишу (OLEContainerScrollView), который должен быть на 100% независимым от представления коллекции. Из-за этого я не могу создать подкласс макета представления коллекции и не могу влиять на код, запускающий анимацию ячеек. В идеале решение должно подойти и для UITableView, поведение которого аналогично.


person Ole Begemann    schedule 24.06.2014    source источник
comment
Не могли бы вы опубликовать пример проекта, демонстрирующий проблему?   -  person Tammo Freese    schedule 27.06.2014
comment
@TammoFreese: посмотрите github.com/ole/CollectionViewContentSizeAnimation.   -  person Ole Begemann    schedule 29.06.2014
comment
@OleBegemann Я хочу расширить свой ответ. Насколько вы открыты для «серой зоны» API и злоупотреблений?   -  person Leo Natan    schedule 03.07.2014
comment
@LeoNatan: Очень открыто, так как я думаю, что это единственный способ сделать это.   -  person Ole Begemann    schedule 04.07.2014
comment
@OleBegemann Пожалуйста, смотрите мой обновленный ответ. Это некрасиво, но может сработать.   -  person Leo Natan    schedule 04.07.2014


Ответы (3)


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

Для табличных представлений триггером запуска анимации является -[UITableView _endCellAnimationsWithContext:]. Этот метод устанавливает все необходимые анимации (только для будущих видимых ячеек), выполняет их и устанавливает блок завершения, который в конечном итоге вызывает -[UITableView _updateContentSize]. _updateContentSize использует внутренний метод -[UITableView _contentSize] для установки правильного размера содержимого прокрутки. Поскольку _endCellAnimationsWithContext: имеет дело только с анимацией, данные за табличным представлением уже обновлены, поэтому вызов _contentSize (или использование valueForKey:@"_contentSize") возвращает правильный размер.

Это очень похоже на представление коллекции. Триггер — -[UICollectionView _endItemAnimations], он запускает множество анимаций для каждой ячейки, верхнего и нижнего колонтитула, и когда все анимации заканчиваются, -[UICollectionView _updateAnimationDidStop:finished:context:] устанавливает правильный размер содержимого. Поскольку это представление коллекции, его макет представления коллекции фактически знает размер целевого содержимого, поэтому вы можете вызвать -[UICollectionViewLayout collectionViewContentSize], чтобы получить обновленный размер содержимого.

Ни один из них не является действительно хорошим вариантом для использования в магазине приложений. Один из вариантов, о котором я могу думать, - это ISA-прокручивание каждого добавленного подкласса представления прокрутки и обертывание всех анимируемых точек входа, отслеживание того, являются ли они пакетными или нет, и либо в конце пакета, либо в конце автономной анимированной операции используйте соответствующие методы (-[UITableView _contentSize] и -[UICollectionViewLayout collectionViewContentSize]) для получения целевого размера содержимого.


Оригинальный ответ, если вы хотите узнать об изменениях размера представления коллекции в ваших собственных представлениях коллекции:

Создайте подкласс макета представления коллекции (если вы еще этого не сделали) и, используя либо центр уведомлений, либо метод делегата, уведомите prepareForAnimatedBoundsChange: о других анимациях. Они будут добавлены в блок анимации.

Из документации:

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

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

person Leo Natan    schedule 28.06.2014
comment
Привет Оле, скоро посмотрю. - person Leo Natan; 29.06.2014
comment
Как сказал Оле Бегеманн, решение должно работать и с UITableView, поэтому prepareForAnimatedBoundsChange: будет работать только для UICollectionView. - person arturdev; 04.07.2014
comment
@arturdev Я ищу общее решение. - person Leo Natan; 04.07.2014
comment
@LeoNatan Вау, большое спасибо за тщательное расследование. Я поэкспериментирую с этим и сообщу о своих результатах (если позволит время, это может занять некоторое время). - person Ole Begemann; 04.07.2014
comment
@ Оле Ура, дай мне знать, как дела. - person Leo Natan; 04.07.2014
comment
@LeoNatan: первые результаты многообещающие, но я еще не заработал. Я просмотрел [UICollectionView _endItemAnimations], и он действительно вызывается в нужное время. Однако макет collectionViewContentSize еще не был обновлен до нового значения, поэтому я не могу использовать его для управления анимацией. Я продолжу экспериментировать. - person Ole Begemann; 08.07.2014
comment
@Оле тоже посмотрю. Для меня это было правильным значением. - person Leo Natan; 08.07.2014
comment
@LeoNatan: Тьфу, моя ошибка, извини. Я вызывал исходный IMP слишком поздно в своем методе swizzled. У меня это работает сейчас. - person Ole Begemann; 08.07.2014
comment
@Ole просто нужно замаскировать swizzle, чтобы пройти через AppStore сейчас. Не так уж и сложно. - person Leo Natan; 08.07.2014

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

#import "ViewController.h"

@interface ViewController () <UICollectionViewDataSource, UICollectionViewDelegate>

@property (weak, nonatomic) IBOutlet UICollectionView *collectionView;
@property (weak, nonatomic) IBOutlet UIView *otherView;
@property (nonatomic) NSInteger numberOfItemsInCollectionView;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.numberOfItemsInCollectionView = 1; // This is our model
}

- (IBAction)addItem:(id)sender
{
    // Change the model
    self.numberOfItemsInCollectionView += 1;

    [UIView animateWithDuration:0.24 animations:^{
        NSIndexPath *indexPathOfInsertedCell = [NSIndexPath indexPathForItem:self.numberOfItemsInCollectionView - 1 inSection:0];
        [self.collectionView insertItemsAtIndexPaths:@[ indexPathOfInsertedCell ]];

        CGRect collectionViewFrame = self.collectionView.frame;
        collectionViewFrame.size.height = (self.numberOfItemsInCollectionView * 40) + 94;
        self.collectionView.frame = collectionViewFrame;
    }];

}

#pragma mark - UICollectionViewDataSource

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return self.numberOfItemsInCollectionView;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath;
{
    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"MyCell" forIndexPath:indexPath];
    return cell;
}

@end

Или, если вы хотите эффект затухания:

- (IBAction)addItem:(id)sender
{
    // Change the model
    self.numberOfItemsInCollectionView += 1;

    CATransition *transition = [CATransition animation];
    transition.type = kCATransitionFade;
    transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    transition.fillMode = kCAFillModeForwards;
    transition.duration = 0.5;

    [[self.collectionView layer] addAnimation:transition forKey:@"UICollectionViewInsertRowAnimationKey"];
    NSIndexPath *indexPathOfInsertedCell = [NSIndexPath indexPathForItem:self.numberOfItemsInCollectionView - 1 inSection:0];
    [self.collectionView insertItemsAtIndexPaths:@[ indexPathOfInsertedCell ]];

    CGRect collectionViewFrame = self.collectionView.frame;
    collectionViewFrame.size.height = (self.numberOfItemsInCollectionView * 40) + 94;
    self.collectionView.frame = collectionViewFrame;
}

Я проверил это с вашим демонстрационным проектом, и он работает для меня. Пожалуйста, попробуйте это и прокомментируйте, если это работает и для вас.

person arturdev    schedule 03.07.2014
comment
Спасибо за предложение, но, как я уже сказал, у меня нет контроля над кодом, запускающим анимацию ячеек. Поэтому я не могу обернуть его в блок анимации. - person Ole Begemann; 03.07.2014
comment
Не могли бы вы объяснить? Вы сказали, что у вас нет контроля, но что такое `[collectionView insertItemsAtIndexPaths:@[indexPathOfAddedCell]];` в этом случае? ` - person arturdev; 03.07.2014
comment
Я не пишу этот код. Как уже упоминалось, я пишу компонент, который должен реагировать на изменения в представлении коллекции. - person Ole Begemann; 03.07.2014

Почему бы просто не добавить блок анимации для изменения рамки представления коллекции?
Это приводит к анимированному изменению границ, которое хорошо сочетается с исчезновением вставки. Материал KVO не нужен для этого решения.

EDIT: вы можете активировать этот код при изменении модели.

Вот соответствующий код для вашего примера проекта:

- (IBAction)addItem:(id)sender
{
    // Change the model
    self.numberOfItemsInCollectionView += 1;

    // Animate cell insertion
    [self.collectionView performBatchUpdates:^{
        NSIndexPath *indexPathOfInsertedCell = [NSIndexPath indexPathForItem:self.numberOfItemsInCollectionView - 1 inSection:0];
        [self.collectionView insertItemsAtIndexPaths:@[ indexPathOfInsertedCell ]];


    } completion:nil];

    // animate the collection view's frame
    [UIView animateWithDuration:.5
                     animations:^{
                         CGRect collectionViewFrame = self.collectionView.frame;
                         collectionViewFrame.size.height = (self.numberOfItemsInCollectionView * 40) + 94;
                         self.collectionView.frame = collectionViewFrame;
                     }];
}
person de.    schedule 03.07.2014