在第 08 篇的時候兑燥,我們已經(jīng)成功做出簡易的拖拉效果亮瓷,今天要來做一個完整的應(yīng)用,而且是實務(wù)上有機(jī)會遇到但不好處理的需求降瞳,那就是優(yōu)酷的影片效果嘱支!
如果還沒有用過優(yōu)酷的讀者可以先前往這裡試用蚓胸。
當(dāng)我們在優(yōu)酷看影片時往下滾動畫面,影片會變成一個小視窗在右下角除师,這個視窗還能夠拖拉移動位置沛膳。這個功能可以讓使用者一邊看留言同時又能看影片,且不影響其他的資訊顯示汛聚,真的是很不錯的 feature锹安。
就讓我們一起來實作這個功能,同時補(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ū)間上(左)的距離,如下圖
當(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 篇的方法是一樣的谤民,如果不熟悉的人可以回頭看一下堰酿!
但這裡有兩個大問題我們還沒有解決
- 第一次拉動的時候會閃一下,不像優(yōu)酷那麼順
- 拖拉會跑出當(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ū)間的距離驹尼,也就是他一開始沒動的位置趣避!
接著讓我們解決第二個問題,拖拉會超出可視范圍新翎。這個問題其實只要給最大最小值就行了程帕,因為需求的關(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 跟 星星藤滥。