Skip to content

Instantly share code, notes, and snippets.

@ZhangHang
Created September 16, 2016 08:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ZhangHang/71a00153293f531ad8aa8591390a1e7c to your computer and use it in GitHub Desktop.
Save ZhangHang/71a00153293f531ad8aa8591390a1e7c to your computer and use it in GitHub Desktop.
DragToSwitchView
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