Created
September 16, 2016 08:30
-
-
Save ZhangHang/71a00153293f531ad8aa8591390a1e7c to your computer and use it in GitHub Desktop.
DragToSwitchView
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
public protocol DragToSwitchViewDelegate: class { | |
func dragToSwitchViewWillStartDragging(dragToSwitchView: DragToSwitchView) | |
func dragToSwitchViewDidEndDragging(dragToSwitchView: DragToSwitchView) | |
func dragToSwitchView( | |
dragToSwitchView: DragToSwitchView, | |
performSwitchingAnimationWithDuration duration: NSTimeInterval) | |
func dragToSwitchView(dragToSwitchView: DragToSwitchView, willSwitchToView view: UIView) | |
func dragToSwitchView(dragToSwitchView: DragToSwitchView, didSwitchToView view: UIView) | |
} | |
extension DragToSwitchViewDelegate { | |
func dragToSwitchViewWillStartDragging(dragToSwitchView: DragToSwitchView) {} | |
func dragToSwitchViewDidEndDragging(dragToSwitchView: DragToSwitchView) {} | |
func dragToSwitchView( | |
dragToSwitchView: DragToSwitchView, | |
performSwitchingAnimationWithDuration duration: NSTimeInterval) {} | |
func dragToSwitchView(dragToSwitchView: DragToSwitchView, willSwitchToView view: UIView) {} | |
func dragToSwitchView(dragToSwitchView: DragToSwitchView, didSwitchToView view: UIView) {} | |
} | |
public class DragToSwitchView: UIControl { | |
public weak var delegate: DragToSwitchViewDelegate? | |
/// 触发两子视图切换的最小距离 | |
public var maximumThresholdDistance: CGFloat = 40 | |
/// 两个子视图之间切换所需要的动画时间 | |
public var transitionDuration: NSTimeInterval = 0.3 | |
override public init(frame: CGRect) { | |
super.init(frame: frame) | |
clipsToBounds = true | |
configureForGestureRecognizer() | |
} | |
required public init?(coder aDecoder: NSCoder) { | |
super.init(coder: aDecoder) | |
clipsToBounds = true | |
configureForGestureRecognizer() | |
} | |
/** | |
配置需要切换的子视图 | |
DragToSwitchView 会自动设置子视图的横向约束(充满),每个子视图必须自行设置纵向约束 | |
- parameter topHiddenView: 默认被隐藏,放置于上方的子视图 | |
- parameter bottomVisibleView: 默认被显示,放置于下方的子视图 | |
*/ | |
public func setup(topHiddenView topHiddenView: UIView, bottomVisibleView: UIView) { | |
self.topView?.removeFromSuperview() | |
self.bottomView?.removeFromSuperview() | |
addSubview(topHiddenView) | |
addSubview(bottomVisibleView) | |
self.topView = topHiddenView | |
self.bottomView = bottomVisibleView | |
addConstraints( | |
[ | |
NSLayoutConstraint | |
.constraintsWithVisualFormat("H:|[view]|", | |
options: .DirectionLeadingToTrailing, | |
metrics: nil, | |
views: ["view": topHiddenView]), | |
NSLayoutConstraint | |
.constraintsWithVisualFormat("H:|[view]|", | |
options: .DirectionLeadingToTrailing, | |
metrics: nil, | |
views: ["view": bottomVisibleView]) | |
] | |
.flatten() | |
.map { $0 } | |
) | |
transitionStatus = .TopViewAboveBottomView(topView: topHiddenView, bottomView: bottomVisibleView) | |
bringSubviewToFront(topHiddenView) | |
invalidateIntrinsicContentSize() | |
superview?.layoutIfNeeded() | |
} | |
// MARK: Transition | |
private weak var topView: UIView? | |
private weak var bottomView: UIView? | |
private var topViewBottomConstraint: NSLayoutConstraint? | |
private var bottomViewBotomConstraint: NSLayoutConstraint? | |
private var viewHeightConstraint: NSLayoutConstraint? | |
/** | |
切换状态 | |
- None: 空,未配置子视图 | |
- TopViewAboveBottomView: TopView(不可见) 在 BottomView(可见) 之上 | |
- TopViewCoverBottomView: TopView(可见) 完整覆盖于 BottomView(不可见)之上 | |
- PrepareForSwapping: 准备进行子视图交换动画 | |
- TopViewOverBottomView: TopView 被下拉(部分可见)覆盖了部分 BottomView(部分可见) | |
*/ | |
private enum TransitionStatus { | |
case None | |
case TopViewAboveBottomView(topView: UIView, bottomView: UIView) | |
case TopViewCoverBottomView(topView: UIView, bottomView: UIView) | |
case PrepareForSwapping | |
case TopViewOverBottomView(yAxisDistance: CGFloat) | |
} | |
private var transitionStatus: TransitionStatus = .None { | |
didSet { | |
switch transitionStatus { | |
case .TopViewAboveBottomView(let topView, let bottomView): | |
if | |
let bottomViewBottomMargin = bottomViewBotomConstraint, | |
let topViewBottomMargin = topViewBottomConstraint, | |
let viewHeight = viewHeightConstraint { | |
removeConstraints([bottomViewBottomMargin, viewHeight, topViewBottomMargin]) | |
} | |
topViewBottomConstraint = NSLayoutConstraint( | |
item: topView, | |
attribute: .Bottom, | |
relatedBy: .Equal, | |
toItem: self, | |
attribute: .Top, | |
multiplier: 1, | |
constant: 0) | |
viewHeightConstraint = NSLayoutConstraint( | |
item: self, | |
attribute: .Height, | |
relatedBy: .Equal, | |
toItem: bottomView, | |
attribute: .Height, | |
multiplier: 1, | |
constant: 0) | |
bottomViewBotomConstraint = NSLayoutConstraint( | |
item: bottomView, | |
attribute: .Bottom, | |
relatedBy: .Equal, | |
toItem: self, | |
attribute: .Bottom, | |
multiplier: 1, | |
constant: 0) | |
addConstraints( | |
[ | |
topViewBottomConstraint!, | |
viewHeightConstraint!, | |
bottomViewBotomConstraint!, | |
topViewBottomConstraint! | |
] | |
) | |
case .TopViewCoverBottomView(let topView, _): | |
if let constraint = topViewBottomConstraint { | |
removeConstraint(constraint) | |
} | |
if let constraint = viewHeightConstraint { | |
removeConstraint(constraint) | |
} | |
viewHeightConstraint = NSLayoutConstraint( | |
item: self, | |
attribute: .Height, | |
relatedBy: .Equal, | |
toItem: topView, | |
attribute: .Height, | |
multiplier: 1, | |
constant: 0) | |
topViewBottomConstraint = NSLayoutConstraint( | |
item: topView, | |
attribute: .Bottom, | |
relatedBy: .Equal, | |
toItem: self, | |
attribute: .Bottom, | |
multiplier: 1, | |
constant: 0) | |
addConstraints( | |
[ | |
viewHeightConstraint!, | |
topViewBottomConstraint! | |
] | |
) | |
case .TopViewOverBottomView(let yAxisDistance): | |
viewHeightConstraint?.constant = yAxisDistance | |
topViewBottomConstraint?.constant = yAxisDistance * 1.1 | |
default: | |
break | |
} | |
} | |
} | |
// MARK: | |
private var verticalPanGestureRecognizer: UIPanGestureRecognizer! | |
private var panStartPoint: CGPoint? | |
private func configureForGestureRecognizer() { | |
verticalPanGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.dynamicType.handleVerticalPan(_:))) | |
verticalPanGestureRecognizer.delegate = self | |
addGestureRecognizer(verticalPanGestureRecognizer) | |
} | |
} | |
// MARK: Geusture method | |
extension DragToSwitchView { | |
@objc | |
private func handleVerticalPan(sender: UIPanGestureRecognizer) { | |
guard | |
let topView = topView, | |
let bottomView = bottomView else { | |
return | |
} | |
switch sender.state { | |
case .Began: | |
handlePanBegan(sender) | |
case .Changed: | |
handlePanChanged(sender, currentTopView: topView, currentBottomView: bottomView) | |
case .Ended, .Cancelled, .Failed: | |
handlePanEnded(sender, currentTopView: topView, currentBottomView: bottomView) | |
case .Possible: | |
break | |
} | |
} | |
private func handlePanBegan(sender: UIPanGestureRecognizer) { | |
delegate?.dragToSwitchViewWillStartDragging(self) | |
panStartPoint = sender.locationInView(self) | |
} | |
private func handlePanChanged( | |
sender: UIPanGestureRecognizer, | |
currentTopView: UIView, | |
currentBottomView: UIView) { | |
let yAxisDistance: CGFloat = { | |
guard let startPoint = panStartPoint else { | |
fatalError() | |
} | |
let currentPoint = sender.locationInView(self) | |
return currentPoint.y - startPoint.y | |
}() | |
if yAxisDistance < 0 { | |
return | |
} | |
if yAxisDistance < maximumThresholdDistance { | |
transitionStatus = .TopViewOverBottomView(yAxisDistance: yAxisDistance) | |
invalidateIntrinsicContentSize() | |
superview?.layoutIfNeeded() | |
return | |
} | |
// 准备进行切换动画,禁用手势识别 | |
transitionStatus = .PrepareForSwapping | |
sender.enabled = false | |
swapTopAndBottomViews( | |
currentTopView, | |
currentBottomView: currentBottomView) { () -> Void in | |
sender.enabled = true | |
} | |
} | |
private func handlePanEnded( | |
sender: UIPanGestureRecognizer, | |
currentTopView: UIView, | |
currentBottomView: UIView) { | |
delegate?.dragToSwitchViewDidEndDragging(self) | |
panStartPoint = nil | |
switch transitionStatus { | |
case .TopViewOverBottomView: | |
sender.enabled = false | |
transitionStatus = .TopViewAboveBottomView( | |
topView: currentTopView, | |
bottomView: currentBottomView) | |
UIView.animateWithDuration(transitionDuration, | |
animations: { () -> Void in | |
self.superview?.layoutIfNeeded() | |
}, completion: { (_) -> Void in | |
sender.enabled = true | |
}) | |
default: | |
return | |
} | |
} | |
private func swapTopAndBottomViews( | |
currentTopView: UIView, | |
currentBottomView: UIView, | |
complitionHandler: () -> Void) { | |
let animationDuration = transitionDuration | |
delegate?.dragToSwitchView(self, performSwitchingAnimationWithDuration: animationDuration) | |
UIView.animateWithDuration( | |
animationDuration, | |
delay: 0, | |
options: .CurveEaseIn, | |
animations: { () -> Void in | |
// 首先将 TopView 降到底部,覆盖住 BottomView | |
self.transitionStatus = .TopViewCoverBottomView( | |
topView: currentTopView, | |
bottomView: currentBottomView) | |
self.invalidateIntrinsicContentSize() | |
self.superview?.layoutIfNeeded() | |
self.delegate?.dragToSwitchView(self, willSwitchToView: currentTopView) | |
}) { (_) -> Void in | |
// 然后将 BottomView 升到 TopView 头部,并置换两者,至此完成切换 | |
(self.topView, self.bottomView) = (self.bottomView, self.topView) | |
self.transitionStatus = .TopViewAboveBottomView( | |
topView: self.topView!, | |
bottomView: self.bottomView!) | |
self.invalidateIntrinsicContentSize() | |
self.superview?.layoutIfNeeded() | |
self.delegate?.dragToSwitchView(self, didSwitchToView: currentTopView) | |
self.bringSubviewToFront(self.topView!) | |
complitionHandler() | |
} | |
} | |
} | |
extension DragToSwitchView: UIGestureRecognizerDelegate { | |
public override func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool { | |
if gestureRecognizer == verticalPanGestureRecognizer { | |
let velocity = verticalPanGestureRecognizer.velocityInView(self) | |
return fabs(velocity.y) > fabs(velocity.x) | |
} | |
return true | |
} | |
public func gestureRecognizer( | |
gestureRecognizer: UIGestureRecognizer, | |
shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) | |
-> Bool { | |
return true | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment