沒錯(cuò)吁峻,這又是關(guān)于地圖方面的一篇文章,如果讀者看過我前幾篇文章的話费尽,是不是會(huì)很好奇為啥最近老是寫地圖,哈哈羊始,主要是因?yàn)樽罱恢痹诟愕貓D了旱幼,從重構(gòu)到現(xiàn)在算起來也快有一個(gè)月的時(shí)間了,基本每天到公司后的第一件事突委,就是打開地圖頁(yè)柏卤,一邊滑動(dòng)著地圖,一邊對(duì)照著代碼和官方文檔匀油,看看有哪些地方可以繼續(xù)優(yōu)化缘缚。不出意外的話,你會(huì)發(fā)現(xiàn)敌蚜,要優(yōu)化的地方桥滨,還真不少。。
之前我們已經(jīng)對(duì)地圖頁(yè)的代碼結(jié)構(gòu)進(jìn)行過優(yōu)化齐媒,并且針對(duì)特定的地圖高亮多邊形需求蒲每,提出過通用的解決方案,前幾天還給過地圖阻尼運(yùn)動(dòng)的簡(jiǎn)易實(shí)現(xiàn)方式喻括。然后我就發(fā)現(xiàn)一件事情邀杏,其實(shí)除了代碼結(jié)構(gòu)這點(diǎn),另外兩點(diǎn)双妨,包括今天要講述的地圖慣性縮放淮阐,其實(shí)都應(yīng)該是百度地圖api
的事情。
只是由于種種原因刁品,百度地圖沒有提供給我們這種便利泣特,那就需要我們自力更生。
好了挑随,下面是我們今天要討論的事情状您,如何hook百度地圖的私有方法,給我們的地圖添加慣性縮放的效果兜挨。
首先膏孟,什么是地圖慣性縮放?
注意是縮放拌汇,不是移動(dòng)柒桑。
移動(dòng)是一個(gè)手指在地圖上劃過,這個(gè)是有慣性的噪舀,也就是說魁淳,你的手指離開后,地圖不會(huì)立馬停止与倡,而是會(huì)有一個(gè)逐漸減速的效果界逛。但是縮放是什么,縮放是兩個(gè)手指做捏合運(yùn)動(dòng)來改變地圖的層級(jí)纺座,這個(gè)在百度開放給我們的效果中是沒有慣性的息拜,也就是說一旦你的兩個(gè)手指離開屏幕,你的地圖會(huì)立馬停止運(yùn)動(dòng)净响,幾乎沒有什么物理世界中的慣性效果少欺。
就像是你開著車在行駛,快到家門的時(shí)候馋贤,你松開了油門狈茉,你希望你的車能夠靠著慣性走完剩下的一小段路,可是你發(fā)現(xiàn)在你松開油門的時(shí)候掸掸,你的車也立馬停止運(yùn)動(dòng)了,先不說你回不了家了,這突如其來的剎車扰付,還有可能會(huì)閃了你的老腰堤撵,導(dǎo)致你上不了床了。(好吧羽莺,我承認(rèn)实昨,一不小心開車了)
思考:如何實(shí)現(xiàn)地圖慣性縮放。
為了讓大伙不閃到腰盐固,我們需要思考怎么解決這個(gè)事情荒给。思考之前我們需要跳出百度api
的束縛,單純的從數(shù)學(xué)角度來思考這個(gè)慣性運(yùn)動(dòng)的問題刁卜。
然后你就會(huì)發(fā)現(xiàn)志电,這個(gè)問題其實(shí)很簡(jiǎn)單,就是一個(gè)拋物線問題蛔趴,并且是地圖層級(jí)zoomLevel
關(guān)于時(shí)間t
的一元二次函數(shù)問題挑辆。
先上一張圖:
圖中顯示的是一條拋物線,橫軸是時(shí)間t,縱軸是對(duì)應(yīng)時(shí)間下的地圖級(jí)別孝情,其中_tPinStart
是雙指剛開始接觸屏幕的時(shí)間鱼蝉,也就是剛開始縮放運(yùn)動(dòng)時(shí)候的時(shí)間,_tPinEnd
是縮放運(yùn)動(dòng)結(jié)束時(shí)候的時(shí)間箫荡,對(duì)應(yīng)的_levelPinStart
與_levelPinEnd
分別是縮放運(yùn)動(dòng)開始與結(jié)束時(shí)候的地圖層級(jí)魁亦。
對(duì)一元二次方程求導(dǎo)數(shù),可以得到對(duì)應(yīng)時(shí)間下的速率羔挡,很明顯洁奈,在A2點(diǎn),也就是在手勢(shì)離開的時(shí)候婉弹,地圖的速度_vPinEnd
并不為0睬魂。
從拋物線中我們可以看到,這條曲線速率為0的時(shí)候镀赌,其實(shí)正是它的拋物線頂點(diǎn)氯哮,這個(gè)時(shí)候它對(duì)應(yīng)的點(diǎn)是圖中的AStirless
點(diǎn),對(duì)應(yīng)的時(shí)間是_tStirless
商佛。如果想讓地圖縮放有一個(gè)慣性效果的話喉钢,也就是說不讓它戛然而止而閃到腰,那么我們就需要在手勢(shì)移開后良姆,讓地圖的層級(jí)按照?qǐng)D中拋物線_tPinEnd
到_tStirless
之間的運(yùn)動(dòng)軌跡變化肠虽。
原理知道了,就是一個(gè)求解拋物線的問題玛追,那么如何獲取這條拋物線的方程呢税课。
先復(fù)習(xí)下一元二次方程的通式:
y = aX^2 + bX + c
如果想求解這個(gè)方程闲延,那么我們起碼需要知道以下幾點(diǎn):
- 捏合手勢(shì)開始時(shí)候的時(shí)間與此時(shí)對(duì)應(yīng)的地圖層級(jí),也就是A1點(diǎn)的坐標(biāo)韩玩。
- 捏合手勢(shì)離開時(shí)候的時(shí)間與此時(shí)對(duì)應(yīng)的地圖層級(jí)垒玲,也就是A2點(diǎn)的坐標(biāo)。
現(xiàn)在假如我們知道了這兩個(gè)點(diǎn)找颓,可是兩個(gè)方程要解出a,b,c
三個(gè)未知數(shù)是不可能的合愈,仔細(xì)回想一下一元二次方程的特點(diǎn),我們會(huì)發(fā)現(xiàn)二次項(xiàng)系數(shù)a
的值不一般击狮,這是一個(gè)決定拋物線開口大小的參數(shù)佛析,并且參數(shù)a
的絕對(duì)值越大,開口越小彪蓬,意味著曲率越大寸莫,曲率越大意味著,相同單位時(shí)間內(nèi)的y
值變化越大寞焙。換句話說储狭,這個(gè)二次項(xiàng)系數(shù)是一個(gè)影響慣性大小的參數(shù),所以這個(gè)參數(shù)捣郊,我們可以自己設(shè)定辽狈。
既然二次項(xiàng)的值,可以自己設(shè)定呛牲,以便于我們控制慣性的大小刮萌,那么在知道A1點(diǎn)與A2點(diǎn)的情況下,我們就可以得到一次項(xiàng)系數(shù)b和常數(shù)項(xiàng)c的值了娘扩。
問題轉(zhuǎn)變成如何獲得捏合手勢(shì)開始與結(jié)束時(shí)候的
A1點(diǎn)
與A2點(diǎn)
坐標(biāo)着茸。
這個(gè)問題看起來比較簡(jiǎn)單,可是在百度地圖沒有直接給你的時(shí)候琐旁,就變得比較費(fèi)事了涮阔。如果你有好的方式,請(qǐng)告訴我灰殴,我相信簡(jiǎn)單的才是好的敬特。如果你還沒有找到好的方式,那么不妨繼續(xù)往下看看我是如何一步一步找到的牺陶。
首先使用蘋果私有的調(diào)試工具,獲取地圖的層級(jí)結(jié)構(gòu)伟阔,然后逐個(gè)分析捏合手勢(shì)最有可能發(fā)生在哪一層上。簡(jiǎn)單介紹下它的幾個(gè)重要的層級(jí)結(jié)構(gòu)掰伸,
BaiduMapEAGLView
(繪制openGL的圖層)皱炉,TapDetctingView
(負(fù)責(zé)展示標(biāo)注的圖層),以上兩個(gè)圖層狮鸭,都是添加在地圖私有的MapView
上的合搅。而這個(gè)MapView
又是放在BMKMapView
上的多搀,BMKMapView
就是百度地圖開放給我們的mapView
,好吧,想說历筝,開放的好少酗昼,上面提到的只開放了一個(gè)。最后我們將目標(biāo)定位到了TapDetectingView
上梳猪。然后我們通過
Runtime
工具,將這個(gè)可疑的TapDetectingView
的所有實(shí)例方法都打印了出來蒸痹,挨個(gè)去找春弥,希望能有什么發(fā)現(xiàn)。下面簡(jiǎn)單羅列幾個(gè)方法:
Some InstanceMethods Of TapDetctingView:(
handleSingleTap, //處理單擊事件
"handleForceTouch:", //處理重壓事件
handleDoubleBeginTouchPoint, //開始處理兩個(gè)手指的點(diǎn)擊事件(可疑方法)
handleDoubleMoveTouchPoint, //處理兩個(gè)手指的運(yùn)動(dòng)事件(可疑方法)
handleMoveTouchPoint, //處理單個(gè)手指的運(yùn)動(dòng)事件
"handleScale:", //暫時(shí)不知道叠荠,但是scale跟縮放有關(guān)(可疑方法)
"handleRoate:", //處理旋轉(zhuǎn)事件
handleEndTouchPoint, //處理單個(gè)手指的結(jié)束事件
handleDoubleEndTouchPoint, //處理兩個(gè)手指的結(jié)束事件(可疑方法)
handleTwoFingerTap, //處理兩個(gè)手指的點(diǎn)擊事件
)
在上面的部分方法中匿沛,我們根據(jù)方法名定位出了四個(gè)可能跟地圖縮放有關(guān)的方法,事實(shí)證明榛鼎,處理縮放事件手勢(shì)的也確實(shí)是這四個(gè)方法逃呼。
從方法名看,那四個(gè)方法確實(shí)可疑者娱,可是我們?cè)撊绾芜M(jìn)一步確定呢抡笼。
使用
Aspects
來 hookTapDetctingView
的所有方法。
我們使用了一個(gè)輕量級(jí)的面向切面編程的庫(kù)Aspects來實(shí)現(xiàn)我們的目的黄鳍。在TapDetctingView
的每一個(gè)方法執(zhí)行完后都會(huì)進(jìn)入我們自己的代碼塊中推姻,在我們的代碼塊中,做了打印方法名的操作框沟。這樣我們就可以在地圖運(yùn)動(dòng)中藏古,來看具體調(diào)用了哪個(gè)方法。
#pragma mark 處理百度地圖的相關(guān)私有視圖
- (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地圖私有視圖的相關(guān)方法
- (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];
};
}
通過上面的代碼忍燥,再通過我們慢慢地操作地圖拧晕,查看打印的方法,我們就可以獲得當(dāng)前響應(yīng)的是哪個(gè)方法了梅垄,讀者可以自己試試厂捞,總之最后我們確定了,跟地圖縮放有關(guān)的就是那四個(gè)方法哎甲。這里需要注意的是那個(gè)handleScale:
函數(shù)蔫敲,這個(gè)函數(shù)有一個(gè)參數(shù),這個(gè)參數(shù)是正的時(shí)候炭玫,說明地圖等級(jí)正在變大奈嘿,為負(fù)的時(shí)候,說明等級(jí)正在變小吞加。
那么現(xiàn)在我們想要在開始縮放和縮放結(jié)束的時(shí)候獲取A1點(diǎn)與A2點(diǎn)就容易多了裙犹。因?yàn)?code>zoomLevel是很好獲取的尽狠,而我們只需要再記錄兩個(gè)時(shí)間戳就可以了。這樣兩個(gè)方程叶圃,兩個(gè)未知數(shù)袄膏,我們可以求出一次項(xiàng)的系數(shù)與常數(shù)項(xiàng)的值,從而進(jìn)一步確定手勢(shì)離開屏幕的時(shí)候掺冠,這條即時(shí)的拋物線的函數(shù)了沉馆。
現(xiàn)在我們得到了手勢(shì)離開時(shí)候的地圖等級(jí)
zoomLevel
相對(duì)于時(shí)間time
的拋物線,那么我們就讓它按照既定的軌跡繼續(xù)運(yùn)動(dòng)吧德崭。
得到拋物線方程后斥黑,我們就可以計(jì)算出速率為0時(shí)候的時(shí)間了,
一元二次方程 :y = aX^2 + bX + c
求導(dǎo)數(shù) :v = 2aX+ b
所以v = 0時(shí)眉厨,X = -b/2a;
這意味著慣性運(yùn)動(dòng)停止的時(shí)候锌奴,時(shí)間是_tStirless = -b/2a
(a
和b
分別是二次項(xiàng)與一次項(xiàng)的系數(shù),此時(shí)都是可知的了)憾股,這樣我們就可以得到手指離開后鹿蜀,它應(yīng)該靠慣性再運(yùn)動(dòng)多長(zhǎng)一段時(shí)間了。也就是_tStirless - _tPinEnd
服球。
看到這里茴恰,是不是累了,好吧有咨,馬上就結(jié)束了琐簇,我們需要把這段時(shí)間均分一下,然后帶入方程座享,獲取此時(shí)對(duì)應(yīng)的zoomLevel
婉商,然后每隔均分的時(shí)間,就更新一下zoomLevel,這個(gè)步驟的思路就跟上篇的地圖阻尼運(yùn)動(dòng)的思路一致了渣叛。代碼如下:
/*
參數(shù)說明:
?NSTimeInterval _tPinEnd:記錄捏合手勢(shì)開始的時(shí)間丈秩。
?NSTimeInterval _tPinStart:記錄捏合手勢(shì)結(jié)束的時(shí)間。
?NSTimeInterval _tPinMoving: 記錄雙指在地圖上的的捏合時(shí)長(zhǎng)淳衙。
_tPinMoving = _tPinEnd - _tPinStart;
? NSTimeInterval _tStirless:記錄慣性運(yùn)動(dòng)停止蘑秽,速度為0(也就是靜止)時(shí)候的時(shí)間。
_tStirless-_tPinMoving可以得出手指離開后箫攀,慣性運(yùn)動(dòng)的時(shí)長(zhǎng)
*/
#pragma mark 刷新拋物線上的y值:也就是地圖的zoomlevel肠牲,時(shí)間段在這里分成了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];
});
}
}
/*
參數(shù)說明:
?_quadraticCoefficient:慣性運(yùn)動(dòng)方程的二次項(xiàng)系數(shù)
?_oneCoefficient :慣性運(yùn)動(dòng)方程的一次項(xiàng)系數(shù)
?_constantCoefficient:慣性運(yùn)動(dòng)方程的常數(shù)項(xiàng)
*/
#pragma mark 實(shí)時(shí)計(jì)算拋物線上time所對(duì)應(yīng)的maplevel
- (CGFloat)parabola_calculateMapZoomLevelAtRealTime:(float)realTime{
return _quadraticCoefficient *(realTime *realTime) + _oneCoefficient * realTime + _zoomLevelPinStart;
}
好了,寫到這里基本就實(shí)現(xiàn)了我們的慣性效果靴跛,在這里只列出了部分的關(guān)鍵代碼缀雳,還有很多其他的沒有羅列,都羅列的話梢睛,篇幅就比較長(zhǎng)了肥印,建議自己下載demo查看具體實(shí)現(xiàn)识椰,demo中對(duì)手勢(shì)持續(xù)時(shí)間超過0.8s的,不做慣性處理深碱,因?yàn)槌^這個(gè)時(shí)間的話腹鹉,那么有很大的可能性證明此時(shí)的用戶正在認(rèn)真的查找某一層級(jí)的東西,這時(shí)候就不對(duì)它進(jìn)行慣性縮放了敷硅。
更新:
最近將地圖慣性運(yùn)動(dòng)相關(guān)的代碼功咒,抽離到了BMKMapViewAdapter
類中,該類提供更簡(jiǎn)潔的api如下:
/**
@param mapView mapView description
@param inertiaCoefficient 慣性系數(shù)绞蹦,系數(shù)越大航瞭,慣性越大,越不容易改變坦辟。
*/
+ (void)mapView:(BMKMapView *)mapView openInertiaDragWithCoefficient:(float)inertiaCoefficient;
/**
@param mapView mapView description
@param close close description
*/
+ (void)mapView:(BMKMapView *)mapView closeMapInertialDrag:(BOOL)close;
除了對(duì)核心代碼的抽離,對(duì)于最后一步(也就是從雙手離開屏幕到它完全停止的這個(gè)過程)也改變了它的實(shí)現(xiàn)方式章办,之前是將這段時(shí)間分成N份锉走,然后逐步設(shè)置當(dāng)前時(shí)間點(diǎn)下的zoomlevel
,現(xiàn)在的話,采用CADisplaylink藕届,使之設(shè)置level的頻率與屏幕的刷新頻率一致挪蹭,并且取消了原來GCD的使用,而改用performSelector:afterDelay:
這樣就可以方便的進(jìn)行取消操作了休偶,代碼如下:
static int zoomDisplayCount;
static CADisplayLink *displayLink;
/**
雙指離開屏幕后梁厉,還會(huì)繼續(xù)運(yùn)動(dòng)的時(shí)間
@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級(jí)別
*/
- (void)changeMapLevelByStep{
zoomDisplayCount ++;
float time = zoomDisplayCount/60.f;
float tempZoomLevel = [self parabola_calculateMapZoomLevelAtRealTime:_tPinMoving + time];
[_mapView setZoomLevel:tempZoomLevel];
}
/**
運(yùn)動(dòng)停止
*/
- (void)moveDidEnd{
displayLink.paused = YES;
[displayLink invalidate];
displayLink = nil;
zoomDisplayCount = 0;
}
建議下載demo,體驗(yàn)效果。
傳送門:地圖系列的其他文章:
給你的地圖模塊動(dòng)手術(shù) -----一種輕量級(jí)的地圖解決方案踏兜。
地圖阻尼運(yùn)動(dòng)
給你的地圖點(diǎn)燈