Как мне повернуть CALay вокруг диагональной линии?
Я пытаюсь реализовать флип-анимацию для использования в настольной игре, такой как iPhone-приложение. Предполагается, что анимация выглядит как игровая фигура, которая вращается и меняется на цвет спины (вроде как реверси). Мне удалось создать анимацию, которая переворачивает кусок вокруг своей ортогональной оси, но когда я пытаюсь перевернуть его вокруг диагональной оси, изменив вращение вокруг оси z, фактическое изображение также поворачивается (что неудивительно). Вместо этого я хотел бы повернуть изображение "как есть" вокруг диагональной оси.
Я пытался изменить layer.sublayerTransform
но безуспешно
Вот моя текущая реализация. Он работает, выполняя трюк, чтобы решить проблему получения зеркального изображения в конце анимации. Решение состоит в том, чтобы фактически не поворачивать слой на 180 градусов, вместо этого он поворачивает его на 90 градусов, изменяет изображение и затем поворачивает его обратно.
Окончательная версия: на основе предложения Лоренца создать анимацию с дискретным ключом и рассчитать матрицу преобразования для каждого кадра. Эта версия вместо этого пытается оценить количество "направляющих" кадров, необходимых на основе размера слоя, а затем использует анимацию с линейным ключом. Эта версия вращается с произвольным углом, поэтому для вращения вокруг диагональной линии используйте угол 45 градусов.
Пример использования:
[someclass flipLayer:layer image:image angle:M_PI/4]
Реализация:
- (void)animationDidStop:(CAAnimationGroup *)animation
finished:(BOOL)finished {
CALayer *layer = [animation valueForKey:@"layer"];
if([[animation valueForKey:@"name"] isEqual:@"fadeAnimation"]) {
/* code for another animation */
} else if([[animation valueForKey:@"name"] isEqual:@"flipAnimation"]) {
layer.contents = [animation valueForKey:@"image"];
}
[layer removeAllAnimations];
}
- (void)flipLayer:(CALayer *)layer
image:(CGImageRef)image
angle:(float)angle {
const float duration = 0.5f;
CAKeyframeAnimation *rotate = [CAKeyframeAnimation
animationWithKeyPath:@"transform"];
NSMutableArray *values = [[[NSMutableArray alloc] init] autorelease];
NSMutableArray *times = [[[NSMutableArray alloc] init] autorelease];
/* bigger layers need more "guiding" values */
int frames = MAX(layer.bounds.size.width, layer.bounds.size.height) / 2;
int i;
for (i = 0; i < frames; i++) {
/* create a scale value going from 1.0 to 0.1 to 1.0 */
float scale = MAX(fabs((float)(frames-i*2)/(frames - 1)), 0.1);
CGAffineTransform t1, t2, t3;
t1 = CGAffineTransformMakeRotation(angle);
t2 = CGAffineTransformScale(t1, scale, 1.0f);
t3 = CGAffineTransformRotate(t2, -angle);
CATransform3D trans = CATransform3DMakeAffineTransform(t3);
[values addObject:[NSValue valueWithCATransform3D:trans]];
[times addObject:[NSNumber numberWithFloat:(float)i/(frames - 1)]];
}
rotate.values = values;
rotate.keyTimes = times;
rotate.duration = duration;
rotate.calculationMode = kCAAnimationLinear;
CAKeyframeAnimation *replace = [CAKeyframeAnimation
animationWithKeyPath:@"contents"];
replace.duration = duration / 2;
replace.beginTime = duration / 2;
replace.values = [NSArray arrayWithObjects:(id)image, nil];
replace.keyTimes = [NSArray arrayWithObjects:
[NSNumber numberWithDouble:0.0f], nil];
replace.calculationMode = kCAAnimationDiscrete;
CAAnimationGroup *group = [CAAnimationGroup animation];
group.duration = duration;
group.timingFunction = [CAMediaTimingFunction
functionWithName:kCAMediaTimingFunctionLinear];
group.animations = [NSArray arrayWithObjects:rotate, replace, nil];
group.delegate = self;
group.removedOnCompletion = NO;
group.fillMode = kCAFillModeForwards;
[group setValue:@"flipAnimation" forKey:@"name"];
[group setValue:layer forKey:@"layer"];
[group setValue:(id)image forKey:@"image"];
[layer addAnimation:group forKey:nil];
}
Оригинальный код:
+ (void)flipLayer:(CALayer *)layer
toImage:(CGImageRef)image
withAngle:(double)angle {
const float duration = 0.5f;
CAKeyframeAnimation *diag = [CAKeyframeAnimation
animationWithKeyPath:@"transform.rotation.z"];
diag.duration = duration;
diag.values = [NSArray arrayWithObjects:
[NSNumber numberWithDouble:angle],
[NSNumber numberWithDouble:0.0f],
nil];
diag.keyTimes = [NSArray arrayWithObjects:
[NSNumber numberWithDouble:0.0f],
[NSNumber numberWithDouble:1.0f],
nil];
diag.calculationMode = kCAAnimationDiscrete;
CAKeyframeAnimation *flip = [CAKeyframeAnimation
animationWithKeyPath:@"transform.rotation.y"];
flip.duration = duration;
flip.values = [NSArray arrayWithObjects:
[NSNumber numberWithDouble:0.0f],
[NSNumber numberWithDouble:M_PI / 2],
[NSNumber numberWithDouble:0.0f],
nil];
flip.keyTimes = [NSArray arrayWithObjects:
[NSNumber numberWithDouble:0.0f],
[NSNumber numberWithDouble:0.5f],
[NSNumber numberWithDouble:1.0f],
nil];
flip.calculationMode = kCAAnimationLinear;
CAKeyframeAnimation *replace = [CAKeyframeAnimation
animationWithKeyPath:@"contents"];
replace.duration = duration / 2;
replace.beginTime = duration / 2;
replace.values = [NSArray arrayWithObjects:(id)image, nil];
replace.keyTimes = [NSArray arrayWithObjects:
[NSNumber numberWithDouble:0.0f], nil];
replace.calculationMode = kCAAnimationDiscrete;
CAAnimationGroup *group = [CAAnimationGroup animation];
group.removedOnCompletion = NO;
group.duration = duration;
group.timingFunction = [CAMediaTimingFunction
functionWithName:kCAMediaTimingFunctionLinear];
group.animations = [NSArray arrayWithObjects:diag, flip, replace, nil];
group.fillMode = kCAFillModeForwards;
[layer addAnimation:group forKey:nil];
}
3 ответа
Вы можете подделать это следующим образом: создайте аффинное преобразование, которое свернет слой вдоль его диагонали:
A-----B B
| | /
| | -> A&D
| | /
C-----D C
измените изображение и трансформируйте CALayer обратно в другую анимацию. Это создаст иллюзию слоя, вращающегося вокруг его диагонали.
Матрица для этого должна быть, если я правильно помню математику:
0.5 0.5 0
0.5 0.5 0
0 0 1
Обновление: хорошо, CA на самом деле не любит использовать вырожденные преобразования, но вы можете аппроксимировать это следующим образом:
CGAffineTransform t1 = CGAffineTransformMakeRotation(M_PI/4.0f);
CGAffineTransform t2 = CGAffineTransformScale(t1, 0.001f, 1.0f);
CGAffineTransform t3 = CGAffineTransformRotate(t2,-M_PI/4.0f);
в моих тестах на симуляторе все еще была проблема, потому что вращение происходит быстрее, чем перевод, поэтому с черным квадратом эффект был немного странным. Я полагаю, что если у вас есть центрированный спрайт с прозрачной областью вокруг него, эффект будет близок к ожидаемому. Затем вы можете настроить значение матрицы t3, чтобы увидеть, получите ли вы более привлекательный результат.
после дальнейших исследований выясняется, что необходимо анимировать свой собственный переход с помощью ключевых кадров, чтобы получить максимальный контроль над самим переходом. скажем, вы должны отобразить эту анимацию за секунду, вы должны сделать десять матриц, которые будут отображаться на каждой десятой секунды без интерполяции с использованием kCAAnimationDiscrete; эти матрицы могут быть сгенерированы с помощью кода ниже:
CGAffineTransform t1 = CGAffineTransformMakeRotation(M_PI/4.0f);
CGAffineTransform t2 = CGAffineTransformScale(t1, animationStepValue, 1.0f);
CGAffineTransform t3 = CGAffineTransformRotate(t2,-M_PI/4.0f);
где animationStepValue для каждого ключевого кадра берется из этой последовательности:
{1 0.7 0.5 0.3 0.1 0.3 0.5 0.7 1}
то есть: вы генерируете десять различных матриц преобразования (фактически 9), выдвигая их как ключевые кадры, которые будут отображаться каждую десятую секунды, а затем используя параметр "не интерполировать". Вы можете настроить номер анимации для балансировки плавности и производительности *
* извините за возможные ошибки, эта последняя часть была написана без проверки орфографии.
Я понял это. Возможно, у вас уже есть решение, но вот что я нашел. Это довольно просто на самом деле...
Вы можете использовать CABasicAnimation для выполнения диагонального поворота, но это должно быть объединение двух матриц, а именно существующей матрицы слоя, плюс CATransform3DRotate. "Хитрость" в том, что в 3DRotate вам нужно указать координаты, чтобы вращаться вокруг.
Код выглядит примерно так:
CATransform3DConcat(theLayer.transform, CATransform3DRotate(CATransform3DIdentity, M_PI/2, -1, 1, 0));
Это сделает вращение, которое выглядит так, как будто левый верхний угол квадрата вращается вокруг оси Y=X и движется к правому нижнему углу.
Код для анимации выглядит так:
CABasicAnimation *ani1 = [CABasicAnimation animationWithKeyPath:@"transform"];
// set self as the delegate so you can implement (void)animationDidStop:finished: to handle anything you might want to do upon completion
[ani1 setDelegate:self];
// set the duration of the animation - a float
[ani1 setDuration:dur];
// set the animation's "toValue" which MUST be wrapped in an NSValue instance (except special cases such as colors)
ani1.toValue = [NSValue valueWithCATransform3D:CATransform3DConcat(theLayer.transform, CATransform3DRotate(CATransform3DIdentity, M_PI/2, -1, 1, 0))];
// give the animation a name so you can check it in the animationDidStop:finished: method
[ani1 setValue:@"shrink" forKey:@"name"];
// finally, apply the animation
[theLayer addAnimation:ani1 forKey@"arbitraryKey"];
Это оно! Этот код будет вращать квадрат (theLayer) до невидимости, поскольку он перемещается на 90 градусов и представляется ортогонально экрану. Затем вы можете изменить цвет и сделать ту же анимацию, чтобы вернуть его. Та же анимация работает, потому что мы объединяем матрицы, поэтому каждый раз, когда вы хотите повернуть, просто сделайте это дважды или измените M_PI/2
в M_PI
,
Наконец, следует отметить, и это сводило меня с ума, что после завершения слой вернется в исходное состояние, если вы явно не установите его в состояние конечной анимации. Это означает, что перед строкой [theLayer addAnimation:ani1 forKey@"arbitraryKey"];
вы хотите добавить
theLayer.transform = CATransform3DConcat(v.theSquare.transform, CATransform3DRotate(CATransform3DIdentity, M_PI/2, -1, 1, 0));
установить его значение после завершения анимации. Это предотвратит возврат к исходному состоянию.
Надеюсь это поможет. Если не вы, то, возможно, кто-то другой, кто бился головой об стену, как мы!:)
Ура,
Крис
Вот пример Xamarin iOS, который я использую для поворота угла квадратной кнопки, например, собачьего уха (легко переносится на obj-c):
Способ 1: использовать анимацию вращения с 1
для обоих x
а также y
оси (примеры в Xamarin.iOS, но легко переносимые в obj-c):
// add to the UIView subclass you wish to rotate, where necessary
AnimateNotify(0.10, 0, UIViewAnimationOptions.CurveEaseOut | UIViewAnimationOptions.AllowUserInteraction | UIViewAnimationOptions.BeginFromCurrentState, () =>
{
// note the final 3 params indicate "rotate around x&y axes, but not z"
var transf = CATransform3D.MakeRotation(-1 * (nfloat)Math.PI / 4, 1, 1, 0);
transf.m34 = 1.0f / -500;
Layer.Transform = transf;
}, null);
Способ 2: просто добавьте x-axis
вращение и y-axis
вращение к CAAnimationGroup
поэтому они бегут одновременно:
// add to the UIView subclass you wish to rotate, where necessary
AnimateNotify(1.0, 0, UIViewAnimationOptions.CurveEaseOut | UIViewAnimationOptions.AllowUserInteraction | UIViewAnimationOptions.BeginFromCurrentState, () =>
{
nfloat angleTo = -1 * (nfloat)Math.PI / 4;
nfloat angleFrom = 0.0f ;
string animKey = "rotate";
// y-axis rotation
var anim = new CABasicAnimation();
anim.KeyPath = "transform.rotation.y";
anim.AutoReverses = false;
anim.Duration = 0.1f;
anim.From = new NSNumber(angleFrom);
anim.To = new NSNumber(angleTo);
// x-axis rotation
var animX = new CABasicAnimation();
animX.KeyPath = "transform.rotation.x";
animX.AutoReverses = false;
animX.Duration = 0.1f;
animX.From = new NSNumber(angleFrom);
animX.To = new NSNumber(angleTo);
// add both rotations to a group, to run simultaneously
var animGroup = new CAAnimationGroup();
animGroup.Duration = 0.1f;
animGroup.AutoReverses = false;
animGroup.Animations = new CAAnimation[] {anim, animX};
Layer.AddAnimation(animGroup, animKey);
// add perspective
var transf = CATransform3D.Identity;
transf.m34 = 1.0f / 500;
Layer.Transform = transf;
}, null);