最近公司SDK新搞了个功能,手势滑动地图后,要具备惯性滑动效果的功能。安卓是先做出来了,然后给我看,由于我早体验过某鸟地图,某鸟地图也有这种效果,加上安卓做得确实不错,还在忙着研究OpenGL的我也只能先放下手中活,看着新功能默默构思了。
先把结果放出来:
寅时室内地图.gif讲一下写这篇文章的原因:安卓是由于有系统的api,在滑动手势结束后调用系统自有api,传入手势结束时的速度(x方向和y方向)就能由系统自己做完往后的操作。而iOS并没有,但我还是自以为这个功能很好做...然而构思之后发现还得找百度啊,但百度给我的结果却没有一个能满足我。所以,在我做出这个效果之后,我得将它分享出来,给有需要的人提供思路,也希望能相互讨论,接受到更好的办法做出更好的效果。(这就跟UIScrollView的滑动效果类似,但是网上是没有代码资料的)
为了公司利益考虑,文章代码我专门写了demo来演示。
进入正题:
1.明确我们的目的:手势滑动后拥有惯性滑动效果
2.思考具体实现:手滑得越快,作用对象的惯性越大,运动时间越长,手滑得慢,作用对象的运动速度就越小,运动时间也越短
3.出现的一些小问题:解决它
OK,想到第2点就已经可以成为嘴强王者了,接下来就看操作是不是青铜了:
demo效果如下:
自写demo.gif请大家不要看gif图好像有点卡,实际是一点都不卡的,很流畅很自然!
动.gif
demo中使用了两种方法让其做惯性滑动。
一、第一种是在手势结束后通过UIView的动画来改蓝色图片的center,因为系统UIView的动画有快进慢出UIViewAnimationOptionCurveEaseOut这种效果可选。
-(void)paned:(UIPanGestureRecognizer *)pan
{
CGPoint locationPoint = [pan locationInView:self.view]; //手指点
CGPoint transPoint = [pan translationInView:self.view]; //移动点
// if (CGRectContainsPoint(blueImgView.frame, locationPoint)) {//若要只作用于蓝地图,以下代码移到此处
//}
blueImgView.center = CGPointMake(blueImgView.center.x+transPoint.x, blueImgView.center.y+transPoint.y);
[pan setTranslation:CGPointZero inView:self.view];
if (pan.state == UIGestureRecognizerStateEnded) {
CGPoint velocity = [pan velocityInView:self.view]; //手指离开时x和y方向速度,单位是points/second
CGFloat magnitude = sqrtf((velocity.x * velocity.x) + (velocity.y * velocity.y)); //真实速度
CGFloat slideMult = magnitude / 200; //自己试出来的比例,改动此处可修改灵敏度
float slideFactor = 0.1 * slideMult;
CGPoint finalPoint = CGPointMake(pan.view.center.x + (velocity.x * slideFactor),
pan.view.center.y + (velocity.y * slideFactor));
finalPoint.x = MIN(MAX(finalPoint.x, 0), self.view.bounds.size.width);
finalPoint.y = MIN(MAX(finalPoint.y, 0), self.view.bounds.size.height);
[UIView animateWithDuration:slideFactor delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ //slideFactor秒内做完改变center的动画,动画效果快进慢出(先快后慢)
blueImgView.center = finalPoint;
} completion:nil];
}
}
重点是看UIGestureRecognizerStateEnded里的处理
CGPoint velocity = [pan velocityInView:self.view];这个方法可以获取手势离开时在x,y方向的速度,单位是点每秒(逻辑尺寸点)。
接着就是根据x、y的速度求出总速度,大家可以输出下velocity,看看它的数据,找到它的规律(我就是这样多次看,看出来的)。根据我们手滑动的快慢,velocity值也会跟着变化,总速度magnitude也会跟着变化,当然是手滑越快magnitude越大,越慢magnitude越小,那么,时间就用magnitude来确定吧,然后就试出来了除以200。另外我们根据velocity知道它在x,y方向上的速度,确定了运动时间,当然也能知道这段时间内它移动的距离:即 距离 = 速度 * 时间。 (毕竟读过小学)
然后就是做UIView的动画了。
[UIView animateWithDuration:slideFactor delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ //slideFactor秒内做完改变center的动画,动画效果快进慢出(先快后慢)
blueImgView.center = finalPoint;
} completion:nil];
第一种方法点评:个人觉得不太自然,可能系统UIViewAnimationOptionCurveEaseOut效果并不是很明显吧,当然也很有可能改改代码,调一调灵敏度,效果会好很多。 最重要的是:我们公司的产品用这种UIView的方式是实现不了的,使用的是矩阵transform,所以接下来就开始第二种方法:
二、两种方法的区别在于处理手势滑动事件,第二种方法我们先定义了几个变量对象:
@interface OtherViewController ()
{
UIImageView *blueImgView;
CGAffineTransform viewTransform; //基础self.view的transform
CGAffineTransform currentTransform; //当前transform
CADisplayLink *dis; //定时器
int updateCount; //需要刷新次数
int currentCount; //当前刷新次数
CGPoint velocity; //速度
}
然后在viewDidLoad里将 viewTransform = self.view.transform;
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
blueImgView = [[UIImageView alloc]init];
blueImgView.frame = CGRectMake(50, 100, 100, 100);
blueImgView.image = [UIImage imageNamed:@"地图1"];
[self.view addSubview:blueImgView];
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(paned:)];
[self.view addGestureRecognizer:pan];
viewTransform = self.view.transform;
UIButton *Btn = [UIButton buttonWithType:UIButtonTypeCustom];
Btn.frame = CGRectMake(40, 40, 100, 40);
Btn.backgroundColor = [UIColor grayColor];
[Btn setTitle:@"上个界面" forState:UIControlStateNormal];
[Btn addTarget:self action:@selector(Btn) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:Btn];
}
在手势滑动事件里我们使用到了CADisplayLink,CADisplayLink也是一种定时器,调用时间间隔跟屏幕刷新频率是一致的(1s60次,X出来了,好像是每秒120帧),为了使我们动画效果高效流畅,我们使用这个。
-(void)paned:(UIPanGestureRecognizer *)pan
{
if (dis) {
[dis invalidate];
dis = nil;
}
CGPoint locationPoint = [pan locationInView:self.view]; //手指点
CGPoint transPoint = [pan translationInView:self.view]; //移动点
// if (CGRectContainsPoint(blueImgView.frame, locationPoint)) {//若要只作用于蓝地图,以下代码移到此处
//}
currentTransform = CGAffineTransformTranslate(viewTransform, transPoint.x, transPoint.y);
blueImgView.transform = currentTransform;
if (pan.state == UIGestureRecognizerStateEnded) {
viewTransform = currentTransform;
velocity = [pan velocityInView:self.view];
CGFloat magnitude = sqrtf((velocity.x * velocity.x) + (velocity.y * velocity.y));
CGFloat slideMult = magnitude / 200;
float slideFactor = 0.1 * slideMult;
updateCount = slideFactor * 120 + 1;
currentCount = 0;
dis = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateView)];
[dis addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
}
代码中关于速度的处理跟第一种方式一样,但接下来的动作是确定动画调用次数updateCount,为什么updateCount = slideFactor * 120 + 1;也是试出来的,本来是*60,大家可以自行更改看看效果。
在CADisplayLink调用的方法里:
-(void)updateView
{
currentCount++;
if (currentCount>updateCount || currentCount>60) {
// dis.paused = YES;
[dis removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[dis invalidate];
dis = nil;
}else{
CGPoint point = CGPointMake(velocity.x/30.0/currentCount, velocity.y/30.0/currentCount);
currentTransform = CGAffineTransformTranslate(viewTransform, point.x, point.y);
blueImgView.transform = currentTransform;
viewTransform = currentTransform;
}
}
我们规定调用次数要不多于60次,即作用对象最多运动1s,在作用对象运动过程中
CGPoint point = CGPointMake(velocity.x/30.0/currentCount, velocity.y/30.0/currentCount);
point就是来确定后续运动时x,y方向速度的,velocity是x,y方向的速度,除以30可以得到一个运动较适合的速度值,除以currentCount的原因是让作用对象做减速运动,currentCount在递增,除以currentCount的话,运动速度就是递减了。 (方法完,可自行修改这个速度来改变灵敏度)
总结:所有代码都在上面了,就不往github上放了。要是有帮到大家是我的荣幸,另外夏天热,可以帮我买块西瓜去去暑 %>_<%。
网友评论