iOS开发之路--仿网易抽屉效果
投稿:hebedich
本文是IOS开发之路系列的第一篇,主要讲诉了如何仿网易新闻客户端实现抽屉效果,全部源代码都分享给大家,希望对大家有所帮助
最终效果图:
MainStoryBoard示意图:
BeyondViewController.h
// // BeyondViewController.h // 19_抽屉效果_仿网易 // // Created by beyond on 14-8-1. // Copyright (c) 2014年 com.beyond. All rights reserved. // #import <UIKit/UIKit.h> #import "LeftTableViewControllerDelegate.h" @interface BeyondViewController : UIViewController // 左半边 (显示 的是栏目列表 ) @property (weak, nonatomic) IBOutlet UIView *leftView; // 右半边 (显示 的是个人信息设置视图) @property (weak, nonatomic) IBOutlet UIView *rightView; // 最上面,最大的全屏的是主视图 @property (weak, nonatomic) IBOutlet UIView *mainView; // 上面标题状态栏视图中的标题按钮 (网易的Logo图片和栏目的名称 水平排列) @property (weak, nonatomic) IBOutlet UIButton *titleBtn; // mainView的下半部分 是 正文的view,显示子栏目的view @property (weak, nonatomic) IBOutlet UIView *contentView; // pan 拽 手势处理 - (IBAction)panGesture:(UIPanGestureRecognizer *)sender; // mainView的上半部分 标题状态栏视图中的左,右按钮 - (IBAction)btnClick:(UIButton *)sender; @end
BeyondViewController.m
// // BeyondViewController.m // 19_抽屉效果_仿网易 // // Created by beyond on 14-8-1. // Copyright (c) 2014年 com.beyond. All rights reserved. // #import "BeyondViewController.h" #import "LeftTableViewController.h" #import "RightViewController.h" #import "Column.h" #import <QuartzCore/QuartzCore.h> // 手势结束时的x #define kEndX frame.origin.x // 左view的宽度 #define kLeftWidth _leftView.frame.size.width // 右view的宽度 #define kRightWidth _rightView.frame.size.width // 对协议进行提前声明 @protocol LeftTableViewControllerDelegate ; @interface BeyondViewController ()<LeftTableViewControllerDelegate> { // 手指按下的时候,记住,mainView的起始x CGFloat _startX; // 成员变量,记住左边控制器的实例 LeftTableViewController *_leftVC; // 成员变量,记住右边控制器的实例 RightViewController *_rightVC; // 字典 ,记住所有实例化了 栏目的子控制器,避免每次都重新创建 NSMutableDictionary *_columnViewControllers; } @end @implementation BeyondViewController // 隐藏状态栏 - (BOOL)prefersStatusBarHidden { return YES; } - (void)viewDidLoad { [super viewDidLoad]; _titleBtn.backgroundColor = [UIColor clearColor]; // 0 字典 ,记住所有实例化了 栏目的子控制器,避免每次都重新创建 _columnViewControllers = [NSMutableDictionary dictionary]; // 0,设置导航条bar的背景 为网 易 红 //[[UINavigationBar appearance] setBackgroundImage:[UIImage imageNamed:@"bg.png"] forBarMetrics:UIBarMetricsDefault]; // 状态条颜色 改成默认的样式 //[UIApplication sharedApplication].statusBarStyle = UIStatusBarStyleDefault; // 1,添加左边控制器的view到左边的view里面 _leftVC = [[LeftTableViewController alloc]init]; // 关键代码 为了拿到左边控制器的某一行被点击时候,对应的栏目数据模型对象,主控制器成为了左边控制器的代理,遵守了它定好的协议,实现了协议中的方法,从而拿到左边控制器被点击行号对应的数据模型对象 _leftVC.delegate = self; _leftVC.view.frame = self.leftView.bounds; [self.leftView addSubview:_leftVC.view]; // 2,同理,添加右边控制器的view到右边的view里面 _rightVC = [[RightViewController alloc]init]; _rightVC.view.frame = self.rightView.bounds; [self.rightView addSubview:_rightVC.view]; // 3,第一次加载时候,就就应该显示新闻 子栏目的控制器到导航控制器,再将导航控制器的view添加到 mainView里面 [self firstLoading]; } // 自定义方法,第一次加载时候,就就应该显示新闻 子栏目的控制器到导航控制器,再将导航控制器的view添加到 mainView里面 - (void)firstLoading { Column * column = [Column columnNamed:@"新闻" imgName:@"news.png" className:@"NewsViewController"]; // 仅需手动调用一个 LeftViewController的代理 方法,leftTableViewRowClicked,传递一个新闻 子栏目即可 [self leftTableViewRowClicked:column]; } // pan 拽 手势处理 - (IBAction)panGesture:(UIPanGestureRecognizer *)sender { // 如果是刚按下的状态,则记住,mainView的起始x if (UIGestureRecognizerStateBegan == sender.state) { _startX = self.mainView.frame.origin.x; } // 平移拖动的距离 CGPoint delta = [sender translationInView:_mainView]; CGRect frame = self.mainView.frame; // 计算新的x值,并做健壮性判断 kEndX = _startX + delta.x; // 1,限制最大拖动范围 if (kEndX >= kLeftWidth) { kEndX = kLeftWidth; } if (kEndX <= - kRightWidth) { kEndX = - kRightWidth; } // 2,由于 左view和右view在重叠,所以要隐藏其中的一个 if (kEndX > 0) { // NSLog(@"--调用频率相当高--"); _rightView.hidden = YES; _leftView.hidden = NO; } else { _rightView.hidden = NO; _leftView.hidden = YES; } if (UIGestureRecognizerStateEnded == sender.state) { // 手势结束的时候,需进行robust判断 // 2,分析end松手时候,的位置x,决定展开到什么程度 /* // 2.1 如果只向右拖了一点点,小于 1/2 的左view的宽度,则归0 if (kEndX < 0.5*kLeftWidth && kEndX >= 0) { kEndX = 0; }else if (kEndX >= 0.5*kLeftWidth && kEndX <= kLeftWidth) { // 2.2 如果向右拖一大半了,大于 1/2 的左view的宽度,虽然还没到位,也可以认为是到位了 kEndX = kLeftWidth; }else if (kEndX > - 0.5*kRightWidth && kEndX <= 0) { // 2.3 如果只向左拖了一点点,小于 1/2 的右view的宽度,则归0 kEndX = 0; }else if (kEndX <= - 0.5*kRightWidth) { // 2.4 如果向左拖一大半了,大于 1/2 的右view的宽度,虽然还没到位,也可以认为是到位了 kEndX = - kRightWidth; } */ // 第2种判断方式 // 起始为0,delta.x大于0 代表向右滑动 if (_startX == 0 && delta.x >0) { kEndX = kLeftWidth; }else if (_startX == 0 && delta.x < 0){ // 起始为0,delta.x小于0 代表向左滑动 kEndX = - kRightWidth; }else if (_startX == kLeftWidth && delta.x < 0){ // 起始为kLeftWidth,delta.x小于0 代表向左滑动 kEndX =0; }else if (_startX == - kRightWidth && delta.x > 0){ // 起始为- kRightWidth,delta.x大于0 代表向右滑动 kEndX = 0; } } // 最后,才设置mainView的新的frame [UIView animateWithDuration:0.2 animations:^{ self.mainView.frame=frame; }]; // 最后,为mainView所在的图层 添加阴影效果 [self addShadowFormainViewWithEndX:kEndX]; } // 自定义方法,为mainView所在的图层 添加阴影效果 (调用频率相当高) - (void)addShadowFormainViewWithEndX:(CGFloat)endX { // 1,点击工程,加号,导入第3方框架 #import <QuartzCore/QuartzCore.h> // 2,拿到mainView所在的图层,设置阴影 参数 // NSLog(@"调用频率很高---"); _mainView.layer.shadowColor = [UIColor blackColor].CGColor; _mainView.layer.shadowOpacity = 0.5; if (endX >= 0) { _mainView.layer.shadowOffset = CGSizeMake(-5, 0); } else { _mainView.layer.shadowOffset = CGSizeMake(5, 0); } } // 单击按钮,也一样可以展开 左右侧边栏 - (IBAction)btnClick:(UIButton *)sender { // 定义一个临时变量 CGFloat startX = _mainView.frame.origin.x; // 先为mainView所在的图层 添加阴影效果 [self addShadowFormainViewWithEndX:sender.tag == 1?1:-1]; // 定义一个临时变量 CGFloat tempEndX = 0; // 左边的按钮被单击 if (1 == sender.tag) { // 隐藏右半边 _leftView.hidden = NO; _rightView.hidden = YES; if (startX == 0) { tempEndX = kLeftWidth; }else if (startX == kLeftWidth){ tempEndX = 0; } } else { // 单击右边按钮, 隐藏左半边 _leftView.hidden = YES; _rightView.hidden = NO; if (startX == 0) { tempEndX = - kRightWidth; }else if (startX == - kRightWidth){ tempEndX = 0; } } // 最后才设置mainView的x,调用抽取出来的公共代码,设置mainView的x,参数是endX [self setmainViewX:tempEndX]; } // 抽取出来的公共代码,设置mainView的x,参数是endX - (void)setmainViewX:(CGFloat)endX { CGRect frame = self.mainView.frame; frame.origin.x = endX; [UIView animateWithDuration:0.2 animations:^{ self.mainView.frame=frame; }]; } // 最关键的方法,左边控制器的代理 方法,当前左边控制器中的某一行被点击的时候 会调用 - (void)leftTableViewRowClicked:(id)columnSelected { Column *column = (Column *)columnSelected; // 1,关闭左边的控制======================= // 调用抽取出来的公共代码,设置mainView的x,参数是endX [self setmainViewX:0]; // 2,更改标题按钮上面的文字 _titleBtn.titleLabel.text = column.columnName; // 根据栏目数据模型中的类名,实例化对应栏目的控制器,并且将其设置为导航控制器的根控制器,最后将导航控制器的view添加到mainView中,目的是方便设置导航条,以及,各控制器的跳转 // 2,从缓存字典中取,如果子控制器字典有曾经创建过的子控制器,直接取出来用 UIViewController *columnVC = _columnViewControllers[column.columnClassName]; // 如果子控制器字典中没有保存过该栏目的控制器,才要创建子控制器 if (columnVC == nil) { Class c = NSClassFromString(column.columnClassName); columnVC = [[c alloc]init]; // 并且一定要将其放到 子控制器字典里面,存起来 [_columnViewControllers setObject:columnVC forKey:column.columnClassName]; } // 4,移除contentView中的正在显示的旧的子view if (_contentView.subviews.count > 0) { UIView *oldView = [_contentView subviews][0]; [oldView removeFromSuperview]; } // 5,最后将子控制器的view添加到contentView中,显示 columnVC.view.frame = _contentView.bounds; [self.contentView addSubview:columnVC.view]; NSLog(@"%@",self.contentView); // 在添加到mainView之前 ,先得到mainView导航控制器的子控制器,并将其移除(如果有的话),然后才将新的栏目对应的子控制器添加到导航控制器容器中,注意,这儿可以用字典 记住 所有的已经实例化出来 的栏目子控制器,这样就避免每次都alloc创建新的栏目子控制器,而是只需要根据类名,从字典取出上一次实例化了的同一栏目的子控制器即可 } @end
栏目数据模型Column.h
// // Column.h // 19_抽屉效果_仿网易 // // Created by beyond on 14-8-1. // Copyright (c) 2014年 com.beyond. All rights reserved. // #import <Foundation/Foundation.h> // 数据模型 代表一个栏目 @interface Column : NSObject // 栏目名称 @property (nonatomic,copy)NSString *columnName; // 栏目图片名称 @property (nonatomic,copy)NSString *columnImgName; // 栏目对应的控制器的类名 @property (nonatomic,copy)NSString *columnClassName; // UI控件用weak,字符串用copy,其他对象用strong // 提供一个类方法,即构造函数,返回封装好数据的对象(返回id亦可) + (Column *)columnNamed:(NSString *)columnName imgName:(NSString*)columnImgName className:(NSString *)columnClassName; @end
栏目数据模型Column.m
// // Column.m // 19_抽屉效果_仿网易 // // Created by beyond on 14-8-1. // Copyright (c) 2014年 com.beyond. All rights reserved. // 数据模型 代表一条栏目 #import "Column.h" @implementation Column // 返回一个包含了 栏目对应控制器名字的 对象实例 + (Column *)columnNamed:(NSString *)columnName imgName:(NSString *)columnImgName className:(NSString *)columnClassName { // 为了兼容子类 使用self Column *column = [[self alloc]init]; column.columnName = columnName; column.columnImgName = columnImgName; column.columnClassName = columnClassName; return column; } @end
左边控制器定义好的协议LeftTableViewControllerDelegate.h
// // LeftTableViewControllerDelegate.h // 19_抽屉效果_仿网易 // // Created by beyond on 14-8-1. // Copyright (c) 2014年 com.beyond. All rights reserved. // #import <Foundation/Foundation.h> #import "Column.h" // 左边控制器 定义的代理/协议 它通过调用自己的成员属性(即代理)的该方法,将数据传递出去(给它的代理去使用) (其实 是主控制器想要数据,所以主控制器在实例化左边控制器的时候,要设置左边控制器对应的代理 为 主控制器 自身) @protocol LeftTableViewControllerDelegate <NSObject> - (void)leftTableViewRowClicked:(Column *)columnSelected; @end
LeftTableViewController.h
// // LeftTableViewController.h // 19_抽屉效果_仿网易 // // Created by beyond on 14-8-1. // Copyright (c) 2014年 com.beyond. All rights reserved. // #import <UIKit/UIKit.h> // 对协议进行提前声明 @protocol LeftTableViewControllerDelegate; @interface LeftTableViewController : UITableViewController // 代理 用weak,防止循环问题,可以是任意类型,但必须遵守协议 @property (nonatomic,weak) id<LeftTableViewControllerDelegate> delegate; @end
LeftTableViewController.m
//
// LeftTableViewController.m
// 19_抽屉效果_仿网易
//
// Created by beyond on 14-8-1.
// Copyright (c) 2014年 com.beyond. All rights reserved.
//
#import "LeftTableViewController.h"
#import "Column.h"
#import "LeftTableViewControllerDelegate.h"
@interface LeftTableViewController ()
{
// 栏目数组,保存的是左边栏目列表中的所有栏目对象
NSArray *_arr;
}
@end
@implementation LeftTableViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// 新闻 栏目
Column *newsColumn = [Column columnNamed:@"新闻" imgName:@"news.png" className:@"NewsViewController"];
// 图片 栏目
Column *picColumn = [Column columnNamed:@"图片" imgName:@"pic.png" className:@"PicViewController"];
// 图片 栏目
Column *commentColumn = [Column columnNamed:@"跟帖" imgName:@"comment.png" className:@"CommentViewController"];
// 以后要添加栏目,只要改这里就可以了
// 将栏目对象,一次性全添加到不可变数组中
_arr = @[newsColumn,picColumn,commentColumn];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return _arr.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellID = @"leftVC";
// 下面这个dequeue只能用于storyboard或xib中
// UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellID forIndexPath:indexPath];
//
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellID];
if (cell == nil) {
cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellID];
}
// 设置独一无二的数据
Column *column = _arr[indexPath.row];
cell.textLabel.text = column.columnName;
cell.imageView.image = [UIImage imageNamed:column.columnImgName];
return cell;
}
// 点击一行时,主控制中的主视图必须展示相应栏目的内容,因此,必须实例化对应点击的行的栏目控制器,并用添加到导航控制器,调用代理 的方法传递数据给代理 使用,此处的代理 其实就是 主控制器
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// 先取消默认的点击 高亮的颜色
[tableView deselectRowAtIndexPath:indexPath animated:YES];
// 取出对应行的数据模型(栏目)
Column *column = _arr[indexPath.row];
if ([self.delegate respondsToSelector:@selector(leftTableViewRowClicked:)]) {
// 传递数据给主控制器 BeyondViewController,通过代理
// 关键代码~
[self.delegate leftTableViewRowClicked:column];
}
}
@end
RightViewController.xib
NewsViewController.xib
PicViewController.xib
CommentViewController.xib