0
votes

I want a layer to behave like this:

correct

Instead, it behaves like this:

improper

The card flip animation is created by two CABasicAnimations applied in a CAAnimationGroup. The incorrect spin effect happens because the implicit animation from the CALayer property change runs first and then my animation specified in the CABasicAnimation runs. How can I stop the implicit animation from running so that only my specified animation runs?

Here's the relevant code:

class ViewController: UIViewController {

  var simpleLayer = CALayer()

  override func viewDidLoad() {
    super.viewDidLoad()

    let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
    self.view.addGestureRecognizer(tap)

    simpleLayer.frame = CGRect(origin: CGPoint(x: view.bounds.width / 2 - 50, y: view.bounds.height / 2 - 50), size: CGSize(width: 100, height: 100))
    simpleLayer.backgroundColor = UIColor.blackColor().CGColor
    view.layer.addSublayer(simpleLayer)
  }

  func handleTap() {
    let xRotation = CABasicAnimation(keyPath: "transform.rotation.x")
    xRotation.toValue = 0
    xRotation.byValue = M_PI

    let yRotation = CABasicAnimation(keyPath: "transform.rotation.y")
    yRotation.toValue = 0
    yRotation.byValue = M_PI

    simpleLayer.setValue(M_PI, forKeyPath: "transform.rotation.y")
    simpleLayer.setValue(M_PI, forKeyPath: "transform.rotation.x")

    let group = CAAnimationGroup()
    group.animations = [xRotation, yRotation]
    group.duration = 0.6
    group.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
    simpleLayer.addAnimation(group, forKey: nil)
  }
}
2
I'm actually not positive if setValue(_:forKeyPath:) is monitored by Core Animation to activate implicit animations or not. If you surround those two function calls with a CATransaction that disables actions, does that fix the problem?CIFilter
Thank you, @LucasTizma! That was the correct code. I described it fully in an answer below.Ben Morrow
I received a downvote on the original question, so now the value is "-1". I'm not sure why I received the downvote. If the downvoter could comment, I'd be happy to address his or her concern. This isn't as specific a use case as it might seem. This question is generically applicable. Unless you add the code to disable the implicit animation, the CAAnimationGroup does not behave as expected.Ben Morrow

2 Answers

1
votes

@LucasTizma had the correct answer.

Surround your animation with CATransaction.begin(); CATransaction.setDisableActions(true) and CATransaction.commit(). This will disable the implicit animation and make the CAAnimationGroup animate correctly.

Here's the final result:

triangle flip animation

This is the important snippet of code in Swift 3:

CATransaction.begin()
CATransaction.setDisableActions(true)

let xRotation = CABasicAnimation(keyPath: "transform.rotation.x")
xRotation.toValue = 0
xRotation.byValue = M_PI

let yRotation = CABasicAnimation(keyPath: "transform.rotation.y")
yRotation.toValue = 0
yRotation.byValue = M_PI

simpleLayer.setValue(M_PI, forKeyPath: "transform.rotation.x")
simpleLayer.setValue(M_PI, forKeyPath: "transform.rotation.y")

let group = CAAnimationGroup()
group.animations = [xRotation, yRotation]
simpleLayer.add(group, forKey: nil)

CATransaction.commit()

And this is the full code for the depicted animation with an iOS app:

class ViewController: UIViewController {

  var simpleLayer = CALayer()

  override func viewDidLoad() {
    super.viewDidLoad()

    let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
    self.view.addGestureRecognizer(tap)

    let ratio: CGFloat = 1 / 5
    let viewWidth = view.bounds.width
    let viewHeight = view.bounds.height
    let layerWidth = viewWidth * ratio
    let layerHeight = viewHeight * ratio

    let rect = CGRect(origin: CGPoint(x: viewWidth / 2 - layerWidth / 2,
                                      y: viewHeight / 2 - layerHeight / 2),
                      size: CGSize(width: layerWidth, height: layerHeight))

    let topRightPoint = CGPoint(x: rect.width, y: 0)
    let bottomRightPoint = CGPoint(x: rect.width, y: rect.height)
    let topLeftPoint = CGPoint(x: 0, y: 0)

    let linePath = UIBezierPath()

    linePath.move(to: topLeftPoint)
    linePath.addLine(to: topRightPoint)
    linePath.addLine(to: bottomRightPoint)
    linePath.addLine(to: topLeftPoint)

    let maskLayer = CAShapeLayer()
    maskLayer.path = linePath.cgPath

    simpleLayer.frame = rect
    simpleLayer.backgroundColor = UIColor.black.cgColor
    simpleLayer.mask = maskLayer

    // Smooth antialiasing
    // * Convert the layer to a simple bitmap that's stored in memory
    // * Saves CPU cycles during complex animations
    // * Rasterization is set to happen during the animation and is disabled afterwards
    simpleLayer.rasterizationScale = UIScreen.main.scale

    view.layer.addSublayer(simpleLayer)
  }

  func handleTap() {
    CATransaction.begin()
    CATransaction.setDisableActions(true)
    CATransaction.setCompletionBlock({
      self.simpleLayer.shouldRasterize = false
    })

    simpleLayer.shouldRasterize = true

    let xRotation = CABasicAnimation(keyPath: "transform.rotation.x")
    xRotation.toValue = 0
    xRotation.byValue = M_PI

    let yRotation = CABasicAnimation(keyPath: "transform.rotation.y")
    yRotation.toValue = 0
    yRotation.byValue = M_PI

    simpleLayer.setValue(M_PI, forKeyPath: "transform.rotation.x")
    simpleLayer.setValue(M_PI, forKeyPath: "transform.rotation.y")

    let group = CAAnimationGroup()
    group.animations = [xRotation, yRotation]
    group.duration = 1.2
    group.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
    simpleLayer.add(group, forKey: nil)

    CATransaction.commit()
  }
}
-1
votes

You are creating 2 separate animations and applying them in an animation group. When you do that they are applied as 2 discrete steps.

It looks like that's not what you want. If not, then don't create 2 separate animations, one on transform.rotation.x and the other on transform.rotation.y. Instead, concatenate both changes onto a transformation matrix and apply the changed transformation matrix as a single animation.