Advertisement
Guest User

Untitled

a guest
May 31st, 2016
51
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 17.41 KB | None | 0 0
  1. //
  2. // In this custom layer, changes to a "highlightedPercentage" key will switch between showing normal and highlighted image views.
  3. // In addition, the size of the layer itself changes depending on these image views as well. This layer needs to fully support atomic,
  4. // model changes to this property as well as animated changes to this key without forcing main thread-bound view layout calls on
  5. // every frame of animated changes.
  6. //
  7.  
  8. //
  9. // CATransaction.begin()
  10. // CATransaction.setDisableActions(true)
  11. //
  12. // layer.highlightedPercentage = 1.0 // Model change, immediate, causes main thread-layout, could be implicit
  13. //
  14. // CATransaction.commit()
  15. //
  16. // let highlightedPercentageAnimation = CABasicAnimation(key: "highlightedPercentage")
  17. // highlightedPercentageAnimation.duration = 1.0
  18. // highlightedPercentageAnimation.fromValue = 0.0
  19. //
  20. // // Animated change, not main thread-bound
  21. // layer.addAnimation(highlightedPercentageAnimation, forKey: "highlightedPercentageAnimation")
  22. //
  23. // Later...
  24. //
  25. // layer.removeAnimationForKey("highlightedPercentageAnimation")
  26. //
  27.  
  28. private let LayerHighlightedPercentageKey = "highlightedPercentage"
  29.  
  30. private let LayerBoundsAnimationKey = "boundsAnimation"
  31. private let LayerOpacityAnimationKey = "opacityAnimation"
  32.  
  33. // MARK: CustomLayer Class -
  34.  
  35. class CustomLayer: CALayer {
  36.  
  37. // MARK: - Properties
  38.  
  39. var normalImageSize: CGSize! = CGSizeZero
  40.  
  41. var normalImageViewLayer: CALayer? {
  42. didSet {
  43. if let normalImageViewLayer = normalImageViewLayer {
  44. CATransaction.begin()
  45. CATransaction.setDisableActions(true)
  46.  
  47. normalImageViewLayer.anchorPoint = CGPointZero // Easier than recentering the position on bounds change
  48.  
  49. CATransaction.commit()
  50. }
  51. }
  52. }
  53.  
  54. var highlightedImageSize: CGSize! = CGSizeZero
  55.  
  56. var highlightedImageViewLayer: CALayer? {
  57. didSet {
  58. if let highlightedImageViewLayer = highlightedImageViewLayer {
  59. CATransaction.begin()
  60. CATransaction.setDisableActions(true)
  61.  
  62. highlightedImageViewLayer.anchorPoint = CGPointZero // Easier than recentering the position on bounds change
  63.  
  64. CATransaction.commit()
  65. }
  66. }
  67. }
  68.  
  69. @NSManaged var highlightedPercentage: Float // @NSManaged is required make this an Objective-C style dynamic property
  70.  
  71. // MARK: - Object Lifecycle
  72.  
  73. // init(layer:) is the designated initializer called by the presentation layer every frame during an animation. It's used to copy
  74. // the custom property values over. "layer" may be a model layer, but it's likely to be another presentation layer.
  75.  
  76. override required init(layer: AnyObject) {
  77. super.init(layer: layer)
  78.  
  79. if let layer = layer as? CustomLayer {
  80. highlightedPercentage = layer.highlightedPercentage
  81. } else {
  82. highlightedPercentage = 0.0
  83. }
  84. }
  85.  
  86. override init() {
  87. super.init()
  88.  
  89. commonInitialization()
  90. }
  91.  
  92. required init?(coder decoder: NSCoder) {
  93. super.init(coder: decoder)
  94.  
  95. commonInitialization()
  96. }
  97.  
  98. private func commonInitialization() {
  99. // @NSManaged properties can't have didSet blocks, so this is the only way to be notified when a property changes. Additionally,
  100. // attempting to remove the observation in deinit crashes. Core Animation is apparently doing this automatically for all Objective-C
  101. // style dynamic properties, so we don't (and can't) do it ourselves.
  102.  
  103. addObserver(self, forKeyPath: LayerHighlightedPercentageKey, options: [], context: nil)
  104. }
  105.  
  106. // MARK: - Providing Actions for Animations
  107.  
  108. // needsDisplayForKey(_:) is required to receive display callbacks for changes to layer properties, including custom properties.
  109. // In the event that display() is overridden to perform actual main-thread bound layer updates, returning custom property keys in this
  110. // function is necessary for it to call display() during every frame of an animation to a custom property.
  111.  
  112. override class func needsDisplayForKey(key: String) -> Bool {
  113. var needsDisplay = super.needsDisplayForKey(key)
  114.  
  115. if key == LayerHighlightedPercentageKey {
  116. needsDisplay = true
  117. }
  118.  
  119. return needsDisplay
  120. }
  121.  
  122. // actionForKey(_:) allows for implicit animation creation for custom properties, even without a UIKit animation context.
  123.  
  124. override func actionForKey(key: String) -> CAAction? {
  125. var action = super.actionForKey(key)
  126.  
  127. if key == LayerHighlightedPercentageKey {
  128. // Create reference action from an existing CALayer key. This reference action will have all the default properties specified
  129. // by the implicit animation context that is invoking an action for the custom animated property.
  130. action = super.actionForKey("opacity")
  131.  
  132. if let action = action as? CABasicAnimation {
  133. action.keyPath = key
  134. action.fromValue = valueForKeyPath(key)
  135. }
  136. }
  137.  
  138. return action
  139. }
  140.  
  141. // MARK: - Sublayer Layout
  142.  
  143. // In the case of this layer, sublayers need to be manipulated in response to the value of a custom root layer property. layoutSublayers()
  144. // needs to be called only when appropriate; while display() _will_ be called at every frame of an animation for the custom property
  145. // (assuming needsDisplayForKey(_:) was set up appropriately), making geometry changes in display() will invoke layer (and view) layout
  146. // logic at every animation frame, which is bad. layoutSublayers() should only be invoked on model changes, not presentation changes.
  147.  
  148. override func layoutSublayers() {
  149. super.layoutSublayers()
  150.  
  151. guard let normalImageViewLayer = normalImageViewLayer, highlightedImageViewLayer = highlightedImageViewLayer else { return }
  152.  
  153. let minimumSize = normalImageSize
  154. let maximumSize = highlightedImageSize
  155. let sizeDifference = CGSize(width: (maximumSize.width - minimumSize.width), height: (maximumSize.height - minimumSize.height))
  156.  
  157. var bounds = CGRectZero
  158. bounds.size.width = minimumSize.width + (CGFloat(highlightedPercentage) * sizeDifference.width)
  159. bounds.size.height = minimumSize.height + (CGFloat(highlightedPercentage) * sizeDifference.height)
  160.  
  161. self.bounds = bounds
  162.  
  163. normalImageViewLayer.bounds = bounds
  164. highlightedImageViewLayer.bounds = bounds
  165.  
  166. normalImageViewLayer.opacity = 1.0 - highlightedPercentage
  167. highlightedImageViewLayer.opacity = highlightedPercentage
  168. }
  169.  
  170. // MARK: - Forwarding Animation Addition and Removal
  171.  
  172. override func addAnimation(animation: CAAnimation, forKey key: String?) {
  173. super.addAnimation(animation, forKey: key)
  174.  
  175. propagateHighlightedPercentageAnimationAdditionIfNecessary(animation)
  176. }
  177.  
  178. override func removeAnimationForKey(key: String) {
  179. guard let animation = animationForKey(key) else { return }
  180.  
  181. super.removeAnimationForKey(key)
  182.  
  183. propagateHighlightedPercentageAnimationRemovalIfNecessary(animation)
  184. }
  185.  
  186. override func removeAllAnimations() {
  187. super.removeAllAnimations()
  188.  
  189. guard let normalImageViewLayer = normalImageViewLayer, highlightedImageViewLayer = highlightedImageViewLayer else { return }
  190.  
  191. normalImageViewLayer.removeAllAnimations()
  192. highlightedImageViewLayer.removeAllAnimations()
  193. }
  194.  
  195. // Here is how we actually animate the sublayers of a layer with a custom animatable property. When we detect that an animation is added
  196. // or removed that contains the keypath to a custom animatable layer property, we have to propagate those animations to the sublayers as
  197. // appropriate. Immutable animation copies are added to layers, so modifying the animation after it's already been added to the layer
  198. // is fine.
  199.  
  200. private func propagateHighlightedPercentageAnimationAdditionIfNecessary(animation: CAAnimation) {
  201. guard let normalImageViewLayer = normalImageViewLayer, highlightedImageViewLayer = highlightedImageViewLayer else { return }
  202.  
  203. var highlightedPercentageAnimation: CAAnimation? = nil
  204.  
  205. if let animationGroup = animation as? CAAnimationGroup, animations = animationGroup.animations {
  206. for animationGroupAnimation in animations {
  207. guard let propertyAnimation = animationGroupAnimation as? CAPropertyAnimation where propertyAnimation.keyPath == LayerHighlightedPercentageKey else { continue }
  208.  
  209. highlightedPercentageAnimation = propertyAnimation
  210. }
  211. } else if let propertyAnimation = animation as? CAPropertyAnimation where propertyAnimation.keyPath == LayerHighlightedPercentageKey {
  212. highlightedPercentageAnimation = propertyAnimation
  213. }
  214.  
  215. if let highlightedPercentageAnimation = highlightedPercentageAnimation {
  216. highlightedPercentageAnimation.beginTime = animation.beginTime
  217.  
  218. let minimumSize = normalImageSize
  219. let maximumSize = highlightedImageSize
  220. let sizeDifference = CGSize(width: (maximumSize.width - minimumSize.width), height: (maximumSize.height - minimumSize.height))
  221.  
  222. if let basicAnimation = highlightedPercentageAnimation as? CABasicAnimation {
  223. // Bounds
  224. let boundsAnimation = basicAnimation.copy() as! CABasicAnimation
  225. boundsAnimation.keyPath = "bounds"
  226.  
  227. if let fromValue = boundsAnimation.fromValue as? NSNumber {
  228. var fromBounds = CGRectZero
  229. fromBounds.size.width = minimumSize.width + (CGFloat(fromValue.floatValue) * sizeDifference.width)
  230. fromBounds.size.height = minimumSize.height + (CGFloat(fromValue.floatValue) * sizeDifference.height)
  231.  
  232. boundsAnimation.fromValue = NSValue(CGRect: fromBounds)
  233. }
  234.  
  235. if let toValue = boundsAnimation.toValue as? NSNumber {
  236. var toBounds = CGRectZero
  237. toBounds.size.width = minimumSize.width + (CGFloat(toValue.floatValue) * sizeDifference.width)
  238. toBounds.size.height = minimumSize.height + (CGFloat(toValue.floatValue) * sizeDifference.height)
  239.  
  240. boundsAnimation.toValue = NSValue(CGRect: toBounds)
  241. }
  242.  
  243. if let byValue = boundsAnimation.byValue as? NSNumber {
  244. var byBounds = CGRectZero
  245. byBounds.size.width = minimumSize.width + (CGFloat(byValue.floatValue) * sizeDifference.width)
  246. byBounds.size.height = minimumSize.height + (CGFloat(byValue.floatValue) * sizeDifference.height)
  247.  
  248. boundsAnimation.byValue = NSValue(CGRect: byBounds)
  249. }
  250.  
  251. addAnimation(boundsAnimation, forKey: LayerBoundsAnimationKey)
  252. normalImageViewLayer.addAnimation(boundsAnimation, forKey: LayerBoundsAnimationKey)
  253. highlightedImageViewLayer.addAnimation(boundsAnimation, forKey: LayerBoundsAnimationKey)
  254.  
  255. // Opacity
  256. let normalOpacityAnimation = basicAnimation.copy() as! CABasicAnimation
  257. normalOpacityAnimation.keyPath = "opacity"
  258.  
  259. if let fromValue = normalOpacityAnimation.fromValue as? NSNumber {
  260. let fromOpacity = 1.0 - fromValue.floatValue
  261.  
  262. normalOpacityAnimation.fromValue = fromOpacity
  263. }
  264.  
  265. if let toValue = normalOpacityAnimation.toValue as? NSNumber {
  266. let toOpacity = 1.0 - toValue.floatValue
  267.  
  268. normalOpacityAnimation.toValue = toOpacity
  269. }
  270.  
  271. if let byValue = normalOpacityAnimation.byValue as? NSNumber {
  272. let byOpacity = -byValue.floatValue
  273.  
  274. normalOpacityAnimation.byValue = byOpacity
  275. }
  276.  
  277. let highlightedOpacityAnimation = basicAnimation.copy() as! CABasicAnimation
  278. highlightedOpacityAnimation.keyPath = "opacity"
  279.  
  280. normalImageViewLayer.addAnimation(normalOpacityAnimation, forKey: LayerOpacityAnimationKey)
  281. highlightedImageViewLayer.addAnimation(highlightedOpacityAnimation, forKey: LayerOpacityAnimationKey)
  282. } else if let keyframeAnimation = highlightedPercentageAnimation as? CAKeyframeAnimation {
  283. guard let values = keyframeAnimation.values else { return }
  284.  
  285. // Bounds
  286. let boundsAnimation = keyframeAnimation.copy() as! CAKeyframeAnimation
  287. boundsAnimation.keyPath = "bounds"
  288. var boundsValues = [NSValue]()
  289.  
  290. let normalOpacityAnimation = keyframeAnimation.copy() as! CAKeyframeAnimation
  291. normalOpacityAnimation.keyPath = "opacity"
  292. var normalOpacityValues = [NSNumber]()
  293.  
  294. let highlightedOpacityAnimation = keyframeAnimation.copy() as! CAKeyframeAnimation
  295. highlightedOpacityAnimation.keyPath = "opacity"
  296. var highlightedOpacityValues = [NSNumber]()
  297.  
  298. for value in values {
  299. var bounds = CGRectZero
  300. var normalOpacity = Float(1.0)
  301. var highlightedOpacity = Float(0.0)
  302.  
  303. if let value = value as? NSNumber {
  304. let floatValue = value.floatValue
  305.  
  306. bounds.size.width = minimumSize.width + (CGFloat(floatValue) * sizeDifference.width)
  307. bounds.size.height = minimumSize.height + (CGFloat(floatValue) * sizeDifference.height)
  308.  
  309. normalOpacity = 1.0 - floatValue
  310. highlightedOpacity = floatValue
  311. }
  312.  
  313. boundsValues.append(NSValue(CGRect: bounds))
  314. normalOpacityValues.append(normalOpacity)
  315. highlightedOpacityValues.append(highlightedOpacity)
  316. }
  317.  
  318. boundsAnimation.values = boundsValues
  319. normalOpacityAnimation.values = normalOpacityValues
  320. highlightedOpacityAnimation.values = highlightedOpacityValues
  321.  
  322. addAnimation(boundsAnimation, forKey: LayerBoundsAnimationKey)
  323. normalImageViewLayer.addAnimation(boundsAnimation, forKey: BoundsAnimationKey)
  324. highlightedImageViewLayer.addAnimation(boundsAnimation, forKey: BoundsAnimationKey)
  325.  
  326. // Opacity
  327. normalImageViewLayer.addAnimation(normalOpacityAnimation, forKey: LayerOpacityAnimationKey)
  328. highlightedImageViewLayer.addAnimation(highlightedOpacityAnimation, forKey: LayerOpacityAnimationKey)
  329. }
  330. }
  331. }
  332.  
  333. private func propagateHighlightedPercentageAnimationRemovalIfNecessary(animation: CAAnimation) {
  334. guard let normalImageViewLayer = normalImageViewLayer, highlightedImageViewLayer = highlightedImageViewLayer else { return }
  335.  
  336. var highlightedPercentageAnimation: CAAnimation? = nil
  337.  
  338. if let animationGroup = animation as? CAAnimationGroup, animations = animationGroup.animations {
  339. for animation in animations {
  340. guard let animation = animation as? CAPropertyAnimation where animation.keyPath == LayerHighlightedPercentageKey else { continue }
  341.  
  342. highlightedPercentageAnimation = animation
  343. }
  344. } else if let animation = animation as? CAPropertyAnimation where animation.keyPath == LayerHighlightedPercentageKey {
  345. highlightedPercentageAnimation = animation
  346. }
  347.  
  348. if highlightedPercentageAnimation != nil {
  349. removeAnimationForKey(LayerBoundsAnimationKey)
  350. normalImageViewLayer.removeAnimationForKey(LayerBoundsAnimationKey)
  351. highlightedImageViewLayer.removeAnimationForKey(LayerBoundsAnimationKey)
  352.  
  353. normalImageViewLayer.removeAnimationForKey(LayerOpacityAnimationKey)
  354. highlightedImageViewLayer.removeAnimationForKey(LayerOpacityAnimationKey)
  355. }
  356. }
  357.  
  358. // MARK: - KVO Overrides
  359.  
  360. // 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
  361. // to change as well.
  362.  
  363. override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
  364. if keyPath == CircularMenuItemLayerHighlightedPercentageKey {
  365. setNeedsLayout()
  366. layoutIfNeeded() // May or may not be needed, depending on whether the layout can wait until the next run loop cycle.
  367. } else {
  368. super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
  369. }
  370. }
  371.  
  372. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement