iOS仿 QQ抽屉效果

先来看看 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

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

总结

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容