再造 “手机QQ” 侧滑菜单(一)——实现侧滑效果

2015-4-11   /   阅读数:73284   /   分类: iOS & Swift

本系列文章中,我们将尝试再造手机QQ的侧滑菜单,力争最大限度接近手Q的实际效果,并使用 Auto Layout 仿造左侧菜单,实现和主视图的联动。

代码示例:https://github.com/johnlui/SwiftSideslipLikeQQ

最终效果:

Image


开发环境

本系列文章的开发环境为:

* OS X 10.10.3

* Xcode Version 6.3 (6D570)

基本数据采集

初步体验,手Q采用的应该是线性动画,即缩放比例等随着手指滑动的距离以一次方程的形式变化。动画达到最大幅度时截图如下(4.7 寸):

Image

提取基本数据:

  1. 右侧主视图左边界距离屏幕左边界的距离占屏幕宽度的比例为:78%
  2. 右侧主视图的高度占屏幕高度的比例为:77%

找出线性关系

1. 比例与手指移动距离的关系

字比较丑 o(╯□╰)o。注意:式(1)中的 x 表示“手指移动距离”这个变量,和上面图中表示屏幕宽度的 x 意义不同。

Image

2. 矩形中心向右移动距离和手指移动距离相等

实现侧滑

1. 新建项目,在 StoryBoard 中新增一个 View Controller,并新增一个名为 HomeViewController 的 UIViewController 类,并在 StoryBoard 中完成绑定。

2. 给 HomeViewController 设置背景颜色以示区分。也可以像我一样设一个大 Label 作为更明显的区分。

Image

3. 给 HomeViewController 拖放一个 UIPanGestureRecognizer 并绑定到代码。

从右下角拖一个 Pan Gesture Recognizer 到主窗体上,这一步会让它与 HomeViewController.view 自动绑定。下图为第二步,绑定到代码。

Image

3. 编写代码实现效果:

新建 Common.swift,存储屏幕宽度、高度:

import UIKit

struct Common {
    static let screenWidth = UIScreen.mainScreen().applicationFrame.maxX
    static let screenHeight = UIScreen.mainScreen().applicationFrame.maxY
}

修改 ViewController:

import UIKit

class ViewController: UIViewController {
    
    var homeViewController: HomeViewController!
    var distance: CGFloat = 0
    
    let FullDistance: CGFloat = 0.78
    let Proportion: CGFloat = 0.77

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 给主视图设置背景
        let imageView = UIImageView(image: UIImage(named: "back"))
        imageView.frame = UIScreen.mainScreen().bounds
        self.view.addSubview(imageView)
        
        // 通过 StoryBoard 取出 HomeViewController 的 view,放在背景视图上面
        homeViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("HomeViewController") as! HomeViewController
        self.view.addSubview(homeViewController.view)
        
        // 绑定 UIPanGestureRecognizer
        homeViewController.panGesture.addTarget(self, action: Selector("pan:"))
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    // 响应 UIPanGestureRecognizer 事件
    func pan(recongnizer: UIPanGestureRecognizer) {
        let x = recongnizer.translationInView(self.view).x
        let trueDistance = distance + x // 实时距离
        
        // 如果 UIPanGestureRecognizer 结束,则激活自动停靠
        if recongnizer.state == UIGestureRecognizerState.Ended {

            if trueDistance > Common.screenWidth * (Proportion / 3) {
                showLeft()
            } else if trueDistance < Common.screenWidth * -(Proportion / 3) {
                showRight()
            } else {
                showHome()
            }
            
            return
        }

        // 计算缩放比例
        var proportion: CGFloat = recongnizer.view!.frame.origin.x >= 0 ? -1 : 1
        proportion *= trueDistance / Common.screenWidth
        proportion *= 1 - Proportion
        proportion /= 0.6
        proportion += 1
        if proportion <= Proportion { // 若比例已经达到最小,则不再继续动画
            return
        }
        // 执行平移和缩放动画
        recongnizer.view!.center = CGPointMake(self.view.center.x + trueDistance, self.view.center.y)
        recongnizer.view!.transform = CGAffineTransformScale(CGAffineTransformIdentity, proportion, proportion)
    }
    
