iOS CAKeyframeAnimation вращениеМод на UIImageView
Я экспериментирую с анимацией по ключевым кадрам положения объекта UIImageView, движущегося по безье-траектории. Эта картинка показывает начальное состояние перед анимацией. Синяя линия - это путь - изначально движущийся прямо вверх, светло-зеленый прямоугольник - это начальный ограничивающий прямоугольник или изображение, а темно-зеленый "призрак" - это изображение, которое я двигаю:
Когда я запускаю анимацию с вращением, установленным в nil
изображение сохраняет ту же ориентацию на всем пути, что и ожидалось.
Но когда я запускаю анимацию с вращением, установленным в kCAAnimationRotateAuto
, изображение немедленно поворачивается на 90 градусов против часовой стрелки и сохраняет эту ориентацию на всем протяжении пути. Когда он достигает конца пути, он перерисовывается в правильной ориентации (ну, на самом деле он показывает UIImageView, который я переместил в окончательное местоположение)
Я наивно ожидал, что RotateMode будет ориентировать изображение по касательной к траектории, а не по нормали, особенно когда Apple документирует состояние CAKeyframeAnimation вращение.
Определяет, вращаются ли объекты, анимируемые вдоль пути, в соответствии с касательной к пути.
Так в чем здесь решение? Нужно ли предварительно поворачивать изображение на 90 градусов по часовой стрелке? Или мне чего-то не хватает?
Спасибо за вашу помощь.
Изменить 2 марта
Я добавил шаг вращения перед анимацией пути, используя вращение Affine, например:
theImage.transform = CGAffineTransformRotate(theImage.transform,90.0*M_PI/180);
и затем после анимации пути, сбросив вращение с помощью:
theImage.transform = CGAffineTransformIdentity;
Это заставляет изображение следовать по пути ожидаемым образом. Однако сейчас я сталкиваюсь с другой проблемой мерцания изображения. Я уже ищу решение проблемы мерцания в этом вопросе SO:
Масштабирование мерцания iOS CAKeyFrameAnimation в конце анимации
Так что теперь я не знаю, сделал ли я вещи лучше или хуже!
Изменить 12 марта
Хотя Калеб отметил, что да, мне пришлось предварительно повернуть изображение, Роб предоставил потрясающий пакет кода, который почти полностью решил мои проблемы. Единственное, что Роб не сделал, это компенсировало, что мои активы были нарисованы с вертикальной, а не горизонтальной ориентацией, поэтому все еще требовалось предварительно повернуть их на 90 градусов перед выполнением анимации. Но эй, это справедливо, что я должен сделать часть работы, чтобы все заработало.
Итак, мои небольшие изменения в решении Роба, отвечающие моим требованиям:
Когда я добавляю UIView, я предварительно поворачиваю его, чтобы противостоять внутреннему вращению, добавленному путем установки
rotationMode
:theImage.transform = CGAffineTransformMakeRotation (90 * M_PI / 180.0);
Мне нужно сохранить это вращение в конце анимации, поэтому вместо того, чтобы просто обрабатывать преобразование вида новым масштабным коэффициентом после определения блока завершения, я строю масштаб на основе текущего преобразования:
theImage.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;
Далее идет секретный соус для предотвращения мерцания в конце анимации. Напомним, что анимация не влияет на свойства "слоя модели", к которому вы их прикрепляете (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);
}
Yow упомяните, что вы запускаете анимацию с помощью "translationMode установлен в YES", но документация утверждает, что вращение должен быть установлен с использованием NSString...
Особенно:
Эти константы используются свойством вращением.
NSString * const kCAAnimationRotateAuto
NSString * const kCAAnimationRotateAutoReverse
Вы пробовали установить:
keyframe.animationMode = kCAAnimationRotateAuto;
В документации говорится:
kCAAnimationRotateAuto: объекты перемещаются по касательной к пути.