Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- //
- // In this custom layer, changes to a "highlightedPercentage" key will switch between showing normal and highlighted image views.
- // In addition, the size of the layer itself changes depending on these image views as well. This layer needs to fully support atomic,
- // model changes to this property as well as animated changes to this key without forcing main thread-bound view layout calls on
- // every frame of animated changes.
- //
- //
- // CATransaction.begin()
- // CATransaction.setDisableActions(true)
- //
- // layer.highlightedPercentage = 1.0 // Model change, immediate, causes main thread-layout, could be implicit
- //
- // CATransaction.commit()
- //
- // let highlightedPercentageAnimation = CABasicAnimation(key: "highlightedPercentage")
- // highlightedPercentageAnimation.duration = 1.0
- // highlightedPercentageAnimation.fromValue = 0.0
- //
- // // Animated change, not main thread-bound
- // layer.addAnimation(highlightedPercentageAnimation, forKey: "highlightedPercentageAnimation")
- //
- // Later...
- //
- // layer.removeAnimationForKey("highlightedPercentageAnimation")
- //
- private let LayerHighlightedPercentageKey = "highlightedPercentage"
- private let LayerBoundsAnimationKey = "boundsAnimation"
- private let LayerOpacityAnimationKey = "opacityAnimation"
- // MARK: CustomLayer Class -
- class CustomLayer: CALayer {
- // MARK: - Properties
- var normalImageSize: CGSize! = CGSizeZero
- var normalImageViewLayer: CALayer? {
- didSet {
- if let normalImageViewLayer = normalImageViewLayer {
- CATransaction.begin()
- CATransaction.setDisableActions(true)
- normalImageViewLayer.anchorPoint = CGPointZero // Easier than recentering the position on bounds change
- CATransaction.commit()
- }
- }
- }
- var highlightedImageSize: CGSize! = CGSizeZero
- var highlightedImageViewLayer: CALayer? {
- didSet {
- if let highlightedImageViewLayer = highlightedImageViewLayer {
- CATransaction.begin()
- CATransaction.setDisableActions(true)
- highlightedImageViewLayer.anchorPoint = CGPointZero // Easier than recentering the position on bounds change
- CATransaction.commit()
- }
- }
- }
- @NSManaged var highlightedPercentage: Float // @NSManaged is required make this an Objective-C style dynamic property
- // MARK: - Object Lifecycle
- // init(layer:) is the designated initializer called by the presentation layer every frame during an animation. It's used to copy
- // the custom property values over. "layer" may be a model layer, but it's likely to be another presentation layer.
- override required init(layer: AnyObject) {
- super.init(layer: layer)
- if let layer = layer as? CustomLayer {
- highlightedPercentage = layer.highlightedPercentage
- } else {
- highlightedPercentage = 0.0
- }
- }
- override init() {
- super.init()
- commonInitialization()
- }
- required init?(coder decoder: NSCoder) {
- super.init(coder: decoder)
- commonInitialization()
- }
- private func commonInitialization() {
- // @NSManaged properties can't have didSet blocks, so this is the only way to be notified when a property changes. Additionally,
- // attempting to remove the observation in deinit crashes. Core Animation is apparently doing this automatically for all Objective-C
- // style dynamic properties, so we don't (and can't) do it ourselves.
- addObserver(self, forKeyPath: LayerHighlightedPercentageKey, options: [], context: nil)
- }
- // MARK: - Providing Actions for Animations
- // needsDisplayForKey(_:) is required to receive display callbacks for changes to layer properties, including custom properties.
- // In the event that display() is overridden to perform actual main-thread bound layer updates, returning custom property keys in this
- // function is necessary for it to call display() during every frame of an animation to a custom property.
- override class func needsDisplayForKey(key: String) -> Bool {
- var needsDisplay = super.needsDisplayForKey(key)
- if key == LayerHighlightedPercentageKey {
- needsDisplay = true
- }
- return needsDisplay
- }
- // actionForKey(_:) allows for implicit animation creation for custom properties, even without a UIKit animation context.
- override func actionForKey(key: String) -> CAAction? {
- var action = super.actionForKey(key)
- if key == LayerHighlightedPercentageKey {
- // Create reference action from an existing CALayer key. This reference action will have all the default properties specified
- // by the implicit animation context that is invoking an action for the custom animated property.
- action = super.actionForKey("opacity")
- if let action = action as? CABasicAnimation {
- action.keyPath = key
- action.fromValue = valueForKeyPath(key)
- }
- }
- return action
- }
- // MARK: - Sublayer Layout
- // In the case of this layer, sublayers need to be manipulated in response to the value of a custom root layer property. layoutSublayers()
- // needs to be called only when appropriate; while display() _will_ be called at every frame of an animation for the custom property
- // (assuming needsDisplayForKey(_:) was set up appropriately), making geometry changes in display() will invoke layer (and view) layout
- // logic at every animation frame, which is bad. layoutSublayers() should only be invoked on model changes, not presentation changes.
- override func layoutSublayers() {
- super.layoutSublayers()
- guard let normalImageViewLayer = normalImageViewLayer, highlightedImageViewLayer = highlightedImageViewLayer else { return }
- let minimumSize = normalImageSize
- let maximumSize = highlightedImageSize
- let sizeDifference = CGSize(width: (maximumSize.width - minimumSize.width), height: (maximumSize.height - minimumSize.height))
- var bounds = CGRectZero
- bounds.size.width = minimumSize.width + (CGFloat(highlightedPercentage) * sizeDifference.width)
- bounds.size.height = minimumSize.height + (CGFloat(highlightedPercentage) * sizeDifference.height)
- self.bounds = bounds
- normalImageViewLayer.bounds = bounds
- highlightedImageViewLayer.bounds = bounds
- normalImageViewLayer.opacity = 1.0 - highlightedPercentage
- highlightedImageViewLayer.opacity = highlightedPercentage
- }
- // MARK: - Forwarding Animation Addition and Removal
- override func addAnimation(animation: CAAnimation, forKey key: String?) {
- super.addAnimation(animation, forKey: key)
- propagateHighlightedPercentageAnimationAdditionIfNecessary(animation)
- }
- override func removeAnimationForKey(key: String) {
- guard let animation = animationForKey(key) else { return }
- super.removeAnimationForKey(key)
- propagateHighlightedPercentageAnimationRemovalIfNecessary(animation)
- }
- override func removeAllAnimations() {
- super.removeAllAnimations()
- guard let normalImageViewLayer = normalImageViewLayer, highlightedImageViewLayer = highlightedImageViewLayer else { return }
- normalImageViewLayer.removeAllAnimations()
- highlightedImageViewLayer.removeAllAnimations()
- }
- // Here is how we actually animate the sublayers of a layer with a custom animatable property. When we detect that an animation is added
- // or removed that contains the keypath to a custom animatable layer property, we have to propagate those animations to the sublayers as
- // appropriate. Immutable animation copies are added to layers, so modifying the animation after it's already been added to the layer
- // is fine.
- private func propagateHighlightedPercentageAnimationAdditionIfNecessary(animation: CAAnimation) {
- guard let normalImageViewLayer = normalImageViewLayer, highlightedImageViewLayer = highlightedImageViewLayer else { return }
- var highlightedPercentageAnimation: CAAnimation? = nil
- if let animationGroup = animation as? CAAnimationGroup, animations = animationGroup.animations {
- for animationGroupAnimation in animations {
- guard let propertyAnimation = animationGroupAnimation as? CAPropertyAnimation where propertyAnimation.keyPath == LayerHighlightedPercentageKey else { continue }
- highlightedPercentageAnimation = propertyAnimation
- }
- } else if let propertyAnimation = animation as? CAPropertyAnimation where propertyAnimation.keyPath == LayerHighlightedPercentageKey {
- highlightedPercentageAnimation = propertyAnimation
- }
- if let highlightedPercentageAnimation = highlightedPercentageAnimation {
- highlightedPercentageAnimation.beginTime = animation.beginTime
- let minimumSize = normalImageSize
- let maximumSize = highlightedImageSize
- let sizeDifference = CGSize(width: (maximumSize.width - minimumSize.width), height: (maximumSize.height - minimumSize.height))
- if let basicAnimation = highlightedPercentageAnimation as? CABasicAnimation {
- // Bounds
- let boundsAnimation = basicAnimation.copy() as! CABasicAnimation
- boundsAnimation.keyPath = "bounds"
- if let fromValue = boundsAnimation.fromValue as? NSNumber {
- var fromBounds = CGRectZero
- fromBounds.size.width = minimumSize.width + (CGFloat(fromValue.floatValue) * sizeDifference.width)
- fromBounds.size.height = minimumSize.height + (CGFloat(fromValue.floatValue) * sizeDifference.height)
- boundsAnimation.fromValue = NSValue(CGRect: fromBounds)
- }
- if let toValue = boundsAnimation.toValue as? NSNumber {
- var toBounds = CGRectZero
- toBounds.size.width = minimumSize.width + (CGFloat(toValue.floatValue) * sizeDifference.width)
- toBounds.size.height = minimumSize.height + (CGFloat(toValue.floatValue) * sizeDifference.height)
- boundsAnimation.toValue = NSValue(CGRect: toBounds)
- }
- if let byValue = boundsAnimation.byValue as? NSNumber {
- var byBounds = CGRectZero
- byBounds.size.width = minimumSize.width + (CGFloat(byValue.floatValue) * sizeDifference.width)
- byBounds.size.height = minimumSize.height + (CGFloat(byValue.floatValue) * sizeDifference.height)
- boundsAnimation.byValue = NSValue(CGRect: byBounds)
- }
- addAnimation(boundsAnimation, forKey: LayerBoundsAnimationKey)
- normalImageViewLayer.addAnimation(boundsAnimation, forKey: LayerBoundsAnimationKey)
- highlightedImageViewLayer.addAnimation(boundsAnimation, forKey: LayerBoundsAnimationKey)
- // Opacity
- let normalOpacityAnimation = basicAnimation.copy() as! CABasicAnimation
- normalOpacityAnimation.keyPath = "opacity"
- if let fromValue = normalOpacityAnimation.fromValue as? NSNumber {
- let fromOpacity = 1.0 - fromValue.floatValue
- normalOpacityAnimation.fromValue = fromOpacity
- }
- if let toValue = normalOpacityAnimation.toValue as? NSNumber {
- let toOpacity = 1.0 - toValue.floatValue
- normalOpacityAnimation.toValue = toOpacity
- }
- if let byValue = normalOpacityAnimation.byValue as? NSNumber {
- let byOpacity = -byValue.floatValue
- normalOpacityAnimation.byValue = byOpacity
- }
- let highlightedOpacityAnimation = basicAnimation.copy() as! CABasicAnimation
- highlightedOpacityAnimation.keyPath = "opacity"
- normalImageViewLayer.addAnimation(normalOpacityAnimation, forKey: LayerOpacityAnimationKey)
- highlightedImageViewLayer.addAnimation(highlightedOpacityAnimation, forKey: LayerOpacityAnimationKey)
- } else if let keyframeAnimation = highlightedPercentageAnimation as? CAKeyframeAnimation {
- guard let values = keyframeAnimation.values else { return }
- // Bounds
- let boundsAnimation = keyframeAnimation.copy() as! CAKeyframeAnimation
- boundsAnimation.keyPath = "bounds"
- var boundsValues = [NSValue]()
- let normalOpacityAnimation = keyframeAnimation.copy() as! CAKeyframeAnimation
- normalOpacityAnimation.keyPath = "opacity"
- var normalOpacityValues = [NSNumber]()
- let highlightedOpacityAnimation = keyframeAnimation.copy() as! CAKeyframeAnimation
- highlightedOpacityAnimation.keyPath = "opacity"
- var highlightedOpacityValues = [NSNumber]()
- for value in values {
- var bounds = CGRectZero
- var normalOpacity = Float(1.0)
- var highlightedOpacity = Float(0.0)
- if let value = value as? NSNumber {
- let floatValue = value.floatValue
- bounds.size.width = minimumSize.width + (CGFloat(floatValue) * sizeDifference.width)
- bounds.size.height = minimumSize.height + (CGFloat(floatValue) * sizeDifference.height)
- normalOpacity = 1.0 - floatValue
- highlightedOpacity = floatValue
- }
- boundsValues.append(NSValue(CGRect: bounds))
- normalOpacityValues.append(normalOpacity)
- highlightedOpacityValues.append(highlightedOpacity)
- }
- boundsAnimation.values = boundsValues
- normalOpacityAnimation.values = normalOpacityValues
- highlightedOpacityAnimation.values = highlightedOpacityValues
- addAnimation(boundsAnimation, forKey: LayerBoundsAnimationKey)
- normalImageViewLayer.addAnimation(boundsAnimation, forKey: BoundsAnimationKey)
- highlightedImageViewLayer.addAnimation(boundsAnimation, forKey: BoundsAnimationKey)
- // Opacity
- normalImageViewLayer.addAnimation(normalOpacityAnimation, forKey: LayerOpacityAnimationKey)
- highlightedImageViewLayer.addAnimation(highlightedOpacityAnimation, forKey: LayerOpacityAnimationKey)
- }
- }
- }
- private func propagateHighlightedPercentageAnimationRemovalIfNecessary(animation: CAAnimation) {
- guard let normalImageViewLayer = normalImageViewLayer, highlightedImageViewLayer = highlightedImageViewLayer else { return }
- var highlightedPercentageAnimation: CAAnimation? = nil
- if let animationGroup = animation as? CAAnimationGroup, animations = animationGroup.animations {
- for animation in animations {
- guard let animation = animation as? CAPropertyAnimation where animation.keyPath == LayerHighlightedPercentageKey else { continue }
- highlightedPercentageAnimation = animation
- }
- } else if let animation = animation as? CAPropertyAnimation where animation.keyPath == LayerHighlightedPercentageKey {
- highlightedPercentageAnimation = animation
- }
- if highlightedPercentageAnimation != nil {
- removeAnimationForKey(LayerBoundsAnimationKey)
- normalImageViewLayer.removeAnimationForKey(LayerBoundsAnimationKey)
- highlightedImageViewLayer.removeAnimationForKey(LayerBoundsAnimationKey)
- normalImageViewLayer.removeAnimationForKey(LayerOpacityAnimationKey)
- highlightedImageViewLayer.removeAnimationForKey(LayerOpacityAnimationKey)
- }
- }
- // MARK: - KVO Overrides
- // If a model update to a custom layer property is made, we need to force a layout to occur because the layer's geometry needs
- // to change as well.
- override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
- if keyPath == CircularMenuItemLayerHighlightedPercentageKey {
- setNeedsLayout()
- layoutIfNeeded() // May or may not be needed, depending on whether the layout can wait until the next run loop cycle.
- } else {
- super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
- }
- }
- }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement