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

// The MIT License (MIT)
//
// Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
public enum HeroDefaultAnimationType {
public enum Direction: HeroStringConvertible {
case left, right, up, down
public static func from(node: ExprNode) -> Direction? {
switch node.name {
case "left": return .left
case "right": return .right
case "up": return .up
case "down": return .down
case "leading": return .leading
case "trailing": return .trailing
default: return nil
}
}
public static var leading: Direction {
return UIApplication.shared.userInterfaceLayoutDirection == .leftToRight ? .left : .right
}
public static var trailing: Direction {
return UIApplication.shared.userInterfaceLayoutDirection == .leftToRight ? .right : .left
}
}
public enum Strategy {
case forceLeftToRight, forceRightToLeft, userInterface
func defaultDirection(presenting: Bool) -> Direction {
switch self {
case .forceLeftToRight:
return presenting ? .left : .right
case .forceRightToLeft:
return presenting ? .right : .left
case .userInterface:
return presenting ? .leading : .trailing
}
}
}
case auto
case push(direction: Direction)
case pull(direction: Direction)
case cover(direction: Direction)
case uncover(direction: Direction)
case slide(direction: Direction)
case zoomSlide(direction: Direction)
case pageIn(direction: Direction)
case pageOut(direction: Direction)
case fade
case zoom
case zoomOut
indirect case selectBy(presenting: HeroDefaultAnimationType, dismissing: HeroDefaultAnimationType)
public static func autoReverse(presenting: HeroDefaultAnimationType) -> HeroDefaultAnimationType {
return .selectBy(presenting: presenting, dismissing: presenting.reversed())
}
case none
func reversed() -> HeroDefaultAnimationType {
switch self {
case .push(direction: .up):
return .pull(direction: .down)
case .push(direction: .right):
return .pull(direction: .left)
case .push(direction: .down):
return .pull(direction: .up)
case .push(direction: .left):
return .pull(direction: .right)
case .pull(direction: .up):
return .push(direction: .down)
case .pull(direction: .right):
return .push(direction: .left)
case .pull(direction: .down):
return .push(direction: .up)
case .pull(direction: .left):
return .push(direction: .right)
case .cover(direction: .up):
return .uncover(direction: .down)
case .cover(direction: .right):
return .uncover(direction: .left)
case .cover(direction: .down):
return .uncover(direction: .up)
case .cover(direction: .left):
return .uncover(direction: .right)
case .uncover(direction: .up):
return .cover(direction: .down)
case .uncover(direction: .right):
return .cover(direction: .left)
case .uncover(direction: .down):
return .cover(direction: .up)
case .uncover(direction: .left):
return .cover(direction: .right)
case .slide(direction: .up):
return .slide(direction: .down)
case .slide(direction: .down):
return .slide(direction: .up)
case .slide(direction: .left):
return .slide(direction: .right)
case .slide(direction: .right):
return .slide(direction: .left)
case .zoomSlide(direction: .up):
return .zoomSlide(direction: .down)
case .zoomSlide(direction: .down):
return .zoomSlide(direction: .up)
case .zoomSlide(direction: .left):
return .zoomSlide(direction: .right)
case .zoomSlide(direction: .right):
return .zoomSlide(direction: .left)
case .pageIn(direction: .up):
return .pageOut(direction: .down)
case .pageIn(direction: .right):
return .pageOut(direction: .left)
case .pageIn(direction: .down):
return .pageOut(direction: .up)
case .pageIn(direction: .left):
return .pageOut(direction: .right)
case .pageOut(direction: .up):
return .pageIn(direction: .down)
case .pageOut(direction: .right):
return .pageIn(direction: .left)
case .pageOut(direction: .down):
return .pageIn(direction: .up)
case .pageOut(direction: .left):
return .pageIn(direction: .right)
case .zoom:
return .zoomOut
case .zoomOut:
return .zoom
default:
return self
}
}
public var label: String? {
let mirror = Mirror(reflecting: self)
if let associated = mirror.children.first {
let valuesMirror = Mirror(reflecting: associated.value)
if !valuesMirror.children.isEmpty {
let parameters = valuesMirror.children.map { ".\($0.value)" }.joined(separator: ",")
return ".\(associated.label ?? "")(\(parameters))"
}
return ".\(associated.label ?? "")(.\(associated.value))"
}
return ".\(self)"
}
}
extension HeroDefaultAnimationType: HeroStringConvertible {
public static func from(node: ExprNode) -> HeroDefaultAnimationType? {
let name: String = node.name
let parameters: [ExprNode] = (node as? CallNode)?.arguments ?? []
switch name {
case "auto":
return .auto
case "push":
if let node = parameters.get(0), let direction = Direction.from(node: node) {
return .push(direction: direction)
}
case "pull":
if let node = parameters.get(0), let direction = Direction.from(node: node) {
return .pull(direction: direction)
}
case "cover":
if let node = parameters.get(0), let direction = Direction.from(node: node) {
return .cover(direction: direction)
}
case "uncover":
if let node = parameters.get(0), let direction = Direction.from(node: node) {
return .uncover(direction: direction)
}
case "slide":
if let node = parameters.get(0), let direction = Direction.from(node: node) {
return .slide(direction: direction)
}
case "zoomSlide":
if let node = parameters.get(0), let direction = Direction.from(node: node) {
return .zoomSlide(direction: direction)
}
case "pageIn":
if let node = parameters.get(0), let direction = Direction.from(node: node) {
return .pageIn(direction: direction)
}
case "pageOut":
if let node = parameters.get(0), let direction = Direction.from(node: node) {
return .pageOut(direction: direction)
}
case "fade": return .fade
case "zoom": return .zoom
case "zoomOut": return .zoomOut
case "selectBy":
if let presentingNode = parameters.get(0),
let presenting = HeroDefaultAnimationType.from(node: presentingNode),
let dismissingNode = parameters.get(1),
let dismissing = HeroDefaultAnimationType.from(node: dismissingNode) {
return .selectBy(presenting: presenting, dismissing: dismissing)
}
case "none": return HeroDefaultAnimationType.none
default: break
}
return nil
}
}
class DefaultAnimationPreprocessor: BasePreprocessor {
func shift(direction: HeroDefaultAnimationType.Direction, appearing: Bool, size: CGSize? = nil, transpose: Bool = false) -> CGPoint {
let size = size ?? context.container.bounds.size
let rtn: CGPoint
switch direction {
case .left, .right:
rtn = CGPoint(x: (direction == .right) == appearing ? -size.width : size.width, y: 0)
case .up, .down:
rtn = CGPoint(x: 0, y: (direction == .down) == appearing ? -size.height : size.height)
}
if transpose {
return CGPoint(x: rtn.y, y: rtn.x)
}
return rtn
}
override func process(fromViews: [UIView], toViews: [UIView]) {
guard let hero = hero, let toView = hero.toView, let fromView = hero.fromView else { return }
var defaultAnimation = hero.defaultAnimation
let inNavigationController = hero.inNavigationController
let inTabBarController = hero.inTabBarController
let toViewController = hero.toViewController
let fromViewController = hero.fromViewController
let presenting = hero.isPresenting
let fromOverFullScreen = hero.fromOverFullScreen
let toOverFullScreen = hero.toOverFullScreen
let animators = hero.animators
if case .auto = defaultAnimation {
if inNavigationController, let navAnim = toViewController?.navigationController?.hero.navigationAnimationType {
defaultAnimation = navAnim
} else if inTabBarController, let tabAnim = toViewController?.tabBarController?.hero.tabBarAnimationType {
defaultAnimation = tabAnim
} else if let modalAnim = (presenting ? toViewController : fromViewController)?.hero.modalAnimationType {
defaultAnimation = modalAnim
}
}
if case .selectBy(let presentAnim, let dismissAnim) = defaultAnimation {
defaultAnimation = presenting ? presentAnim : dismissAnim
}
if case .auto = defaultAnimation {
if animators.contains(where: { $0.canAnimate(view: toView, appearing: true) || $0.canAnimate(view: fromView, appearing: false) }) {
defaultAnimation = .none
} else if inNavigationController {
let direction = hero.defaultAnimationDirectionStrategy.defaultDirection(presenting: presenting)
defaultAnimation = .push(direction: direction)
} else if inTabBarController {
let direction = hero.defaultAnimationDirectionStrategy.defaultDirection(presenting: presenting)
defaultAnimation = .slide(direction: direction)
} else {
defaultAnimation = .fade
}
}
if case .none = defaultAnimation {
return
}
context[fromView] = [.timingFunction(.standard), .duration(0.35)]
context[toView] = [.timingFunction(.standard), .duration(0.35)]
let shadowState: [HeroModifier] = [.shadowOpacity(0.5),
.shadowColor(.black),
.shadowRadius(5),
.shadowOffset(.zero),
.masksToBounds(false)]
switch defaultAnimation {
case .push(let direction):
context.insertToViewFirst = false
context[toView]!.append(contentsOf: [.translate(shift(direction: direction, appearing: true)),
.shadowOpacity(0),
.beginWith(modifiers: shadowState),
.timingFunction(.deceleration)])
context[fromView]!.append(contentsOf: [.translate(shift(direction: direction, appearing: false) / 3),
.overlay(color: .black, opacity: 0.1),
.timingFunction(.deceleration)])
case .pull(let direction):
context.insertToViewFirst = true
context[fromView]!.append(contentsOf: [.translate(shift(direction: direction, appearing: false)),
.shadowOpacity(0),
.beginWith(modifiers: shadowState)])
context[toView]!.append(contentsOf: [.translate(shift(direction: direction, appearing: true) / 3),
.overlay(color: .black, opacity: 0.1)])
case .slide(let direction):
context[fromView]!.append(contentsOf: [.translate(shift(direction: direction, appearing: false))])
context[toView]!.append(contentsOf: [.translate(shift(direction: direction, appearing: true))])
case .zoomSlide(let direction):
context[fromView]!.append(contentsOf: [.translate(shift(direction: direction, appearing: false)), .scale(0.8)])
context[toView]!.append(contentsOf: [.translate(shift(direction: direction, appearing: true)), .scale(0.8)])
case .cover(let direction):
context.insertToViewFirst = false
context[toView]!.append(contentsOf: [.translate(shift(direction: direction, appearing: true)),
.shadowOpacity(0),
.beginWith(modifiers: shadowState),
.timingFunction(.deceleration)])
context[fromView]!.append(contentsOf: [.overlay(color: .black, opacity: 0.1),
.timingFunction(.deceleration)])
case .uncover(let direction):
context.insertToViewFirst = true
context[fromView]!.append(contentsOf: [.translate(shift(direction: direction, appearing: false)),
.shadowOpacity(0),
.beginWith(modifiers: shadowState)])
context[toView]!.append(contentsOf: [.overlay(color: .black, opacity: 0.1)])
case .pageIn(let direction):
context.insertToViewFirst = false
context[toView]!.append(contentsOf: [.translate(shift(direction: direction, appearing: true)),
.shadowOpacity(0),
.beginWith(modifiers: shadowState),
.timingFunction(.deceleration)])
context[fromView]!.append(contentsOf: [.scale(0.7),
.overlay(color: .black, opacity: 0.1),
.timingFunction(.deceleration)])
case .pageOut(let direction):
context.insertToViewFirst = true
context[fromView]!.append(contentsOf: [.translate(shift(direction: direction, appearing: false)),
.shadowOpacity(0),
.beginWith(modifiers: shadowState)])
context[toView]!.append(contentsOf: [.scale(0.7),
.overlay(color: .black, opacity: 0.1)])
case .fade:
// TODO: clean up this. overFullScreen logic shouldn't be here
if !(fromOverFullScreen && !presenting) {
context[toView] = [.fade]
}
#if os(tvOS)
context[fromView] = [.fade]
#else
if (!presenting && toOverFullScreen) || !fromView.isOpaque || (fromView.backgroundColor?.alphaComponent ?? 1) < 1 {
context[fromView] = [.fade]
}
#endif
context[toView]!.append(.durationMatchLongest)
context[fromView]!.append(.durationMatchLongest)
case .zoom:
context.insertToViewFirst = true
context[fromView]!.append(contentsOf: [.scale(1.3), .fade])
context[toView]!.append(contentsOf: [.scale(0.7)])
case .zoomOut:
context.insertToViewFirst = false
context[toView]!.append(contentsOf: [.scale(1.3), .fade])
context[fromView]!.append(contentsOf: [.scale(0.7)])
default:
fatalError("Not implemented")
}
}
}