    // 封装三个方法,便于后期调用
    
    // 展示左视图
    func showLeft() {
        distance = self.view.center.x * (FullDistance + Proportion / 2)
        doTheAnimate(self.Proportion)
    }
    // 展示主视图
    func showHome() {
        distance = 0
        doTheAnimate(1)
    }
    // 展示右视图
    func showRight() {
        distance = self.view.center.x * -(FullDistance + Proportion / 2)
        doTheAnimate(self.Proportion)
    }
    // 执行三种试图展示
    func doTheAnimate(proportion: CGFloat) {
        UIView.animateWithDuration(0.3, delay: 0, options: UIViewAnimationOptions.CurveEaseInOut, animations: { () -> Void in
            self.homeViewController.view.center = CGPointMake(self.view.center.x + self.distance, self.view.center.y)
            self.homeViewController.view.transform = CGAffineTransformScale(CGAffineTransformIdentity, proportion, proportion)
            }, completion: nil)
    }

}

4. 查看效果

Image


下一步:再造 “手机QQ” 侧滑菜单(二)——高仿左视图

WRITTEN BY

avatar

评论:

Justin
2015-08-18 15:28
为什么我感觉和版主的操作一样 代码也是复制的 没有实现呢?
bruce
2015-08-15 19:57
楼主有没有oc的,我对swift不了解
黑豆腐
2015-07-17 16:58
弱弱的问一下,为什么滑动最大距离时,水平移动距离distance = self.view.center.x * (FullDistance + Proportion / 2)而不是由你画的图得到distance = common.screenWidth * (FullDistance + Proportion / 2) - self.view.center.x,求解?
JohnLui
2015-07-17 17:14
@黑豆腐:因为我在后面的文章中精确了算法
小班
2015-12-22 16:08
@JohnLui:博主。我不是很明白你的动态变化算法,能不能说的详细一点。let x = recongnizer.translationInView(self.view).x 这里的x是指手指滑动的距离。
distance是什么,truedistance又是指什么,是什么的真实距离
JohnLui
2015-12-22 16:17
@小班:一次滑动过程中,这个事件会不断触发,distance 是触发一次滑动的距离,truedistance 是累计的距离。
lastcc
2015-07-16 22:53
static let screenWidth = UIScreen.mainScreen().applicationFrame.maxX

这里如果稍后旋转屏幕会怎样?这个值不会变了
是否应该用observer?
JohnLui
2015-07-17 17:09
@lastcc:对,目前是不支持横屏切换的。
lastcc
2015-07-16 22:45
我想问下楼主 手势从Storyboard绑定时可以直接绑定到method,为何要先持有一个outlet,再调用其addTarget方法呢?
JohnLui
2015-07-17 17:10
@lastcc:两种方式都可以,我只是选择了一种而已。。。。
lastcc
2015-07-16 21:23

很好!
就是想看这样有水平的文章!
楼主你敢回复我一个吗。

顺便我想问下怎么看起来好像有点hack的意味
homeViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("HomeViewController") as! HomeViewController
JohnLui
2015-07-17 17:11
@lastcc:这就是基础的用法
eskimog
2015-06-17 16:45
还好是线性而不是非线性关系。。
之前就很想完成这样一个效果,只不过看来层主用的是不是纯代码。。可惜了。。。
呵呵哒
2015-06-12 14:39
少这一句   // 绑定 UIPanGestureRecognizer
        homeViewController.myPan.addTarget(self, action: Selector("pan:"))
        homeViewController.view.addGestureRecognizer(homeViewController.myPan);
