跟我學(xué)Rx編程——慣性滑動(dòng)

在移動(dòng)設(shè)備上,滾動(dòng)一個(gè)視圖不會(huì)立即停止?jié)L動(dòng),往往需要再滑動(dòng)一小段距離然后再停止,模擬出慣性的效果?;瑒?dòng)的時(shí)候速度越快,那么就滾動(dòng)的越遠(yuǎn)。一般組件都會(huì)幫開發(fā)者寫好這些基本功能,不需要開發(fā)者操心。但有的時(shí)候我們需要使用類似的邏輯,比如我需要在手指滑動(dòng)后,通過一些列序列幀變化來顯示動(dòng)畫,那么這時(shí)候就可能需要開發(fā)者自己來寫這個(gè)慣性滑動(dòng)的邏輯了。不管怎樣,我們用Rx來實(shí)現(xiàn)一遍這個(gè)慣性滑動(dòng),也是一種不錯(cuò)的體驗(yàn)。

涉及操作符

  • scan
  • switchMapTo
  • switchMap
  • mapTo
  • takeUntil
  • takeWhile
  • filter

基本事件流

我們需要三個(gè)基本的事件流,分別是鼠標(biāo)(手指)按下、移動(dòng)、抬起。不同環(huán)境可能創(chuàng)建的方式不同,但性質(zhì)是相同的,下面是偽代碼

let mdOb = fromEvent(...,MOUSE_DOWN)
let mmOb = fromEvent(...,MOUSE_MOVE)
let muOb = fromEvent(...,MOUSE_UP)

這些事件流觸發(fā)的規(guī)律是,由一個(gè)MOUSE_DOWN事件,一連串的MOUSE_MOVE事件,和一個(gè)MOUSE_UP事件組成


MOUSE_DOWN    MOUSE_MOVE    MOUSE_MOVE......    MOUSE_UP

(*)-------------(o)--------------(o)......-------------|>

接下來我們就要從這3個(gè)Observable來組合出合適的邏輯,來實(shí)現(xiàn)慣性滑動(dòng)效果。

手勢移動(dòng)的偏移量和實(shí)時(shí)速度

我們需要取得手指或者鼠標(biāo)按下后移動(dòng)的距離來確定每時(shí)每刻的速度,因?yàn)槲覀冃枰谑种富蚴髽?biāo)抬起的瞬間利用這個(gè)速度進(jìn)行慣性移動(dòng)

let speedOb = mdOb.pipe(switchMapTo(mmOb.pipe(takeUntil(muOb), scan((aac, v) => {
        let { stageY, nativeEvent: { timeStamp } } = v
        if (aac.nativeEvent) aac = { stageY: aac.stageY, timeStamp: aac.nativeEvent.timeStamp }
        aac.delta = stageY - aac.stageY
        aac.lastTs = aac.timeStamp
        aac.stageY = stageY
        aac.timeStamp = timeStamp
        return aac
    }))));

其中

mdOb.pipe(switchMapTo(mmOb.pipe(takeUntil(muOb),......

這一段邏輯是非常常用的固定的搭配,表示我們需要獲取手指按下到手指抬起之間的所有移動(dòng)事件。
所以本段邏輯只有一個(gè)關(guān)鍵操作符scan。使用這個(gè)操作符的目的是,為了取得上次計(jì)算的結(jié)果,因?yàn)槲覀冃枰容^前一個(gè)事件和這個(gè)事件的手指或鼠標(biāo)的Y坐標(biāo)變化。
下面我們來逐句分析其邏輯

let { stageY, nativeEvent: { timeStamp } } = v

這句話是js的解構(gòu)賦值,我們獲取了移動(dòng)事件數(shù)據(jù)中的手指Y坐標(biāo),和此時(shí)的時(shí)間戳,當(dāng)然在不同場合下,可能數(shù)據(jù)對(duì)象不同,我們可以自己獲取一個(gè)時(shí)間戳也是沒有問題的比如:

let { stageY } = v
let timeStamp = new Date()

第二行

if (aac.nativeEvent) aac = { stageY: aac.stageY, timeStamp: aac.nativeEvent.timeStamp }

判斷aac.nativeEvent的目的是,判斷我們是否已經(jīng)接收過移動(dòng)事件了,如果已經(jīng)接收過了,我們就用之前數(shù)據(jù)創(chuàng)建一個(gè)新的aac對(duì)象,為什么要?jiǎng)?chuàng)建一個(gè)新的對(duì)象呢,因?yàn)樵瓉淼膶?duì)象會(huì)被復(fù)用,出現(xiàn)臟數(shù)據(jù)。
第三行,根據(jù)前一次的y坐標(biāo)(aac.stageY)和當(dāng)前的y坐標(biāo)stageY計(jì)算出差值,就是本次移動(dòng)的距離。

aac.delta = stageY - aac.stageY

第四行,我們把上一次的時(shí)間戳存放起來,這個(gè)是給后面的邏輯使用的。

aac.lastTs = aac.timeStamp

第五、六兩行,是把本次的y坐標(biāo)和時(shí)間戳存起來,作為下一次計(jì)算時(shí)使用的數(shù)據(jù)

aac.stageY = stageY
aac.timeStamp = timeStamp

scan操作符會(huì)在每次都傳入aac(累加結(jié)果),v(當(dāng)前事件對(duì)象)兩個(gè)參數(shù),我們利用aac來存放上一次的數(shù)據(jù)。
此外scan操作符和reduce十分相似,只是后者的結(jié)果會(huì)在事件流結(jié)束的時(shí)候傳出,而scan會(huì)每次把結(jié)果輸出。

計(jì)算慣性偏移,阻尼運(yùn)動(dòng)

我們有了speedOb這個(gè)事件流,就可以用來模擬手指抬起的時(shí)候慣性移動(dòng)效果了。

let inertiaOb = rxjs.combineLatest(muOb, speedOb).pipe(switchMap(([, { delta, lastTs, timeStamp }]) => rxjs.interval(20).pipe(mapTo({ delta: delta * 10 / (timeStamp - lastTs) }), takeWhile(_ => {
        _.delta *= 0.9
        return _.delta > 0.1 || _.delta < -0.1
    }))));

我們來分析上面的邏輯

rxjs.combineLatest(muOb, speedOb)

上面這句話可以讓我們得到當(dāng)鼠標(biāo)或手指抬起的時(shí)候,speedOb事件流里面最新的數(shù)據(jù),我們用這個(gè)數(shù)據(jù)作為用戶滑動(dòng)的速度,然后做一個(gè)逐漸減速的過程。
switchMap就是上述行為發(fā)生的時(shí)候,我們開始監(jiān)聽switchMap傳入的函數(shù)所返回出來的那個(gè)事件流。
這個(gè)事件流是

rxjs.interval(20).pipe(mapTo({ delta: delta * 10 / (timeStamp - lastTs) }), takeWhile(_ => {

此時(shí)會(huì)每個(gè)20毫秒產(chǎn)生一個(gè)事件,這個(gè)事件被轉(zhuǎn)換成了一個(gè)對(duì)象,其中delta: delta * 10 / (timeStamp - lastTs),這是一個(gè)距離除以時(shí)間的公式,得到的是速度即v=s/t 這個(gè)對(duì)象中的delta從一個(gè)距離轉(zhuǎn)變成了速度值。

    _.delta *= 0.9
        return _.delta > 0.1 || _.delta < -0.1

這里的速度將逐漸減少,如果速度值低于某個(gè)范圍,則終止事件流(takeWhile的行為),但由于我們終止只是switchMap內(nèi)部的事件流,并不會(huì)終止外層的事件流,所以只要用戶繼續(xù)按下手指滑動(dòng),邏輯又會(huì)再次啟動(dòng)。

執(zhí)行滑動(dòng)操作

本例是改變序列幀的索引,也可以用其他邏輯代替

    return rxjs.merge(speedOb, inertiaOb).pipe(filter(_ => _.delta != 0), scan((aac, v) => {
        aac += (v.delta / speed >> 0);
        if (aac < 0) aac = 0;
        else if (aac > totalFrames) aac = totalFrames
        return aac
    }, initFrame))

此時(shí)我們用到了之前創(chuàng)建的兩個(gè)事件流,并且merge了他們。因?yàn)楫?dāng)用戶按住屏幕移動(dòng)的時(shí)候,內(nèi)容也要跟著改變,放開手指或鼠標(biāo)的時(shí)候會(huì)接著改變一小段時(shí)間,所以兩個(gè)事件流的事件合并來處理。我們過濾了不需要改變內(nèi)容的事件,就是當(dāng)速度為0的時(shí)候。
此時(shí)再次出現(xiàn)scan操作符。
這里很多邏輯是和具體業(yè)務(wù)有關(guān),這里僅供參考,aac存放是此時(shí)的序列幀的索引,速度越快那么索引向后累加的就越快,動(dòng)畫就越快的播放,反之則播放的慢。其中speed和initFrame是傳入的常數(shù),用來調(diào)整姿勢。

這個(gè)事件流將流出你需要的數(shù)據(jù),最后進(jìn)行subscribe即可

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,741評(píng)論 25 709
  • 用兩張圖告訴你,為什么你的 App 會(huì)卡頓? - Android - 掘金 Cover 有什么料? 從這篇文章中你...
    hw1212閱讀 13,910評(píng)論 2 59
  • 天是黑的,遠(yuǎn)處燈光燦爛,手里捧一杯咖啡取暖。想起了你,桌子上的手機(jī)拿了幾次,沒打開就放下。 苦...
    才知道原來閱讀 328評(píng)論 0 1
  • 2010年8月5日 這次要坐四個(gè)小時(shí)的車,目的地是室韋小鎮(zhèn)。沿途是草原風(fēng)光,大片大片的綠色像大海的波浪此起彼伏地涌...
    夢暖傾城的小屋閱讀 1,387評(píng)論 1 1

友情鏈接更多精彩內(nèi)容