Swift4.1转场动画实现侧滑抽屉效果

时间:2022-03-01 14:27:08

本文实现使用了Modal转场动画,原因是项目多由导航控制器和标签控制器作为基类,为了不影响导航控制器的代理,转场动画使用模态交互。

代码使用SnapKit进行布局,能够适应屏幕旋转。手势速率大于300或进度超过30%的时候直接完成动画,否则动画回滚取消,具体数值可以修改对应的常量。抽屉出现的时候,主控制有遮罩,对应关键字是mask。

Swift4.1转场动画实现侧滑抽屉效果

实现文件只有两个

DrawerControl:控制抽屉出现,一行代码即可调用

Animator:负责动画实现,包括了交互式的代理事件和非交互式的代理事件

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
//
// DrawerControl.swift
// PratiseSwift
//
// Created by EugeneLaw on 2018/7/31.
// Copyright © 2018年 EugeneLaw. All rights reserved.
//
 
import UIKit
 
enum DrawerSize {
 case Left
 case Right
}
 
class DrawerControl: NSObject {
 
 /**主页面*/
 var base: UIViewController?
 /**抽屉控制器*/
 var drawer: UIViewController?
 /**抽屉在左边还是右边,默认左边,没有实现右边,要右边自己去animator里面加判断*/
 var whichSize = DrawerSize.Left
 /**拖拽手势*/
 var panBase: UIPanGestureRecognizer?
 var panDrawer: UIPanGestureRecognizer?
 /**主页面在抽屉显示时保留的宽度*/
 var baseWidth: CGFloat {
 get {
 return self.animator!.baseWidth
 }
 set {
 self.animator?.baseWidth = newValue
 }
 }
 /**是否应该响应手势*/
 var shouldResponseRecognizer = false
 /**效果响应*/
 var animator: Animator?
 
 
 init(base: UIViewController, drawer: UIViewController) {
 super.init()
 self.base = base
 self.drawer = drawer
 animator = Animator(base: self.base!, drawer: self.drawer!)
 self.panBase = UIPanGestureRecognizer(target: self, action: #selector(panBaseAction(pan:)))
 base.view.addGestureRecognizer(self.panBase!)
 self.panDrawer = UIPanGestureRecognizer(target: self, action: #selector(panDrawerAction(pan:)))
 drawer.view.addGestureRecognizer(self.panDrawer!)
 self.drawer?.transitioningDelegate = self.animator
 }
 
 deinit {
 if self.panBase != nil {
 self.base?.view.removeGestureRecognizer(self.panBase!)
 self.panBase = nil
 }
 if self.panDrawer != nil {
 self.drawer?.view.removeGestureRecognizer(self.panDrawer!)
 self.panDrawer = nil
 }
 }
 
}
 
extension DrawerControl {
 
 ///显示抽屉
 func show() {
 if (self.base?.view.frame.origin.x)! > SCREEN_WIDTH/2 {
 return
 }
 self.animator?.interative = false
 self.base?.present(self.drawer!, animated: true, completion: nil)
 }
 
 ///关闭抽屉,或直接dismiss即可
 func close() {
 self.animator?.interative = false
 self.drawer?.dismiss(animated: true, completion: nil)
 }
 
}
 
extension DrawerControl {
 
 @objc func panBaseAction(pan: UIPanGestureRecognizer) {
 let transition = pan.translation(in: self.drawer?.view)
 let percentage = CGFloat(transition.x/SCREEN_WIDTH)
 let velocity = CGFloat(fabs(pan.velocity(in: self.drawer?.view).x))
 switch pan.state {
 case .began:
 if transition.x < 0 {
 shouldResponseRecognizer = false
 }else {
 shouldResponseRecognizer = true
 }
 if shouldResponseRecognizer {
 self.beginAnimator(showDrawer: true)
 }
 case .changed:
 if shouldResponseRecognizer {
 self.updateAnimator(percentage)
 }
 default:
 if shouldResponseRecognizer {
 self.cancelAnimator(percentage, velocity: velocity)
 }
 }
 }
 
 @objc func panDrawerAction(pan: UIPanGestureRecognizer) {
 let transition = pan.translation(in: self.drawer?.view)
 let percentage = CGFloat(-transition.x/SCREEN_WIDTH)
 let velocity = CGFloat(fabs(pan.velocity(in: self.drawer?.view).x))
 switch pan.state {
 case .began:
 if transition.x > 0 {
 shouldResponseRecognizer = false
 }else {
 shouldResponseRecognizer = true
 }
 if shouldResponseRecognizer {
 self.beginAnimator(showDrawer: false)
 }
 case .changed:
 if shouldResponseRecognizer {
 self.updateAnimator(percentage)
 }
 default:
 if shouldResponseRecognizer {
 self.cancelAnimator(percentage, velocity: velocity)
 }
 }
 }
 
 func beginAnimator(showDrawer: Bool) {
 self.animator?.interative = true
 if showDrawer {
 self.base?.transitioningDelegate = self.animator
 self.base?.present(self.drawer!, animated: true, completion: nil)
 }else {
 self.drawer?.transitioningDelegate = self.animator
 self.drawer?.dismiss(animated: true, completion: nil)
 }
 }
 
 func updateAnimator(_ percentage: CGFloat) {
 self.animator?.update(percentage)
 }
 
 func cancelAnimator(_ percentage: CGFloat, velocity: CGFloat) {
 if percentage < 0.3 && velocity < 300 {
 self.animator?.cancel()
 }else {
 self.animator?.finish()
 }
 }
 
}
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
//
// Animator.swift
// PratiseSwift
//
// Created by EugeneLaw on 2018/7/31.
// Copyright © 2018年 EugeneLaw. All rights reserved.
//
 
import UIKit
 
let DRAWER_ANIMATION_TIME = 0.3
 
class Animator: UIPercentDrivenInteractiveTransition, UIViewControllerTransitioningDelegate, UIViewControllerAnimatedTransitioning {
 
 /**是否交互转场*/
 var interative = false
 var showDrawer = false
 var base: UIViewController?
 var drawer:UIViewController?
 /**主页面在抽屉显示时保留的宽度*/
 var baseWidth: CGFloat = 100
 lazy var mask = { () -> UIButton in
 let mask = UIButton()
 mask.addTarget(self, action: #selector(maskClicked(_:)), for: .touchUpInside)
 return mask
 }()
 
 init(base: UIViewController, drawer: UIViewController) {
 super.init()
 self.base = base
 self.drawer = drawer
 UIDevice.current.beginGeneratingDeviceOrientationNotifications()
 NotificationCenter.default.addObserver(self, selector: #selector(observeDeviceOrientation(_:)), name: .UIDeviceOrientationDidChange, object: nil)
 }
 
 @objc func observeDeviceOrientation(_ notification: NSObject) {
 if let superView = self.base?.view.superview {
 if showDrawer {
 self.base?.view.snp.remakeConstraints({ (make) in
 make.width.equalTo(SCREEN_WIDTH)
 make.left.equalTo(superView.snp.right).offset(-self.baseWidth)
 make.top.bottom.equalTo(superView)
 })
 }else {
 self.base?.view.snp.remakeConstraints({ (make) in
 make.edges.equalTo(superView)
 })
 }
 superView.layoutIfNeeded()
 }
 }
 
 deinit {
 NotificationCenter.default.removeObserver(self)
 }
 
}
 
extension Animator {
 
 func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
 if showDrawer {
 let fromView = transitionContext.view(forKey: .from)
 addShadowToView(fromView!, color: .black, offset: CGSize(width: -1, height: 0), radius: 3, opacity: 0.1)
 let toView = transitionContext.view(forKey: .to)
 let containerView = transitionContext.containerView
 containerView.addSubview(toView!)
 containerView.addSubview(fromView!)
 fromView?.snp.remakeConstraints({ (make) in
 make.edges.equalTo(containerView)
 })
 toView?.snp.remakeConstraints({ (make) in
 make.edges.equalTo(containerView)
 })
 containerView.layoutIfNeeded()
 UIView.animate(withDuration: DRAWER_ANIMATION_TIME, animations: {
 fromView?.snp.remakeConstraints({ (make) in
 make.left.equalTo((toView?.snp.right)!).offset(-self.baseWidth)
 make.width.top.bottom.equalTo(toView!)
 })
 containerView.layoutIfNeeded()
 }) { (finish) in
 let cancel = transitionContext.transitionWasCancelled
 transitionContext.completeTransition(!cancel)
 if !cancel {//取消状态下区分添加到哪一个父视图,弄错会导致黑屏
 if self.drawer?.view.superview != nil {
 self.drawer?.view?.snp.remakeConstraints({ (make) in
 make.edges.equalTo((self.drawer?.view?.superview)!)
 })
 }
 self.showPartOfView()
 }else {
 fromView?.snp.remakeConstraints({ (make) in
 make.edges.equalTo((fromView?.superview)!)
 })
 }
 }
 }else {
 let fromView = transitionContext.view(forKey: .from)
 let toView = transitionContext.view(forKey: .to)
 addShadowToView(toView!, color: .black, offset: CGSize(width: -1, height: 0), radius: 3, opacity: 0.1)
 let containerView = transitionContext.containerView
 containerView.addSubview(fromView!)
 containerView.addSubview(toView!)
 fromView?.snp.remakeConstraints({ (make) in
 make.edges.equalTo(containerView)
 })
 toView?.snp.remakeConstraints({ (make) in
 make.left.equalTo(containerView.snp.right).offset(-self.baseWidth)
 make.width.equalTo(SCREEN_WIDTH)
 make.height.equalTo(SCREEN_HEIGHT)
 make.top.bottom.equalTo(containerView)
 })
 containerView.layoutIfNeeded()
 UIView.animate(withDuration: DRAWER_ANIMATION_TIME, animations: {
 toView?.snp.remakeConstraints({ (make) in
 make.edges.equalTo(containerView)
 })
 containerView.layoutIfNeeded()
 }) { (finish) in
 let cancel = transitionContext.transitionWasCancelled
 transitionContext.completeTransition(!cancel)
 toView?.snp.remakeConstraints({ (make) in
 make.edges.equalTo((toView?.superview)!)
 })
 if minX((self.base?.view)!) <= 0 {//判断结束时候是否回到主视图
 self.base?.view.isUserInteractionEnabled = true
 }
 }
 }
 }
 
 func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
 return DRAWER_ANIMATION_TIME
 }
 
 override func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning) {
 super.startInteractiveTransition(transitionContext)
 }
 
 func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
 self.showDrawer = true
 return self
 }
 
