美文网首页
iOS仿 QQ抽屉效果

iOS仿 QQ抽屉效果

作者: 小青蛙的花 | 来源:发表于2017-11-29 15:50 被阅读81次

    先来看看 QQ的效果

    随着 QQ版本的更新QQ 抽屉效果也更新了好多次,现在的版本个人感觉是返璞归真,简约实用。先看下效果:


    QQ.gif

    先来分析一波:

    1. 首先看主页
    主页是一个 tabbarController,这个不难看出来,但是有个层级就是导航栏,一般常用的结构如下: 屏幕快照 2017-11-29 14.23.57.png

    这样的好处是切换 tabbar 是切换了 navigationController,这样 navigationController 设置更加灵活。

    但是我这里是用了下面这个结构: 屏幕快照 2017-11-29 14.26.09.png
    因为这里切换tabbar 的3个 tabbarItem,点击导航栏的 leftItem功能都是触发抽屉效果。
    2. 再来看看左边栏

    在网上看过别的抽屉效果,有一个文章写得用 scrollview 来实现抽屉,他觉得 scrollview 的代理获取的滑动距离就是为抽屉量身定做的。其实刚看到的时候觉得这个有一定道理,但是看到下面的一条评论 这么说:“你去看一下 tabbarController和 navigationController 是怎么实现,都是Container View Controller,这里是 scrollview 无法比拟的 ”。
    后来找到了一篇文章iOS中Container View Controller的使用,有兴趣的可以看一下。
    现在回到主线,到这基本结构就确定了:

    屏幕快照 2017-11-29 14.45.50.png

    在 OC 中实现效果

    创建 tabbarController等一些基础代码就不贴出来了,下面只贴一些核心代码。

    1. 创建抽屉DBDrawerController

    这里用的单例方便管理

    /* 主页 */
    @property (nonatomic,strong)UINavigationController * centerController;
    /* 左边栏 */
    @property (nonatomic,strong)UIViewController * leftController;
    /* 添加手势 View */
    @property (nonatomic,strong)UIView * drawerPanView;
    
    
    + (DBDrawerController*)shareManager{
        static dispatch_once_t onceToken;
        static id sharedInstance;
        dispatch_once(&onceToken, ^{
            if (sharedInstance == nil) {
                sharedInstance = [[DBDrawerController alloc]init];
            }
        });
        return sharedInstance;
    }
    - (void)initWintCenterController:(UINavigationController*)centerControll leftController:(UIViewController*)letfContrller{
       
        self.centerController = centerControll;
        self.leftController = letfContrller;
        /* 初始化控制器 */
        [self initController];
    }
    
    /* 初始化控制器 */
    - (void)initController{
    
        self.leftController.view.frame = CGRectMake( -0.25 * SCREENWIDTH, 0, 0.75 * SCREENWIDTH, SCREENHEIGHT);
        
        [self addChildViewController:self.leftController];
        [self.view addSubview:self.leftController.view];
        
        [self addChildViewController:self.centerController];
        [self.view addSubview:self.centerController.view];
       
        /* 初始化手势 */
        [self initDrawerController];
        
    }
    
    /* 初始化手势 */
    - (void)initDrawerController{
        
        for (UITabBarController * tabbar in self.centerController.viewControllers ) {
            tabbar.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc]initWithImage:[[UIImage imageNamed:@"back_white"]imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] style:UIBarButtonItemStylePlain target:self action:@selector(viewMove)];
        }
        
        self.drawerPanView = [[UIView alloc]initWithFrame:CGRectMake(0,64, 0.25 * SCREENWIDTH, SCREENHEIGHT - 64 - 50)];
        [self.centerController.view addSubview:self.drawerPanView];
        [self addGestureRecognizer];
    }
    

    这里有一个注意的地方,在主页点击导航栏左标签的时候需要触发抽屉效果的,所以在这里自定义了leftBarButtonItem,因为前面我们只有一个导航栏,这里只要实现一次就好了

     for (UITabBarController * tabbar in self.centerController.viewControllers ) {
            tabbar.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc]initWithImage:[[UIImage imageNamed:@"back_white"]imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] style:UIBarButtonItemStylePlain target:self action:@selector(viewMove)];
        }
    

    到这里DBDrawerController基本就创建好了,下面就是处理手势滑动,和导航栏左标签点击事件

    2. DBDrawerController 手势及点击事件处理

    下面就是最核心的代码,手势的滑动距离处理,先看代码,下面会做解释

    /* 界面滑动过程 */
    - (void)centerControllerMove:(UIPanGestureRecognizer*)pan{
    
        CGPoint transition = [pan translationInView:self.centerController.view];
        CGFloat originX = self.centerController.view.frame.origin.x;
        CGFloat originX_left = self.leftController.view.frame.origin.x;
        if (pan.state==UIGestureRecognizerStateChanged){
            /*距离边界较近时需要判断是否超出边界,不然有明显卡顿白边*/
            BOOL resCent = transition.x > 0 &&
                        originX + transition.x <= SCREENWIDTH * 0.75 &&
                        originX + transition.x >= 0 &&
                        originX_left + transition.x / 3 <= 0 ;
            BOOL resCent_1 = transition.x > 0 &&
                        originX < SCREENWIDTH * 0.75 &&
                        originX + transition.x >= SCREENWIDTH * 0.75 ;
            BOOL resLeft = transition.x < 0 &&
                        originX + transition.x >= 0;
            BOOL resLeft_1 = transition.x < 0 &&
                        originX + transition.x <= 0;
    
            if (resCent || resLeft) {
                [self viewFrameChange:transition.x with:pan];
            }else if (resCent_1){
                CGFloat newOffset = transition.x - (originX + transition.x - SCREENWIDTH * 0.75);
                [UIView animateWithDuration:0.05 animations:^{
                    [self viewFrameChange:newOffset with:pan];
                }];
            }else if (resLeft_1){
                [UIView animateWithDuration:0.05 animations:^{
                    [self viewReset];
                }];
            }
        }
        //拖动手势结束
        if (pan.state==UIGestureRecognizerStateEnded) {
            CGFloat originX =self.centerController.view.frame.origin.x;
            CGFloat offsetX=0;
            //大于屏幕的一半进入新的位置
            if (originX >= SCREENWIDTH * 0.5 && originX <= SCREENWIDTH * 0.75) {
                offsetX = SCREENWIDTH * 0.75 - originX;
                [self viewMoveToEnd:offsetX];
            }else if(originX < SCREENWIDTH * 0.5 && fabs(originX) > 0 ){
                //小于屏幕的一半,大于屏幕负一半的时候,则恢复到初始状态
                [self viewReset];
            }
        }
    }
    
    先说一下这里为什么会判断四次:

    originX : view 当前的x轴坐标
    transition.x :view滑动的偏移量
    SCREENWIDTH * 0.75 :我们滑动的终点

    BOOL resCent = transition.x > 0 &&
                        originX + transition.x <= SCREENWIDTH * 0.75 &&
                        originX + transition.x >= 0 &&
                        originX_left + transition.x / 3 <= 0 
    BOOL resCent_1 = transition.x > 0 &&
                        originX < SCREENWIDTH * 0.75 &&
                        originX + transition.x >= SCREENWIDTH * 0.75 ;
    BOOL resLeft = transition.x < 0 &&
                        originX + transition.x >= 0;
    BOOL resLeft_1 = transition.x < 0 &&
                        originX + transition.x <= 0;
    

    transition.x这个参数需要注意一下,这个参数跟scrollview不一样,transition.x是偏移量,是相对上一次位置的偏移量,所以这个数值会很小。
    但是这个值跟滑动速度有关系,滑动越快这个偏移量的绝对值就越大,所以还需要处理一下特殊情况:

    1. 右滑时当前的位置加上滑动的距离大于终点
    2. 左滑时当前位置加上滑动距离小于0

    我这里想的解决办法是判断 transition.x 的大小,如果这个值比较大,那么可以肯定滑动的速度会很快,然后通过originX + transition.x来判断 view 是否会画出边界,在快到达边界的时候加一个0.05s 的动画效果:

    else if (resCent_1){
                CGFloat newOffset = transition.x - (originX + transition.x - SCREENWIDTH * 0.75);
                [UIView animateWithDuration:0.05 animations:^{
                    [self viewFrameChange:newOffset with:pan];
                }];
            }else if (resLeft_1){
                [UIView animateWithDuration:0.05 animations:^{
                    [self viewReset];
                }];
            }
    

    下面是滑动的动画效果:

    /* 滑动过程 界面滑动 */
    - (void)viewFrameChange:(CGFloat)offsetX with:(UIPanGestureRecognizer*)pan{
        self.centerController.view.frame=[self frameWithOffset:offsetX];
        self.leftController.view.frame = [self leftframeWithOffset:offsetX / 3];
        [pan setTranslation:CGPointZero inView:self.centerController.view];
    }
    
    /* 滑动手势松开 界面滑动到结束位置 */
    - (void)viewMoveToEnd:(CGFloat)offsetX{
    
        [UIView animateWithDuration:0.2 animations:^{
            self.centerController.view.frame=[self frameWithOffset:offsetX];
            self.leftController.view.frame = [self leftframeWithOffset:offsetX/3];
        }];
    }
    
    - (void)viewReset{
        [UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
            self.centerController.view.frame = self.centerController.view.bounds;
            self.leftController.view.frame = CGRectMake( -0.25 * SCREENWIDTH, 0, 0.75 * SCREENWIDTH, SCREENHEIGHT);
        } completion:nil];
    }
    
    - (void)viewMoveToEnd{
        [UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
            self.centerController.view.frame=CGRectMake( 0.75 * SCREENWIDTH, 0,SCREENWIDTH, SCREENHEIGHT);
            self.leftController.view.frame = CGRectMake( 0, 0, 0.75 * SCREENWIDTH, SCREENHEIGHT);
        } completion:nil];
    }
    
    到这基本核心代码就完成了,下面看下效果 text.gif

    另外自己还可以添加一些额外的属性,例如:滑动的时间可以自定义,添加一个遮罩层,遮罩层的透明度,添加一个参数控制抽屉效果是否可用。

    总结

    前都是一个人瞎捉摸,越是知道自己的代码写的不规范也没有可读性,越是不敢让别人看自己写的东西,现在明白了交流才是进步的关键,这篇文章只是记录一些学习经历,还有很多不足的地方,希望能指出来交流一下,知道自己的问题,才能解决问题,才能进步!
    路漫漫其修远兮,吾将上下而求索!

    相关文章

      网友评论

          本文标题:iOS仿 QQ抽屉效果

          本文链接:https://www.haomeiwen.com/subject/aekubxtx.html