iOS页面右滑返回交互实现方案
来源:互联网 发布:南极洲有多大 知乎 编辑:程序博客网 时间:2024/06/16 02:10
唠叨最近
好久没有写技术日志了,上一篇日志已经是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吧,原理很简单。
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页面右滑返回交互实现方案
- iOS 7.0实现右滑返回
- iOS系统右滑返回全局控制方案
- iOS系统右滑返回全局控制方案
- iOS 右滑返回
- iOS右滑返回
- iOS开发 指定页面关闭右滑手势返回功能
- iOS自定义NavigationBar后如何实现全屏右滑返回
- iOS 实现全屏右滑返回功能解析!
- Android实现类似IOS右滑返回的效果
- iOS右滑返回问题
- IOS 取消右滑返回
- iOS 为自定义返回按钮的页面添加右滑返回
- iOS 为自定义返回按钮leftBarButtonItem的页面添加右滑返回功能
- iOS 自定义返回按钮,仍实现右滑返回的功能.
- iOS 7 UINavigationController 右滑 返回
- iOS 8 完美禁止右滑返回
- 仿ios右滑界面返回功能
- android应用开发之AsyncTask
- UVALive 5058
- oracle删除数据文件 解决办法
- Javascript入门教程
- Javascript入门教程
- iOS页面右滑返回交互实现方案
- Unity Tweak Tool使用
- 机器告诉我对了百分之12
- 稻盛和夫_经典语录
- 高通的android4.2的代码中SD卡驱动分析
- 《Linux内核并发与竞态》分析Linux2.6.29内核死锁
- android应用开发之HttpConnection
- 卡特兰数-N个结点二叉树个数
- sublime 编译C++