 func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
 self.showDrawer = false
 return self
 }
 
 func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
 if interative {
 return self
 }else {
 return nil
 }
 }
 
 func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
 if interative {
 return self
 }else {
 return nil
 }
 }
 
}
 
 
extension Animator {
 
 func showPartOfView() {
 self.drawer?.view.addSubview((self.base?.view)!)
 self.base?.view.snp.remakeConstraints({ (make) in
 make.left.equalTo((self.drawer?.view.snp.right)!).offset(-self.baseWidth)
 make.top.bottom.equalTo((self.drawer?.view)!)
 make.width.equalTo(SCREEN_WIDTH)
 })
 //遮罩
 self.drawer?.view.insertSubview(mask, aboveSubview: (self.base?.view)!)
 self.base?.view.isUserInteractionEnabled = false//阻止交互
 mask.snp.remakeConstraints { (make) in
 make.left.equalTo((mask.superview?.snp.right)!).offset(-baseWidth)
 make.top.width.bottom.equalTo(mask.superview!);
 }
 self.drawer?.view.superview?.layoutIfNeeded()
 }
 
 @objc func maskClicked(_ button: UIButton) {
 button.removeFromSuperview()
 self.drawer?.dismiss(animated: true, completion: nil)
 }
 
}

按钮调用例子:(手势控制已经自动添加到主控制器和抽屉控制器的view上)

