IOS CAKeyframeAnimation rotationMode в UIImageView

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

Initial state

Когда я запускаю анимацию с параметром rotationMode, установленным на nil, изображение сохраняет ту же ориентацию до конца, как и ожидалось.

Но когда я запускаю анимацию с параметром rotationMode, установленным на kCAAnimationRotateAuto, изображение сразу поворачивается на 90 градусов против часовой стрелки и сохраняет эту ориентацию полностью через путь. Когда он достигает конца пути, он перерисовывается в правильной ориентации (ну, на самом деле это показывает UIImageView, который я переместил в конечном месте)

enter image description here

Я наивно ожидал, что rotationMode будет ориентировать изображение на касательную пути, а не на нормальную, особенно когда Apple документирует CAKeyframeAnimation rotationMode состояние

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

Так в чем же тут решение? Должен ли я предварительно поворачивать изображение на 90 градусов по часовой стрелке? Или есть что-то, что мне не хватает?

Спасибо за вашу помощь.


Изменить 2 марта

Я добавил шаг поворота перед анимацией пути, используя аффинное вращение, например:

theImage.transform = CGAffineTransformRotate(theImage.transform,90.0*M_PI/180);

а затем после анимации пути, сбросив вращение с помощью:

theImage.transform = CGAffineTransformIdentity;

Это заставляет изображение следовать по пути ожидаемым образом. Однако теперь я сталкиваюсь с другой проблемой мерцания изображения. Я уже ищу решение мерцающей проблемы в этом вопросе SO:

iOS CAKeyFrameAnimation масштабирование мерцает в конце анимации

Итак, теперь я не знаю, улучшилось или улучшилось ли я!


Редактировать 12 марта

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

Итак, мои незначительные изменения в решении Rob для решения my:

  • Когда я добавляю UIView, я предварительно поворачиваю его, чтобы встретить встроенный поворот, добавленный установкой rotationMode:

    theImage.transform = CGAffineTransformMakeRotation (90 * M_PI/180.0);

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

    theImage.transform = CGAffineTransformScale (theImage.transform, scaleFactor, scaleFactor);

И это все, что мне нужно было сделать, чтобы мой образ прошел по пути, как я ожидал!


Изменить 22 марта

Я только что загрузил в GitHub демонстрационный проект, который показывает перемещение объекта по пути безье. Код можно найти на PathMove

Я также написал об этом в своем блоге в Перемещение объектов по пути безье в iOS

Ответ 1

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

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

Ответ 2

Вот что вам нужно знать, чтобы устранить мерцание:

  • Как отметил Калеб, Core Animation вращает ваш слой так, что его положительная ось X лежит вдоль касательной вашего пути. Вам нужно сделать свой образ "естественной" ориентацией с этим. Итак, предположим, что зеленый космический корабль в ваших образцовых изображениях, вам нужно, чтобы космический корабль указывал вправо, когда он не имел поворота, применяемого к нему.

  • Настройка преобразования, включающего вращение, влияет на вращение, применяемое `kCAAnimationRotateAuto '. Перед применением анимации необходимо удалить поворот из вашего преобразования.

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

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

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

Итак, в моем тестовом проекте я связал свой UIImageView с действием с именем animate:. Когда вы касаетесь его, изображение перемещается вдоль половины фигуры 8 и удваивается по размеру. Когда вы снова коснетесь его, изображение перемещается вдоль другой половины рисунка 8 (назад в исходное положение) и возвращается к исходному размеру. Обе анимации используют kCAAnimationRotateAuto, поэтому изображение указывает вдоль касательной пути.

Здесь начало animate:, где я выясню, какой путь, масштаб и конечная точка должны быть в конце изображения:

- (IBAction)animate:(id)sender {
    UIImageView* theImage = self.imageView;

    UIBezierPath *path = _isReset ? _path0 : _path1;
    CGFloat newScale = 3 - _currentScale;

    CGPoint destination = [path currentPoint];

Итак, первое, что мне нужно сделать, это удалить любое вращение от преобразования изображения, поскольку, как я уже упоминал, это будет мешать kCAAnimationRotateAuto:

    // Strip off the image rotation, because it interferes with `kCAAnimationRotateAuto`.
    theImage.transform = CGAffineTransformMakeScale(_currentScale, _currentScale);

Затем я перехожу в блок анимации UIView, чтобы система применила анимацию к представлению изображения:

    [UIView animateWithDuration:3 animations:^{

Я создаю анимацию ключевого кадра для позиции и задаю пару ее свойств:

        // Prepare my own keypath animation for the layer position.
        // The layer position is the same as the view center.
        CAKeyframeAnimation *positionAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
        positionAnimation.path = path.CGPath;
        positionAnimation.rotationMode = kCAAnimationRotateAuto;

Далее находится секретный соус для предотвращения мерцания в конце анимации. Напомним, что анимация не влияет на свойства "модельного слоя", к которым вы присоединяете их (theImage.layer в этом случае). Вместо этого они обновляют свойства "уровня представления", который отражает то, что на самом деле находится на экране.

Итак, сначала я установил removedOnCompletion в NO для анимации ключевого кадра. Это означает, что анимация останется прикрепленной к слою модели, когда анимация будет завершена, а это значит, что я могу получить доступ к уровню презентации. Я получаю преобразование из слоя презентации, удаляю анимацию и применяю преобразование к слою модели. Поскольку это происходит в основном потоке, эти изменения свойств происходят в одном цикле обновления экрана, поэтому нет мерцания.

        positionAnimation.removedOnCompletion = NO;
        [CATransaction setCompletionBlock:^{
            CGAffineTransform finalTransform = [theImage.layer.presentationLayer affineTransform];
            [theImage.layer removeAnimationForKey:positionAnimation.keyPath];
            theImage.transform = finalTransform;
        }];

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

        // UIView will add animations for both of these changes.
        theImage.transform = CGAffineTransformMakeScale(newScale, newScale);
        theImage.center = destination;

Я копирую некоторые свойства ключа из автоматически добавленной анимации позиции в анимацию ключевого кадра:

        // Copy properties from UIView animation.
        CAAnimation *autoAnimation = [theImage.layer animationForKey:positionAnimation.keyPath];
        positionAnimation.duration = autoAnimation.duration;
        positionAnimation.fillMode = autoAnimation.fillMode;

и, наконец, я заменю автоматически добавленную анимацию позиции анимацией ключевого кадра:

        // Replace UIView animation with my animation.
        [theImage.layer addAnimation:positionAnimation forKey:positionAnimation.keyPath];
    }];

Двойной - наконец, я обновляю свои переменные экземпляра, чтобы отразить изменение в представлении изображения:

    _currentScale = newScale;
    _isReset = !_isReset;
}

Это для анимации изображения без мерцания.

И теперь, как сказал Стив Джобс, "Одна последняя вещь". Когда я загружаю представление, мне нужно установить преобразование изображения, чтобы оно повернулось, чтобы указать вдоль касательной первого пути, который я буду использовать для его анимации. Я делаю это в методе reset:

- (void)reset {
    self.imageView.center = _path1.currentPoint;
    self.imageView.transform = CGAffineTransformMakeRotation(startRadiansForPath(_path0));
    _currentScale = 1;
    _isReset = YES;
}

Конечно, сложный бит скрыт в этой функции startRadiansForPath. Это действительно не так сложно. Я использую функцию CGPathApply для обработки элементов пути, выбирая первые две точки, которые фактически образуют подпуть, и я вычисляю угол линии, образованной этими двумя точками. (Изогнутый участок пути является либо квадратичным, либо кубическим сплайном сплайном, и эти сплайны обладают тем свойством, что касательная в первой точке сплайна является линией от первой точки до следующей контрольной точки.)

Я просто собираюсь сбрасывать код здесь без объяснения причин, для потомков:

typedef struct {
    CGPoint p0;
    CGPoint p1;
    CGPoint firstPointOfCurrentSubpath;
    CGPoint currentPoint;
    BOOL p0p1AreSet : 1;
} PathState;

static inline void updateStateWithMoveElement(PathState *state, CGPathElement const *element) {
    state->currentPoint = element->points[0];
    state->firstPointOfCurrentSubpath = state->currentPoint;
}

static inline void updateStateWithPoints(PathState *state, CGPoint p1, CGPoint currentPoint) {
    if (!state->p0p1AreSet) {
        state->p0 = state->currentPoint;
        state->p1 = p1;
        state->p0p1AreSet = YES;
    }
    state->currentPoint = currentPoint;
}

static inline void updateStateWithPointsElement(PathState *state, CGPathElement const *element, int newCurrentPointIndex) {
    updateStateWithPoints(state, element->points[0], element->points[newCurrentPointIndex]);
}

static void updateStateWithCloseElement(PathState *state, CGPathElement const *element) {
    updateStateWithPoints(state, state->firstPointOfCurrentSubpath, state->firstPointOfCurrentSubpath);
}

static void updateState(void *info, CGPathElement const *element) {
    PathState *state = info;
    switch (element->type) {
        case kCGPathElementMoveToPoint: return updateStateWithMoveElement(state, element);
        case kCGPathElementAddLineToPoint: return updateStateWithPointsElement(state, element, 0);
        case kCGPathElementAddQuadCurveToPoint: return updateStateWithPointsElement(state, element, 1);
        case kCGPathElementAddCurveToPoint: return updateStateWithPointsElement(state, element, 2);
        case kCGPathElementCloseSubpath: return updateStateWithCloseElement(state, element);
    }
}

CGFloat startRadiansForPath(UIBezierPath *path) {
    PathState state;
    memset(&state, 0, sizeof state);
    CGPathApply(path.CGPath, &state, updateState);
    return atan2f(state.p1.y - state.p0.y, state.p1.x - state.p0.x);
}

Ответ 3

Расскажите, что вы запускаете анимацию с параметром rotationMode равным YES, но в документации указано, что rotationMode должен быть установлен с помощью NSString...

В частности:

Эти константы используются свойством rotationMode.

NSString * const kCAAnimationRotateAuto

NSString * const kCAAnimationRotateAutoReverse

Вы пробовали настройку:

keyframe.animationMode = kCAAnimationRotateAuto;

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

kCAAnimationRotateAuto: объекты перемещаются по касательной к пути.