当前位置: 首页 > 编程笔记 >

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

马国源
2023-03-14
本文向大家介绍Swift4.1转场动画实现侧滑抽屉效果,包括了Swift4.1转场动画实现侧滑抽屉效果的使用技巧和注意事项,需要的朋友参考一下

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

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

实现文件只有两个

DrawerControl:控制抽屉出现,一行html" target="_blank">代码即可调用

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
}

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

 类似资料:
  • 本文向大家介绍Android实现3种侧滑效果(仿qq侧滑、抽屉侧滑、普通侧滑),包括了Android实现3种侧滑效果(仿qq侧滑、抽屉侧滑、普通侧滑)的使用技巧和注意事项,需要的朋友参考一下 自己实现了一下侧滑的三种方式(注释都写代码里了) 本文Demo下载地址:Andriod侧滑 本文实现所需框架:nineoldandroids下载地址:nineoldandroids 1.普通侧滑: 主要是基于

  • 本文向大家介绍IOS实现点击滑动抽屉效果,包括了IOS实现点击滑动抽屉效果的使用技巧和注意事项,需要的朋友参考一下 最近,看到好多Android上的抽屉效果,也忍不住想要自己写一个。在Android里面可以用SlidingDrawer,很方便的实现。IOS上面就只有自己写了。其实原理很简单就是 UIView 的移动,和一些手势的操作。 效果图: 以上就是本文的全部内容,希望对大家的学习有所帮助。

  • 问题内容: 我是新手,并且使用Java swing设计接口。我希望抽屉在单击按钮时以滑动动画拉出。首先,是否可以这样做,如果可以,我该怎么做。谢谢。对于某些特定的方法信息,我将不胜感激。 问题答案: 根据要实现的目标,您可以采用多种可能的方法。 基本方法是简单地绘制图形和摆动 这样您就可以简单地更新一个变量,该变量将作为绘制大小的基础,例如… 这真的很基础,并且没有考虑到概念变慢/变慢等问题。对于

  • 本文向大家介绍Android实现自定义滑动式抽屉菜单效果,包括了Android实现自定义滑动式抽屉菜单效果的使用技巧和注意事项,需要的朋友参考一下 在Andoird使用Android自带的那些组件,像SlidingDrawer和DrawerLayout都是抽屉效果的菜单,但是在项目很多要实现的功能都收到Android这些自带组件的限制,导致很难完成项目的需求,自定义的组件,各方面都在自己的控制之下

  • 本文向大家介绍使用DrawerLayout组件实现侧滑抽屉的功能,包括了使用DrawerLayout组件实现侧滑抽屉的功能的使用技巧和注意事项,需要的朋友参考一下 DrawerLayout组件同样是V4包中的组件,也是直接继承于ViewGroup类,所以这个类也是一个容器类。使用DrawerLayout可以轻松的实现抽屉效果,使用DrawerLayout的步骤有以下几点: 1)在DrawerLay

  • 就像Android开发者的页面上说的 用户可以通过从屏幕左边缘滑动或通过触摸操作栏上的应用图标将导航抽屉带到屏幕上。 但奇怪的是,我的活动上的导航抽屉对滑动动作没有反应。它只在触摸操作栏上的图标时切换。下面是我对导航抽屉的实现 对此有什么可能的解释吗?我怀疑的是我的活动默认有它的一个片段的布局。所以这是原因吗? 编辑:我的活动的布局文件