没错,这又是关于地图方面的一篇文章,如果读者看过我前几篇文章的话,是不是会很好奇为啥最近老是写地图,哈哈,主要是因为最近一直在搞地图了,从重构到现在算起来也快有一个月的时间了,基本每天到公司后的第一件事,就是打开地图页,一边滑动着地图,一边对照着代码和官方文档,看看有哪些地方可以继续优化。不出意外的话,你会发现,要优化的地方,还真不少。。
之前我们已经对地图页的代码结构进行过优化,并且针对特定的地图高亮多边形需求,提出过通用的解决方案,前几天还给过地图阻尼运动的简易实现方式。然后我就发现一件事情,其实除了代码结构这点,另外两点,包括今天要讲述的地图惯性缩放,其实都应该是百度地图api
的事情。
只是由于种种原因,百度地图没有提供给我们这种便利,那就需要我们自力更生。
好了,下面是我们今天要讨论的事情,如何hook百度地图的私有方法,给我们的地图添加惯性缩放的效果。
首先,什么是地图惯性缩放?
注意是缩放,不是移动。
移动是一个手指在地图上划过,这个是有惯性的,也就是说,你的手指离开后,地图不会立马停止,而是会有一个逐渐减速的效果。但是缩放是什么,缩放是两个手指做捏合运动来改变地图的层级,这个在百度开放给我们的效果中是没有惯性的,也就是说一旦你的两个手指离开屏幕,你的地图会立马停止运动,几乎没有什么物理世界中的惯性效果。
就像是你开着车在行驶,快到家门的时候,你松开了油门,你希望你的车能够靠着惯性走完剩下的一小段路,可是你发现在你松开油门的时候,你的车也立马停止运动了,先不说你回不了家了,这突如其来的刹车,还有可能会闪了你的老腰,导致你上不了床了。(好吧,我承认,一不小心开车了)
思考:如何实现地图惯性缩放。
为了让大伙不闪到腰,我们需要思考怎么解决这个事情。思考之前我们需要跳出百度api
的束缚,单纯的从数学角度来思考这个惯性运动的问题。
然后你就会发现,这个问题其实很简单,就是一个抛物线问题,并且是地图层级zoomLevel
关于时间t
的一元二次函数问题。
先上一张图:
level-time抛物线.png图中显示的是一条抛物线,横轴是时间t,纵轴是对应时间下的地图级别,其中_tPinStart
是双指刚开始接触屏幕的时间,也就是刚开始缩放运动时候的时间,_tPinEnd
是缩放运动结束时候的时间,对应的_levelPinStart
与_levelPinEnd
分别是缩放运动开始与结束时候的地图层级。
对一元二次方程求导数,可以得到对应时间下的速率,很明显,在A2点,也就是在手势离开的时候,地图的速度_vPinEnd
并不为0。
从抛物线中我们可以看到,这条曲线速率为0的时候,其实正是它的抛物线顶点,这个时候它对应的点是图中的AStirless
点,对应的时间是_tStirless
。如果想让地图缩放有一个惯性效果的话,也就是说不让它戛然而止而闪到腰,那么我们就需要在手势移开后,让地图的层级按照图中抛物线_tPinEnd
到_tStirless
之间的运动轨迹变化。
原理知道了,就是一个求解抛物线的问题,那么如何获取这条抛物线的方程呢。
先复习下一元二次方程的通式:
y = aX^2 + bX + c
如果想求解这个方程,那么我们起码需要知道以下几点:
- 捏合手势开始时候的时间与此时对应的地图层级,也就是A1点的坐标。
- 捏合手势离开时候的时间与此时对应的地图层级,也就是A2点的坐标。
现在假如我们知道了这两个点,可是两个方程要解出a,b,c
三个未知数是不可能的,仔细回想一下一元二次方程的特点,我们会发现二次项系数a
的值不一般,这是一个决定抛物线开口大小的参数,并且参数a
的绝对值越大,开口越小,意味着曲率越大,曲率越大意味着,相同单位时间内的y
值变化越大。换句话说,这个二次项系数是一个影响惯性大小的参数,所以这个参数,我们可以自己设定。
既然二次项的值,可以自己设定,以便于我们控制惯性的大小,那么在知道A1点与A2点的情况下,我们就可以得到一次项系数b和常数项c的值了。
问题转变成如何获得捏合手势开始与结束时候的
A1点
与A2点
坐标。
这个问题看起来比较简单,可是在百度地图没有直接给你的时候,就变得比较费事了。如果你有好的方式,请告诉我,我相信简单的才是好的。如果你还没有找到好的方式,那么不妨继续往下看看我是如何一步一步找到的。
-
首先使用苹果私有的调试工具,获取地图的层级结构,然后逐个分析捏合手势最有可能发生在哪一层上。简单介绍下它的几个重要的层级结构,
BaiduMapEAGLView
(绘制openGL的图层),TapDetctingView
(负责展示标注的图层),以上两个图层,都是添加在地图私有的MapView
上的。而这个MapView
又是放在BMKMapView
上的,BMKMapView
就是百度地图开放给我们的mapView
,好吧,想说,开放的好少,上面提到的只开放了一个。最后我们将目标定位到了TapDetectingView
上。 -
然后我们通过
Runtime
工具,将这个可疑的TapDetectingView
的所有实例方法都打印了出来,挨个去找,希望能有什么发现。下面简单罗列几个方法:
Some InstanceMethods Of TapDetctingView:(
handleSingleTap, //处理单击事件
"handleForceTouch:", //处理重压事件
handleDoubleBeginTouchPoint, //开始处理两个手指的点击事件(可疑方法)
handleDoubleMoveTouchPoint, //处理两个手指的运动事件(可疑方法)
handleMoveTouchPoint, //处理单个手指的运动事件
"handleScale:", //暂时不知道,但是scale跟缩放有关(可疑方法)
"handleRoate:", //处理旋转事件
handleEndTouchPoint, //处理单个手指的结束事件
handleDoubleEndTouchPoint, //处理两个手指的结束事件(可疑方法)
handleTwoFingerTap, //处理两个手指的点击事件
)
在上面的部分方法中,我们根据方法名定位出了四个可能跟地图缩放有关的方法,事实证明,处理缩放事件手势的也确实是这四个方法。
从方法名看,那四个方法确实可疑,可是我们该如何进一步确定呢。
使用
Aspects
来 hookTapDetctingView
的所有方法。
我们使用了一个轻量级的面向切面编程的库Aspects来实现我们的目的。在TapDetctingView
的每一个方法执行完后都会进入我们自己的代码块中,在我们的代码块中,做了打印方法名的操作。这样我们就可以在地图运动中,来看具体调用了哪个方法。
#pragma mark 处理百度地图的相关私有视图
- (void)handleRelevantPrivateViewOfBaiduMap{
id mapViewClass = NSClassFromString(@"MapView");
for (UIView *subView in _mapView.subviews) {
if ([subView isKindOfClass:[mapViewClass class]]) {
id TapDetectingView = NSClassFromString(@"TapDetectingView");
for (UIView *subclassView in subView.subviews) {
if ([subclassView isKindOfClass:[TapDetectingView class]]) {
[self hookMethodsInTapDetectingView:subclassView];
}
}
}
}
}
#pragma mark hook地图私有视图的相关方法
- (void)hookMethodsInTapDetectingView:(UIView *)mapTapDetectingView{
void (^MethodHookedBlock)(id<AspectInfo>,...) = ^(id<AspectInfo>hookedObject,...){
NSInvocation *invocation = [hookedObject originalInvocation];
SEL hookedSelector = invocation.selector;
NSLog(@"hookedSelector = %@",NSStringFromSelector(hookedSelector));
};
for (NSString * selString in [mapTapDetectingView.class arrayOfInstanceMethods]) {
[mapTapDetectingView aspect_hookSelector:NSSelectorFromString(selString) withOptions:AspectPositionAfter usingBlock:MethodHookedBlock error:nil];
};
}
通过上面的代码,再通过我们慢慢地操作地图,查看打印的方法,我们就可以获得当前响应的是哪个方法了,读者可以自己试试,总之最后我们确定了,跟地图缩放有关的就是那四个方法。这里需要注意的是那个handleScale:
函数,这个函数有一个参数,这个参数是正的时候,说明地图等级正在变大,为负的时候,说明等级正在变小。
那么现在我们想要在开始缩放和缩放结束的时候获取A1点与A2点就容易多了。因为zoomLevel
是很好获取的,而我们只需要再记录两个时间戳就可以了。这样两个方程,两个未知数,我们可以求出一次项的系数与常数项的值,从而进一步确定手势离开屏幕的时候,这条即时的抛物线的函数了。
现在我们得到了手势离开时候的地图等级
zoomLevel
相对于时间time
的抛物线,那么我们就让它按照既定的轨迹继续运动吧。
得到抛物线方程后,我们就可以计算出速率为0时候的时间了,
一元二次方程 :y = aX^2 + bX + c
求导数 :v = 2aX+ b
所以v = 0时,X = -b/2a;
这意味着惯性运动停止的时候,时间是_tStirless = -b/2a
(a
和b
分别是二次项与一次项的系数,此时都是可知的了),这样我们就可以得到手指离开后,它应该靠惯性再运动多长一段时间了。也就是_tStirless - _tPinEnd
。
看到这里,是不是累了,好吧,马上就结束了,我们需要把这段时间均分一下,然后带入方程,获取此时对应的zoomLevel
,然后每隔均分的时间,就更新一下zoomLevel,这个步骤的思路就跟上篇的地图阻尼运动的思路一致了。代码如下:
/*
参数说明:
•NSTimeInterval _tPinEnd:记录捏合手势开始的时间。
•NSTimeInterval _tPinStart:记录捏合手势结束的时间。
•NSTimeInterval _tPinMoving: 记录双指在地图上的的捏合时长。
_tPinMoving = _tPinEnd - _tPinStart;
• NSTimeInterval _tStirless:记录惯性运动停止,速度为0(也就是静止)时候的时间。
_tStirless-_tPinMoving可以得出手指离开后,惯性运动的时长
*/
#pragma mark 刷新抛物线上的y值:也就是地图的zoomlevel,时间段在这里分成了10份
- (void)parabola_refreshMapZoomLevel{
float unitZoomLevelDuringTime = (_tStirless - _tPinMoving)/MAP_ZOOM_NUMS;
for (int i = 1; i<=MAP_ZOOM_NUMS; i++) {
float tempZoomLevel = [self parabola_calculateMapZoomLevelAtRealTime:_tPinMoving+i*unitZoomLevelDuringTime];
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (uint64_t)(NSEC_PER_SEC * (i*unitZoomLevelDuringTime)));
dispatch_after(time, dispatch_get_main_queue(), ^{
[_mapView setZoomLevel:tempZoomLevel];
});
}
}
/*
参数说明:
•_quadraticCoefficient:惯性运动方程的二次项系数
•_oneCoefficient :惯性运动方程的一次项系数
•_constantCoefficient:惯性运动方程的常数项
*/
#pragma mark 实时计算抛物线上time所对应的maplevel
- (CGFloat)parabola_calculateMapZoomLevelAtRealTime:(float)realTime{
return _quadraticCoefficient *(realTime *realTime) + _oneCoefficient * realTime + _zoomLevelPinStart;
}
好了,写到这里基本就实现了我们的惯性效果,在这里只列出了部分的关键代码,还有很多其他的没有罗列,都罗列的话,篇幅就比较长了,建议自己下载demo查看具体实现,demo中对手势持续时间超过0.8s的,不做惯性处理,因为超过这个时间的话,那么有很大的可能性证明此时的用户正在认真的查找某一层级的东西,这时候就不对它进行惯性缩放了。
更新:
最近将地图惯性运动相关的代码,抽离到了BMKMapViewAdapter
类中,该类提供更简洁的api如下:
/**
@param mapView mapView description
@param inertiaCoefficient 惯性系数,系数越大,惯性越大,越不容易改变。
*/
+ (void)mapView:(BMKMapView *)mapView openInertiaDragWithCoefficient:(float)inertiaCoefficient;
/**
@param mapView mapView description
@param close close description
*/
+ (void)mapView:(BMKMapView *)mapView closeMapInertialDrag:(BOOL)close;
除了对核心代码的抽离,对于最后一步(也就是从双手离开屏幕到它完全停止的这个过程)也改变了它的实现方式,之前是将这段时间分成N份,然后逐步设置当前时间点下的zoomlevel
,现在的话,采用CADisplaylink,使之设置level的频率与屏幕的刷新频率一致,并且取消了原来GCD的使用,而改用performSelector:afterDelay:
这样就可以方便的进行取消操作了,代码如下:
static int zoomDisplayCount;
static CADisplayLink *displayLink;
/**
双指离开屏幕后,还会继续运动的时间
@param time time description
*/
- (void)startAnimateWithtimeDuration:(NSTimeInterval)time{
if (time > 0) {
if (displayLink) {
displayLink.paused = YES;
}
displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(changeMapLevelByStep)];
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
displayLink.paused = NO;
[self performSelector:@selector(moveDidEnd) withObject:nil afterDelay:time];
}
}
/**
逐步改变地图的level级别
*/
- (void)changeMapLevelByStep{
zoomDisplayCount ++;
float time = zoomDisplayCount/60.f;
float tempZoomLevel = [self parabola_calculateMapZoomLevelAtRealTime:_tPinMoving + time];
[_mapView setZoomLevel:tempZoomLevel];
}
/**
运动停止
*/
- (void)moveDidEnd{
displayLink.paused = YES;
[displayLink invalidate];
displayLink = nil;
zoomDisplayCount = 0;
}
地图惯性缩放.gif
建议下载demo,体验效果。
传送门:地图系列的其他文章:
给你的地图模块动手术 -----一种轻量级的地图解决方案。
地图阻尼运动
给你的地图点灯
网友评论