Как сделать анимацию кривой/дуги с помощью CAAnimation?
У меня есть пользовательский интерфейс, в котором элемент удаляется, я хотел бы подражать эффекту "переместить в папку" в почте iOS. Эффект, когда значок маленькой буквы "бросается" в папку. Вместо этого шахта будет сброшена в корзину.
Я попытался реализовать его, используя CAAnimation
на этом слое. Насколько я могу читать в документах, мне нужно установить byValue
и a toValue
, а CAAnimation - интерполировать значения. Я ищу, чтобы сделать небольшую кривую, поэтому элемент проходит через точку немного выше и слева от позиции позиции.
CABasicAnimation* animation = [CABasicAnimation animationWithKeyPath:@"position"];
[animation setDuration:2.0f];
[animation setRemovedOnCompletion:NO];
[animation setFillMode:kCAFillModeForwards];
[animation setTimingFunction:[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut]];
[animation setFromValue:[NSValue valueWithCGPoint:fromPoint]];
[animation setByValue:[NSValue valueWithCGPoint:byPoint]];
[animation setToValue:[NSValue valueWithCGPoint:CGPointMake(512.0f, 800.0f)]];
[animation setRepeatCount:1.0];
Я играл с этим в течение некоторого времени, но мне кажется, что Apple означает линейную интерполяцию.
Добавление byValue не вычисляет хорошую дугу или кривую и анимирует элемент через него.
Как я могу сделать такую анимацию?
Спасибо за предоставленную помощь.
Ответы
Ответ 1
Использование UIBezierPath
(Не забудьте указать ссылку, а затем import QuartzCore
, если вы используете iOS 6 или ранее)
Пример кода
Вы можете использовать анимацию, которая будет следовать по пути, достаточно удобно, CAKeyframeAnimation
поддерживает CGPath
, который можно получить из UIBezierPath
. Swift 3
func animate(view : UIView, fromPoint start : CGPoint, toPoint end: CGPoint)
{
// The animation
let animation = CAKeyframeAnimation(keyPath: "position")
// Animation path
let path = UIBezierPath()
// Move the "cursor" to the start
path.move(to: start)
// Calculate the control points
let c1 = CGPoint(x: start.x + 64, y: start.y)
let c2 = CGPoint(x: end.x, y: end.y - 128)
// Draw a curve towards the end, using control points
path.addCurve(to: end, controlPoint1: c1, controlPoint2: c2)
// Use this path as the animation path (casted to CGPath)
animation.path = path.cgPath;
// The other animations properties
animation.fillMode = kCAFillModeForwards
animation.isRemovedOnCompletion = false
animation.duration = 1.0
animation.timingFunction = CAMediaTimingFunction(name:kCAMediaTimingFunctionEaseIn)
// Apply it
view.layer.add(animation, forKey:"trash")
}
Понимание UIBezierPath
Безье пути (или Безье кривые, чтобы быть точным) работают точно так же, как те, которые вы найдете в фотошопе, фейерверках, эскизе.. Они имеют две "контрольные точки", по одной для каждой вершины. Например, анимация, которую я только что сделал:
![enter image description here]()
Создает путь безье. См. Документацию о специфике, но в основном это две точки, которые "тянут" дугу в определенном направлении.
Рисование пути
Одна интересная функция UIBezierPath
заключается в том, что вы можете нарисовать их на экране с помощью CAShapeLayer
, таким образом, помогая вам визуализировать путь, которым он будет следовать.
// Drawing the path
let *layer = CAShapeLayer()
layer.path = path.cgPath
layer.strokeColor = UIColor.black.cgColor
layer.lineWidth = 1.0
layer.fillColor = nil
self.view.layer.addSublayer(layer)
Улучшение исходного примера
Идея расчета собственного пути безье заключается в том, что вы можете сделать полностью динамичную, таким образом, анимация может изменить кривую, которую она собирается делать, исходя из нескольких факторов, а не просто жесткого кодирования, как это было в Например, например, контрольные точки могут быть рассчитаны следующим образом:
// Calculate the control points
let factor : CGFloat = 0.5
let deltaX : CGFloat = end.x - start.x
let deltaY : CGFloat = end.y - start.y
let c1 = CGPoint(x: start.x + deltaX * factor, y: start.y)
let c2 = CGPoint(x: end.x , y: end.y - deltaY * factor)
Этот последний бит кода делает его таким, чтобы точки были похожими на предыдущий рисунок, но в переменной величине по отношению к треугольнику, который формируется точками, умноженному на коэффициент, который был бы эквивалентен значению "натяжения".
Ответ 2
Вы абсолютно правы, что анимирование позиции с помощью CABasicAnimation
приводит к тому, что она идет по прямой. Для более продвинутых анимаций существует еще один класс под названием CAKeyframeAnimation
.
Массив values
Вместо toValue
, fromValue
и byValue
для базовых анимаций вы можете использовать массив values
или полный path
для определения значений на этом пути. Если вы хотите анимировать позицию сначала в сторону, а затем вниз, вы можете передать массив из трех позиций (начальный, средний, конечный).
CGPoint startPoint = myView.layer.position;
CGPoint endPoint = CGPointMake(512.0f, 800.0f); // or any point
CGPoint midPoint = CGPointMake(endPoint.x, startPoint.y);
CAKeyframeAnimation *move = [CAKeyframeAnimation animationWithKeyPath:@"position"];
move.values = @[[NSValue valueWithCGPoint:startPoint],
[NSValue valueWithCGPoint:midPoint],
[NSValue valueWithCGPoint:endPoint]];
move.duration = 2.0f;
myView.layer.position = endPoint; // instead of removeOnCompletion
[myView.layer addAnimation:move forKey:@"move the view"];
Если вы сделаете это, вы заметите, что представление перемещается из начальной точки по прямой линии в середину и в другую прямую линию до конечной точки. Часть, которая отсутствует, чтобы сделать ее дугой от начала до конца через среднюю точку, заключается в изменении calculationMode
анимации.
move.calculationMode = kCAAnimationCubic;
Вы можете управлять дугой, изменяя свойства tensionValues
, continuityValues
и biasValues
. Если вы хотите более тонкое управление, вы можете определить свой собственный путь вместо массива values
.
A path
следовать
Вы можете создать любой путь и указать, что это свойство должно следовать. Здесь я использую простую дугу
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL,
startPoint.x, startPoint.y);
CGPathAddCurveToPoint(path, NULL,
controlPoint1.x, controlPoint1.y,
controlPoint2.x, controlPoint2.y,
endPoint.x, endPoint.y);
CAKeyframeAnimation *move = [CAKeyframeAnimation animationWithKeyPath:@"position"];
move.path = path;
move.duration = 2.0f;
myView.layer.position = endPoint; // instead of removeOnCompletion
[myView.layer addAnimation:move forKey:@"move the view"];
Ответ 3
Попробуйте, это решит вашу проблему: я использовал это в своем проекте:
UILabel *label = [[[UILabel alloc] initWithFrame:CGRectMake(10, 126, 320, 24)] autorelease];
label.text = @"Animate image into trash button";
label.textAlignment = UITextAlignmentCenter;
[label sizeToFit];
[scrollView addSubview:label];
UIImageView *icon = [[[UIImageView alloc] initWithImage:[UIImage imageNamed:@"carmodel.png"]] autorelease];
icon.center = CGPointMake(290, 150);
icon.tag = ButtonActionsBehaviorTypeAnimateTrash;
[scrollView addSubview:icon];
UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
button.center = CGPointMake(40, 200);
button.tag = ButtonActionsBehaviorTypeAnimateTrash;
[button setTitle:@"Delete Icon" forState:UIControlStateNormal];
[button sizeToFit];
[button addTarget:self action:@selector(buttonClicked:) forControlEvents:UIControlEventTouchUpInside];
[scrollView addSubview:button];
[scrollView bringSubviewToFront:icon];
- (void)buttonClicked:(id)sender {
UIView *senderView = (UIView*)sender;
if (![senderView isKindOfClass:[UIView class]])
return;
switch (senderView.tag) {
case ButtonActionsBehaviorTypeExpand: {
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"transform"];
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
anim.duration = 0.125;
anim.repeatCount = 1;
anim.autoreverses = YES;
anim.removedOnCompletion = YES;
anim.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(1.2, 1.2, 1.0)];
[senderView.layer addAnimation:anim forKey:nil];
break;
}
case ButtonActionsBehaviorTypeAnimateTrash: {
UIView *icon = nil;
for (UIView *theview in senderView.superview.subviews) {
if (theview.tag != ButtonActionsBehaviorTypeAnimateTrash)
continue;
if ([theview isKindOfClass:[UIImageView class]]) {
icon = theview;
break;
}
}
if (!icon)
return;
UIBezierPath *movePath = [UIBezierPath bezierPath];
[movePath moveToPoint:icon.center];
[movePath addQuadCurveToPoint:senderView.center
controlPoint:CGPointMake(senderView.center.x, icon.center.y)];
CAKeyframeAnimation *moveAnim = [CAKeyframeAnimation animationWithKeyPath:@"position"];
moveAnim.path = movePath.CGPath;
moveAnim.removedOnCompletion = YES;
CABasicAnimation *scaleAnim = [CABasicAnimation animationWithKeyPath:@"transform"];
scaleAnim.fromValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
scaleAnim.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.1, 0.1, 1.0)];
scaleAnim.removedOnCompletion = YES;
CABasicAnimation *opacityAnim = [CABasicAnimation animationWithKeyPath:@"alpha"];
opacityAnim.fromValue = [NSNumber numberWithFloat:1.0];
opacityAnim.toValue = [NSNumber numberWithFloat:0.1];
opacityAnim.removedOnCompletion = YES;
CAAnimationGroup *animGroup = [CAAnimationGroup animation];
animGroup.animations = [NSArray arrayWithObjects:moveAnim, scaleAnim, opacityAnim, nil];
animGroup.duration = 0.5;
[icon.layer addAnimation:animGroup forKey:nil];
break;
}
}
}
Ответ 4
Я узнал, как это сделать. Вполне возможно анимировать X и Y отдельно. Если вы анимируете их в одно и то же время (на 2.0 секунды ниже) и установите другую функцию синхронизации, это заставит ее выглядеть так, как будто она перемещается по дуге вместо прямой линии от начала до конца. Чтобы отрегулировать дугу, вам нужно будет играть с настройкой другой функции синхронизации. Не уверен, что CAAnimation поддерживает любые "сексуальные" функции синхронизации.
const CFTimeInterval DURATION = 2.0f;
CABasicAnimation* animation = [CABasicAnimation animationWithKeyPath:@"position.y"];
[animation setDuration:DURATION];
[animation setRemovedOnCompletion:NO];
[animation setFillMode:kCAFillModeForwards];
[animation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]];
[animation setFromValue:[NSNumber numberWithDouble:400.0]];
[animation setToValue:[NSNumber numberWithDouble:0.0]];
[animation setRepeatCount:1.0];
[animation setDelegate:self];
[myview.layer addAnimation:animation forKey:@"animatePositionY"];
animation = [CABasicAnimation animationWithKeyPath:@"position.x"];
[animation setDuration:DURATION];
[animation setRemovedOnCompletion:NO];
[animation setFillMode:kCAFillModeForwards];
[animation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
[animation setFromValue:[NSNumber numberWithDouble:300.0]];
[animation setToValue:[NSNumber numberWithDouble:0.0]];
[animation setRepeatCount:1.0];
[animation setDelegate:self];
[myview.layer addAnimation:animation forKey:@"animatePositionX"];
Edit:
Должно быть возможно изменить функцию синхронизации с помощью https://developer.apple.com/library/ios/#documentation/Cocoa/Reference/CAMediaTimingFunction_class/Introduction/Introduction.html (CAMediaTimingFunction, введенная функциейWithControlPoints::)
Это "кубическая кривая Безье". Я уверен, что у Google есть ответы. http://en.wikipedia.org/wiki/B%C3%A9zier_curve#Cubic_B.C3.A9zier_curves: -)
Ответ 5
У меня был несколько похожий вопрос несколько дней назад, и я реализовал его с помощью таймера, поскольку Brad говорит, но не NSTimer
. CADisplayLink
- это таймер, который следует использовать для этой цели, поскольку он синхронизируется с frameRate приложения и обеспечивает более плавную и более естественную анимацию. Вы можете посмотреть мою реализацию в моем ответе здесь. Этот метод действительно дает намного больший контроль над анимацией, чем CAAnimation
, и не намного сложнее.
CAAnimation
не может ничего рисовать, так как он даже не перерисовывает вид. Он только перемещается, деформирует и исчезает то, что уже нарисовано.