You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

397 lines
13 KiB

  1. // The MIT License (MIT)
  2. //
  3. // Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
  4. //
  5. // Permission is hereby granted, free of charge, to any person obtaining a copy
  6. // of this software and associated documentation files (the "Software"), to deal
  7. // in the Software without restriction, including without limitation the rights
  8. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9. // copies of the Software, and to permit persons to whom the Software is
  10. // furnished to do so, subject to the following conditions:
  11. //
  12. // The above copyright notice and this permission notice shall be included in
  13. // all copies or substantial portions of the Software.
  14. //
  15. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  16. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  17. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  18. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  19. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  20. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  21. // THE SOFTWARE.
  22. import UIKit
  23. public class HeroContext {
  24. internal var heroIDToSourceView = [String: UIView]()
  25. internal var heroIDToDestinationView = [String: UIView]()
  26. internal var snapshotViews = [UIView: UIView]()
  27. internal var viewAlphas = [UIView: CGFloat]()
  28. internal var targetStates = [UIView: HeroTargetState]()
  29. internal var superviewToNoSnapshotSubviewMap: [UIView: [(Int, UIView)]] = [:]
  30. internal var insertToViewFirst = false
  31. internal var defaultCoordinateSpace: HeroCoordinateSpace = .local
  32. internal init(container: UIView) {
  33. self.container = container
  34. }
  35. internal func set(fromViews: [UIView], toViews: [UIView]) {
  36. self.fromViews = fromViews
  37. self.toViews = toViews
  38. process(views: fromViews, idMap: &heroIDToSourceView)
  39. process(views: toViews, idMap: &heroIDToDestinationView)
  40. }
  41. internal func process(views: [UIView], idMap: inout [String: UIView]) {
  42. for view in views {
  43. view.layer.removeAllHeroAnimations()
  44. let targetState: HeroTargetState?
  45. if let modifiers = view.hero.modifiers {
  46. targetState = HeroTargetState(modifiers: modifiers)
  47. } else {
  48. targetState = nil
  49. }
  50. if targetState?.forceAnimate == true || container.convert(view.bounds, from: view).intersects(container.bounds) {
  51. if let heroID = view.hero.id {
  52. idMap[heroID] = view
  53. }
  54. targetStates[view] = targetState
  55. }
  56. }
  57. }
  58. /**
  59. The container holding all of the animating views
  60. */
  61. public let container: UIView
  62. /**
  63. A flattened list of all views from source ViewController
  64. */
  65. public var fromViews: [UIView] = []
  66. /**
  67. A flattened list of all views from destination ViewController
  68. */
  69. public var toViews: [UIView] = []
  70. }
  71. // public
  72. extension HeroContext {
  73. /**
  74. - Returns: a source view matching the heroID, nil if not found
  75. */
  76. public func sourceView(for heroID: String) -> UIView? {
  77. return heroIDToSourceView[heroID]
  78. }
  79. /**
  80. - Returns: a destination view matching the heroID, nil if not found
  81. */
  82. public func destinationView(for heroID: String) -> UIView? {
  83. return heroIDToDestinationView[heroID]
  84. }
  85. /**
  86. - Returns: a view with the same heroID, but on different view controller, nil if not found
  87. */
  88. public func pairedView(for view: UIView) -> UIView? {
  89. if let id = view.hero.id {
  90. if sourceView(for: id) == view {
  91. return destinationView(for: id)
  92. } else if destinationView(for: id) == view {
  93. return sourceView(for: id)
  94. }
  95. }
  96. return nil
  97. }
  98. /**
  99. - Returns: a snapshot view for animation
  100. */
  101. public func snapshotView(for view: UIView) -> UIView {
  102. if let snapshot = snapshotViews[view] {
  103. return snapshot
  104. }
  105. var containerView = container
  106. let coordinateSpace = targetStates[view]?.coordinateSpace ?? defaultCoordinateSpace
  107. switch coordinateSpace {
  108. case .local:
  109. containerView = view
  110. while containerView != container, snapshotViews[containerView] == nil, let superview = containerView.superview {
  111. containerView = superview
  112. }
  113. if let snapshot = snapshotViews[containerView] {
  114. containerView = snapshot
  115. }
  116. if let visualEffectView = containerView as? UIVisualEffectView {
  117. containerView = visualEffectView.contentView
  118. }
  119. case .global:
  120. break
  121. }
  122. unhide(view: view)
  123. // capture a snapshot without alpha, cornerRadius, or shadows
  124. let oldCornerRadius = view.layer.cornerRadius
  125. let oldAlpha = view.alpha
  126. let oldShadowRadius = view.layer.shadowRadius
  127. let oldShadowOffset = view.layer.shadowOffset
  128. let oldShadowPath = view.layer.shadowPath
  129. let oldShadowOpacity = view.layer.shadowOpacity
  130. view.layer.cornerRadius = 0
  131. view.alpha = 1
  132. view.layer.shadowRadius = 0.0
  133. view.layer.shadowOffset = .zero
  134. view.layer.shadowPath = nil
  135. view.layer.shadowOpacity = 0.0
  136. let snapshot: UIView
  137. let snapshotType: HeroSnapshotType = self[view]?.snapshotType ?? .optimized
  138. switch snapshotType {
  139. case .normal:
  140. snapshot = view.snapshotView() ?? UIView()
  141. case .layerRender:
  142. snapshot = view.slowSnapshotView()
  143. case .noSnapshot:
  144. if view.superview != container {
  145. if superviewToNoSnapshotSubviewMap[view.superview!] == nil {
  146. superviewToNoSnapshotSubviewMap[view.superview!] = []
  147. }
  148. superviewToNoSnapshotSubviewMap[view.superview!]!.append((view.superview!.subviews.index(of: view)!, view))
  149. }
  150. snapshot = view
  151. case .optimized:
  152. #if os(tvOS)
  153. snapshot = view.snapshotView(afterScreenUpdates: true)!
  154. #else
  155. if let customSnapshotView = view as? HeroCustomSnapshotView, let snapshotView = customSnapshotView.heroSnapshot {
  156. snapshot = snapshotView
  157. } else if #available(iOS 9.0, *), let stackView = view as? UIStackView {
  158. snapshot = stackView.slowSnapshotView()
  159. } else if let imageView = view as? UIImageView, view.subviews.filter({!$0.isHidden}).isEmpty {
  160. let contentView = UIImageView(image: imageView.image)
  161. contentView.frame = imageView.bounds
  162. contentView.contentMode = imageView.contentMode
  163. contentView.tintColor = imageView.tintColor
  164. contentView.backgroundColor = imageView.backgroundColor
  165. contentView.layer.magnificationFilter = imageView.layer.magnificationFilter
  166. contentView.layer.minificationFilter = imageView.layer.minificationFilter
  167. contentView.layer.minificationFilterBias = imageView.layer.minificationFilterBias
  168. let snapShotView = UIView()
  169. snapShotView.addSubview(contentView)
  170. snapshot = snapShotView
  171. } else if let barView = view as? UINavigationBar, barView.isTranslucent {
  172. let newBarView = UINavigationBar(frame: barView.frame)
  173. newBarView.barStyle = barView.barStyle
  174. newBarView.tintColor = barView.tintColor
  175. newBarView.barTintColor = barView.barTintColor
  176. newBarView.clipsToBounds = false
  177. // take a snapshot without the background
  178. barView.layer.sublayers![0].opacity = 0
  179. let realSnapshot = barView.snapshotView(afterScreenUpdates: true)!
  180. barView.layer.sublayers![0].opacity = 1
  181. newBarView.addSubview(realSnapshot)
  182. snapshot = newBarView
  183. } else if let effectView = view as? UIVisualEffectView {
  184. snapshot = UIVisualEffectView(effect: effectView.effect)
  185. snapshot.frame = effectView.bounds
  186. } else {
  187. snapshot = view.snapshotView() ?? UIView()
  188. }
  189. #endif
  190. }
  191. #if os(tvOS)
  192. if let imageView = view as? UIImageView, imageView.adjustsImageWhenAncestorFocused {
  193. snapshot.frame = imageView.focusedFrameGuide.layoutFrame
  194. }
  195. #endif
  196. view.layer.cornerRadius = oldCornerRadius
  197. view.alpha = oldAlpha
  198. view.layer.shadowRadius = oldShadowRadius
  199. view.layer.shadowOffset = oldShadowOffset
  200. view.layer.shadowPath = oldShadowPath
  201. view.layer.shadowOpacity = oldShadowOpacity
  202. snapshot.layer.anchorPoint = view.layer.anchorPoint
  203. snapshot.layer.position = containerView.convert(view.layer.position, from: view.superview!)
  204. snapshot.layer.transform = containerView.layer.flatTransformTo(layer: view.layer)
  205. snapshot.layer.bounds = view.layer.bounds
  206. snapshot.hero.id = view.hero.id
  207. if snapshotType != .noSnapshot {
  208. if !(view is UINavigationBar), let contentView = snapshot.subviews.get(0) {
  209. // the Snapshot's contentView must have hold the cornerRadius value,
  210. // since the snapshot might not have maskToBounds set
  211. contentView.layer.cornerRadius = view.layer.cornerRadius
  212. contentView.layer.masksToBounds = true
  213. }
  214. snapshot.layer.allowsGroupOpacity = false
  215. snapshot.layer.cornerRadius = view.layer.cornerRadius
  216. snapshot.layer.zPosition = view.layer.zPosition
  217. snapshot.layer.opacity = view.layer.opacity
  218. snapshot.layer.isOpaque = view.layer.isOpaque
  219. snapshot.layer.anchorPoint = view.layer.anchorPoint
  220. snapshot.layer.masksToBounds = view.layer.masksToBounds
  221. snapshot.layer.borderColor = view.layer.borderColor
  222. snapshot.layer.borderWidth = view.layer.borderWidth
  223. snapshot.layer.contentsRect = view.layer.contentsRect
  224. snapshot.layer.contentsScale = view.layer.contentsScale
  225. if self[view]?.displayShadow ?? true {
  226. snapshot.layer.shadowRadius = view.layer.shadowRadius
  227. snapshot.layer.shadowOpacity = view.layer.shadowOpacity
  228. snapshot.layer.shadowColor = view.layer.shadowColor
  229. snapshot.layer.shadowOffset = view.layer.shadowOffset
  230. snapshot.layer.shadowPath = view.layer.shadowPath
  231. }
  232. hide(view: view)
  233. }
  234. if let pairedView = pairedView(for: view), let pairedSnapshot = snapshotViews[pairedView] {
  235. let siblingViews = pairedView.superview!.subviews
  236. let nextSiblings = siblingViews[siblingViews.index(of: pairedView)!+1..<siblingViews.count]
  237. containerView.addSubview(pairedSnapshot)
  238. containerView.addSubview(snapshot)
  239. for subview in pairedView.subviews {
  240. insertGlobalViewTree(view: subview)
  241. }
  242. for sibling in nextSiblings {
  243. insertGlobalViewTree(view: sibling)
  244. }
  245. } else {
  246. containerView.addSubview(snapshot)
  247. }
  248. containerView.addSubview(snapshot)
  249. snapshotViews[view] = snapshot
  250. return snapshot
  251. }
  252. func insertGlobalViewTree(view: UIView) {
  253. if targetStates[view]?.coordinateSpace == .global, let snapshot = snapshotViews[view] {
  254. container.addSubview(snapshot)
  255. }
  256. for subview in view.subviews {
  257. insertGlobalViewTree(view: subview)
  258. }
  259. }
  260. public subscript(view: UIView) -> HeroTargetState? {
  261. get {
  262. return targetStates[view]
  263. }
  264. set {
  265. targetStates[view] = newValue
  266. }
  267. }
  268. public func clean() {
  269. for (superview, subviews) in superviewToNoSnapshotSubviewMap {
  270. for (index, view) in subviews.reversed() {
  271. superview.insertSubview(view, at: index)
  272. }
  273. }
  274. }
  275. }
  276. // internal
  277. extension HeroContext {
  278. public func hide(view: UIView) {
  279. if viewAlphas[view] == nil {
  280. if view is UIVisualEffectView {
  281. view.isHidden = true
  282. viewAlphas[view] = 1
  283. } else {
  284. viewAlphas[view] = view.alpha
  285. view.alpha = 0
  286. }
  287. }
  288. }
  289. public func unhide(view: UIView) {
  290. if let oldAlpha = viewAlphas[view] {
  291. if view is UIVisualEffectView {
  292. view.isHidden = false
  293. } else {
  294. view.alpha = oldAlpha
  295. }
  296. viewAlphas[view] = nil
  297. }
  298. }
  299. internal func unhideAll() {
  300. for view in viewAlphas.keys {
  301. unhide(view: view)
  302. }
  303. viewAlphas.removeAll()
  304. }
  305. internal func unhide(rootView: UIView) {
  306. unhide(view: rootView)
  307. for subview in rootView.subviews {
  308. unhide(rootView: subview)
  309. }
  310. }
  311. internal func removeAllSnapshots() {
  312. for (view, snapshot) in snapshotViews {
  313. if view != snapshot {
  314. snapshot.removeFromSuperview()
  315. } else {
  316. view.layer.removeAllHeroAnimations()
  317. }
  318. }
  319. }
  320. internal func removeSnapshots(rootView: UIView) {
  321. if let snapshot = snapshotViews[rootView] {
  322. if rootView != snapshot {
  323. snapshot.removeFromSuperview()
  324. } else {
  325. rootView.layer.removeAllHeroAnimations()
  326. }
  327. }
  328. for subview in rootView.subviews {
  329. removeSnapshots(rootView: subview)
  330. }
  331. }
  332. internal func snapshots(rootView: UIView) -> [UIView] {
  333. var snapshots = [UIView]()
  334. for v in rootView.flattenedViewHierarchy {
  335. if let snapshot = snapshotViews[v] {
  336. snapshots.append(snapshot)
  337. }
  338. }
  339. return snapshots
  340. }
  341. internal func loadViewAlpha(rootView: UIView) {
  342. if let storedAlpha = rootView.hero.storedAlpha {
  343. rootView.alpha = storedAlpha
  344. rootView.hero.storedAlpha = nil
  345. }
  346. for subview in rootView.subviews {
  347. loadViewAlpha(rootView: subview)
  348. }
  349. }
  350. internal func storeViewAlpha(rootView: UIView) {
  351. rootView.hero.storedAlpha = viewAlphas[rootView]
  352. for subview in rootView.subviews {
  353. storeViewAlpha(rootView: subview)
  354. }
  355. }
  356. }
  357. /// Allows a view to create their own custom snapshot when using **Optimized** snapshot
  358. public protocol HeroCustomSnapshotView {
  359. var heroSnapshot: UIView? { get }
  360. }