iOS cakeyframeanimation rotationMode на UIImageView

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

Initial state

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

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

enter image description here

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

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

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

спасибо помощь.


редактировать 2 марта

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

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

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

theImage.transform = CGAffineTransformIdentity;

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

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

так что теперь я не знаю, если я сделал вещи лучше или хуже!


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

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

Итак, мои небольшие изменения в решении Роба для suite мой требования:

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

    образ.трансформировать= CGAffineTransformMakeRotation(90*M_PI/180.0);

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

    образ.transform = CGAffineTransformScale (theImage.transform, scaleFactor, scaleFactor);

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


Редактировать 22 Марта

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

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

3 ответов


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

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


вот что вам нужно знать, чтобы исключить фликер:

  • как отметил Калеб, 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's 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;

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

Итак, сначала я поставил removedOnCompletion to 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's animation.
        CAAnimation *autoAnimation = [theImage.layer animationForKey:positionAnimation.keyPath];
        positionAnimation.duration = autoAnimation.duration;
        positionAnimation.fillMode = autoAnimation.fillMode;

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

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

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

    _currentScale = newScale;
    _isReset = !_isReset;
}

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

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

конечно, сложность заключается в том, что . Это не так уж сложно. Я использую 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);
}

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

в частности:

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

NSString * const kCAAnimationRotateAuto

NSString * const kCAAnimationRotateAutoReverse

вы пробовали устанавливать:

ключевой кадр.animationMode = kCAAnimationRotateAuto;

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

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