当前位置: > > > Swift - 侧滑菜单的实现(样例2:仿QQ,菜单带缩放效果)

Swift - 侧滑菜单的实现(样例2:仿QQ,菜单带缩放效果)

前面我写了一篇文章介绍如何实现侧滑菜单:Swift - 侧滑菜单的实现(样例1:主页向右滑动,露出下方菜单页)
其实现方式是,通过手势拖动主页面移动,从而露出下面的菜单页(其实后面的菜单页是固定不动的)。

下面演示另一种样式的实现(模仿手机QQ的侧滑菜单),主页面滑动停靠的过程中会逐渐缩小,同时菜单页也会逐渐移动放大,浮现出来。
(注:本文样例是基于前面文章的demo修改的,如果没阅读前文的话可以先去看下。为便于理解,下面将效果分两步实现。)

1,主页停靠侧边时尺寸逐渐缩小
(1)定义了新属性 minProportion,表示停靠时的缩小比例。在滑动时,再根据页面的位置实时计算出当前的缩放比例。
(2)在主页面与菜单页之间添加了个黑色遮罩层(blackCover), 初始化时是不透明的。随着菜单的展开透明度逐渐变为0。这样侧滑菜单有逐渐显示出来的效果。
      

ViewController.swift 代码如下(高亮处为修改过的地方):
import UIKit

class ViewController: UIViewController {
    // 主页导航控制器
    var mainNavigationController:UINavigationController!
    
    // 主页面控制器
    var mainViewController:MainViewController!
    
    // 菜单页控制器
    var menuViewController:MenuViewController?
    
    // 菜单页当前状态
    var currentState = MenuState.Collapsed {
        didSet {
            //菜单展开的时候,给主页面边缘添加阴影
            let shouldShowShadow = currentState != .Collapsed
            showShadowForMainViewController(shouldShowShadow)
        }
    }
    
    // 菜单打开后主页在屏幕右侧露出部分的宽度
    let menuViewExpandedOffset: CGFloat = 60
    
    // 侧滑菜单黑色半透明遮罩层
    var blackCover: UIView?
    
    // 最小缩放比例
    let minProportion: CGFloat = 0.77
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        //状态栏文字改成白色
        UIApplication.sharedApplication().statusBarStyle = .LightContent;
        
        // 给根容器设置背景
        let imageView = UIImageView(image: UIImage(named: "back"))
        imageView.frame = UIScreen.mainScreen().bounds
        self.view.addSubview(imageView)
        
        //初始化主视图
        mainNavigationController = UIStoryboard(name: "Main", bundle: nil)
            .instantiateViewControllerWithIdentifier("mainNavigaiton")
            as! UINavigationController
        view.addSubview(mainNavigationController.view)
        
        //指定Navigation Bar左侧按钮的事件
        mainViewController = mainNavigationController.viewControllers.first
            as! MainViewController
        mainViewController.navigationItem.leftBarButtonItem?.action = Selector("showMenu")
        
        //添加拖动手势
        let panGestureRecognizer = UIPanGestureRecognizer(target: self,
            action: "handlePanGesture:")
        mainNavigationController.view.addGestureRecognizer(panGestureRecognizer)
        
