iOS CAKeyframeAnimation вращениеМод на UIImageView - PullRequest
11 голосов
/ 27 февраля 2012

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

Initial state

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

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

enter image description here

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

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

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

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


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

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

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

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

theImage.transform = CGAffineTransformIdentity;

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

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

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


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

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

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

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

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

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

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

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


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

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

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

Ответы [ 3 ]

8 голосов
/ 03 марта 2012

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

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

6 голосов
/ 06 марта 2012

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

  • Как указывает Калеб, 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;

Следующий секретный соус для предотвращения мерцания в конце анимации.Напомним, что анимации не влияют на свойства «слоя модели», к которому вы их прикрепляете (в данном случае 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'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];
    }];

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

    _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 голосов
/ 03 марта 2012

Yow упомяните, что вы запускаете анимацию с параметром «translationMode установлен в YES», но в документации говорится, что вращение Mode должно быть установлено с использованием NSString ...

В частности:

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

NSString * const kCAAnimationRotateAuto

NSString * const kCAAnimationRotateAutoReverse

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

keyframe.animationMode = kCAAnimationRotateAuto;

Документация гласит:

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

...