30 天精通 RxJS (11): 實務(wù)范例 - 完整拖拉應(yīng)用

在第 08 篇的時候兑燥,我們已經(jīng)成功做出簡易的拖拉效果亮瓷,今天要來做一個完整的應(yīng)用,而且是實務(wù)上有機(jī)會遇到但不好處理的需求降瞳,那就是優(yōu)酷的影片效果嘱支!

如果還沒有用過優(yōu)酷的讀者可以先前往這裡試用蚓胸。

當(dāng)我們在優(yōu)酷看影片時往下滾動畫面,影片會變成一個小視窗在右下角除师,這個視窗還能夠拖拉移動位置沛膳。這個功能可以讓使用者一邊看留言同時又能看影片,且不影響其他的資訊顯示汛聚,真的是很不錯的 feature锹安。

image.png

就讓我們一起來實作這個功能,同時補(bǔ)完拖拉所需要注意的細(xì)節(jié)吧倚舀!

需求分析

首先我們會有一個影片在最上方叹哭,原本是位置是靜態(tài)(static)的,卷軸滾動到低于影片高度后痕貌,影片改為相對于視窗的絕對位置(fixed)风罩,往回滾會再變回原本的狀態(tài)。當(dāng)影片為 fixed 時舵稠,滑鼠移至影片上方(hover)會有遮罩(masker)與鼠標(biāo)變化(cursor)超升,可以拖拉移動(drag),且移動范圍不超過可視區(qū)間哺徊!

上面可以拆分成以下幾個步驟

  • 準(zhǔn)備 static 樣式與 fixed 樣式
  • HTML 要有一個固定位置的錨點(anchor)
  • 當(dāng)滾動超過錨點室琢,則影片變成 fixed
  • 當(dāng)往回滾動過錨點上方,則影片變回 static
  • 影片 fixed 時落追,要能夠拖拉
  • 拖拉范圍限制在當(dāng)前可視區(qū)間

基本的 HTML 跟 CSS 筆者已經(jīng)幫大家完成盈滴,大家可以直接到下面的連結(jié)接著實作:

先讓我們看一下 HTML儿奶,首先在 HTML 裡有一個 div(#anchor),這個 div(#anchor) 就是待會要做錨點用的承疲,它內(nèi)部有一個 div(#video)皂冰,則是滾動后要改變成 fixed 的元件。

CSS 的部分我們只需要知道滾動到下方后荷鼠,要把 div(#video) 加上 video-fixed 這個 class。

接著我們就開始實作滾動的效果切換 class 的效果吧!

第一步继谚,取得會用到的 DOM

因為先做滾動切換 class,所以這裡用到的 DOM 只有 #video, #anchor阵幸。

const video = document.getElementById('video');
const anchor = document.getElementById('anchor');

第二步花履,建立會用到的 observable

這裡做滾動效果,所以只需要監(jiān)聽滾動事件挚赊。

const scroll = Rx.Observable.fromEvent(document, 'scroll');

第三步诡壁,撰寫程式邏輯

這裡我們要取得了 scroll 事件的 observable,當(dāng)滾過 #anchor 最底部時荠割,就改變 #video 的 class妹卿。

首先我們會需要滾動事件發(fā)生時旺矾,去判斷是否滾過 #anchor 最底部,所以把原本的滾動事件變成是否滾過最底部的 true or false夺克。

scroll.map(e => anchor.getBoundingClientRect().bottom < 0)

這裡我們用到了 getBoundingClientRect 這個瀏覽器原生的 API箕宙,他可以取得 DOM 物件的寬高以及上下左右離螢?zāi)豢梢晠^(qū)間上(左)的距離,如下圖

image.png

當(dāng)我們可視范圍區(qū)間滾過 #anchor 底部時铺纽, anchor.getBoundingClientRect().bottom 就會變成負(fù)值柬帕,此時我們就改變 #video 的 class。

scroll
.map(e => anchor.getBoundingClientRect().bottom < 0)
.subscribe(bool => {
    if(bool) {
        video.classList.add('video-fixed');
    } else {
        video.classList.remove('video-fixed');
    }
})

到這裡我們就已經(jīng)完成滾動變更樣式的效果了狡门!

全部的 JS 程式碼陷寝,如下

const video = document.getElementById('video');
const anchor = document.getElementById('anchor');

const scroll = Rx.Observable.fromEvent(document, 'scroll');

scroll
.map(e => anchor.getBoundingClientRect().bottom < 0)
.subscribe(bool => {
    if(bool) {
        video.classList.add('video-fixed');
    } else {
        video.classList.remove('video-fixed');
    }
})

當(dāng)然這段還能在用 debounce/throttle 或 requestAnimationFrame 做優(yōu)化,這個部分我們?nèi)蘸蟮奈恼聲谔峒啊?/p>

