摘要
UIScrollView是iOS開發(fā)中不可或缺也是使用最多的基礎(chǔ)組件孽椰;常用的Feed流岖赋、Pager芒涡、輪播圖等等都與UIScrollView存在著密不可分的關(guān)系浮驳。日常開發(fā)中,我們通常都局限于其必要的幾個調(diào)用接口和代理滑负,而不曾探究過其隱藏在那幾個簡單接口背后那些精妙的設(shè)計在张,比如:滾動視圖如何在有限的區(qū)域內(nèi)展示無限的內(nèi)容?每一次在滾動區(qū)域觸控屏幕會產(chǎn)生哪些反應(yīng)橙困?它在現(xiàn)實(shí)世界中又是怎樣的物理形態(tài)瞧掺?本文從基本的參數(shù)觀測開始,以數(shù)學(xué)凡傅、物理學(xué)和優(yōu)化方法中的一些基本方法和概念為工具辟狈,探索UIScrollView流暢交互背后隱藏的規(guī)律,領(lǐng)略蘋果工程師在實(shí)現(xiàn)一個基礎(chǔ)組件時所做出的精妙設(shè)計夏跷。
UIScrollView的局部顯示原理
為了印證這部分觀點(diǎn)哼转,我們從蘋果的官方文檔上摘抄了一段描述:
*Documents:UIScrollView is the superclass of several UIKit classes, including UITableView and UITextView. *
*A scroll view is a view with an origin that’s adjustable over the content view. A scroll view tracks the movements of fingers, and adjusts the origin accordingly[1]. The view that shows its content through the scroll view draws that portion of itself according to the new origin, which is pinned to an offset[2] in the content view. By default, it bounces[3] back when scrolling exceeds the bounds of the content. *
這段文字的大意有以下幾點(diǎn):
UIScrollView會追蹤手指的動作并適當(dāng)調(diào)整顯示內(nèi)容的原點(diǎn)(一個矩形左上角所在的坐標(biāo))。
根據(jù)被調(diào)整矩形的原點(diǎn)坐標(biāo)展示不同的內(nèi)容槽华。
默認(rèn)在接觸到邊界的時候會反彈壹蔓。
同時,UIScrollView是UITableView猫态、UITextView的父類(也包括UICollectionView)佣蓉。
從UIScrollView的父類UIView的角度出發(fā),UIView的屬性:bounds.origin(x,y) 標(biāo)記了一個UIView的所有子元素依賴的參考系原點(diǎn)亲雪,被添加在這個UIView上的所有子視圖在繪制時均會參考這個原點(diǎn)勇凭,這意味著:如果這個原點(diǎn)被標(biāo)記為{-40, -40.f},那么這個視圖的所有子視圖都會基于(-40, -40)這個點(diǎn)繪制义辕。例如虾标,這種情況下,一個frame = {20.f,20.f,100.f,100.f} 的子視圖 會從點(diǎn)(-20.f, -20.f) 開始繪制,-20的來源是子視圖的原點(diǎn)(20,20)加偏移量(-40,-40)灌砖,所以璧函,在此種情況下你只能看到這個子元素右下角大小為20*20的一小部分,其余超出邊界的部分無法看到傀蚌。
在UIScrollView中,為了將這個特性與常規(guī)的UIView區(qū)分開來蘸吓,bounds.origin 被獨(dú)立出來叫做:contentOffset,兩者都包含兩個數(shù)值x善炫、y的二維向量(CGPoint),只要根據(jù)用戶手勢美澳、動畫規(guī)律不斷變更c(diǎn)ontentOffset销部,就能做出滾動的效果。
調(diào)整contentOffset并非難事制跟,但想做出順暢的滾動效果卻需要考慮很多方面;這體現(xiàn)了蘋果提供基礎(chǔ)組件的精妙之處酱虎,讓我們可以不了解任何細(xì)節(jié)的同時對UIScrollView操作自如雨膨,而將大部分復(fù)雜邏輯封裝在了輕描淡寫的一句:accordingly背后。
UIScrollView交互細(xì)節(jié)
我們匯總了在setContentOffset的之前需要考慮的所有情況:
當(dāng)panGesture在容器的規(guī)定范圍(contentSize)內(nèi)生效時读串,UIScrollView通常要移動相同的位置和方向聊记,contentOffset與panGesture位移距離保持1:1數(shù)值關(guān)系。
在panGesture結(jié)束后恢暖,如果Gesture有剩余速度依然生效排监,那么在之后的一段時間里要持續(xù)進(jìn)行減速。
在panGesture結(jié)束后杰捂,如果當(dāng)前的contentOffset超出了contentSize舆床,在之后的一段時間里需要進(jìn)行反彈并恢復(fù)到邊界處。
(2)和(3)結(jié)合起來嫁佳,panGesture結(jié)束后挨队,有速度依然生效,在之后的一段時間內(nèi)減速蒿往,減速未結(jié)束的情況下觸及了邊界盛垦,開始進(jìn)行反彈,并回彈到非拉伸狀態(tài)瓤漏。
根據(jù)正交向量互不影響腾夯,contentOffset需要在x、y兩個方向上獨(dú)立生效蔬充。
在超過邊界進(jìn)行panGestures時蝶俱,panGesture轉(zhuǎn)化為contentOffset距離的有效比例逐漸減小,呈現(xiàn)出一種拉扯到極限的效果娃惯。
(如果我們只在panGesture生效期間setContentOffset跷乐,這樣做出來的效果和在屏幕上畫一個可以跟手的View別無二致,這種感覺就像你在通常的Windows桌面拖拽一個文件夾到處移動一樣)趾浅。
正是這些復(fù)雜的交互特性才使得UIScrollView在我們的手中呈現(xiàn)出了絕佳的交互體驗(yàn)愕提。
Decelerate運(yùn)動探究
數(shù)值觀測
由簡入繁馒稍,首先從比較容易的Decelerate動畫開始,觀察這部分動畫的運(yùn)行規(guī)律浅侨,我們不妨創(chuàng)建一個常規(guī)的UIScrollView(為了清楚地觀測滾動纽谒,最好創(chuàng)建UICollectionView來用格子劃分整個contentSize),在以下代理中打印出統(tǒng)計信息 :
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
NSLog(@"DecelerateVelocity:%lf", velocity.y);
NSLog(@"DecelerateDistance:%lf", targetContentOffset->y - scrollView.contentOffset.y);
}
DecelerateVelocity記錄了如输,panGesture手勢結(jié)束瞬間的瞬時速度鼓黔,也就是Decelerate起始速度。
DecelerateDistance記錄了不见,panGesture結(jié)束后直到停止澳化,在這段時間內(nèi),自然減速移動的總距離
我們截取了幾次panGesture手勢結(jié)束瞬間捕獲到的數(shù)據(jù)稳吮,為了保證不受到bounces和邊界的影響缎谷,應(yīng)當(dāng)盡量保證這些減速過程中不會觸及邊界,數(shù)據(jù)如下:
序號 | 減速初速度 | 截止到停止的移動距離 |
---|---|---|
1 | 5.0270956 | 2506.5 |
2 | 1.802126 | 895.0 |
3 | 1.412374 | 700.5 |
4 | 1.687861 | 838.0 |
- 不難看出二者之間的關(guān)系:不考慮1000倍的話灶似,減速的初速度總是它依靠慣性移動總距離的2倍列林。
關(guān)于這個1000倍:我們從這個代理的注釋中可以看出:
Called on finger up if the user dragged. velocity is in points/millisecond. targetContentOffset may be changed to adjust where the scroll view comes to rest
那么這個速度的單位是points/millisecond,而通常的速度應(yīng)以points/second為單位酪惭;因此希痴,我們默認(rèn)為這個速度乘1000以方便之后的計算,同時我們將1point的寬/高視為單位長度1米春感,這樣速度單位也被我們統(tǒng)一為標(biāo)準(zhǔn)速度單位:m/s砌创。
結(jié)果分析
根據(jù)上面的觀察結(jié)果,我們試圖尋找一種常用的函數(shù)甥厦,這個函數(shù)的特點(diǎn)是:他對時間(t)的導(dǎo)數(shù)(v)似乎總他自身(y)的兩倍纺铭。
那么顯然這個函數(shù)和其導(dǎo)數(shù)分別是:
這兩個函數(shù)是指數(shù)加速,而由于Decelerate是減速刀疙,所以我們需要根據(jù)實(shí)際情況增加一個負(fù)號來保證速度總是隨著時間逐漸減小的舶赔,所以有:
(細(xì)心的同學(xué)可以發(fā)現(xiàn)除了指數(shù)增加了負(fù)號外,C2常數(shù)前面也加了一個負(fù)號谦秧,這是因?yàn)槲覀冊谏鲜鲇^測數(shù)值的時候velocity直接使用了初速度v0竟纳,而位移卻是用了末位移減初位移;而正確的做法是使用速度改變量疚鲤,也就是(0-v0),因?yàn)槟顟B(tài)是停止?fàn)顟B(tài)锥累;因此真正的結(jié)論應(yīng)該是:速度的改變量應(yīng)當(dāng)總是位移改變量的-2倍,但此前為了數(shù)值觀測方便集歇,沒有強(qiáng)調(diào)正負(fù)關(guān)系桶略。)
那么根據(jù)已知條件:當(dāng)減速剛開始t=0時,v=v0,因此际歼,右側(cè)的常系數(shù)C2可以被固定為v0惶翻;而C1則是一個跟初始位置相關(guān)的常數(shù),此處我們不關(guān)心鹅心。
那么最終我們得到的速度衰減公式為:
相關(guān)解釋
以下是正常的推導(dǎo)(來解釋那個突兀的指數(shù)函數(shù)):
以不同的速度v1吕粗、v2開始的減速效果滿足相同的運(yùn)動規(guī)律,不妨令v2>v1旭愧,那么v1是v2減為0的必經(jīng)狀態(tài)颅筋,那么根據(jù)之前的觀察結(jié)果:
兩式相減:
當(dāng)v1與v2無限接近時:
兩邊同時除以時間間隔dy:
兩邊積分:
顯然;
結(jié)論
UIScrollView的減速運(yùn)動就是阻尼系數(shù)為2的阻尼運(yùn)動输枯,這種阻尼運(yùn)動對應(yīng)于現(xiàn)實(shí)中的低速流體阻力(不考慮壓縮損耗)议泵。
其他
Q:y與v的倍數(shù)關(guān)系為什么是約等于?
A:這個不是誤差用押,是Apple有意為之肢簿,因?yàn)樽枘釡p速是指數(shù)衰減,所以無限趨近于0的過程是非常漫長的蜻拨,如果你看v/t的關(guān)系會發(fā)現(xiàn),即使是t無限大桩引,v都不會減為0缎讼,因?yàn)橹笖?shù)總是正的,所以為省略后面那段很長很慢的減速坑匠,蘋果就把它截斷了血崭。 這個截斷量大概是 v = (12 ~ 13)pt/s , 小于這個速度就會被截斷,所以y總是比v/2小一點(diǎn)(6左右)厘灼。所以夹纫,這其實(shí)是嚴(yán)格遵循自然規(guī)律原則為提升用戶體驗(yàn)?zāi)繕?biāo)做出的讓步。
Bounces運(yùn)動探索
數(shù)值觀測
我們?nèi)绶ㄅ谥艱ecelerate觀測過程设凹,主要從時間空間兩個維度觀察Bounces運(yùn)動規(guī)律舰讹,那么觀測數(shù)值包括:
UIScrollView首次接觸到邊界時的初速度v0,一個直觀的感受就是當(dāng)這個v0越大時闪朱,反彈過程中能達(dá)到的最遠(yuǎn)距離就越長月匣,可以理解為慣性越大。
那么第二個值得觀察的數(shù)值就是每次Bounces運(yùn)動所能達(dá)到超過邊界的最遠(yuǎn)距離奋姿。
第三個數(shù)值為整個Bounces運(yùn)動過程總持續(xù)時間锄开,這個事件從首次接觸邊界開始,直到自然恢復(fù)到邊界處結(jié)束称诗。
具體的統(tǒng)計方法在此只做簡要介紹萍悴,讀者可自行編寫Demo進(jìn)行實(shí)踐:
為了方便區(qū)分,我們每次令UIScrollView沖擊頂部的邊界,這樣當(dāng)contentOffset.y為負(fù)數(shù)時癣诱,意味著Bounces動畫開始计维,在此處記錄一個起始時間t0。
為了獲取到精準(zhǔn)的初速度狡刘,我們每次在UIScrollView接觸到邊界前停止拖拽享潜,令其依賴自身慣性進(jìn)行反彈,這樣我們就可以依賴上文Decelerate中提供的規(guī)律計算出接觸邊界時的初速度:(停止拖拽時的速度-停止拖拽時距離頂部的距離*2)嗅蔬。
最遠(yuǎn)距離剑按,通過在scrollViewDidScroll代理中不換捕獲最小值獲取。
結(jié)束時間澜术,通過endDecelerate代理捕獲(Bounces結(jié)束時也被視為減速結(jié)束)艺蝴。
經(jīng)過這些記錄后,我們成功捕獲了幾組數(shù)據(jù):
序號 | 接觸邊界時初速度(p/s) | Bounces最遠(yuǎn)距離(p) | Bounces持續(xù)時間(s) |
---|---|---|---|
1 | 986.497 | -32.5 | 0.6668 |
2 | 2404.116 | -80.5 | 0.7509 |
3 | 1793.594 | -60 | 0.7337 |
4 | 1251.628 | -41.5 | 0.6836 |
觀察到的現(xiàn)象:
- v0與最遠(yuǎn)距離y總是正比例關(guān)系鸟废,這個比例大概是30猜敢。
- Bounces運(yùn)動總時長不會隨著慣性的變化而發(fā)生太大改變,這個值基本穩(wěn)定在0.6s~0.8s之間盒延。這是典型的阻尼運(yùn)動特點(diǎn)缩擂,類似于log(n)時間復(fù)雜度的算法不會n產(chǎn)生較大增量。
那么顯然Bounces中存才的兩種力:彈力和阻力添寺;彈力用來保證接觸到邊界發(fā)生反彈胯盯,阻力用來限制彈力的簡諧振動。
結(jié)果分析
我們列出經(jīng)過以上兩種合力的數(shù)學(xué)模型:
- 首項(xiàng)中k為胡克定律彈力模型中的勁度系數(shù)k计露,y為彈簧被壓縮的長度博脑,也就是UIScrollView超出邊界的距離。因?yàn)檫@個彈力總是與y相反票罐,所以符號是負(fù)號叉趣。
- 次項(xiàng)中c為阻力的阻尼系數(shù)c(參考Decelerate中的數(shù)值2),阻力與速度v的方向總相反该押,所以符號為負(fù)號疗杉。
- 整個式子就是我們熟知牛頓第二定律:合力等于質(zhì)量乘加速度。
另外一個我們熟知的結(jié)論是:瞬時速度v是位移y對時間t的導(dǎo)數(shù)沈善;瞬時加速度a是速度v對時間t的導(dǎo)數(shù)乡数,也就是位移y對時間t的二階導(dǎo)數(shù)。那么上述方程可寫成如下形式:
等式兩側(cè)同除質(zhì)量m:
這是個二階線性齊次微分方程闻牡,根據(jù)其特征根返程解個數(shù)有三種不同形式通解净赴,為了方便討論特征根,我們對其系數(shù)做一些代換罩润,令:
注意玖翅,由于等式中已經(jīng)考慮了符號,所以阻尼系數(shù)c和進(jìn)度系數(shù)k在此處均為正數(shù);關(guān)于質(zhì)量m, 我們不考慮質(zhì)量小于等于0的情況金度;因此应媚,這些參數(shù)均為正數(shù),代換后也為正數(shù)猜极。
那么這個式子化簡為:
其特征根方程為:
討論其解形式
情況1:
特征根方程兩相異實(shí)根a,b中姜,其通解形式為:
情況2:
特征根方程一對相同實(shí)根,同為a跟伏,其通解形式為:
情況3:
特征根方程實(shí)數(shù)域無根丢胚,復(fù)數(shù)域一對共軛復(fù)根:,
受扳,其通解形式為:
這三種分別對應(yīng)阻尼震動的三種形式:過阻尼携龟、臨界阻尼和欠阻尼
從用戶體驗(yàn)角度講,在不發(fā)生震蕩的前提下反彈后以最快速度恢復(fù)到邊界通常會獲得最佳手感勘高。因此在這里峡蟋,蘋果必然會選擇臨界阻尼為自己通用組件邊界減震,因此我們選擇臨界阻尼條件下的通解形式:
此時华望,我們?nèi)ˇ娜锘龋敲唇馓卣鞲匠蹋?img class="math-inline" src="https://math.jianshu.com/math?formula=%CE%BB%5E2%2B2%CE%B4%CE%BB%2B%CE%B4%5E2%3D0" alt="λ^2+2δλ+δ^2=0" mathimg="1"> ,得到:
將λ解帶入替換通解的a赖舟,得到:
根據(jù)初始條件:Bounces開始時t=0,y=0匿又,得到C1=0,因此化簡為:
C建蹄、δ是兩個待定系數(shù),后面介紹測量方法裕偿,這里直接給出:C是首次接觸邊界時的速度v0洞慎,δ是常數(shù)10.9,故Bounces公式為:
相關(guān)解釋
特征根方程與微分方程的關(guān)系:
對于二階齊次方程: 嘿棘,考慮一個函數(shù)y最容易湊出二階導(dǎo)劲腿、一階導(dǎo)、自身最容易提取出包含變量的公因式鸟妙,并能夠?qū)⑹S喑?shù)通過加減法抵消焦人。
那么這個函數(shù)理應(yīng)是指數(shù)函數(shù),因?yàn)橹笖?shù)函數(shù)的導(dǎo)數(shù)總是能提取出自身:
如果確認(rèn)了這種解形式重父,那么此方程可寫為:
指數(shù)不可能為0花椭,所以左側(cè)多項(xiàng)式的解λ即是微分方程的解,所以這個簡化后的方程就是特征根方程房午。
結(jié)論
Bounces運(yùn)動是一種阻尼震動矿辽,這種震動依賴的彈性k和阻尼c滿足了一種特殊的數(shù)量關(guān)系,使得Bounces遵循臨界阻尼震動,從而呈現(xiàn)出一種“回彈永遠(yuǎn)不會彈過邊界或發(fā)生反復(fù)震動”現(xiàn)象袋倔。經(jīng)過以上討論雕蔽,我們嘗試構(gòu)建了一下UIScrollView的內(nèi)容展示區(qū)的復(fù)原圖如下:
UIScrollView的contentSize內(nèi)部充滿了阻力比較小的流體c=2,可理解為水宾娜,在圖中表現(xiàn)為藍(lán)色部分批狐。
contentSize外部充滿了阻力比較大的流體c=21.8,可理解為油前塔,在圖中表現(xiàn)為黃色部分嚣艇。
UIScrollView的contentSize邊界處安裝了4根彈簧,每根的進(jìn)度系數(shù)為119嘱根,在圖中表現(xiàn)為紫色部分髓废。
UIScrollView展示的內(nèi)容區(qū)域?yàn)橐粔K質(zhì)量為1kg的光滑鐵板,可以在整個區(qū)域內(nèi)跟隨手勢自由移動该抒,處于不同區(qū)域時收到相應(yīng)力的作用慌洪。
參數(shù)測量方法
由于存在C、δ兩個待定系數(shù)凑保,而我們能觀測到的數(shù)值主要是contentOffset冈爹,也就是位移,C欧引、δ 的影響因素是速度频伤、加速度這種更高級別的參數(shù),通過觀察位移確定A芝此、δ難度較高憋肖,所以在這里我們使用一種簡單的優(yōu)化方法來讓這我們預(yù)測的運(yùn)動趨勢與實(shí)際趨勢逐漸吻合。
梯度下降思路
給定一組UIScrollView沖擊邊界觸發(fā)Bounces的真實(shí)數(shù)據(jù)集合S婚苹,S內(nèi)包含Bounces動畫期間內(nèi)所有的contentOffset取值:[y0岸更,y1, y2, y3...]。
給定一組待定系數(shù)的解(C膊升、δ怎炊、φ),通過公式
計算出所有的理論值[y0'廓译,y1', y2', y3'...]评肆。
計算出理論值與實(shí)際值的方差:
,Sum函數(shù)越小非区,理論曲線與實(shí)際曲線的越接近瓜挽。
如果我讓A、δ院仿、φ中任意一個值秸抚,單獨(dú)進(jìn)行變化速和,能夠讓目標(biāo)函數(shù)的值變小,那么就對這個變化予以肯定剥汤,將原值修改為變化后的值颠放。對于三個待定系數(shù),我們有六個方向進(jìn)行變化吭敢,A+△A碰凶、A-△A、δ+△δ鹿驼、δ-△δ欲低、φ+△φ、φ-△φ畜晰,當(dāng)這六中變化中有多種變化都可以讓目標(biāo)函數(shù)結(jié)果變小砾莱,那么我們?nèi)∽兊米钚〉哪莻€變化,因此這個方法也叫最速下降法凄鼻。
當(dāng)目標(biāo)函數(shù)被優(yōu)化到0時腊瑟,說明我們的目標(biāo)曲線與實(shí)際觀測出的曲線已經(jīng)完全重合了;而我們機(jī)器上觀測到的數(shù)值總是離散的块蚌,所以實(shí)際上闰非,這個數(shù)值被優(yōu)化到個位數(shù)時就已經(jīng)非常接近了。
以下是一組Bounces數(shù)據(jù)的優(yōu)化過程
φ的值是我們?yōu)榱俗屨w盡量收斂引入的偏移量峭范,我們給出的公式:
的前提是:總是認(rèn)為這個運(yùn)動開始時t = 0财松,但我們實(shí)際取到Bounces開始時候的y的值也許并不恰好是0,因?yàn)槭謾C(jī)中的屏幕刷新的信號都是離散的纱控,每0.0167s一次刷新辆毡,所以大部分情況下是從一個正數(shù)如:y=10直接變化到了一個負(fù)數(shù):y=-10,而不會恰好落在0處甜害,而我們?nèi)〉降牡谝粋€值就成了-10,這樣的話曲線整體在時間上會有一個微小的偏移胚迫, 所以加入了這個變量φ,可以讓結(jié)果收斂得更準(zhǔn)確唾那。
一般didScroll的回調(diào)頻率是小于CADisplayLink回調(diào)頻率的,在滾動緩慢的狀態(tài)下褪尝,離散取整可能導(dǎo)致contentOffset在某次刷新中不發(fā)生變化闹获,也就是說didScroll的兩次打點(diǎn)間隔有一定可能大于0.0167s,是2個或者3個刷新周期河哑,因此使用CADisplayLink打點(diǎn)是最穩(wěn)妥的方法避诽,但后來經(jīng)過實(shí)踐這方面影響不大,因?yàn)榈退贍顟B(tài)下本身y值的差別就不大璃谨,對sum函數(shù)影響的比重非常小沙庐,所以使用didScroll打點(diǎn)鲤妥,默認(rèn)間隔是0.0167s即可。
最終我們對多組類似上面的數(shù)據(jù)進(jìn)行測量拱雏,δ值總是接近于10.9的一個數(shù)棉安;而C則根據(jù)每次Bounces的力度不同發(fā)生變化。
參數(shù)C的確定
由于我們發(fā)現(xiàn)了的大小受到每次Bounces的力度影響铸抑,因此找到了一個包含兩種參數(shù)的關(guān)系探究具體的算法贡耽,利用這個公式:
F是合力,t是持續(xù)時間鹊汛,由于合力F中的彈力蒲赂、阻力兩部分隨時間變化,因此這兩部都被寫成積分形式(小c是阻尼系數(shù)刁憋,大C是待固定的常數(shù))
然后把上面那里拿到的y滥嘴、v的公式:
代入到左邊那個積分里:
發(fā)現(xiàn)左邊的積分里面總是能提取出來一個,里面就沒有
了,只剩一堆已知的量,這個時候恰好右面有個
倾芝,所以
和初速度
是呈正比的范舀,比例系數(shù)就是m除以剩余的那一堆積分。不用算這個積分灸姊,我們只需要找到一組Bounces的數(shù)據(jù),通過減速的規(guī)律得到
,通過上面那個優(yōu)化方法優(yōu)化出此次Bounces與實(shí)際值最接近的C饺汹,(上面動圖里的第二個變量:a),求出此次Bounces中
與
的比例就是所有情況下的固定比值痰催,非常湊巧這個比值就是1兜辞。所以整體的算式就成為:
其他相關(guān)數(shù)值:
勁度比:
阻尼比:
Bounces最遠(yuǎn)距離
根據(jù),滿足
的點(diǎn)即是最遠(yuǎn)處夸溶,得到:
得到:
兩個結(jié)論:
Bounces總是會在開始后
秒時達(dá)到最遠(yuǎn)距離,到達(dá)最遠(yuǎn)距離的時間是固定的缝裁。
最遠(yuǎn)距離確實(shí)與初速度呈正比扫皱,比例系數(shù)為
,這兩個:2.71828×10.9,近似于30捷绑,也就是我們此前在表格中觀察到的30韩脑。
Bounces遞推形式
考慮到我們自己實(shí)現(xiàn)一個CustomScrollView,在使用CADisplayLink執(zhí)行Bounces動畫時,已知的條件只有某個瞬間的瞬時速度v和當(dāng)前所在位置y粹污;而這個y不一定0段多,v也不一定是v0;所以在這里我們提供任意瞬時狀態(tài){v,y}轉(zhuǎn)v0的方法:
兩式相除:
右側(cè)等式化簡的到t表達(dá)式:
帶入y得到表達(dá)式:
這樣壮吩,我們可以根據(jù)任意時刻的狀態(tài){v,y}計算出完整的Bounces表達(dá)式和當(dāng)前的時間t进苍,然后t=t+△t(△t=0.0167),使用y和y'的算式計算出下次刷新時候的{y加缘、v},不斷更新這個狀態(tài)觉啊,就實(shí)現(xiàn)了UIScrollView一樣Bounces變化拣宏。
實(shí)際開發(fā)中的用途
使用Decelerate在兩個UIScrollView減傳遞能量
你可以在下面這種多層UIScrollView嵌套的復(fù)雜結(jié)構(gòu)中使用Decelerate傳遞能量,這個結(jié)構(gòu)的最外層是縱向的滾動視圖柄延,承載了頭部蚀浆、可吸頂區(qū)域,和分頁容器三個部分搜吧;分頁容器是一個橫向的滾動視圖市俊;內(nèi)部又分為多個縱向滾動視圖,由此組成了三層嵌套UIScrollView滤奈,如下:
用戶在頭部和吸頂區(qū)域向上拖拽后生效的是最外層藍(lán)色的縱向視圖摆昧,當(dāng)藍(lán)色的層級觸底后,會有一個明顯的停頓效果蜒程;因?yàn)楦敢晥D的手勢生效绅你,剩余的慣性無法傳遞到內(nèi)部的橘黃色縱向視圖;為了彌補(bǔ)這部分缺陷昭躺,讓整個縱向列表看起來更加融為一體忌锯,我們?yōu)橥鈱拥乃{(lán)色視圖分配了一個剩余速度(或沖量)檢測的機(jī)制:
這個Detector利用上文提及的
得到了觸及邊界后剩余的速度。*
乘自身質(zhì)量m后通過圖中的藍(lán)色通路傳遞給當(dāng)前選中tab對應(yīng)的橘黃色視圖领炫。
橘黃色視圖配置了一個沖量接收機(jī)制Impulser偶垮,將接收到的
除去自身質(zhì)量,得到作用在自身的速度帝洪。
橘黃色視圖以此速度為初速度似舵,執(zhí)行向下滾動的減速動畫。
Q:關(guān)于為什么傳遞mv
A:我們?yōu)槊總€Detector和Impulser分配了額外的一個屬性m葱峡,從而讓這這些動效可以在二者之間以不同比例傳遞砚哗,這看起來就像:一個密度較大的球體撞向了一個密度較小的球體。通常狀況下默認(rèn)質(zhì)量都是1砰奕,因此不同的UIScrollView之間的剩余速度會以1:1的比例傳遞蛛芥。
使用Bounces實(shí)現(xiàn)POP類型彈幕
你可以使用Bounces動畫制作一款POP類型彈幕軌道,我們使用與Bounces類似的參數(shù)關(guān)系構(gòu)建臨界阻尼军援,使用此種曲線控制彈幕縮放:
為了節(jié)省性能常空,你可以將幾組配置好參數(shù)的臨界阻尼曲線執(zhí)行了間隔0.0167s的打點(diǎn),并將這些點(diǎn)的數(shù)值存儲在一個靜態(tài)的數(shù)組中盖溺,彈幕軌道執(zhí)行時直接從固定的幾個數(shù)組中獲取響應(yīng)的數(shù)值,這樣在使每個POP軌道中的彈幕均以相同的規(guī)律運(yùn)行的同時铣缠,也不必去反復(fù)計算那些繁瑣的指數(shù)烘嘱,減少了普通POP動畫執(zhí)行時數(shù)值計算的大量性能消耗昆禽。
結(jié)語
蘋果工程師們?yōu)殚_發(fā)者們構(gòu)建和諧社會中處處透露著精妙,作為iOS工程師的我們透過簡單的幾個接口和代理窺伺其追求極致用戶體驗(yàn)背后付出的巨大努力蝇庭。如果能有重新再來的機(jī)會醉鳖,我相信你一定會毫不猶豫的選購一部iPhone13XSProPlusMax,亦或是成為一名在追求極致的道路上腳踏實(shí)地哮内,披荊斬棘的iOS工程師盗棵。
這篇文章寫得比較正規(guī),學(xué)會了使用markdown中的公式語法北发,是基于之前寫過的一篇簡陋版本改進(jìn)的(之前的公式寫得實(shí)在太爛了纹因,自己都看不下去hhh):UIScrollView滾動特性
文章中使用到的Demo和手工實(shí)現(xiàn)的CustomScrollView均可以在我的Github下載:Github
如果覺得還不錯的話幫忙給個Star吧!感謝琳拨!