创建推出抽屉的控制类,参数分别是主控制器和抽屉控制器。在我自己的练习工程中,把这个控制类定义为总控制器(包括了导航控制器和标签控制器的控制类)的一个属性。创建这个抽屉控制类的时候,我把导航控制器(它的root是标签控制器)当做主控制器传给第一个参数。 

?
1
self.drawer = DrawerControl(base: self.navigation!, drawer: self.drawerPage)

调用的时候只需要使用抽屉控制类的show方法即可,练习工程中我把该按钮封装在导航菜单里面,它响应的时候会调用总控制器的单例,调用单例记录的抽屉控制器属性。

?
1
2
3
@objc func btnMenuClicked(_ button: UIButton) {
 TotalControl.instance().drawer?.show()
}

附录:用到的一些变量

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
//
// Headers.swift
// PratiseSwift
//
// Created by EugeneLaw on 2018/7/23.
// Copyright © 2018年 EugeneLaw. All rights reserved.
//
 
import UIKit
 
//MARK: 设备
let isRetina = (UIScreen.instancesRespond(to: #selector(getter: UIScreen.currentMode)) ? __CGSizeEqualToSize(CGSize(width: 640, height: 960), (UIScreen.main.currentMode?.size)!) : false)
let iPhone5 = (UIScreen.instancesRespond(to: #selector(getter: UIScreen.currentMode)) ? __CGSizeEqualToSize(CGSize(width: 640, height: 1136), (UIScreen.main.currentMode?.size)!) : false)
let iPhone6 = (UIScreen.instancesRespond(to: #selector(getter: UIScreen.currentMode)) ? __CGSizeEqualToSize(CGSize(width: 750, height: 1334), (UIScreen.main.currentMode?.size)!) : false)
let iPhone6Plus = (UIScreen.instancesRespond(to: #selector(getter: UIScreen.currentMode)) ? __CGSizeEqualToSize(CGSize(width: 1242, height: 2208), (UIScreen.main.currentMode?.size)!) : false)
let isPad = (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiom.pad)
let isPhone = (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiom.phone)
let isiPhoneX = (UIScreen.instancesRespond(to: #selector(getter: UIScreen.currentMode)) ? __CGSizeEqualToSize(CGSize(width: 1125, height: 2436), (UIScreen.main.currentMode?.size)!) : false)
 
//MARK: 界面
let TABBAR_HEIGHT = (isiPhoneX ? 83 : 49)
let NAVIGATION_HEIGHT = (isiPhoneX ? 88 : 64)
var SCREEN_WIDTH: CGFloat {
 get {
 return SCREEN_WIDTH_FUNC()
 }
}
var SCREEN_HEIGHT: CGFloat {
 get {
 return SCREEN_HEIGHT_FUNC()
 }
}
 
func SCREEN_WIDTH_FUNC() -> CGFloat {
 return UIScreen.main.bounds.size.width
}
 
func SCREEN_HEIGHT_FUNC() -> CGFloat {
 return UIScreen.main.bounds.size.height
}
 
//MARK: 颜色
let COLOR_WHITESMOKE = ColorHex("#F5F5F5")
 
/**
 *十六进制颜色值转换成UIColor
 *@param "#000000"
 */
func ColorHex(_ color: String) -> UIColor? {
 if color.count <= 0 || color.count != 7 || color == "(null)" || color == "<null>" {
 return nil
 }
 var red: UInt32 = 0x0
 var green: UInt32 = 0x0
 var blue: UInt32 = 0x0
 let redString = String(color[color.index(color.startIndex, offsetBy: 1)...color.index(color.startIndex, offsetBy: 2)])
 let greenString = String(color[color.index(color.startIndex, offsetBy: 3)...color.index(color.startIndex, offsetBy: 4)])
 let blueString = String(color[color.index(color.startIndex, offsetBy: 5)...color.index(color.startIndex, offsetBy: 6)])
 Scanner(string: redString).scanHexInt32(&red)
 Scanner(string: greenString).scanHexInt32(&green)
 Scanner(string: blueString).scanHexInt32(&blue)
 let hexColor = UIColor.init(red: CGFloat(red)/255.0, green: CGFloat(green)/255.0, blue: CGFloat(blue)/255.0, alpha: 1)
 return hexColor
}
 
/**
 *给图层添加阴影
 */
func addShadowToView(_ view: UIView, color: UIColor, offset: CGSize, radius: CGFloat, opacity: Float) {
 view.layer.shadowColor = color.cgColor
 view.layer.shadowOffset = offset
 view.layer.shadowOpacity = opacity
 view.layer.shadowRadius = radius
}
 
/**
 *计算图层的宽度
 */
func width(_ object: UIView) -> CGFloat {
 return object.frame.width
}
 
/**
 *在父视图中的x坐标
 */
func minX(_ object: UIView) -> CGFloat {
 return object.frame.origin.x
}
 
/**
 *在父视图中的x坐标+自身宽度
 */
func maxX(_ object: UIView) -> CGFloat {
 return object.frame.origin.x+width(object)
}
 
/**
 *在父视图中的y坐标
 */
func minY(_ object: UIView) -> CGFloat {
 return object.frame.origin.y
}
 
/**
 *在父视图中的y坐标+自身高度
 */
func maxY(_ object: UIView) -> CGFloat {
 return object.frame.origin.y+height(object)
}
 
/**
 *计算图层的高度
 */
func height(_ object: UIView) -> CGFloat {
 return object.frame.height
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。

原文链接:https://blog.csdn.net/weixin_40287666/article/details/81329332