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


Category: iOS   Publish Date: 2013年4月30日    Comments: 9   Views: 1287

唠叨最近

好久没有写技术日志了,上一篇日志已经是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下载最新的代码

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值