Animating shape layer type custom UIViews, during a UIView.animate?

Imagine a tall thin view L (say, it looks like a ladder).

Say it's 100 high, and we animate it to be the full height of the screen. So, in pseudocode..

func expandLadder() {
view.layoutIfNeeded()
UIView.animate(withDuration: 4 ...
  ladderTopToTopOfScreenConstraint = 0
  ladderBottomToBottomOfScreenConstraint = 0
  view.layoutIfNeeded()
}

No problem. Now say we draw the ladder in layoutSubviews along these lines ..

@IBDesignable class LadderView: UIView {
override func layoutSubviews() {
  super.layoutSubviews()
  setup()
}

func setup() {
  heightNow = frame size height
  print("I've recalculated the height!")
  shapeLayer ...
  topPoint = (0, 0)
  bottomPoint = (0, heightNow)
  p.addLines(between: [topPoint, bottomPoint])
  shapeLayer!.path = p
  shapeLayer add a CABasicAnimation or whatever
  layer.addSublayer(shapeLayer!)
}

No problem.

So we're just making a line p that fills the height "heightNow" of the view.

The problem is of course, when you do this ...

func expandLadder() {
view.layoutIfNeeded()
UIView.animate(withDuration: 4 ...
  ladderToTopOfScreenConstraint = 0
  ladderBottomOfScreenConstraint = 0
  view.layoutIfNeeded()
}

... when you do that, LayoutSubviews (or setBounds, or draw#rect, or anything else you can think of) is only called "before and after" the UIView.animate animation. It's a real pain.

(In the example, the "actual" ladder, the shapeLayer, will jump to the next values, before you animate the size of the view L in or out. ie, "I've recalculated the height!" is called only once before and after the UIView.animate animation. Assuming you don't clip subviews, you'll see the "wrong, upcoming" size before each animation of the length of L.)

What's the solution?


BTW, of course (so to speak) you can do this using draw#rect and animating yourself, ie with CADisplayLink (as mentioned by Josh). So that's fine. Of course, it's incredibly easier to draw shapes and the like just using a layer/path: my question here is how to make that code (ie, the code in setup() above) run during every step of the UIView animation of the constraints of the view in question?


The behavior you describe has to do with the way that iOS animations work. Specifically when you set the frame of a view in a UIAnimation block (that's what layoutIfNeeded does -- it forces a synchronous recalculation of the frame according to the autolayout rules) the view's frame immediately changes to the new value which is the apparent value at the end of the animation (add a print statement after layoutIfNeeded and you can see this is true). Incidentally changing the constraints doesn't do anything until layoutIfNeeded is called so you don't even need to put the constraint change in the animation block.

However during the animation, the system shows the presentation layer instead of the backing layer and it interpolates the value of the frame from the presentation layer from the initial value to the final value over the duration of the animation. This makes it look like the view is moving even though inspecting the value will reveal the view has already assumed its final position.

Therefore if you want the actual coordinates of the on screen layer while an animation is in flight, you have to use view.layer.presentationLayer to get them. See: https://developer.apple.com/reference/quartzcore/calayer/1410744-presentationlayer

This doesn't entirely solve your problem through because I assume that what you are trying to do is to spawn more layers as time goes on. If you cannot do with simple interpolation then you may be better off spawning all of the layers at the beginning and then using a keyframe animation to turn them on at certain intervals as the animation progresses. You can base your layer positions on view.layer.presentationLayer.bounds (your sublayers are in their super layers coordinates so don't use frame). It's difficult to provide an exact solution without knowing what you are trying to accomplish.

Another possible solution is to use drawRect with CADisplayLink which lets you redraw each frame however you want, so it's much more suited to things that cannot be easily interpolated from frame to frame (ie games or in your case adding/removing and animating geometry as a function of time).

Edit: Here is an example Playground showing how to animate the layer in parallel with the view.

    //: Playground - noun: a place where people can play

    import UIKit
    import PlaygroundSupport

    class V: UIViewController {
        var a = UIView()
        var b = CAShapeLayer()
        var constraints: [NSLayoutConstraint] = []
        override func viewDidLoad() {
            view.addSubview(a)
            a.translatesAutoresizingMaskIntoConstraints = false
            constraints = [
                a.topAnchor.constraint(equalTo: view.topAnchor, constant: 100),
                a.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 100),
                a.widthAnchor.constraint(equalToConstant: 200),
                a.heightAnchor.constraint(equalToConstant: 200)

            ]
            constraints.forEach {$0.isActive = true}
            a.backgroundColor = .blue
        }

        override func viewDidAppear(_ animated: Bool) {
            b.path = UIBezierPath(ovalIn: a.bounds).cgPath
            b.fillColor = UIColor.red.cgColor
            a.layer.addSublayer(b)
            constraints[3].constant = 400
            constraints[2].constant = 400
            let oldBounds = a.bounds
            UIView.animate(withDuration: 3, delay: 0, options: .curveLinear, animations: {
                self.view.layoutIfNeeded()
            }, completion: nil)
            let scale = a.bounds.size.width / oldBounds.size.width
            let animation = CABasicAnimation(keyPath: "transform.scale")
            animation.fromValue = 1
            animation.toValue = scale
            animation.duration = 3
            b.add(animation, forKey: "scale")
            b.transform = CATransform3DMakeScale(scale, scale, 1)

        }
    }

    let v = V()
    PlaygroundPage.current.liveView = v

layoutIfNeeded() will call layoutSubviews() only when it's needed. I don't see in your code calling setNeedsLayout() . That's how it's done when there are no constraints! If you have constraints, you should call setNeedsUpdateConstraints() and in your animationBlock layoutIfNeeded .

view.setNeedsUpdateConstraints()
UIView.animate(...) {
    ...
    view.layoutIfNeeded()
}
链接地址: http://www.djcxy.com/p/39150.html

上一篇: 如何使用Visual Studio扩展创建绿色(或蓝色)扭曲装饰

下一篇: 在UIView.animate期间动画形状图层类型自定义UIViews?