如何围绕一条对角线旋转CALayer?

13

我正在尝试实现一个翻转动画,用于像iPhone应用程序中的棋盘游戏。该动画应该像一个游戏棋子那样旋转并变成其背面的颜色(类似于翻转棋)。我已经成功创建了一个按正交轴翻转棋子的动画,但是当我试图通过改变绕z轴的旋转来沿对角线轴翻转它时,实际的图像也被旋转了(这不足为奇)。相反,我想要以“原样”的方式围绕对角线轴旋转图像。

我尝试过更改layer.sublayerTransform,但没有成功。

这是我目前的实现。它通过一个技巧解决了在动画结束时获取镜像图像的问题。解决方案是不实际旋转图层180度,而是将其旋转90度,更改图像,然后将其旋转回来。

最终版本:根据Lorenzo的建议创建离散的关键帧动画,并为每个帧计算变换矩阵。此版本尝试基于图层大小估算所需的“引导”帧数,然后使用线性关键帧动画。此版本以任意角度旋转,因此要围绕对角线旋转,请使用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];
}

什么是对角轴? - kennytm
我所说的对角轴是指旋转应该围绕层的对角线进行。就像如果你抓住层的两个对角,然后将其旋转。 - Mattias Wadman
2
喜欢绕直线 y = x 旋转吗? - kennytm
3个回答

6
你可以这样伪造它:创建一个仿射变换,将图层沿其对角线折叠。
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个),将它们推送为关键帧,在每十分之一秒显示,并使用“不插值”参数。您可以调整动画编号以平衡平滑度和性能。

*对于可能的错误,抱歉,此部分是在没有拼写检查器的情况下编写的。


这是因为它不是通过在每个图层“角落”的初始和最终位置之间进行过渡插值来工作,而是通过创建从初始值到目标值的变换矩阵中每个值的线性插值来工作。 - Lorenzo Boccaccia
任意角度似乎运行良好,但我并不完全理解为什么。 - Mattias Wadman
抱歉,我不小心撤销了我的投票,现在我被困在这个链接中:http://meta.stackexchange.com/questions/21462/undoing-an-old-vote-cannot-be-recast-because-vote-is-too-old。 - Mattias Wadman
不需要假装什么。请参见下面的动态图解决方案。 - bunkerdive
你是如此渴望虚假的网络积分,以至于要来为一个已经死了七年的问题宣传你的解决方案吗?而且这个解决方案需要安装一个全新的堆栈并改变整个项目语言? - Lorenzo Boccaccia
显示剩余9条评论

4
我已经解决了。你可能也已经有了一个解决方案,但是这是我找到的。实际上非常简单...
您可以使用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));

在动画完成后设置其值。这将防止回到原始状态。

希望这能帮到你。如果不能,那也许可以帮到其他像我们一样挣扎的人们! :)

祝好,

Chris


我认为我尝试了类似的解决方案,但问题在于旋转变换会旋转实际图像内容。但如果图层是对称图像,则您不会注意到它。 - Mattias Wadman
你能解释一下这里的 v.theSquare 是什么吗? - aBikis

1

这是一个 Xamarin iOS 的示例,我用它来翻转正方形按钮的角落,就像一只狗耳朵(很容易移植到 obj-c):

enter image description here

方法1:使用旋转动画,并将xy轴均设为1(示例在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);

方法二:只需向CAAnimationGroup添加x轴旋转和y轴旋转,使它们同时运行:

// 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);

不确定为什么会被踩。这是关于对角线的CALayer旋转。下投票者,请展示你的工作。 - bunkerdive
这真的很好!正是我在寻找的。我没有看到你在哪里分配角落区域,在你的例子中,角落很大,像狗耳朵一样移动,我想要小耳朵。 - nikhilgohil11
@nikhilgohil11,大的“+”区域是一个单独的按钮(UIView)。角落按钮实际上是一个正方形(被大按钮遮盖了一半),正在旋转。总之,派生出2个按钮子类:BigButton和CornerButton。将CornerButton实例放在BigButton下面(被遮盖了一半),当CornerButton被按下时,调用旋转代码。 - bunkerdive
谢谢 @bunkerdive,我会尝试在Swift中实现。 - nikhilgohil11

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接