先来看看 QQ的效果
随着 QQ版本的更新QQ 抽屉效果也更新了好多次,现在的版本个人感觉是返璞归真,简约实用。先看下效果:
先来分析一波:
1. 首先看主页
主页是一个 tabbarController,这个不难看出来,但是有个层级就是导航栏,一般常用的结构如下:这样的好处是切换 tabbar 是切换了 navigationController,这样 navigationController 设置更加灵活。
因为这里切换tabbar 的3个 tabbarItem,点击导航栏的 leftItem功能都是触发抽屉效果。
2. 再来看看左边栏
在网上看过别的抽屉效果,有一个文章写得用 scrollview 来实现抽屉,他觉得 scrollview 的代理获取的滑动距离就是为抽屉量身定做的。其实刚看到的时候觉得这个有一定道理,但是看到下面的一条评论 这么说:“你去看一下 tabbarController和 navigationController 是怎么实现,都是Container View Controller,这里是 scrollview 无法比拟的 ”。
后来找到了一篇文章iOS中Container View Controller的使用,有兴趣的可以看一下。
现在回到主线,到这基本结构就确定了:
在 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是偏移量,是相对上一次位置的偏移量,所以这个数值会很小。
但是这个值跟滑动速度有关系,滑动越快这个偏移量的绝对值就越大,所以还需要处理一下特殊情况:
- 右滑时当前的位置加上滑动的距离大于终点
- 左滑时当前位置加上滑动距离小于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];
}
到这基本核心代码就完成了,下面看下效果:
另外自己还可以添加一些额外的属性,例如:滑动的时间可以自定义,添加一个遮罩层,遮罩层的透明度,添加一个参数控制抽屉效果是否可用。
总结
前都是一个人瞎捉摸,越是知道自己的代码写的不规范也没有可读性,越是不敢让别人看自己写的东西,现在明白了交流才是进步的关键,这篇文章只是记录一些学习经历,还有很多不足的地方,希望能指出来交流一下,知道自己的问题,才能解决问题,才能进步!
路漫漫其修远兮,吾将上下而求索!