iOS页面右滑返回交互实现方案


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更好办法了:复制页面快照。

 

实现

按照上面的思路,实现这个交互应该没有什么问题了。

至于我的实现是这样的:

  1. 创建一个UINavigationController的子类,每次在push的时候,先把当前页面视图快照截取一下,把快照塞到快照堆栈里头,然后pop的时候把快照拿出来。这样可以保证快照栈和viewController栈保持一致。
  2. 当用户开始往右拉动页面的时候,把上一个页面的快照拿出来,创建成一个背景view,然后当前页面和上一页面的远近大小都会联动展示。
  3. 当用户拉动到大于某个数值的时候,页面会自动右滑消失;而上一级页面则展现;然后我们把消失的页面快照也pop出来;
  4. 当用户开始拉到小于某个数值的时候,页面会回复原来位置和状态,快照栈不需要改变。

 

代码

懒得解释了,直接看comment吧,原理很简单。

001 //
002 //  MLNavigationController.m
003 //  MultiLayerNavigation
004 //
005 //  Created by Feather Chan on 13-4-12.
006 //  Copyright (c) 2013年 Feather Chan. All rights reserved.
007 //
008  
009 #define KEY_WINDOW  [[UIApplication sharedApplication]keyWindow]
010  
011 @interface MLNavigationController ()
012 {
013     CGPoint startTouch;
014      
015     UIImageView *lastScreenShotView;
016     UIView *blackMask;
017 }
018  
019 @property (nonatomic,retain) UIView *backgroundView;
020 @property (nonatomic,retain) NSMutableArray *screenShotsList;
021  
022 @property (nonatomic,assign) BOOL isMoving;
023  
024 @end
025  
026 @implementation MLNavigationController
027  
028 - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
029 {
030     self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
031     if (self) {
032         // Custom initialization
033          
034         self.screenShotsList = [[[NSMutableArray alloc]initWithCapacity:2]autorelease];
035         self.canDragBack = YES;
036          
037     }
038     return self;
039 }
040  
041 - (void)dealloc
042 {
043     self.screenShotsList = nil;
044      
045     [self.backgroundView removeFromSuperview];
046     self.backgroundView = nil;
047      
048      
049     [super dealloc];
050 }
051  
052 - (void)viewDidLoad
053 {
054     [super viewDidLoad];
055     // Do any additional setup after loading the view.
056      
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];
060      
061     UIPanGestureRecognizer *recognizer = [[[UIPanGestureRecognizer alloc]initWithTarget:self
062                                                                                  action:@selector(paningGestureReceive:)]autorelease];
063     [recognizer delaysTouchesBegan];
064     [self.view addGestureRecognizer:recognizer];
065 }
066  
067 // override the push method
068 - (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
069 {
070     [self.screenShotsList addObject:[self capture]];
071      
072     [super pushViewController:viewController animated:animated];
073 }
074  
075 // override the pop method
076 - (UIViewController *)popViewControllerAnimated:(BOOL)animated
077 {
078     [self.screenShotsList removeLastObject];
079      
080     return [super popViewControllerAnimated:animated];
081 }
082  
083 #pragma mark - Utility Methods -
084  
085 // get the current view screen shot
086 - (UIImage *)capture
087 {
088     UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, self.view.opaque, 0.0);
089     [self.view.layer renderInContext:UIGraphicsGetCurrentContext()];
090      
091     UIImage * img = UIGraphicsGetImageFromCurrentImageContext();
092      
093     UIGraphicsEndImageContext();
094      
095     return img;
096 }
097  
098 // set lastScreenShotView 's position and alpha when paning
099 - (void)moveViewWithX:(float)x
100 {
101      
102     NSLog(@"Move to:%f",x);
103     x = x>320?320:x;
104     x = x<0?0:x;
105      
106     CGRect frame = self.view.frame;
107     frame.origin.x = x;
108     self.view.frame = frame;
109      
110     float scale = (x/6400)+0.95;
111     float alpha = 0.4 - (x/800);
112  
113     lastScreenShotView.transform = CGAffineTransformMakeScale(scale, scale);
114     blackMask.alpha = alpha;
115      
116 }
117  
118 #pragma mark - Gesture Recognizer -
119  
120 - (void)paningGestureReceive:(UIPanGestureRecognizer *)recoginzer
121 {
122     // If the viewControllers has only one vc or disable the interaction, then return.
123     if (self.viewControllers.count <= 1 || !self.canDragBack) return;
124      
125     // we get the touch position by the window's coordinate
126     CGPoint touchPoint = [recoginzer locationInView:KEY_WINDOW];
127      
128     // begin paning, show the backgroundView(last screenshot),if not exist, create it.
129     if (recoginzer.state == UIGestureRecognizerStateBegan) {
130          
131         _isMoving = YES;
132         startTouch = touchPoint;
133          
134         if (!self.backgroundView)
135         {
136             CGRect frame = self.view.frame;
137              
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];
140              
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];
144         }
145          
146         self.backgroundView.hidden = NO;
147          
148         if (lastScreenShotView) [lastScreenShotView removeFromSuperview];
149          
150         UIImage *lastScreenShot = [self.screenShotsList lastObject];
151         lastScreenShotView = [[[UIImageView alloc]initWithImage:lastScreenShot]autorelease];
152         [self.backgroundView insertSubview:lastScreenShotView belowSubview:blackMask];
153          
154         //End paning, always check that if it should move right or move left automatically
155     }else if (recoginzer.state == UIGestureRecognizerStateEnded){
156          
157         if (touchPoint.x - startTouch.x > 50)
158         {
159             [UIView animateWithDuration:0.3 animations:^{
160                 [self moveViewWithX:320];
161             } completion:^(BOOL finished) {
162                  
163                 [self popViewControllerAnimated:NO];
164                 CGRect frame = self.view.frame;
165                 frame.origin.x = 0;
166                 self.view.frame = frame;
167                  
168                 _isMoving = NO;
169             }];
170         }
171         else
172         {
173             [UIView animateWithDuration:0.3 animations:^{
174                 [self moveViewWithX:0];
175             } completion:^(BOOL finished) {
176                 _isMoving = NO;
177                 self.backgroundView.hidden = YES;
178             }];
179              
180         }
181         return;
182          
183         // cancal panning, alway move to left side automatically
184     }else if (recoginzer.state == UIGestureRecognizerStateCancelled){
185          
186         [UIView animateWithDuration:0.3 animations:^{
187             [self moveViewWithX:0];
188         } completion:^(BOOL finished) {
189             _isMoving = NO;
190             self.backgroundView.hidden = YES;
191         }];
192          
193         return;
194     }
195      
196     // it keeps move with touch
197     if (_isMoving) {
198         [self moveViewWithX:touchPoint.x - startTouch.x];
199     }
200 }
201  
202 @end

 

问题

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下载最新的代码

你可能感兴趣的:(iOS页面右滑返回交互实现方案)