react hooks 封装一个月亮兔悬浮球组件

905 阅读2分钟

我正在参加「兔了个兔」创意投稿大赛,详情请看:「兔了个兔」创意投稿大赛

开发技术

react, hooks, ts

需求分析

实现一个可以跟随鼠标的点击或手指的触摸进行悬浮移动的悬浮球

rabbish_ball_2.gif

码上掘金体验 可移动的月亮兔子悬浮球

代码讲解

HTML

html 结构比较简单,使用两个 div 包裹住内部的 children 即可。这里的移动是采用的 transformtranslate 来进行的。代码从 jsxreturn 中进行截取

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 额外监听鼠标的 mousemovemouseup 事件。

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}
}

总结

以上就是该悬浮球组件的主要实现代码了,更全的代码放到码上掘金了,可以进去自提。