Swift4.1轉(zhuǎn)場動畫實現(xiàn)側(cè)滑抽屜效果
本文實現(xiàn)使用了Modal轉(zhuǎn)場動畫,原因是項目多由導航控制器和標簽控制器作為基類,為了不影響導航控制器的代理,轉(zhuǎn)場動畫使用模態(tài)交互。
代碼使用SnapKit進行布局,能夠適應(yīng)屏幕旋轉(zhuǎn)。手勢速率大于300或進度超過30%的時候直接完成動畫,否則動畫回滾取消,具體數(shù)值可以修改對應(yīng)的常量。抽屜出現(xiàn)的時候,主控制有遮罩,對應(yīng)關(guān)鍵字是mask。
實現(xiàn)文件只有兩個
DrawerControl:控制抽屜出現(xiàn),一行代碼即可調(diào)用
Animator:負責動畫實現(xiàn),包括了交互式的代理事件和非交互式的代理事件
// // 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? /**抽屜在左邊還是右邊,默認左邊,沒有實現(xiàn)右邊,要右邊自己去animator里面加判斷*/ var whichSize = DrawerSize.Left /**拖拽手勢*/ var panBase: UIPanGestureRecognizer? var panDrawer: UIPanGestureRecognizer? /**主頁面在抽屜顯示時保留的寬度*/ var baseWidth: CGFloat { get { return self.animator!.baseWidth } set { self.animator?.baseWidth = newValue } } /**是否應(yīng)該響應(yīng)手勢*/ var shouldResponseRecognizer = false /**效果響應(yīng)*/ 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) } ///關(guān)閉抽屜,或直接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() } } }
// // 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 { /**是否交互轉(zhuǎn)場*/ 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 {//取消狀態(tài)下區(qū)分添加到哪一個父視圖,弄錯會導致黑屏 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 {//判斷結(jié)束時候是否回到主視圖 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) } }
按鈕調(diào)用例子:(手勢控制已經(jīng)自動添加到主控制器和抽屜控制器的view上)
創(chuàng)建推出抽屜的控制類,參數(shù)分別是主控制器和抽屜控制器。在我自己的練習工程中,把這個控制類定義為總控制器(包括了導航控制器和標簽控制器的控制類)的一個屬性。創(chuàng)建這個抽屜控制類的時候,我把導航控制器(它的root是標簽控制器)當做主控制器傳給第一個參數(shù)。
self.drawer = DrawerControl(base: self.navigation!, drawer: self.drawerPage)
調(diào)用的時候只需要使用抽屜控制類的show方法即可,練習工程中我把該按鈕封裝在導航菜單里面,它響應(yīng)的時候會調(diào)用總控制器的單例,調(diào)用單例記錄的抽屜控制器屬性。
@objc func btnMenuClicked(_ button: UIButton) { TotalControl.instance().drawer?.show() }
附錄:用到的一些變量
// // Headers.swift // PratiseSwift // // Created by EugeneLaw on 2018/7/23. // Copyright © 2018年 EugeneLaw. All rights reserved. // import UIKit //MARK: 設(shè)備 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") /** *十六進制顏色值轉(zhuǎn)換成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 }
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持我們。
欄 目:Swift
本文標題:Swift4.1轉(zhuǎn)場動畫實現(xiàn)側(cè)滑抽屜效果
本文地址:http://mengdiqiu.com.cn/a1/Swift/11927.html
您可能感興趣的文章


閱讀排行
本欄相關(guān)
- 01-11Swift利用Decodable解析JSON的一個小問題
- 01-11swift中defer幾個簡單的使用場景詳解
- 01-11Swift中初始化init的方法小結(jié)
- 01-11Swift中defer關(guān)鍵字推遲執(zhí)行示例詳解
- 01-11Swift利用純代碼實現(xiàn)時鐘效果實例代碼
- 01-11Swift中定義單例的方法實例
- 01-11Swift中排序算法的簡單取舍詳解
- 01-11Swift Json實例詳細解析
- 01-11Swift如何為設(shè)置中心添加常用功能
- 01-11Swift利用指紋識別或面部識別為應(yīng)用添
隨機閱讀
- 01-10delphi制作wav文件的方法
- 08-05織夢dedecms什么時候用欄目交叉功能?
- 08-05dedecms(織夢)副欄目數(shù)量限制代碼修改
- 04-02jquery與jsp,用jquery
- 01-10使用C語言求解撲克牌的順子及n個骰子
- 01-11Mac OSX 打開原生自帶讀寫NTFS功能(圖文
- 01-10SublimeText編譯C開發(fā)環(huán)境設(shè)置
- 01-10C#中split用法實例總結(jié)
- 08-05DEDE織夢data目錄下的sessions文件夾有什
- 01-11ajax實現(xiàn)頁面的局部加載