Animate changes during device rotation based on si

2019-03-15 23:01发布

问题:

I have a dynamic setup of buttons that automatically adjust in width and height based on auto layout constraints set up in a storyboard. When in portrait the buttons have equal widths and heights so the frame is perfectly square, and when the device is rotated to landscape the buttons get shorter and wider. I have set cornerRadius on the buttons' layers so they're perfectly circular when in portrait and that works well, but when I rotate to landscape the corner radius doesn't look right anymore obviously. I need to change that so it becomes an oval. The problem is, no matter where I try to put that code, I cannot get the correct width and height the buttons will be after the rotation occurs. I want this to occur while the device is being rotated - don't want to wait until the rotation has completed. Ideally the cornerRadius would animate the change while the transition animates.

If I use viewWillLayoutSubviews or viewDidLayoutSubviews, it correctly gets the button frame when the app is launched, but upon rotating to landscape this method is called before the button's frame is updated for the new orientation, so I can't calculate the correct corner radius.

If I use viewWillTransitionToSize:withTransitionCoordinator: or willTransitionToTraitCollection:withTransitionCoordinator: or willRotateToInterfaceOrientation, it doesn't get called when the app is launched, and upon rotating the device it is called before the frame is updated for the new size.

If I use willAnimateRotationToInterfaceOrientation, it doesn't get called when the app is launched, but upon rotating the device it does correctly get the new button frame. But this method is deprecated.

So the question is, what method can you use to set the properties of a button based on the size the button will be when rotation has completed, that is called before the rotation has completed?

Note that it needs to be called for every orientation change, not just size class changes (rotating iPad doesn't change the size class). I only need to support iOS 8.0+.

This is the code I'm placing in the methods to know if it is getting the correct size:

println("\(button.frame.size.width) x \(button.frame.size.height)")

回答1:

Below I outline how one could use a CADisplayLink to update the properties during every frame of the animation, but there is an easier way. Subclass the button (or view or whatever) with a layoutSubviews that updates the corner radius:

class RoundedCornerView: UIView {

    override func layoutSubviews() {
        super.layoutSubviews()

        layer.cornerRadius = min(bounds.width, bounds.height) / 2
    }

}

Then, during the animation, as the frame size changes, the corner radius is updated automatically:


As Joey points out, one can use viewWillTransitionToSize to be notified of the new size, and then use animateAlongsideTransition to coordinate your additional animation along with the main animation.

If you want to animate an un-animatable property, such as corner radius (in block-based animations, anyway), you can use a display link for the duration of the animation. This effectively calls our method for every frame of the rotation animation. thus, we'll have a method that looks at the presentation layer (which shows you the current state of a layer mid-animation) to adjust the corner radius on the fly. You can use animateAlongsideTransition for this, too, but we're not using the animation closure in this case, but rather merely using the completion closure to know when the animation is done and therefore can stop the display link:

override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)

    let displayLink = CADisplayLink(target: self, selector: "handleDisplayLink:")
    displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)

    coordinator.animateAlongsideTransition(nil) { (context) -> Void in
        displayLink.invalidate()
    }
}

func handleDisplayLink(displayLink: CADisplayLink) {
    updateButtonsCornerRadius()
}

func updateButtonsCornerRadius() {
    for button in [button1, button2] {
        let presentationLayer = button.layer.presentationLayer() as CALayer
        let minDimension = min(presentationLayer.frame.size.width, presentationLayer.frame.size.height)
        button.layer.cornerRadius = minDimension / 2.0
    }
}

Frankly, I'm not sure this corner radius animation is enough to justify this display link (I had to slow down animations in the simulator with command+T to appreciate the difference), but this illustrates the basic idea.



回答2:

The answer is to use viewWillTransitionToSize:withTransitionCoordinator:, with your code placed inside the animation closure of UIViewControllerTransitionCoordinator's animateAlongsideTransition: method. This will allow you to animate the changes based on the size it will be when rotation completes.

override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)

    coordinator.animateAlongsideTransition({ (context: UIViewControllerTransitionCoordinatorContext!) -> Void in
        self.updateButtonsCornerRadius()
    }, completion: nil)
}

This method is not called when the app launches though so you'll want to call self.updateButtonsCornerRadius() in viewDidLoad as well.