React Native 自主实现“悬浮球”组件

865 阅读2分钟

需求描述: 要在页面中显示一个悬浮球,可以点击与拖动,拖动不能移出屏幕,松手会自动贴到靠近的左/右侧,并可以半吸附到左右侧

遇到的问题:

  • 正常情况下拖动就会触发点击事件,如何区分二者?

    解决方案:拖动操作正常,若松开与按下时间间隔足够短,则视为触发点击;

    其他可能的方案:

    • 拖动操作正常,若松开与按下时的位移足够小,则视为触发点击;
    • 长按拖动(onLongPress),短按点击(onPress);
  • 如何确定合适的y坐标上下界?

    解决方案:接收参数topbottom用于限制上下界,由父组件规定上下界,并根据bottom的变化调整y坐标

实现如下:

import React, { useEffect, useState } from "react";
import { View, StyleSheet, PanResponder, Dimensions } from "react-native";

const FloatingButton = ({handlePress,size=50,top,bottom}:{handlePress:Function,size:number,top:number,bottom:number|null}) => {
  const screenWidth=Dimensions.get("window").width
  const screenHeight=Dimensions.get("window").height

  const [position, setPosition] = useState({ x: -size/2, y: screenHeight/2 /*可以根据实际情况调整初始y坐标*/ });
  const [pressStartTime, setPressStartTime] = useState(0);

  const panResponder = PanResponder.create({
    onStartShouldSetPanResponder: () => true,
    onMoveShouldSetPanResponder: () => true,
    onPanResponderMove: (e, gestureState) => {
      let posX = gestureState.moveX - size/2;
      if(posX<0)posX=-size/2;
      else if(posX>screenWidth-size)posX=screenWidth-size/2;

      let posY = gestureState.moveY - size/2;
      if(posY<0)posY=-size/2;

      setPosition({
        x: posX,
        y: posY,
      });
    },
    onPanResponderGrant: () => {
      setPressStartTime(Date.now());
    },
    onPanResponderRelease: () => {
      const pressEndTime = Date.now();
      const pressDuration = pressEndTime - pressStartTime;

      //如果点击时间(松开时间与按下时间的差值)过短,则视作点击
      if (pressDuration < 200 /*时间阈值,可以根据实际情况调整*/) {
        handlePress();
      }

      let posX = position.x;
      if(posX<0) posX=-size/2;
      else if(posX>screenWidth-size) posX=screenWidth-size/2;
      else if(posX<(screenWidth-size)/2) posX=0;
      else if(posX>(screenWidth-size)/2) posX=screenWidth-size;

      let posY = position.y;
      if(posY<top)posY=top;
      else if(bottom && posY>bottom-size) posY=bottom-size; 

      setPosition({
        x: posX,
        y: posY,
      });
    },
  });

  useEffect(()=>{
    if(bottom && position.y>bottom-size) {
      setPosition({
        x: position.x,
        y: bottom - size,
      });
    }
  },[bottom])

  return (
    <>
      <View
        style={[styles.ball, { left: position.x, top: position.y, width: size, height: size, borderRadius: size/2,}]}
        {...panResponder.panHandlers} />
    </>
  );
};

const styles = StyleSheet.create({
  ball: {
    backgroundColor: '#FFC88B',
    position: 'absolute',
    zIndex: 10000,
    borderColor: 'grey',
    borderWidth: 0.5,
    opacity: 0.6,
  },
});

export default FloatingButton;

使用示例:

<FloatingButton handlePress={handleOpenAI} size={50} bottom={scrollHeight} top={80}/>

2023-10-18 15 02 31.gif