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
}

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

(0)

相关推荐

  • IOS中Swift仿QQ最新版抽屉侧滑和弹框视图

    导读 简单用Swift写了一个抽屉效果,可以直接使用并且简单; 很多软件都运了抽屉效果,比如qq的左抽屉,英雄联盟,滴滴打车,和uber等等都运用了抽屉; 效果 iOS抽屉式结构实现分析 主要是在控制器的View上添加了两个View,一个左侧leftView和一个mainView.这里我们自定义一个DrawerViewController,init(mainVC: UIViewController, leftMenuVC: UIViewController, leftWidth: CGFloat

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

    实现使用了Modal转场动画,原因是项目多由导航控制器和标签控制器作为基类,为了不影响导航控制器的代理,转场动画使用模态交互. 代码使用SnapKit进行布局,能够适应屏幕旋转.手势速率大于300或进度超过30%的时候直接完成动画,否则动画回滚取消,具体数值可以修改对应的常量.抽屉出现的时候,主控制有遮罩,对应关键字是mask. 实现文件只有两个 DrawerControl:控制抽屉出现,一行代码即可调用 Animator:负责动画实现,包括了交互式的代理事件和非交互式的代理事件 // // D

  • IOS实战之自定义转场动画详解

    转场动画这事,说简单也简单,可以通过presentViewController:animated:completion:和dismissViewControllerAnimated:completion:这一组函数以模态视图的方式展现.隐藏视图.如果用到了navigationController,还可以调用pushViewController:animated:和popViewController这一组函数将新的视图控制器压栈.弹栈. 下图中所有转场动画都是自定义的动画,这些效果如果不用自定义动

  • vue router自动判断左右翻页转场动画效果

    前段时间做了一个移动端spa项目,技术基于 :vue + vue-router + vuex + mint-ui 因为使用了vue-cli脚手架的webpack模版,所有页面都以.vue为后缀的文件作为一个组件 最近公司项目比较少终于有空来记录一下自己对vue-router的一些小小的使用心得, 一般的移动端口单页应用在跳转页面时候会有相应的转场动画,比如: 1. 从当前一级页面跳转二级页面需要展示的转场动画是一级页面向屏幕左边移动消失的同时, 二级页面从屏幕的右边向左边移动出现.(类似翻书翻到

  • 详解IOS图层转场动画

    CAAnimation的子类,用于做转场动画,能够为层提供移出屏幕和移入屏幕的动画效果.iOS比Mac OS X的转场动画效果少一点 UINavigationController就是通过CATransition实现了将控制器的视图推入屏幕的动画效果 属性解析: type:动画过渡类型 subtype:动画过渡方向 startProgress:动画起点(在整体动画的百分比) endProgress:动画终点(在整体动画的百分比) 具体代码: /* 过渡效果 fade //交叉淡化过渡(不支持过渡方

  • iOS swift实现转场动画的方法示例

    转场动画介绍 转场动画在我们日常开发中是经常遇到的,所谓转场动画,就是一个控制器的view切到另一个控制器的view上过程中过的动画效果.本例子是实现了在导航控制器的titleView边上慢慢弹出一个控制器.下面话不多说,来一起看看详细的介绍: 效果图: 专场前 专场后 示例代码 首先自定义一个animator类.在需要转场的控制器内,设置代理 //需要设置转场动画的控制器titleViewVc.transitioningDelegate = aniamator//这里的animator是ani

  • 实例讲解iOS中的CATransition转场动画使用

    一.简介 CATransition是CAAnimation的子类,用于做转场动画 能够为图层提供移出屏幕和移入屏幕的动画效果.iOS比Mac OS X的转场动画效果少一点 如:UINavigationController导航控制器就是通过CATransition转场动画实现了将控制器的视图推入屏幕的动画效果 CATransition头文件 动画属性: type:动画过渡类型 subtype:动画过渡方向 startProgress:动画起点(在整体动画的百分比) endProgress:动画终点

  • IOS轻松几步实现自定义转场动画

    一.系统提供的转场动画 目前,系统给我们提供了push/pops和present/dismiss两种控制器之间跳转方.当然,通过设置UIModalTransitionStyle属性,可以实现下面4种modal效果,相信大家都比较熟悉了,这里就不再展示效果图. UIModalTransitionStyleCoverVertical // 从下往上, UIModalTransitionStyleFlipHorizontal // 水平翻转 UIModalTransitionStyleCrossDis

  • 详解iOS开发中的转场动画和组动画以及UIView封装动画

    一.转场动画 CAAnimation的子类,用于做转场动画,能够为层提供移出屏幕和移入屏幕的动画效果.iOS比Mac OS X的转场动画效果少一点 UINavigationController就是通过CATransition实现了将控制器的视图推入屏幕的动画效果 属性解析: type:动画过渡类型 subtype:动画过渡方向 startProgress:动画起点(在整体动画的百分比) endProgress:动画终点(在整体动画的百分比) 转场动画代码示例 1.界面搭建 2.实现代码 复制代码

  • 深入学习iOS7自定义导航转场动画

    在iOS7以前,开发者如果希望定制导航控制器推入推出视图时的转场动画,一般都只能通过子类化UINavigationController或者自己编写动画代码去覆盖相应的方法,现在iOS7为开发者带来了福音,苹果公司引入了大量新API,给予了开发者很高的自由度,在处理由UIViewController管理的UIView动画时,这些API使用方便,可扩展性也很强,定制起来非常轻松: 全新的针对UIView的动画block方法 全新的UIViewControllerAnimatedTransitioni

  • iOS实现类似格瓦拉电影的转场动画

    用过格瓦拉电影,或者其他app可能都知道,一种点击按钮用放大效果实现转场的动画现在很流行,效果大致如下 自定义转场动画 首先就要声明一个遵守UIViewControllerAnimatedTransitioning协议的类. 然后实现协议中的两个函数 // This is used for percent driven interactive transitions, as well as for container controllers that have companion animati

随机推荐