我正在参加「兔了个兔」创意投稿大赛,详情请看:「兔了个兔」创意投稿大赛
开发技术
react
, hooks
, ts
需求分析
实现一个可以跟随鼠标的点击或手指的触摸进行悬浮移动的悬浮球
码上掘金体验 可移动的月亮兔子悬浮球
代码讲解
HTML
html
结构比较简单,使用两个 div
包裹住内部的 children
即可。这里的移动是采用的 transform
的 translate
来进行的。代码从 jsx
的 return
中进行截取
const classPrefix = `com-floating-ball`;
// 这里是 jsx 的 html 返回
return withNativeProps(
props,
<div className={`${classPrefix} ${idRef.current}`}>
<div
ref={buttonRef}
className={`${classPrefix}-button`}
style={{transform: `translate(${info.x}px, ${info.y}px)`}}
{...handleEvent()}
>
{props.children}
</div>
</div>
)
css
css
部分采用 fixed
定位,其中的上下左右值根据传入的参数来进行定位。采用 transition
属性来实现鼠标或手指触摸后,悬浮球跟随移动的动画效果。
.com-floating-ball {
&-button {
position: fixed;
top: var(--initial-position-top);
bottom: var(--initial-position-bottom);
left: var(--initial-position-left);
right: var(--initial-position-right);
user-select: none;
touch-action: none;
transition: transform ease-out 0.15s;
z-index: var(--z-index);
}
}
js
Props
- axis 控制拖动的方向
- magnetic 觉得是否自动吸附到边界
- onMagnetic 贴边时触发
- onOffsetChange 位置偏移时触发
export type FloatingBallProps = {
/** 可以进行拖动的方向,'xy' 表示自由移动 默认值xy */
axis?: 'x' | 'y' | 'xy'
/** 自动磁吸到边界 */
magnetic?: 'x' | 'y'
/** 贴边时触发 isLeft: true 代表是左或上方向上贴边 */
onMagnetic?: (isLeft: boolean) => void
/** 位置偏移时触发 */
onOffsetChange?: (offset: {x: number, y: number}) => void
children?: React.ReactNode
} & NativeProps<
| '--initial-position-left'
| '--initial-position-right'
| '--initial-position-top'
| '--initial-position-bottom'
| '--z-index'
>
初始化获取悬浮球的宽,高,上下左右距离的信息。
/** 悬浮球的宽,高,上下左右距离 */
const ball = useRef({w: 0, h: 0, r: 0, l: 0, t: 0, b: 0})
useEffect(() => {
const init = () => {
const ballDom = document.querySelector(`.${idRef.current} .${classPrefix}-button`)
if(!ballDom) return
const ballInfo = ballDom.getBoundingClientRect()
ball.current.w = ballInfo.width
ball.current.h = ballInfo.height
ball.current.l = ballInfo.left
ball.current.r = screenW - ballInfo.right
ball.current.t = ballInfo.top
ball.current.b = screenH - ballInfo.bottom
}
setTimeout(() => {
init()
}, 10);
}, [])
触摸事件的监听
根据当前是移动端还是pc端监听鼠标事件或手指触摸事件
const handleEvent = () => {
if(!isMobile()) {
return {
onMouseDown: onTouchStart,
onMouseUp: onTouchEnd,
}
} else {
return {
onTouchStart: onTouchStart,
onTouchMove: onTouchMove,
onTouchEnd: onTouchEnd,
onTouchCancel: onTouchEnd,
}
}
}
判断当前是pc端还是移动端的方法
function isMobile() {
return navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i)
}
- state
info
是用来移动悬浮球时触发 render
的。
touchRef
用来记录起始触摸位置。
const touchRef = useRef({
startX: 0,
startY: 0,
})
const [info, setInfo] = useState({
x: 0,
y: 0,
})
- onTouchStart
记录触摸的起始坐标,当当前为pc端时,需要给 document
额外监听鼠标的 mousemove
和 mouseup
事件。
const onTouchStart = (e: MouseEventType | TouchEventType) => {
e.stopPropagation()
const newE = handleMouseOfTouch(e)
touchRef.current.startX = newE.x - info.x
touchRef.current.startY = newE.y - info.y
if(!isMobile()) {
document.addEventListener('mousemove', onTouchMove, true)
document.addEventListener('mouseup', onTouchEnd, true)
}
}
- onTouchMove
当鼠标或手指移动时,都会触发 onTouchMove
根据触摸的起始坐标和移动坐标相减即可算出当前应该移动的位置。给 setInfo
赋值,触发 render
,悬浮球移动。
const onTouchMove = useCallback((e: MouseEventType | TouchEventType) => {
e.stopPropagation()
const newE = handleMouseOfTouch(e)
const x = axis === 'y' ? 0 : newE.x - touchRef.current.startX
const y = axis === 'x' ? 0 : newE.y - touchRef.current.startY
setInfo({x, y})
props.onOffsetChange?.({x, y})
}, [axis])
- onTouchEnd
触摸结束时,如果是pc端需要给 document
移除事件的监听,然后根据当前移动的位置,判断是否需要贴边,是贴x边还是y边,还有判断移动的方式等,再通过计算算出最后应该触发的x值和y值。
const onTouchEnd = useCallback((e: MouseEventType | TouchEventType) => {
e.stopPropagation()
if(!isMobile()) {
document.removeEventListener('mousemove', onTouchMove, true)
document.removeEventListener('mouseup', onTouchEnd, true)
}
const newE = handleMouseOfTouch(e)
let x = axis === 'y' ? 0 : newE.x - touchRef.current.startX
let y = axis === 'x' ? 0 : newE.y - touchRef.current.startY
const {w, h, l, r, t, b} = ball.current
if (magnetic === 'x') {
const l_r = l < r ? l : r
const _v = l < r ? -1 : 1
const middleX = screenW / 2 - l_r - w / 2 // 中间分隔线的值
const distance = -1 * _v * (screenW - w - l_r * 2) // 另一边的位置
x = (Math.abs(x) > middleX) ? (x * _v < 0 ? distance : 0) : 0
props.onMagnetic?.(x === 0 ? l < r : l > r)
} else if (magnetic === 'y') {
const l_r = t < b ? t : b
const _v = t < b ? -1 : 1
const middleX = screenH / 2 - l_r - h / 2 // 中间分隔线的值
const distance = -1 * _v * (screenH - h - l_r * 2) // 另一边的位置
y = (Math.abs(y) > middleX) ? (y * _v < 0 ? distance : 0) : 0
props.onMagnetic?.(y === 0 ? t < b : t > b)
}
setInfo({x, y})
}, [axis, magnetic, screenW, screenH])
- handleMouseOfTouch
根据是pc端还是移动端,处理事件中需要获取到的x和y值。
const handleMouseOfTouch = (e: MouseEventType | TouchEventType) => {
let x = 0, y = 0;
if(!isMobile()) {
x = (e as MouseEventType).clientX
y = (e as MouseEventType).clientY
} else {
e = e as TouchEventType
x = e.touches[0]?.clientX || e.changedTouches[0].clientX
y = e.touches[0]?.clientY || e.changedTouches[0].clientY
}
return {x, y}
}
总结
以上就是该悬浮球组件的主要实现代码了,更全的代码放到码上掘金了,可以进去自提。