react版模擬亞馬遜人機(jī)交互菜單的實(shí)現(xiàn)
前言
前段時間接了一個需求,實(shí)現(xiàn)一個模仿亞馬遜和京東的菜單交互效果,這種效果被稱為模擬人機(jī)交互。在網(wǎng)上搜了一下,目前沒有見到有 react
和Vue
的版本,然后就自己參考了一下現(xiàn)有的方式,實(shí)現(xiàn)了一個react
版本。
需求介紹
本文都是在web端的需求
參考亞馬遜和京東商城的首頁左側(cè)菜單效果,實(shí)現(xiàn)一個react版本的組件,以供業(yè)務(wù)使用。
我們先看下亞馬遜和京東商城的效果:
亞馬遜商城
京東商城
從上面的效果得出我們的菜單效果需求點(diǎn):
- 當(dāng)我們的鼠標(biāo)懸浮在左側(cè)菜單的時候,右側(cè)會對應(yīng)展示它對應(yīng)的子菜單項(xiàng),
- 當(dāng)我們的鼠標(biāo)在左側(cè)菜單上下移動時,左側(cè)可以快速切換為對應(yīng)的子菜單
- 當(dāng)我們的鼠標(biāo)移動以一定的傾斜角度移動到右側(cè)的時候,鼠標(biāo)雖然會經(jīng)過其它的左側(cè)菜單,但是不會執(zhí)行切換。
到目前為止,我們就搞情況了我們的需求。接下來就要去實(shí)現(xiàn)我們的方案了。
實(shí)現(xiàn)方案
要實(shí)現(xiàn)我們的需求,復(fù)雜點(diǎn)主要是在如何實(shí)現(xiàn)上述的需求3。需求1和需求2 的基本切換效果我就不再說了,直接進(jìn)入需求3
實(shí)現(xiàn)需求3
如果要實(shí)現(xiàn)這個需求,我們需要記錄鼠標(biāo)從左往右(從左側(cè)菜單區(qū)域移動到右側(cè)菜單區(qū)域)的移動軌跡,然后根據(jù)它的移動軌跡去判斷它是否是在一個三角形的區(qū)域之內(nèi),如果在的話,就不讓它切換菜單。
我們先看一張圖:
- P1:鼠標(biāo)的起始位置
- P2:左側(cè)菜單的固定點(diǎn)1,鼠標(biāo)在左側(cè)區(qū)域的最大位移點(diǎn)
- P3:左側(cè)菜單的固定點(diǎn)2,鼠標(biāo)在左側(cè)區(qū)域的最大位移點(diǎn)
- M:鼠標(biāo)在左側(cè)菜單移動的結(jié)束位置
從上圖我們可以得出:
如果鼠標(biāo)的起始點(diǎn)是在 P1 的話,當(dāng)鼠標(biāo)移動到右側(cè)區(qū)域,鼠標(biāo)可能經(jīng)過的三角形區(qū)域就是 P1-P2-P3所在的三角形,M點(diǎn)是鼠標(biāo)的結(jié)束位置。所以我們判斷鼠標(biāo)的運(yùn)動軌跡是否在三角形中就可以了。
部分邏輯代碼
const [active, setActive] = useState(null) // 選中的菜單 const [showSub, setShowSub] = useState(false) // 是否顯示子菜單 let timeout = useRef(null) // 設(shè)置延遲定時器,防止鼠標(biāo)移到tab內(nèi)容經(jīng)過菜單時的切換 let mouseLocs = useRef([]) // 記錄鼠標(biāo)移動時的坐標(biāo)數(shù)組 let firstSlope = useRef(null) // 菜單欄的固定點(diǎn)1, 根據(jù)菜單欄和內(nèi)容的位置而改變 let secondSlope = useRef(null) // 菜單欄的固定點(diǎn)2, 根據(jù)菜單欄和內(nèi)容的位置而改變 const refNavigation = useRef(null) const refNav = useRef(null) const refSubnav = useRef(null) /** * 根據(jù)內(nèi)容欄相對于菜單欄的位置,判斷移動過程中的點(diǎn)是否在三角形內(nèi) * @param {Object} p1 開始位置 * @param {Object} p2 菜單欄固定點(diǎn)1 * @param {Object} p3 菜單欄固定點(diǎn)2 * @param {Object} m 結(jié)束位置 * @return {*} */ function proPosInTriangle(p1, p2, p3, m) { // 結(jié)束時鼠標(biāo)坐標(biāo)位置 let x = m.x, y = m.y, // 開始鼠標(biāo)坐標(biāo)位置 x1 = p1.x, y1 = p1.y, // 菜單欄包裹層右上角坐標(biāo) x2 = p2.x, y2 = p2.y, // 右下角坐標(biāo) x3 = p3.x, y3 = p3.y, // (y2 - y1) / (x2 - x1)為兩坐標(biāo)連成直線的斜率 // 因?yàn)橹本€的公式為y=kx+b;當(dāng)斜率相同時,只要比較 // b1和b2的差值就可以知道該點(diǎn)是在 // (x1,y1),(x2,y2)的直線的哪個方向 // 當(dāng)r1大于0,說明該點(diǎn)在直線右側(cè),其它以此類推 r1 = y - y1 - ((y2 - y1) / (x2 - x1)) * (x - x1), r2 = y - y2 - ((y3 - y2) / (x3 - x2)) * (x - x2), r3 = y - y3 - ((y1 - y3) / (x1 - x3)) * (x - x3), compare compare = r1 * r2 * r3 < 0 && r1 > 0 // 返回是否在三角形內(nèi)的結(jié)果 return compare } /** * 獲取元素相對于瀏覽器左上角的坐標(biāo)位置,為正值 * @param element * @return {{x: Number, y: Number}} * @constructor */ function LocFromdoc(element) { const { x, y, width, height } = element.getBoundingClientRect() return { x: x, y: y, width, height, } } /** * 記錄元素的位置信息 * @param element * @return {{top: *, topAndHeight: number, left: *, leftAndWidth: number}} */ function getInfo(element) { const location = LocFromdoc(element) return { top: location.y, topAndHeight: location.y + element.offsetHeight, // offsetHeight 元素的像素高度, 高度包含該元素的垂直內(nèi)邊距和邊框,且是一個整數(shù) left: location.x, leftAndWidth: location.x + element.offsetWidth, } } /** * 根據(jù)內(nèi)容欄相對于菜單欄的位置, 返回菜單欄的固定點(diǎn)1,和固定點(diǎn)2,保存在this.firstSlope和this.secondSlope對象里 * 即 左側(cè)菜單欄的右上角和右下角的位置 */ function ensureTriangleDots() { // 獲取菜單欄的位置 const info = getInfo(refNav.current) const x1 = info.leftAndWidth const y1 = info.top const x2 = x1 const y2 = info.topAndHeight firstSlope.current = { x: x1, y: y1, } secondSlope.current = { x: x2, y: y2, } } const onMouseOver = useCallback( obj => { let diff try { // 是否在指定三角形內(nèi) diff = proPosInTriangle( mouseLocs.current[0], firstSlope.current, secondSlope.current, mouseLocs.current[2] ) } catch (ex) {} // 是就啟動延遲顯示, // 否則不延遲 if (diff) { timeout.current = setTimeout(() => { setActive(obj.key) setShowSub(true) }, 300) } else { setActive(obj.key) setShowSub(true) } }, [mouseLocs, timeout] ) const onMouseEnter = () => { // 計(jì)算位置 if (refNav.current) { ensureTriangleDots() } setShowSub(true) } // 移出菜單所在區(qū)域 const onMouseLeave = () => { if (refSubnav.current) { setActive(null) setShowSub(false) } } // 記錄鼠標(biāo)在菜單欄中移動的最后三個坐標(biāo)位置 const onMousemove = event => { mouseLocs.current.push({ x: event.pageX, y: event.pageY, }) if (mouseLocs.current.length > 3) { // 移除超過三項(xiàng)的數(shù)據(jù) mouseLocs.current.shift() } } // 鼠標(biāo)移出的時候,清除延時器 const onMouseout = () => { if (timeout.current) { clearTimeout(timeout.current) } }
實(shí)現(xiàn)效果
到此這篇關(guān)于react版模擬亞馬遜人機(jī)交互菜單的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)react 交互菜單內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React學(xué)習(xí)之受控組件與數(shù)據(jù)共享實(shí)例分析
這篇文章主要介紹了React學(xué)習(xí)之受控組件與數(shù)據(jù)共享,結(jié)合實(shí)例形式分析了React受控組件與組件間數(shù)據(jù)共享相關(guān)原理與使用技巧,需要的朋友可以參考下2020-01-01antd-react使用Select組件defaultValue踩的坑及解決
這篇文章主要介紹了antd-react使用Select組件defaultValue踩的坑及解決方案,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-05-05React組件三大核心屬性State?props?Refs介紹
組件實(shí)例的三大核心屬性是:State、Props、Refs。類組件中這三大屬性都存在。函數(shù)式組件中訪問不到?this,也就不存在組件實(shí)例這種說法,但由于它的特殊性(函數(shù)可以接收參數(shù)),所以存在Props這種屬性2023-02-02react?實(shí)現(xiàn)表格列表拖拽排序的示例
本文主要介紹了react?實(shí)現(xiàn)表格列表拖拽排序,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-02-02react native帶索引的城市列表組件的實(shí)例代碼
本篇文章主要介紹了react-native城市列表組件的實(shí)例代碼,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-08-08React 模塊聯(lián)邦多模塊項(xiàng)目實(shí)戰(zhàn)詳解
這篇文章主要介紹了React 模塊聯(lián)邦多模塊項(xiàng)目實(shí)戰(zhàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10ReactJS應(yīng)用程序中設(shè)置Axios攔截器方法demo
這篇文章主要為大家介紹了ReactJS應(yīng)用程序中設(shè)置Axios攔截器方法demo,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12