接下來我們就可以接著做拖拉的行為了其馏。

第一步盼铁,取得會用到的 DOM

這裡我們會用到的 DOM 跟前面是一樣的(#video),所以不用多做什麼尝偎。

第二步饶火,建立會用到的 observable

這裡跟上次一樣,我們會用到 mousedown, mouseup, mousemove 三個事件致扯。

const mouseDown = Rx.Observable.fromEvent(video, 'mousedown')
const mouseUp = Rx.Observable.fromEvent(document, 'mouseup')
const mouseMove = Rx.Observable.fromEvent(document, 'mousemove')

第三步肤寝,撰寫程式邏輯

跟上次是差不多的,首先我們會點擊 #video 元件抖僵,點擊(mousedown)后要變成移動事件(mousemove)鲤看,而移動事件會在滑鼠放開(mouseup)時結(jié)束(takeUntil)

mouseDown
.map(e => mouseMove.takeUntil(mouseUp))
.concatAll()

因為把 mouseDown observable 發(fā)送出來的事件換成了 mouseMove observable,所以變成了 observable(mouseDown) 送出 observable(mouseMove)耍群。因此最后用 concatAll 把后面送出的元素變成 mouse move 的事件义桂。

這段如果不清楚的可以回去看一下 08 篇的講解

但這裡會有一個問題,就是我們的這段拖拉事件其實只能做用到 video-fixed 的時候蹈垢,所以我們要加上 filter

mouseDown
.filter(e => video.classList.contains('video-fixed'))
.map(e => mouseMove.takeUntil(mouseUp))
.concatAll()

這裡我們用 filter 如果當(dāng)下 #video 沒有 video-dragable class 的話慷吊,事件就不會送出。

再來我們就能跟上次一樣曹抬,把 mousemove 事件變成 { x, y } 的物件溉瓶,并訂閱來改變 #video 元件

mouseDown
    .filter(e => video.classList.contains('video-fixed'))
    .map(e => mouseMove.takeUntil(mouseUp))
    .concatAll()
    .map(m => {
        return {
            x: m.clientX,
            y: m.clientY
        }
    })
    .subscribe(pos => {
        video.style.top = pos.y + 'px';
        video.style.left = pos.x + 'px';
    })

到這裡我們基本上已經(jīng)完成了所有功能,其步驟跟 08 篇的方法是一樣的谤民,如果不熟悉的人可以回頭看一下堰酿!

但這裡有兩個大問題我們還沒有解決

  1. 第一次拉動的時候會閃一下,不像優(yōu)酷那麼順
  2. 拖拉會跑出當(dāng)前可視區(qū)間张足,跑上出去后就抓不回來了

讓我們一個一個解決触创,首先第一個問題是因為我們的拖拉直接給元件滑鼠的位置(clientX, clientY),而非給滑鼠相對移動的距離为牍!

所以要解決這個問題很簡單哼绑,我們只要把點擊目標(biāo)的左上角當(dāng)作 (0,0)顺饮,并以此改變元件的樣式,就不會有閃動的問題凌那。

這個要怎麼做呢兼雄? 很簡單,我們在昨天講了一個 operator 叫做 withLatestFrom帽蝶,我們可以用它來把 mousedown 與 mousemove 兩個 Event 的值同時傳入 callback赦肋。

mouseDown
    .filter(e => video.classList.contains('video-fixed'))
    .map(e => mouseMove.takeUntil(mouseUp))
    .concatAll()
    .withLatestFrom(mouseDown, (move, down) => {
        return {
            x: move.clientX - down.offsetX,
            y: move.clientY - down.offsetY
        }
    })
    .subscribe(pos => {
        video.style.top = pos.y + 'px';
        video.style.left = pos.x + 'px';
    })

當(dāng)我們能夠同時得到 mousemove 跟 mousedown 的事件,接著就只要把 滑鼠相對可視區(qū)間的距離(client) 減掉點按下去時 滑鼠相對元件邊界的距離(offset) 就行了励稳。這時拖拉就不會先閃動一下蘿佃乘!

大家只要想一下,其實 client - offset 就是元件相對于可視區(qū)間的距離驹尼,也就是他一開始沒動的位置趣避!

image.png

接著讓我們解決第二個問題,拖拉會超出可視范圍新翎。這個問題其實只要給最大最小值就行了程帕,因為需求的關(guān)系,這裡我們的元件是相對可視居間的絕對位置(fixed)地啰,也就是說

  • top 最小是 0
  • left 最小是 0
  • top 最大是可視高度扣掉元件本身高度
  • left 最大是可視寬度扣掉元件本身寬度

這裡我們先宣告一個 function 來處理這件事

const validValue = (value, max, min) => {
    return Math.min(Math.max(value, min), max)
}

第一個參數(shù)給原本要給的位置值愁拭,后面給最大跟最小,如果今天大于最大值我們就取最大值亏吝,如果今天小于最小值則取最小值岭埠。

再來我們就可以直接把這個問題解掉了

mouseDown
    .filter(e => video.classList.contains('video-fixed'))
    .map(e => mouseMove.takeUntil(mouseUp))
    .concatAll()
    .withLatestFrom(mouseDown, (move, down) => {
        return {
            x: validValue(move.clientX - down.offsetX, window.innerWidth - 320, 0),
            y: validValue(move.clientY - down.offsetY, window.innerHeight - 180, 0)
        }
    })
    .subscribe(pos => {
        video.style.top = pos.y + 'px';
        video.style.left = pos.x + 'px';
    })

這裡我偷懶了一下,直接寫死元件的寬高(320, 180)蔚鸥,實際上應(yīng)該用 getBoundingClientRect 計算是比較好的惜论。

現(xiàn)在我們就完成整個應(yīng)用蘿!

這裡有最后完成的結(jié)果止喷。

今日結(jié)語

我們簡單地用了不到 35 行的程式碼馆类,完成了一個還算複雜的功能。更重要的是我們還保持了整支程式的可讀性启盛,讓我們之后維護(hù)更加的輕鬆蹦掐。

今天的練習(xí)就到這邊結(jié)束了,不知道讀者有沒有收穫呢僵闯? 如果有任何問題歡迎在下方留言給我!

如果你喜歡本篇文章請幫我按個 like 跟 星星藤滥。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鳖粟,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子拙绊,更是在濱河造成了極大的恐慌向图,老刑警劉巖泳秀,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異榄攀,居然都是意外死亡嗜傅,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進(jìn)店門檩赢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來吕嘀,“玉大人,你說我怎么就攤上這事贞瞒∨挤浚” “怎么了?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵军浆,是天一觀的道長棕洋。 經(jīng)常有香客問我,道長乒融,這世上最難降的妖魔是什么掰盘? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮赞季,結(jié)果婚禮上庆杜,老公的妹妹穿的比我還像新娘。我一直安慰自己碟摆,他們只是感情好晃财,可當(dāng)我...
    茶點故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著典蜕,像睡著了一般断盛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上愉舔,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天钢猛,我揣著相機(jī)與錄音,去河邊找鬼轩缤。 笑死命迈,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的火的。 我是一名探鬼主播壶愤,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼馏鹤!你這毒婦竟也來了征椒?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤湃累,失蹤者是張志新(化名)和其女友劉穎勃救,沒想到半個月后碍讨,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡蒙秒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年勃黍,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片晕讲。...
    茶點故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡覆获,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出益兄,到底是詐尸還是另有隱情锻梳,我是刑警寧澤,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布净捅,位于F島的核電站疑枯,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏蛔六。R本人自食惡果不足惜荆永,卻給世界環(huán)境...
    茶點故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望国章。 院中可真熱鬧具钥,春花似錦、人聲如沸液兽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽四啰。三九已至宁玫,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間柑晒,已是汗流浹背欧瘪。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留匙赞,地道東北人佛掖。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像涌庭,于是被迫代替她去往敵國和親芥被。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,465評論 2 348

推薦閱讀更多精彩內(nèi)容

  • 我們今天要接著講 take, first, takeUntil, concatAll 這四個 operators脾猛,...
    readilen閱讀 4,431評論 6 11
  • 噩夢中驚醒撕彤,讓我明白生命誠可貴。 我夢到自己身體不舒服猛拴,胃不舒服羹铅,發(fā)燒,就去醫(yī)院洗胃了愉昆,結(jié)果职员,胃中洗出來一個異物,...
    尋找自己的節(jié)奏閱讀 321評論 2 0
  • 校區(qū):科學(xué)創(chuàng)想機(jī)器人和平校區(qū) 時間:周三5:30-6:30 學(xué)員:王楚涵跛溉,張謙灝 任教老師:楊玲 教學(xué)目標(biāo): 1....
    Happy00閱讀 250評論 0 0