The question is how should I define and set my shape layer's position and how should it be updated so that the layer appears where I'm expecting it to during the animation? Namely, the shape should be stuck on the end of the stick.
I have a CALayer instance called containerLayer
, and it has a sublayer which is a CAShapeLayer instance called shape
. containerLayer
is supposed to place shape
at a specific position unitLoc
like this:
class ContainerLayer: CALayer, CALayerDelegate {
// ...
override func layoutSublayers() {
super.layoutSublayers()
if !self.didSetup {
self.setup()
self.didSetup = true
}
updateFigure()
setNeedsDisplay()
}
func updateFigure() {
figureCenter = self.bounds.center
figureDiameter = min(self.bounds.width, self.bounds.height)
figureRadius = figureDiameter/2
shapeDiameter = round(figureDiameter / 5)
shapeRadius = shapeDiameter/2
locRadius = figureRadius - shapeRadius
angle = -halfPi
unitLoc = CGPoint(x: self.figureCenter.x + cos(angle) * locRadius, y: self.figureCenter.y + sin(angle) * locRadius)
shape.bounds = CGRect(x: 0, y: 0, width: shapeDiameter, height: shapeDiameter)
shape.position = unitLoc
shape.updatePath()
}
// ...
}
I'm having trouble finding the right way to specify what this position should be before, and during a resize animation which changes containerLayer.bounds
. I do understand that the problem I'm having is that I'm not setting the position in such a way that the animation will display it the way that I'm expecting it would.
I have tried using a CABasicAnimation(keyPath: "position")
to animate the position, and it improved the result over what I had tried previously, but it's still off.
@objc func resize(sender: Any) {
// MARK:- animate containerLayer bounds & shape position
// capture bounds value before changing
let oldBounds = self.containerLayer.bounds
// capture shape position value before changing
let oldPos = self.containerLayer.shape.position
// update the constraints to change the bounds
isLarge.toggle()
updateConstraints()
self.view.layoutIfNeeded()
let newBounds = self.containerLayer.bounds
let newPos = self.containerLayer.unitLoc
// set up the bounds animation and add it to containerLayer
let baContainerBounds = CABasicAnimation(keyPath: "bounds")
baContainerBounds.fromValue = oldBounds
baContainerBounds.toValue = newBounds
containerLayer.add(baContainerBounds, forKey: "bounds")
// set up the position animation and add it to shape layer
let baShapePosition = CABasicAnimation(keyPath: "position")
baShapePosition.fromValue = oldPos
baShapePosition.toValue = newPos
containerLayer.shape.add(baShapePosition, forKey: "position")
containerLayer.setNeedsLayout()
self.view.layoutIfNeeded()
}
I also tried using the presentation layer like this to set the position, and it also seems to get it close, but it's still off.
class ViewController: UIViewController {
//...
override func loadView() {
super.loadView()
displayLink = CADisplayLink(target: self, selector: #selector(animationDidUpdate))
displayLink.add(to: RunLoop.main, forMode: RunLoop.Mode.default)
//...
}
@objc func animationDidUpdate(displayLink: CADisplayLink) {
let newCenter = self.containerLayer.presentation()!.bounds.center
let new = CGPoint(x: newCenter.x + cos(containerLayer.angle) * containerLayer.locRadius, y: newCenter.y + sin(containerLayer.angle) * containerLayer.locRadius)
containerLayer.shape.position = new
}
//...
}
class ContainerLayer: CALayer, CALayerDelegate {
// ...
func updateFigure() {
//...
//shape.position = unitLoc
//...
}
// ...
}
With some slight exaggeration, I was able to make it clearer what's happening in your code. In my example, the circle layer is supposed to remain 1/3 the height of the background view:
At the time the animation starts, the background view has already been set to its ultimate size at the end of the animation. You don't see that, because animation relies on portraying the layer's presentation layer, which is unchanged; but the view itself has changed. Therefore, when you position the shape of the shape layer, and you do it in terms of the view, you are sizing and positioning it at the place it will need to be when the animation ends. Thus it jumps to its final size and position, which makes sense only when we reach the end of the animation.
Okay, but now consider this:
Isn't that nicer? How is it done? Well, using the principles I have already described elsewhere, I've got a layer with a custom animatable property. The result is that on every frame of the animation, I get an event (the draw(in:)
method for that layer). I respond to this by recalculating the path of the shape layer. Thus I am giving the shape layer a new path on every frame of the animation, and so it behaves smoothly. It stays in the right place, it resizes in smooth proportion to the size of the background view, and its stroke thickness remains constant throughout.