Category: iOS Publish Date: 2013年4月30日 Comments:
9 Views: 1287
undefined undefined undefined undefined undefined undefined undefined undefined
唠叨最近
好久没有写技术日志了,上一篇日志已经是1年前写的了。
不过,我也发现,几乎全部我认识搞技术的同学朋友,毕业之后也没更新Blog了。或许因为工作太忙,也或许当时写技术日志就是为了找工作。
对于我自己来说,上面两个原因都有,另外还有一个原因就是,加入百度不久之后就开始搞iOS开发,在很大一段时间内,基本是处于学习阶段,基础知识一般都是信息量大但是难度低,动不动就写基础的学习笔记会减低学习进度,且意义不大。
现在,我已经在iOS领域学习了大半年,也参与过4+个项目开发,可以算入门吧。
年头的时候自己买了一年个人开发者帐号,把一些平时不敢用公司开发者帐号随便弄的东西都弄了一遍,也算完善iOS知识体系;更重要的时,从此灵感不断,想出了很多自己想做的小产品,小组件,小工具之类的。
所以,我也决定重新写技术日志,记录一些想法/灵感/经验,同时,也顺带锻炼一些文字表达能力。
之后,估计前端技术相关的日志会相对较少,主要以iOS技术为主。想了好久,还是决定第一篇日志就写一下最近开发的一个小组件,估计内容比较浅显且简短。
MultiLayerNavigation
缘由
前段时间更新了网易客户端和新浪微博客户端,发现它们都有一种很好的交互,就是又右滑页面,随当前页面滑动离开屏幕,上一页联动地由远及近地展现出来。你可以点击这里 或者通过下面视频看演示效果动画。
然后,我在想,如何实现这种效果?如何做成一个通用的组件?
思路
说到通用,我就想到这个交互功能组件的实现方式要不集成在UINavigationController中,要不在其上面扩展;然后我最终的决定是通过子类化方法来扩展实现这个方法。主要原因是,子类可以重写push/pop方法以及touch...系列方法,这样开发者只需要用这个子类(MLNavigationController)代替UINavigationController或者继承自它,即可。最大限度地简化了接口且解耦;
至于实现,我一开始想到就办法也很简单,不外乎就是把UINavigationController里面的viewController们的view与触摸点位置联动地实现一些移动缩放动画而已。
但是,这时候我想到一个问题,UINavigationController中的navigationBar是共用的,但是滑动途中,两个页面都需要展示出各自的bar,难道在滑动途中还要把那个navigationBar复制出来,但是复制出来带来的问题又有许多……
于是我分别研究了一下网易和新浪客户端的交互效果,发现它们是有一些区别的。
比较明显的一个地方就是,新浪微博的navigationBar是公共的(应该是属于UINavigationController的),网易新闻的是独立的(应该属于各个页面的);这其实很容易看出来,点击返回的时候,看navigationBar跟随页面滑动还是只是内容渐隐渐显。
独立的navigationBar明显是不存在这个问题的,但是新浪微博是navigationBar是共用的,却依然能完好地完成目标交互。
这时候,我就想到的一个比复制view更好办法了:复制页面快照。
实现
按照上面的思路,实现这个交互应该没有什么问题了。
至于我的实现是这样的:
- 创建一个UINavigationController的子类,每次在push的时候,先把当前页面视图快照截取一下,把快照塞到快照堆栈里头,然后pop的时候把快照拿出来。这样可以保证快照栈和viewController栈保持一致。
- 当用户开始往右拉动页面的时候,把上一个页面的快照拿出来,创建成一个背景view,然后当前页面和上一页面的远近大小都会联动展示。
- 当用户拉动到大于某个数值的时候,页面会自动右滑消失;而上一级页面则展现;然后我们把消失的页面快照也pop出来;
- 当用户开始拉到小于某个数值的时候,页面会回复原来位置和状态,快照栈不需要改变。
代码
懒得解释了,直接看comment吧,原理很简单。
009 |
#define KEY_WINDOW [[UIApplication sharedApplication]keyWindow] |
011 |
@interface MLNavigationController () |
015 |
UIImageView *lastScreenShotView; |
019 |
@property ( nonatomic , retain ) UIView *backgroundView; |
020 |
@property ( nonatomic , retain ) NSMutableArray *screenShotsList; |
022 |
@property ( nonatomic ,assign) BOOL isMoving; |
026 |
@implementation MLNavigationController |
028 |
- ( id )initWithNibName:( NSString *)nibNameOrNil bundle:( NSBundle *)nibBundleOrNil |
030 |
self = [ super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; |
034 |
self .screenShotsList = [[[ NSMutableArray alloc]initWithCapacity:2]autorelease]; |
035 |
self .canDragBack = YES ; |
043 |
self .screenShotsList = nil ; |
045 |
[ self .backgroundView removeFromSuperview]; |
046 |
self .backgroundView = nil ; |
057 |
UIImageView *shadowImageView = [[[ UIImageView alloc]initWithImage:[ UIImage imageNamed:@ "leftside_shadow_bg" ]]autorelease]; |
058 |
shadowImageView.frame = CGRectMake(-10, 0, 10, self .view.frame.size.height); |
059 |
[ self .view addSubview:shadowImageView]; |
061 |
UIPanGestureRecognizer *recognizer = [[[ UIPanGestureRecognizer alloc]initWithTarget: self |
062 |
action: @selector (paningGestureReceive:)]autorelease]; |
063 |
[recognizer delaysTouchesBegan]; |
064 |
[ self .view addGestureRecognizer:recognizer]; |
068 |
- ( void )pushViewController:( UIViewController *)viewController animated:( BOOL )animated |
070 |
[ self .screenShotsList addObject:[ self capture]]; |
072 |
[ super pushViewController:viewController animated:animated]; |
076 |
- ( UIViewController *)popViewControllerAnimated:( BOOL )animated |
078 |
[ self .screenShotsList removeLastObject]; |
080 |
return [ super popViewControllerAnimated:animated]; |
083 |
#pragma mark - Utility Methods - |
088 |
UIGraphicsBeginImageContextWithOptions ( self .view.bounds.size, self .view.opaque, 0.0); |
089 |
[ self .view.layer renderInContext: UIGraphicsGetCurrentContext ()]; |
091 |
UIImage * img = UIGraphicsGetImageFromCurrentImageContext (); |
093 |
UIGraphicsEndImageContext (); |
099 |
- ( void )moveViewWithX:( float )x |
102 |
NSLog (@ "Move to:%f" ,x); |
106 |
CGRect frame = self .view.frame; |
108 |
self .view.frame = frame; |
110 |
float scale = (x/6400)+0.95; |
111 |
float alpha = 0.4 - (x/800); |
113 |
lastScreenShotView.transform = CGAffineTransformMakeScale(scale, scale); |
114 |
blackMask.alpha = alpha; |
118 |
#pragma mark - Gesture Recognizer - |
120 |
- ( void )paningGestureReceive:( UIPanGestureRecognizer *)recoginzer |
123 |
if ( self .viewControllers.count <= 1 || ! self .canDragBack) return ; |
126 |
CGPoint touchPoint = [recoginzer locationInView:KEY_WINDOW]; |
129 |
if (recoginzer.state == UIGestureRecognizerStateBegan ) { |
132 |
startTouch = touchPoint; |
134 |
if (! self .backgroundView) |
136 |
CGRect frame = self .view.frame; |
138 |
self .backgroundView = [[[ UIView alloc]initWithFrame:CGRectMake(0, 0, frame.size.width , frame.size.height)]autorelease]; |
139 |
[ self .view.superview insertSubview: self .backgroundView belowSubview: self .view]; |
141 |
blackMask = [[[ UIView alloc]initWithFrame:CGRectMake(0, 0, frame.size.width , frame.size.height)]autorelease]; |
142 |
blackMask.backgroundColor = [ UIColor blackColor]; |
143 |
[ self .backgroundView addSubview:blackMask]; |
146 |
self .backgroundView.hidden = NO ; |
148 |
if (lastScreenShotView) [lastScreenShotView removeFromSuperview]; |
150 |
UIImage *lastScreenShot = [ self .screenShotsList lastObject]; |
151 |
lastScreenShotView = [[[ UIImageView alloc]initWithImage:lastScreenShot]autorelease]; |
152 |
[ self .backgroundView insertSubview:lastScreenShotView belowSubview:blackMask]; |
155 |
} else if (recoginzer.state == UIGestureRecognizerStateEnded ){ |
157 |
if (touchPoint.x - startTouch.x > 50) |
159 |
[ UIView animateWithDuration:0.3 animations:^{ |
160 |
[ self moveViewWithX:320]; |
161 |
} completion:^( BOOL finished) { |
163 |
[ self popViewControllerAnimated: NO ]; |
164 |
CGRect frame = self .view.frame; |
166 |
self .view.frame = frame; |
173 |
[ UIView animateWithDuration:0.3 animations:^{ |
174 |
[ self moveViewWithX:0]; |
175 |
} completion:^( BOOL finished) { |
177 |
self .backgroundView.hidden = YES ; |
184 |
} else if (recoginzer.state == UIGestureRecognizerStateCancelled ){ |
186 |
[ UIView animateWithDuration:0.3 animations:^{ |
187 |
[ self moveViewWithX:0]; |
188 |
} completion:^( BOOL finished) { |
190 |
self .backgroundView.hidden = YES ; |
198 |
[ self moveViewWithX:touchPoint.x - startTouch.x]; |
问题
1.未解决webview不响应手势的问题;
这个问题是个经典问题:webview不响应手势。网上有很多办法,能work的貌似就一个:重写UIWindow的sentEvent方法,首先截取到窗口事件,然后再去分析一下是否是在webview上的手势,是的话,把事件首先抛给MLNavigationController,然后按照里面的逻辑去处理。之所以没有把这个solution写进组件,原因有:1、需子类化UIWindow,侵入性太强了;2、webview之所以不响应手势,原因是webview展现网页内容,往往需要横向滑动,这个交互动作如果两者都响应,就会发生冲突,估计UIScrollView的横向滑动也会有这个问题(未测)。
2.未解决当用户直接setViewController的问题。
改变UINavigationController的viewControllers堆栈的办法有三类:push/pop/setViewControllers,由于我们需要在新页面切入前,给旧页面来一张快照,然后pop之后就会把快照拿掉。快照堆栈的和viewControllers的同步,是在push/pop里面实现的,但setViewControllers是可以随意设置堆栈的,这使得我们要同步快照会变得复杂很多,我现在也甚至怀疑如果一个只初始化一个Controllers放进堆栈的非顶层,页面是否会被绘制?(即loadView方法和viewDidLoad方法会被被执行?)这个后面我验证一下,不过,相信这个问题不难解决。
下载
请到Github下载最新的代码