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.

380 lines
16 KiB

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