JohnLui
2015-06-12 15:10
@呵呵哒:看这里:http://lvwenhan.com/ios/445.html#comment-9777
xingqiba
2015-06-07 22:04
缩放比例没看懂原理
hmb
2015-06-03 09:59
拜读了,非常感谢分享
isan
2015-05-06 20:03
为什么手指移动的最大距离是0.6x
JohnLui
2015-05-06 20:07
@isan: 这都被你发现了

0.6只是一个假设的值,后面对其进行了精确计算
machine9
2015-05-03 15:52
楼主厉害!!!
p.s. 似乎这段代码里少了一句addGestureRecognizer,可能会导致panGestureRecognizer无法使用
JohnLui
2015-05-03 17:07
@machine9:这个在storyboard 里拖动的时候就绑定啦
阿里百秀
2015-04-29 10:40
具体的内容不是很懂,但是画面看着很不错的样子,常来常往
大喵
2015-04-27 18:17
求帮助 为什么我按照上面的方法来做没有完成完成滑动 好像手势都没添加进去
JohnLui
2015-04-27 18:28
@大喵:功能没有实现,不是具体的报错,这个我真帮不了你呀
大喵
2015-04-27 18:29
@JohnLui:我能把源码发给你瞧瞧么
JohnLui
2015-04-27 18:31
@大喵: 如果手势都还不会用的话,建议先打好基础。。。
大喵
2015-04-27 18:32
@JohnLui:请帮我看看。。。谢谢
JohnLui
2015-04-27 18:39
@大喵:建议你找个QQ群发上去看看有没有好心人帮你
NullPointer
2015-05-08 10:28
@大喵:目测你在StoryBoard中拖UIPanRecognizer到HomeViewController视图中的时候没有往View上面拖。
如果把UIPanRecognizer往View上面拖,其实是直接完成了绑定的,如果你是直接拖到那个视图框架顶部的,那么是没有和任何UIView绑定的。你可以在viewDidload方法中addTarget之前,给homeViewController的view调用addGestureRecognizer,将outlet进来的变量赋给它。
大喵
2015-04-27 18:13
请问什么我代码全部按照你得写的没有实现滑动效果呢??请留个邮箱。
萧瑟的魔笛
2015-04-18 22:06
可惜是switch,要是有oc就好了。
Orange-W
2015-05-30 15:14
@萧瑟的魔笛:
Swift 吧
wuou
2015-04-17 16:31
重点是找出线性关系! 这里给了我很大启发,然后联动可以这样做 :  
  1、这是leftVC.view的缩放比例:
    找出这两点 (0.77 , 0)  (1 , screenwidth*0.78)   ==>  (left.view的缩放比例, main.view.or.x坐标),线性关系

  2、这是leftVC.view的移动:
   找出这两点(self.view.center.x , screenwidth * 0.78) (center - 80 , 0)  ==>  (屏幕中心点x的坐标 ,main.view.or.x坐标),线性关系
wuou
2015-04-15 20:29
用UITouch更方便
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
Tommy
2015-04-15 11:22
Lz,貌似少了一句addChildViewController
JohnLui
2015-04-15 11:39
@Tommy:我没有用那个 API,通过对象指针直接控制的
Scofield
2015-11-08 12:40
@JohnLui:博主,你是VC中的一个con指针指向使用storyboard加载的homeVC,就可以触发homeVC上的action(比如button的addTarget,自行多添加了一个)。
但是如果把VC中的con指针指向使用 allocInit的某个VC(不用storyboard来加载),那么action将不能触发,而添加addChildViewController后就可以了。博主求分享,求解答
JohnLui
2015-11-08 15:30
@Scofield:内存被回收了呗
lion
2015-04-13 13:37
为什么运行会有6个错误
JohnLui
2015-04-13 14:11
@lion:需要 Xcode 6.3 ~

发表评论:

© 2011-2019 岁寒  |  Powered by Emlog