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.

353 lines
15 KiB

6 years ago
  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 enum HeroDefaultAnimationType {
  24. public enum Direction: HeroStringConvertible {
  25. case left, right, up, down
  26. public static func from(node: ExprNode) -> Direction? {
  27. switch node.name {
  28. case "left": return .left
  29. case "right": return .right
  30. case "up": return .up
  31. case "down": return .down
  32. default: return nil
  33. }
  34. }
  35. }
  36. case auto
  37. case push(direction: Direction)
  38. case pull(direction: Direction)
  39. case cover(direction: Direction)
  40. case uncover(direction: Direction)
  41. case slide(direction: Direction)
  42. case zoomSlide(direction: Direction)
  43. case pageIn(direction: Direction)
  44. case pageOut(direction: Direction)
  45. case fade
  46. case zoom
  47. case zoomOut
  48. indirect case selectBy(presenting: HeroDefaultAnimationType, dismissing: HeroDefaultAnimationType)
  49. public static func autoReverse(presenting: HeroDefaultAnimationType) -> HeroDefaultAnimationType {
  50. return .selectBy(presenting: presenting, dismissing: presenting.reversed())
  51. }
  52. case none
  53. func reversed() -> HeroDefaultAnimationType {
  54. switch self {
  55. case .push(direction: .up):
  56. return .pull(direction: .down)
  57. case .push(direction: .right):
  58. return .pull(direction: .left)
  59. case .push(direction: .down):
  60. return .pull(direction: .up)
  61. case .push(direction: .left):
  62. return .pull(direction: .right)
  63. case .pull(direction: .up):
  64. return .push(direction: .down)
  65. case .pull(direction: .right):
  66. return .push(direction: .left)
  67. case .pull(direction: .down):
  68. return .push(direction: .up)
  69. case .pull(direction: .left):
  70. return .push(direction: .right)
  71. case .cover(direction: .up):
  72. return .uncover(direction: .down)
  73. case .cover(direction: .right):
  74. return .uncover(direction: .left)
  75. case .cover(direction: .down):
  76. return .uncover(direction: .up)
  77. case .cover(direction: .left):
  78. return .uncover(direction: .right)
  79. case .uncover(direction: .up):
  80. return .cover(direction: .down)
  81. case .uncover(direction: .right):
  82. return .cover(direction: .left)
  83. case .uncover(direction: .down):
  84. return .cover(direction: .up)
  85. case .uncover(direction: .left):
  86. return .cover(direction: .right)
  87. case .slide(direction: .up):
  88. return .slide(direction: .down)
  89. case .slide(direction: .down):
  90. return .slide(direction: .up)
  91. case .slide(direction: .left):
  92. return .slide(direction: .right)
  93. case .slide(direction: .right):
  94. return .slide(direction: .left)
  95. case .zoomSlide(direction: .up):
  96. return .zoomSlide(direction: .down)
  97. case .zoomSlide(direction: .down):
  98. return .zoomSlide(direction: .up)
  99. case .zoomSlide(direction: .left):
  100. return .zoomSlide(direction: .right)
  101. case .zoomSlide(direction: .right):
  102. return .zoomSlide(direction: .left)
  103. case .pageIn(direction: .up):
  104. return .pageOut(direction: .down)
  105. case .pageIn(direction: .right):
  106. return .pageOut(direction: .left)
  107. case .pageIn(direction: .down):
  108. return .pageOut(direction: .up)
  109. case .pageIn(direction: .left):
  110. return .pageOut(direction: .right)
  111. case .pageOut(direction: .up):
  112. return .pageIn(direction: .down)
  113. case .pageOut(direction: .right):
  114. return .pageIn(direction: .left)
  115. case .pageOut(direction: .down):
  116. return .pageIn(direction: .up)
  117. case .pageOut(direction: .left):
  118. return .pageIn(direction: .right)
  119. case .zoom:
  120. return .zoomOut
  121. case .zoomOut:
  122. return .zoom
  123. default:
  124. return self
  125. }
  126. }
  127. public var label: String? {
  128. let mirror = Mirror(reflecting: self)
  129. if let associated = mirror.children.first {
  130. let valuesMirror = Mirror(reflecting: associated.value)
  131. if !valuesMirror.children.isEmpty {
  132. let parameters = valuesMirror.children.map { ".\($0.value)" }.joined(separator: ",")
  133. return ".\(associated.label ?? "")(\(parameters))"
  134. }
  135. return ".\(associated.label ?? "")(.\(associated.value))"
  136. }
  137. return ".\(self)"
  138. }
  139. }
  140. extension HeroDefaultAnimationType: HeroStringConvertible {
  141. public static func from(node: ExprNode) -> HeroDefaultAnimationType? {
  142. let name: String = node.name
  143. let parameters: [ExprNode] = (node as? CallNode)?.arguments ?? []
  144. switch name {
  145. case "auto":
  146. return .auto
  147. case "push":
  148. if let node = parameters.get(0), let direction = Direction.from(node: node) {
  149. return .push(direction: direction)
  150. }
  151. case "pull":
  152. if let node = parameters.get(0), let direction = Direction.from(node: node) {
  153. return .pull(direction: direction)
  154. }
  155. case "cover":
  156. if let node = parameters.get(0), let direction = Direction.from(node: node) {
  157. return .cover(direction: direction)
  158. }
  159. case "uncover":
  160. if let node = parameters.get(0), let direction = Direction.from(node: node) {
  161. return .uncover(direction: direction)
  162. }
  163. case "slide":
  164. if let node = parameters.get(0), let direction = Direction.from(node: node) {
  165. return .slide(direction: direction)
  166. }
  167. case "zoomSlide":
  168. if let node = parameters.get(0), let direction = Direction.from(node: node) {
  169. return .zoomSlide(direction: direction)
  170. }
  171. case "pageIn":
  172. if let node = parameters.get(0), let direction = Direction.from(node: node) {
  173. return .pageIn(direction: direction)
  174. }
  175. case "pageOut":
  176. if let node = parameters.get(0), let direction = Direction.from(node: node) {
  177. return .pageOut(direction: direction)
  178. }
  179. case "fade": return .fade
  180. case "zoom": return .zoom
  181. case "zoomOut": return .zoomOut
  182. case "selectBy":
  183. if let presentingNode = parameters.get(0),
  184. let presenting = HeroDefaultAnimationType.from(node: presentingNode),
  185. let dismissingNode = parameters.get(1),
  186. let dismissing = HeroDefaultAnimationType.from(node: dismissingNode) {
  187. return .selectBy(presenting: presenting, dismissing: dismissing)
  188. }
  189. case "none": return HeroDefaultAnimationType.none
  190. default: break
  191. }
  192. return nil
  193. }
  194. }
  195. class DefaultAnimationPreprocessor: BasePreprocessor {
  196. func shift(direction: HeroDefaultAnimationType.Direction, appearing: Bool, size: CGSize? = nil, transpose: Bool = false) -> CGPoint {
  197. let size = size ?? context.container.bounds.size
  198. let rtn: CGPoint
  199. switch direction {
  200. case .left, .right:
  201. rtn = CGPoint(x: (direction == .right) == appearing ? -size.width : size.width, y: 0)
  202. case .up, .down:
  203. rtn = CGPoint(x: 0, y: (direction == .down) == appearing ? -size.height : size.height)
  204. }
  205. if transpose {
  206. return CGPoint(x: rtn.y, y: rtn.x)
  207. }
  208. return rtn
  209. }
  210. override func process(fromViews: [UIView], toViews: [UIView]) {
  211. guard let hero = hero, let toView = hero.toView, let fromView = hero.fromView else { return }
  212. var defaultAnimation = hero.defaultAnimation
  213. let inNavigationController = hero.inNavigationController
  214. let inTabBarController = hero.inTabBarController
  215. let toViewController = hero.toViewController
  216. let fromViewController = hero.fromViewController
  217. let presenting = hero.isPresenting
  218. let fromOverFullScreen = hero.fromOverFullScreen
  219. let toOverFullScreen = hero.toOverFullScreen
  220. let animators = hero.animators
  221. if case .auto = defaultAnimation {
  222. if inNavigationController, let navAnim = toViewController?.navigationController?.hero.navigationAnimationType {
  223. defaultAnimation = navAnim
  224. } else if inTabBarController, let tabAnim = toViewController?.tabBarController?.hero.tabBarAnimationType {
  225. defaultAnimation = tabAnim
  226. } else if let modalAnim = (presenting ? toViewController : fromViewController)?.hero.modalAnimationType {
  227. defaultAnimation = modalAnim
  228. }
  229. }
  230. if case .selectBy(let presentAnim, let dismissAnim) = defaultAnimation {
  231. defaultAnimation = presenting ? presentAnim : dismissAnim
  232. }
  233. if case .auto = defaultAnimation {
  234. if animators.contains(where: { $0.canAnimate(view: toView, appearing: true) || $0.canAnimate(view: fromView, appearing: false) }) {
  235. defaultAnimation = .none
  236. } else if inNavigationController {
  237. defaultAnimation = presenting ? .push(direction:.left) : .pull(direction:.right)
  238. } else if inTabBarController {
  239. defaultAnimation = presenting ? .slide(direction:.left) : .slide(direction:.right)
  240. } else {
  241. defaultAnimation = .fade
  242. }
  243. }
  244. if case .none = defaultAnimation {
  245. return
  246. }
  247. context[fromView] = [.timingFunction(.standard), .duration(0.35)]
  248. context[toView] = [.timingFunction(.standard), .duration(0.35)]
  249. let shadowState: [HeroModifier] = [.shadowOpacity(0.5),
  250. .shadowColor(.black),
  251. .shadowRadius(5),
  252. .shadowOffset(.zero),
  253. .masksToBounds(false)]
  254. switch defaultAnimation {
  255. case .push(let direction):
  256. context.insertToViewFirst = false
  257. context[toView]!.append(contentsOf: [.translate(shift(direction: direction, appearing: true)),
  258. .shadowOpacity(0),
  259. .beginWith(modifiers: shadowState),
  260. .timingFunction(.deceleration)])
  261. context[fromView]!.append(contentsOf: [.translate(shift(direction: direction, appearing: false) / 3),
  262. .overlay(color: .black, opacity: 0.1),
  263. .timingFunction(.deceleration)])
  264. case .pull(let direction):
  265. context.insertToViewFirst = true
  266. context[fromView]!.append(contentsOf: [.translate(shift(direction: direction, appearing: false)),
  267. .shadowOpacity(0),
  268. .beginWith(modifiers: shadowState)])
  269. context[toView]!.append(contentsOf: [.translate(shift(direction: direction, appearing: true) / 3),
  270. .overlay(color: .black, opacity: 0.1)])
  271. case .slide(let direction):
  272. context[fromView]!.append(contentsOf: [.translate(shift(direction: direction, appearing: false))])
  273. context[toView]!.append(contentsOf: [.translate(shift(direction: direction, appearing: true))])
  274. case .zoomSlide(let direction):
  275. context[fromView]!.append(contentsOf: [.translate(shift(direction: direction, appearing: false)), .scale(0.8)])
  276. context[toView]!.append(contentsOf: [.translate(shift(direction: direction, appearing: true)), .scale(0.8)])
  277. case .cover(let direction):
  278. context.insertToViewFirst = false
  279. context[toView]!.append(contentsOf: [.translate(shift(direction: direction, appearing: true)),
  280. .shadowOpacity(0),
  281. .beginWith(modifiers: shadowState),
  282. .timingFunction(.deceleration)])
  283. context[fromView]!.append(contentsOf: [.overlay(color: .black, opacity: 0.1),
  284. .timingFunction(.deceleration)])
  285. case .uncover(let direction):
  286. context.insertToViewFirst = true
  287. context[fromView]!.append(contentsOf: [.translate(shift(direction: direction, appearing: false)),
  288. .shadowOpacity(0),
  289. .beginWith(modifiers: shadowState)])
  290. context[toView]!.append(contentsOf: [.overlay(color: .black, opacity: 0.1)])
  291. case .pageIn(let direction):
  292. context.insertToViewFirst = false
  293. context[toView]!.append(contentsOf: [.translate(shift(direction: direction, appearing: true)),
  294. .shadowOpacity(0),
  295. .beginWith(modifiers: shadowState),
  296. .timingFunction(.deceleration)])
  297. context[fromView]!.append(contentsOf: [.scale(0.7),
  298. .overlay(color: .black, opacity: 0.1),
  299. .timingFunction(.deceleration)])
  300. case .pageOut(let direction):
  301. context.insertToViewFirst = true
  302. context[fromView]!.append(contentsOf: [.translate(shift(direction: direction, appearing: false)),
  303. .shadowOpacity(0),
  304. .beginWith(modifiers: shadowState)])
  305. context[toView]!.append(contentsOf: [.scale(0.7),
  306. .overlay(color: .black, opacity: 0.1)])
  307. case .fade:
  308. // TODO: clean up this. overFullScreen logic shouldn't be here
  309. if !(fromOverFullScreen && !presenting) {
  310. context[toView] = [.fade]
  311. }
  312. #if os(tvOS)
  313. context[fromView] = [.fade]
  314. #else
  315. if (!presenting && toOverFullScreen) || !fromView.isOpaque || (fromView.backgroundColor?.alphaComponent ?? 1) < 1 {
  316. context[fromView] = [.fade]
  317. }
  318. #endif
  319. context[toView]!.append(.durationMatchLongest)
  320. context[fromView]!.append(.durationMatchLongest)
  321. case .zoom:
  322. context.insertToViewFirst = true
  323. context[fromView]!.append(contentsOf: [.scale(1.3), .fade])
  324. context[toView]!.append(contentsOf: [.scale(0.7)])
  325. case .zoomOut:
  326. context.insertToViewFirst = false
  327. context[toView]!.append(contentsOf: [.scale(1.3), .fade])
  328. context[fromView]!.append(contentsOf: [.scale(0.7)])
  329. default:
  330. fatalError("Not implemented")
  331. }
  332. }
  333. }