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
397 lines
13 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 class HeroContext {
|
|
internal var heroIDToSourceView = [String: UIView]()
|
|
internal var heroIDToDestinationView = [String: UIView]()
|
|
internal var snapshotViews = [UIView: UIView]()
|
|
internal var viewAlphas = [UIView: CGFloat]()
|
|
internal var targetStates = [UIView: HeroTargetState]()
|
|
internal var superviewToNoSnapshotSubviewMap: [UIView: [(Int, UIView)]] = [:]
|
|
internal var insertToViewFirst = false
|
|
|
|
internal var defaultCoordinateSpace: HeroCoordinateSpace = .local
|
|
|
|
internal init(container: UIView) {
|
|
self.container = container
|
|
}
|
|
|
|
internal func set(fromViews: [UIView], toViews: [UIView]) {
|
|
self.fromViews = fromViews
|
|
self.toViews = toViews
|
|
process(views: fromViews, idMap: &heroIDToSourceView)
|
|
process(views: toViews, idMap: &heroIDToDestinationView)
|
|
}
|
|
|
|
internal func process(views: [UIView], idMap: inout [String: UIView]) {
|
|
for view in views {
|
|
view.layer.removeAllHeroAnimations()
|
|
let targetState: HeroTargetState?
|
|
if let modifiers = view.hero.modifiers {
|
|
targetState = HeroTargetState(modifiers: modifiers)
|
|
} else {
|
|
targetState = nil
|
|
}
|
|
if targetState?.forceAnimate == true || container.convert(view.bounds, from: view).intersects(container.bounds) {
|
|
if let heroID = view.hero.id {
|
|
idMap[heroID] = view
|
|
}
|
|
targetStates[view] = targetState
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
The container holding all of the animating views
|
|
*/
|
|
public let container: UIView
|
|
|
|
/**
|
|
A flattened list of all views from source ViewController
|
|
*/
|
|
public var fromViews: [UIView] = []
|
|
|
|
/**
|
|
A flattened list of all views from destination ViewController
|
|
*/
|
|
public var toViews: [UIView] = []
|
|
}
|
|
|
|
// public
|
|
extension HeroContext {
|
|
|
|
/**
|
|
- Returns: a source view matching the heroID, nil if not found
|
|
*/
|
|
public func sourceView(for heroID: String) -> UIView? {
|
|
return heroIDToSourceView[heroID]
|
|
}
|
|
|
|
/**
|
|
- Returns: a destination view matching the heroID, nil if not found
|
|
*/
|
|
public func destinationView(for heroID: String) -> UIView? {
|
|
return heroIDToDestinationView[heroID]
|
|
}
|
|
|
|
/**
|
|
- Returns: a view with the same heroID, but on different view controller, nil if not found
|
|
*/
|
|
public func pairedView(for view: UIView) -> UIView? {
|
|
if let id = view.hero.id {
|
|
if sourceView(for: id) == view {
|
|
return destinationView(for: id)
|
|
} else if destinationView(for: id) == view {
|
|
return sourceView(for: id)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
/**
|
|
- Returns: a snapshot view for animation
|
|
*/
|
|
public func snapshotView(for view: UIView) -> UIView {
|
|
if let snapshot = snapshotViews[view] {
|
|
return snapshot
|
|
}
|
|
|
|
var containerView = container
|
|
let coordinateSpace = targetStates[view]?.coordinateSpace ?? defaultCoordinateSpace
|
|
switch coordinateSpace {
|
|
case .local:
|
|
containerView = view
|
|
while containerView != container, snapshotViews[containerView] == nil, let superview = containerView.superview {
|
|
containerView = superview
|
|
}
|
|
if let snapshot = snapshotViews[containerView] {
|
|
containerView = snapshot
|
|
}
|
|
|
|
if let visualEffectView = containerView as? UIVisualEffectView {
|
|
containerView = visualEffectView.contentView
|
|
}
|
|
case .global:
|
|
break
|
|
}
|
|
|
|
unhide(view: view)
|
|
|
|
// capture a snapshot without alpha, cornerRadius, or shadows
|
|
let oldCornerRadius = view.layer.cornerRadius
|
|
let oldAlpha = view.alpha
|
|
let oldShadowRadius = view.layer.shadowRadius
|
|
let oldShadowOffset = view.layer.shadowOffset
|
|
let oldShadowPath = view.layer.shadowPath
|
|
let oldShadowOpacity = view.layer.shadowOpacity
|
|
view.layer.cornerRadius = 0
|
|
view.alpha = 1
|
|
view.layer.shadowRadius = 0.0
|
|
view.layer.shadowOffset = .zero
|
|
view.layer.shadowPath = nil
|
|
view.layer.shadowOpacity = 0.0
|
|
|
|
let snapshot: UIView
|
|
let snapshotType: HeroSnapshotType = self[view]?.snapshotType ?? .optimized
|
|
|
|
switch snapshotType {
|
|
case .normal:
|
|
snapshot = view.snapshotView() ?? UIView()
|
|
case .layerRender:
|
|
snapshot = view.slowSnapshotView()
|
|
case .noSnapshot:
|
|
if view.superview != container {
|
|
if superviewToNoSnapshotSubviewMap[view.superview!] == nil {
|
|
superviewToNoSnapshotSubviewMap[view.superview!] = []
|
|
}
|
|
superviewToNoSnapshotSubviewMap[view.superview!]!.append((view.superview!.subviews.index(of: view)!, view))
|
|
}
|
|
snapshot = view
|
|
case .optimized:
|
|
#if os(tvOS)
|
|
snapshot = view.snapshotView(afterScreenUpdates: true)!
|
|
#else
|
|
if let customSnapshotView = view as? HeroCustomSnapshotView, let snapshotView = customSnapshotView.heroSnapshot {
|
|
snapshot = snapshotView
|
|
} else if #available(iOS 9.0, *), let stackView = view as? UIStackView {
|
|
snapshot = stackView.slowSnapshotView()
|
|
} else if let imageView = view as? UIImageView, view.subviews.filter({!$0.isHidden}).isEmpty {
|
|
let contentView = UIImageView(image: imageView.image)
|
|
contentView.frame = imageView.bounds
|
|
contentView.contentMode = imageView.contentMode
|
|
contentView.tintColor = imageView.tintColor
|
|
contentView.backgroundColor = imageView.backgroundColor
|
|
contentView.layer.magnificationFilter = imageView.layer.magnificationFilter
|
|
contentView.layer.minificationFilter = imageView.layer.minificationFilter
|
|
contentView.layer.minificationFilterBias = imageView.layer.minificationFilterBias
|
|
let snapShotView = UIView()
|
|
snapShotView.addSubview(contentView)
|
|
snapshot = snapShotView
|
|
} else if let barView = view as? UINavigationBar, barView.isTranslucent {
|
|
let newBarView = UINavigationBar(frame: barView.frame)
|
|
|
|
newBarView.barStyle = barView.barStyle
|
|
newBarView.tintColor = barView.tintColor
|
|
newBarView.barTintColor = barView.barTintColor
|
|
newBarView.clipsToBounds = false
|
|
|
|
// take a snapshot without the background
|
|
barView.layer.sublayers![0].opacity = 0
|
|
let realSnapshot = barView.snapshotView(afterScreenUpdates: true)!
|
|
barView.layer.sublayers![0].opacity = 1
|
|
|
|
newBarView.addSubview(realSnapshot)
|
|
snapshot = newBarView
|
|
} else if let effectView = view as? UIVisualEffectView {
|
|
snapshot = UIVisualEffectView(effect: effectView.effect)
|
|
snapshot.frame = effectView.bounds
|
|
} else {
|
|
snapshot = view.snapshotView() ?? UIView()
|
|
}
|
|
#endif
|
|
}
|
|
|
|
#if os(tvOS)
|
|
if let imageView = view as? UIImageView, imageView.adjustsImageWhenAncestorFocused {
|
|
snapshot.frame = imageView.focusedFrameGuide.layoutFrame
|
|
}
|
|
#endif
|
|
|
|
view.layer.cornerRadius = oldCornerRadius
|
|
view.alpha = oldAlpha
|
|
view.layer.shadowRadius = oldShadowRadius
|
|
view.layer.shadowOffset = oldShadowOffset
|
|
view.layer.shadowPath = oldShadowPath
|
|
view.layer.shadowOpacity = oldShadowOpacity
|
|
|
|
snapshot.layer.anchorPoint = view.layer.anchorPoint
|
|
snapshot.layer.position = containerView.convert(view.layer.position, from: view.superview!)
|
|
snapshot.layer.transform = containerView.layer.flatTransformTo(layer: view.layer)
|
|
snapshot.layer.bounds = view.layer.bounds
|
|
snapshot.hero.id = view.hero.id
|
|
|
|
if snapshotType != .noSnapshot {
|
|
if !(view is UINavigationBar), let contentView = snapshot.subviews.get(0) {
|
|
// the Snapshot's contentView must have hold the cornerRadius value,
|
|
// since the snapshot might not have maskToBounds set
|
|
contentView.layer.cornerRadius = view.layer.cornerRadius
|
|
contentView.layer.masksToBounds = true
|
|
}
|
|
|
|
snapshot.layer.allowsGroupOpacity = false
|
|
snapshot.layer.cornerRadius = view.layer.cornerRadius
|
|
snapshot.layer.zPosition = view.layer.zPosition
|
|
snapshot.layer.opacity = view.layer.opacity
|
|
snapshot.layer.isOpaque = view.layer.isOpaque
|
|
snapshot.layer.anchorPoint = view.layer.anchorPoint
|
|
snapshot.layer.masksToBounds = view.layer.masksToBounds
|
|
snapshot.layer.borderColor = view.layer.borderColor
|
|
snapshot.layer.borderWidth = view.layer.borderWidth
|
|
snapshot.layer.contentsRect = view.layer.contentsRect
|
|
snapshot.layer.contentsScale = view.layer.contentsScale
|
|
|
|
if self[view]?.displayShadow ?? true {
|
|
snapshot.layer.shadowRadius = view.layer.shadowRadius
|
|
snapshot.layer.shadowOpacity = view.layer.shadowOpacity
|
|
snapshot.layer.shadowColor = view.layer.shadowColor
|
|
snapshot.layer.shadowOffset = view.layer.shadowOffset
|
|
snapshot.layer.shadowPath = view.layer.shadowPath
|
|
}
|
|
|
|
hide(view: view)
|
|
}
|
|
|
|
if let pairedView = pairedView(for: view), let pairedSnapshot = snapshotViews[pairedView] {
|
|
let siblingViews = pairedView.superview!.subviews
|
|
let nextSiblings = siblingViews[siblingViews.index(of: pairedView)!+1..<siblingViews.count]
|
|
containerView.addSubview(pairedSnapshot)
|
|
containerView.addSubview(snapshot)
|
|
for subview in pairedView.subviews {
|
|
insertGlobalViewTree(view: subview)
|
|
}
|
|
for sibling in nextSiblings {
|
|
insertGlobalViewTree(view: sibling)
|
|
}
|
|
} else {
|
|
containerView.addSubview(snapshot)
|
|
}
|
|
containerView.addSubview(snapshot)
|
|
snapshotViews[view] = snapshot
|
|
return snapshot
|
|
}
|
|
|
|
func insertGlobalViewTree(view: UIView) {
|
|
if targetStates[view]?.coordinateSpace == .global, let snapshot = snapshotViews[view] {
|
|
container.addSubview(snapshot)
|
|
}
|
|
for subview in view.subviews {
|
|
insertGlobalViewTree(view: subview)
|
|
}
|
|
}
|
|
|
|
public subscript(view: UIView) -> HeroTargetState? {
|
|
get {
|
|
return targetStates[view]
|
|
}
|
|
set {
|
|
targetStates[view] = newValue
|
|
}
|
|
}
|
|
|
|
public func clean() {
|
|
for (superview, subviews) in superviewToNoSnapshotSubviewMap {
|
|
for (index, view) in subviews.reversed() {
|
|
superview.insertSubview(view, at: index)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// internal
|
|
extension HeroContext {
|
|
public func hide(view: UIView) {
|
|
if viewAlphas[view] == nil {
|
|
if view is UIVisualEffectView {
|
|
view.isHidden = true
|
|
viewAlphas[view] = 1
|
|
} else {
|
|
viewAlphas[view] = view.alpha
|
|
view.alpha = 0
|
|
}
|
|
}
|
|
}
|
|
public func unhide(view: UIView) {
|
|
if let oldAlpha = viewAlphas[view] {
|
|
if view is UIVisualEffectView {
|
|
view.isHidden = false
|
|
} else {
|
|
view.alpha = oldAlpha
|
|
}
|
|
viewAlphas[view] = nil
|
|
}
|
|
}
|
|
internal func unhideAll() {
|
|
for view in viewAlphas.keys {
|
|
unhide(view: view)
|
|
}
|
|
viewAlphas.removeAll()
|
|
}
|
|
internal func unhide(rootView: UIView) {
|
|
unhide(view: rootView)
|
|
for subview in rootView.subviews {
|
|
unhide(rootView: subview)
|
|
}
|
|
}
|
|
|
|
internal func removeAllSnapshots() {
|
|
for (view, snapshot) in snapshotViews {
|
|
if view != snapshot {
|
|
snapshot.removeFromSuperview()
|
|
} else {
|
|
view.layer.removeAllHeroAnimations()
|
|
}
|
|
}
|
|
}
|
|
internal func removeSnapshots(rootView: UIView) {
|
|
if let snapshot = snapshotViews[rootView] {
|
|
if rootView != snapshot {
|
|
snapshot.removeFromSuperview()
|
|
} else {
|
|
rootView.layer.removeAllHeroAnimations()
|
|
}
|
|
}
|
|
for subview in rootView.subviews {
|
|
removeSnapshots(rootView: subview)
|
|
}
|
|
}
|
|
internal func snapshots(rootView: UIView) -> [UIView] {
|
|
var snapshots = [UIView]()
|
|
for v in rootView.flattenedViewHierarchy {
|
|
if let snapshot = snapshotViews[v] {
|
|
snapshots.append(snapshot)
|
|
}
|
|
}
|
|
return snapshots
|
|
}
|
|
internal func loadViewAlpha(rootView: UIView) {
|
|
if let storedAlpha = rootView.hero.storedAlpha {
|
|
rootView.alpha = storedAlpha
|
|
rootView.hero.storedAlpha = nil
|
|
}
|
|
for subview in rootView.subviews {
|
|
loadViewAlpha(rootView: subview)
|
|
}
|
|
}
|
|
internal func storeViewAlpha(rootView: UIView) {
|
|
rootView.hero.storedAlpha = viewAlphas[rootView]
|
|
for subview in rootView.subviews {
|
|
storeViewAlpha(rootView: subview)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Allows a view to create their own custom snapshot when using **Optimized** snapshot
|
|
public protocol HeroCustomSnapshotView {
|
|
var heroSnapshot: UIView? { get }
|
|
}
|