• 分类

  • 重置

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

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

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

    实现文件只有两个

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

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

    
    //
    // 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()
     }
     }
     
    }
    
    
    //
    // 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是标签控制器)当做主控制器传给第一个参数。 

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

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

    
    @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: 设备
    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
    }

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