        //单击收起菜单手势
        let tapGestureRecognizer = UITapGestureRecognizer(target: self,
            action: "handlePanGesture")
        mainNavigationController.view.addGestureRecognizer(tapGestureRecognizer)
    }
    
    //导航栏左侧按钮事件响应
    func showMenu() {
        //如果菜单是展开的则会收起,否则就展开
        if currentState == .Expanded {
            animateMainView(false)
        }else {
            addMenuViewController()
            animateMainView(true)
        }
    }
    
    //拖动手势响应
    func handlePanGesture(recognizer: UIPanGestureRecognizer) {
        
        switch(recognizer.state) {
            // 刚刚开始滑动
        case .Began:
            // 判断拖动方向
            let dragFromLeftToRight = (recognizer.velocityInView(view).x > 0)
            // 如果刚刚开始滑动的时候还处于主页面,从左向右滑动加入侧面菜单
            if (currentState == .Collapsed && dragFromLeftToRight) {
                currentState = .Expanding
                addMenuViewController()
            }
            
            // 如果是正在滑动,则偏移主视图的坐标实现跟随手指位置移动
        case .Changed:
            let screenWidth = view.bounds.size.width
            var centerX = recognizer.view!.center.x +
                recognizer.translationInView(view).x
            //页面滑到最左侧的话就不许要继续往左移动
            if centerX < screenWidth/2 { centerX = screenWidth/2 }
            
            // 计算缩放比例
            var proportion:CGFloat = (centerX - screenWidth/2) /
                (view.bounds.size.width - menuViewExpandedOffset)
            proportion = 1 - (1 - minProportion) * proportion
        
            // 执行视差特效
            blackCover?.alpha = (proportion - minProportion) / (1 - minProportion)
            
            //主页面滑到最左侧的话就不许要继续往左移动
            recognizer.view!.center.x = centerX
            recognizer.setTranslation(CGPointZero, inView: view)
            //缩放主页面
            recognizer.view!.transform =
                CGAffineTransformScale(CGAffineTransformIdentity, proportion, proportion)
            
            // 如果滑动结束
        case .Ended:
            //根据页面滑动是否过半,判断后面是自动展开还是收缩
            let hasMovedhanHalfway = recognizer.view!.center.x > view.bounds.size.width
            animateMainView(hasMovedhanHalfway)
        default:
            break
        }
    }
    
    //单击手势响应
    func handlePanGesture() {
        //如果菜单是展开的点击主页部分则会收起
        if currentState == .Expanded {
            animateMainView(false)
        }
    }
    
    // 添加菜单页
    func addMenuViewController() {
        if (menuViewController == nil) {
            menuViewController = UIStoryboard(name: "Main", bundle: nil)
                .instantiateViewControllerWithIdentifier("menuView") as? MenuViewController
            
            // 插入当前视图并置顶
            view.insertSubview(menuViewController!.view,
                belowSubview: mainNavigationController.view)
            
            // 建立父子关系
            addChildViewController(menuViewController!)
            menuViewController!.didMoveToParentViewController(self)
            
            // 在侧滑菜单之上增加黑色遮罩层,目的是实现视差特效
            blackCover = UIView(frame: CGRectOffset(self.view.frame, 0, 0))
            blackCover!.backgroundColor = UIColor.blackColor()
            self.view.insertSubview(blackCover!,
                belowSubview: mainNavigationController.view)
        }
    }
    
    //主页自动展开、收起动画
    func animateMainView(shouldExpand: Bool) {
        // 如果是用来展开
        if (shouldExpand) {
            // 更新当前状态
            currentState = .Expanded
            // 动画
            let mainPosition = view.bounds.size.width * (1+minProportion/2)
                - menuViewExpandedOffset
            doTheAnimate(mainPosition, mainProportion: minProportion, blackCoverAlpha: 0)
        }
            // 如果是用于隐藏
        else {
            // 动画
            doTheAnimate(view.bounds.size.width/2, mainProportion: 1, blackCoverAlpha: 1) {
                finished in
                // 动画结束之后更新状态
                self.currentState = .Collapsed
                // 移除左侧视图
                self.menuViewController?.view.removeFromSuperview()
                // 释放内存
                self.menuViewController = nil;
                // 移除黑色遮罩层
                self.blackCover?.removeFromSuperview()
                // 释放内存
                self.blackCover = nil;
            }
        }
    }
    
    //主页移动动画、黑色遮罩层动画
    func doTheAnimate(mainPosition: CGFloat, mainProportion: CGFloat,
        blackCoverAlpha: CGFloat, completion: ((Bool) -> Void)! = nil) {
            //usingSpringWithDamping:1.0表示没有弹簧震动动画
            UIView.animateWithDuration(0.5, delay: 0, usingSpringWithDamping: 1.0,
                initialSpringVelocity: 0, options: .CurveEaseInOut, animations: {
                    self.mainNavigationController.view.center.x = mainPosition
                    self.blackCover?.alpha = blackCoverAlpha
                    // 缩放主页面
                    self.mainNavigationController.view.transform =
                        CGAffineTransformScale(CGAffineTransformIdentity,
                            mainProportion, mainProportion)
                }, completion: completion)
    }
    
    //给主页面边缘添加、取消阴影
    func showShadowForMainViewController(shouldShowShadow: Bool) {
        if (shouldShowShadow) {
            mainNavigationController.view.layer.shadowOpacity = 0.8
        } else {
            mainNavigationController.view.layer.shadowOpacity = 0.0
        }
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

// 菜单状态枚举
enum MenuState {
    case Collapsed  // 未显示(收起)
    case Expanding   // 展开中
    case Expanded   // 展开
}
源码下载:hangge_1035.zip

2,侧滑展开时,菜单页逐渐放大
(1)菜单页的背景是透明的,背景图(大海背景)是添加在容器页(ViewController)里面的。
(2)菜单页的移动缩放原理和上面一样,也是根据滑动时,主页面的位置实时计算出菜单页的尺寸和位置。 
(3)由于侧滑菜单打开时是从屏幕外滑入的,新增属性 menuViewStartOffset 表示它的起始位置(超出屏幕多少距离)。
      

ViewController.swift 代码如下(高亮处为修改过的地方):
import UIKit

class ViewController: UIViewController {
    // 主页导航控制器
    var mainNavigationController:UINavigationController!
    
    // 主页面控制器
    var mainViewController:MainViewController!
    
    // 菜单页控制器
    var menuViewController:MenuViewController?
    
    // 菜单页当前状态
    var currentState = MenuState.Collapsed {
        didSet {
            //菜单展开的时候,给主页面边缘添加阴影
            let shouldShowShadow = currentState != .Collapsed
            showShadowForMainViewController(shouldShowShadow)
        }
    }
    
    // 菜单打开后主页在屏幕右侧露出部分的宽度
    let menuViewExpandedOffset: CGFloat = 60
    
    // 侧滑开始时,菜单视图起始的偏移量
    let menuViewStartOffset: CGFloat = 70
    
    // 侧滑菜单黑色半透明遮罩层
    var blackCover: UIView?
    
    // 最小缩放比例
    let minProportion: CGFloat = 0.77
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        //状态栏文字改成白色
        UIApplication.sharedApplication().statusBarStyle = .LightContent;
        
        // 给根容器设置背景
        let imageView = UIImageView(image: UIImage(named: "back"))
        imageView.frame = UIScreen.mainScreen().bounds
        self.view.addSubview(imageView)
        
        //初始化主视图
        mainNavigationController = UIStoryboard(name: "Main", bundle: nil)
            .instantiateViewControllerWithIdentifier("mainNavigaiton")
            as! UINavigationController
        view.addSubview(mainNavigationController.view)
        
        //指定Navigation Bar左侧按钮的事件
        mainViewController = mainNavigationController.viewControllers.first
            as! MainViewController
        mainViewController.navigationItem.leftBarButtonItem?.action = Selector("showMenu")
        
        //添加拖动手势
        let panGestureRecognizer = UIPanGestureRecognizer(target: self,
            action: "handlePanGesture:")
        mainNavigationController.view.addGestureRecognizer(panGestureRecognizer)
        
        //单击收起菜单手势
        let tapGestureRecognizer = UITapGestureRecognizer(target: self,
            action: "handlePanGesture")
        mainNavigationController.view.addGestureRecognizer(tapGestureRecognizer)
    }
    
    //导航栏左侧按钮事件响应
    func showMenu() {
        //如果菜单是展开的则会收起,否则就展开
        if currentState == .Expanded {
            animateMainView(false)
        }else {
            addMenuViewController()
            animateMainView(true)
        }
    }
    
    //拖动手势响应
    func handlePanGesture(recognizer: UIPanGestureRecognizer) {
        
        switch(recognizer.state) {
            // 刚刚开始滑动
        case .Began:
            // 判断拖动方向
            let dragFromLeftToRight = (recognizer.velocityInView(view).x > 0)
            // 如果刚刚开始滑动的时候还处于主页面,从左向右滑动加入侧面菜单
            if (currentState == .Collapsed && dragFromLeftToRight) {
                currentState = .Expanding
                addMenuViewController()
            }
            
            // 如果是正在滑动,则偏移主视图的坐标实现跟随手指位置移动
        case .Changed:
            let screenWidth = view.bounds.size.width
            var centerX = recognizer.view!.center.x +
                recognizer.translationInView(view).x
            //页面滑到最左侧的话就不许要继续往左移动
            if centerX < screenWidth/2 { centerX = screenWidth/2 }
            
            // 计算缩放比例
            let percent:CGFloat = (centerX - screenWidth/2) /
                (view.bounds.size.width - menuViewExpandedOffset)
            let proportion = 1 - (1 - minProportion) * percent
        
            // 执行视差特效
            blackCover?.alpha = (proportion - minProportion) / (1 - minProportion)
            
            recognizer.view!.center.x = centerX
            recognizer.setTranslation(CGPointZero, inView: view)
            //缩放主页面
            recognizer.view!.transform =
                CGAffineTransformScale(CGAffineTransformIdentity, proportion, proportion)
            
            //菜单视图移动
            menuViewController?.view.center.x = screenWidth/2 -
                menuViewStartOffset * (1 - percent)
            //菜单视图缩放
            let menuProportion = (1 + minProportion) - proportion
            menuViewController?.view.transform = CGAffineTransformScale(
                CGAffineTransformIdentity, menuProportion, menuProportion)
            
            // 如果滑动结束
        case .Ended:
            //根据页面滑动是否过半,判断后面是自动展开还是收缩
            let hasMovedhanHalfway = recognizer.view!.center.x > view.bounds.size.width
            animateMainView(hasMovedhanHalfway)
        default:
            break
        }
    }
    
    //单击手势响应
    func handlePanGesture() {
        //如果菜单是展开的点击主页部分则会收起
        if currentState == .Expanded {
            animateMainView(false)
        }
    }
    
    // 添加菜单页
    func addMenuViewController() {
        if (menuViewController == nil) {
            menuViewController = UIStoryboard(name: "Main", bundle: nil)
                .instantiateViewControllerWithIdentifier("menuView") as? MenuViewController
            
            menuViewController!.view.center.x = view.bounds.size.width/2
                * (1-(1-minProportion)/2) - menuViewStartOffset
            menuViewController!.view.transform = CGAffineTransformScale(
                CGAffineTransformIdentity, minProportion, minProportion)
            
            // 插入当前视图并置顶
            view.insertSubview(menuViewController!.view,
                belowSubview: mainNavigationController.view)
            
            // 建立父子关系
            addChildViewController(menuViewController!)
            menuViewController!.didMoveToParentViewController(self)
            
            // 在侧滑菜单之上增加黑色遮罩层,目的是实现视差特效
            blackCover = UIView(frame: CGRectOffset(self.view.frame, 0, 0))
            blackCover!.backgroundColor = UIColor.blackColor()
            self.view.insertSubview(blackCover!,
                belowSubview: mainNavigationController.view)
        }
    }
    
    //主页自动展开、收起动画
    func animateMainView(shouldExpand: Bool) {
        // 如果是用来展开
        if (shouldExpand) {
            // 更新当前状态
            currentState = .Expanded
            // 动画
            let mainPosition = view.bounds.size.width * (1+minProportion/2)
                - menuViewExpandedOffset
            doTheAnimate(mainPosition, mainProportion: minProportion,
                menuPosition: view.bounds.size.width/2, menuProportion: 1,
                blackCoverAlpha: 0)
        }
            // 如果是用于隐藏
        else {
            // 动画
            let menuPosition = view.bounds.size.width/2 * (1-(1-minProportion)/2)
                - menuViewStartOffset
            doTheAnimate(view.bounds.size.width/2, mainProportion: 1,
                menuPosition: menuPosition, menuProportion: minProportion,
                blackCoverAlpha: 1) { finished in
                // 动画结束之后更新状态
                self.currentState = .Collapsed
                // 移除左侧视图
                self.menuViewController?.view.removeFromSuperview()
                // 释放内存
                self.menuViewController = nil;
                // 移除黑色遮罩层
                self.blackCover?.removeFromSuperview()
                // 释放内存
                self.blackCover = nil;
            }
        }
    }
    
    //主页移动动画、黑色遮罩层动画、菜单页移动动画
    func doTheAnimate(mainPosition: CGFloat, mainProportion: CGFloat,
        menuPosition: CGFloat, menuProportion: CGFloat,
        blackCoverAlpha: CGFloat, completion: ((Bool) -> Void)! = nil) {
            //usingSpringWithDamping:1.0表示没有弹簧震动动画
            UIView.animateWithDuration(0.5, delay: 0, usingSpringWithDamping: 1.0,
                initialSpringVelocity: 0, options: .CurveEaseInOut, animations: {
                    self.mainNavigationController.view.center.x = mainPosition
                    self.blackCover?.alpha = blackCoverAlpha
                    // 缩放主页面
                    self.mainNavigationController.view.transform =
                        CGAffineTransformScale(CGAffineTransformIdentity,
                            mainProportion, mainProportion)
                    
                    // 菜单页移动
                    self.menuViewController?.view.center.x = menuPosition
                    // 菜单页缩放
                    self.menuViewController?.view.transform =
                        CGAffineTransformScale(CGAffineTransformIdentity,
                            menuProportion, menuProportion)
                    
                }, completion: completion)
    }
    
    //给主页面边缘添加、取消阴影
    func showShadowForMainViewController(shouldShowShadow: Bool) {
        if (shouldShowShadow) {
            mainNavigationController.view.layer.shadowOpacity = 0.8
        } else {
            mainNavigationController.view.layer.shadowOpacity = 0.0
        }
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

// 菜单状态枚举
enum MenuState {
    case Collapsed  // 未显示(收起)
    case Expanding   // 展开中
    case Expanded   // 展开
}
源码下载:hangge_1035_2.zip

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作。

打赏支持
评论4
  • 4楼
    2016-07-29 18:04
    linjoe

    站长,我做个登录界面,登录完之后跳转到这个有侧栏的界面,但是传值的时候出现:Presenting view controllers on detached view controllers is discouraged 。这怎么回事

    站长回复

    提供的信息太少,不太确定是什么问题。

  • 3楼
    2016-06-30 17:54
    风之沙

    站长,我在MainViewController里面放了一个按钮,push到下一个视图控制器(OneViewController),在OneViewController这个视图控制器里面我还是可以进行侧滑,我想让它在OneViewController这个视图控制器里面不能侧滑该怎么办?

    站长回复

    你可以在拖动手势响应中加个判断,如果不是根视图则不执行侧滑。 


        //拖动手势响应
        func handlePanGesture(recognizer: UIPanGestureRecognizer) {
            
            if mainNavigationController.visibleViewController != mainViewController {
                return
            }

    .........

  • 2楼
    2016-03-23 16:54
    AllocAndInit

    当我用代码实现了相应的效果后,发现可能是因为父子关系的问题,出现了一些莫名其妙的问题,比如连TableView 都不能点击了,你有什么方法吗?

    站长回复

    不知道你的代码是什么样的,所以这个只能你自己找找看,我也帮不上什么忙了。

  • 1楼
    2016-01-30 20:37
    wIng_Z

    关注站长 学到了太多的东西

    站长回复

    欢迎常来看看