移动端浏览器:react-swipeable-views你小子在我的地盘还想run!

1,765 阅读3分钟

什么是react-swipeable-views

react-swipeable-views 是一个触屏端手势滑动进行整屏切换的react组件。 ​

使用痛点

api无法满足业务需求

有一个场景,当用户在页面中完成某些操作后,才可以上划 但是此组件提供的disabled属性,会将上划,下划都禁用 ​

页面底部的区域元素会被操作栏遮挡

image.png

因为设计和布局都是按照整个浏览器高度,但是在地址栏和操作栏的影响下 一些定位在底部元素就无法看到了

容器高度无法自动适配浏览器视窗高度

image.png

某些浏览器行为(如上图),比如上划到底部后,继续上划,浏览器地址栏会消失,此时视窗大小会发生变化 如果不进行高度重新适配,transform:translateY的位置还是依据之前的视窗高度,造成显示错位 ​

原生局部滚动会和组件干架

1ec411d0488d50c487bf33676fd73e25 (2).gif

如何解决

api无法满足业务需求

基本思路:克隆一份源码到本地,对源码进行扩展

扩展属性

image.png

在handleSwipeMove中处理

  handleSwipeMove = event => {
  ...............................
  ...............................
  ...............................
  ...............................
    const { axis, 
            children, 
            ignoreNativeScroll,
            onSwitching, 
            resistance,
            disableTouchUp,
            disableTouchDown
          } = this.props;
    const touch = applyRotationMatrix(event.touches[0], axis);
    const isTouchUp = touch.pageX - this.startX < 0
    const isTouchDown = touch.pageX - this.startX > 0
    if (disableTouchUp && isTouchUp) {
      console.log('isTouchUp',isTouchUp)
      return;
    }
    if (disableTouchDown && isTouchDown) {
      return;
    }
  ...............................
  ...............................
  ...............................
  ...............................
 }

页面底部的区域元素会被操作栏遮挡

获取正确height

const [rightHeight, setRightHeight] = useState(0)   
useEffect(() => {
     setRightHeight(window.innerHeight)                   
   },[])

给swipeView中的组件设置高度

const getRightHeight = () =>{
	return {height:rightHeight}
}
<SwipeableViews
  resistance
  axis="y"
  animateHeight={true}
  >
  <APage style={getRightHeight()}/>
  <BPage style={getRightHeight()}/>
  <CPage style={getRightHeight()}/> 
</SwipeableViews>
const A|B|CPage =({style}) => {

  return <div style={style}>
    ...........
    ...........
    ...........
    ...........
  </div>
}

容器高度无法自动适配浏览器视窗高度

方法一

const [animateHeight, setAnimateHeight] = useState(true)
useEffect(() => {
    window.onresize = function () {
      // 重新调整swipeview内元素高度并通知组件重新适配高度
      setRightHeight(window.innerHeight)
      setAnimateHeight(false)
      setTimeout(() => {
        setAnimateHeight(true)
      }, 60)
    }
},[])

const getRightHeight = () =>{
	return {height:rightHeight}
}
<SwipeableViews
  resistance
  axis="y"
  animateHeight={animateHeight}
  >
  <APage style={getRightHeight()}/>
  <BPage style={getRightHeight()}/>
  <CPage style={getRightHeight()}/> 
</SwipeableViews>

这是通过对animateHeight的更改触屏对containerStyle的高度的修复 见源码

image.png 这种方法会导致高度瞬间消失,使得页面会瞬间白屏下(如下图)

d8e8940fb2d62a28d82d553fb257d969 (1).gif

所以,看到这,笔者开始思考,会不会直接修改containerStyle来的更简单呢 ​

方法二

const getRightHeight = () =>{
	return {height:rightHeight}
}

<SwipeableViews
  resistance
  axis="y"
  containerStyle={getRightHeight()}
  >
  <APage style={getRightHeight()}/>
  <BPage style={getRightHeight()}/>
  <CPage style={getRightHeight()}/> 
</SwipeableViews>

事实证明确,确实不会闪屏,但还会存在视窗resize时,下一页内容出现的问题 究其根本原因,因为该组件是使用的transform:translateY对一屏位置进行确定,所以屏与屏之间是连续的 当resize事件发生时,我们绑定的回调函函数执行,state改变到页面重新绘制是个异步行为,所以页面会先保持未重新适配前的状态,再变为适配后的状态。

原生局部滚动会和组件干架

思路:如果放在容器内部会互相感扰,那就放到容器外部

将最后一页从SwipeableViews容器中拷贝一份到外面


<>
<SwipeableViews
  resistance
  axis="y"
  containerStyle={getRightHeight()} 
  >
  <APage style={getRightHeight()}/>
  <BPage style={getRightHeight()}/>
  <CPage style={getRightHeight()}/> 
</SwipeableViews>
<CPage style={getRightHeight()}/> 
</>

到第三页隐藏swipeView 显示外部第三页

const [currentIndex, setCurrentIndex] = useState(0)  

const onSwitching = (index) => {
  setCurrentIndex(index)
}

<>
<SwipeableViews
   style={{display: currentIndex < 2 ? 'block' : 'none'}}
  resistance
  axis="y"
  containerStyle={getRightHeight()}
  onSwitching={onSwitching}
  >
  <APage style={getRightHeight()}/>
  <BPage style={getRightHeight()}/>
  <CPage style={getRightHeight()}/> 
</SwipeableViews>
<CPage style={{...getRightHeight(),display: currentIndex >= 2 ? 'block' : 'none'}}/> 
</>

  1. 为什么不能使用currentIndex < 2?<SwipeableViews/>:<CPage>这种形式?

答:这种方式会导致组件的重新渲染,滑动位置的状态会丢失

  1. 为什么还要保留SwipeableViews内部的CPage

答:保证流畅的过渡,内部CPage的存在会使得切换到第三页的临界状态依然会存在过渡动画 ​

给第三页加上一个touch事件,保证可从第三页回到第二页

// Parent 回到第二页
 const onTouchDownToLastPage = () => {
    setCurrentIndex(1)
  }

// Child
const CPage =({style,onTouchDownToLastPage}) => {
    const onTouchStart = (e) => {
    if(touchStartY.current === null) {
      setHasTouched(true)
    }
    touchStartY.current = e.changedTouches[0].pageY
  }
    const onTouchMove = (e) => {

    let delta = e.changedTouches[0].pageY - touchStartY.current
    const dom = comicImgRef.current
    const scrollTop = comicWrap.current.scrollTop
    // (delta > 0 保证下滑 Math.abs(scrollTop) < 50 保证滚动调离顶部阈值范围
    if (delta > 0 && Math.abs(scrollTop) < 50) {
      onTouchDownToLastPage && onTouchDownToLastPage()
    }
  }

  return <div style={style} onTouchStart={onTouchStart} onTouchMove={onTouchMove}>
    ...........
    ...........
    <div className={styles.comic} ref={comicImgRef}></div>
    ...........
    ...........
  </div>
}

其他开发问题

image.png

底部引导提示在滑动时需消失,但是当手指滑动到该图标上,会阻止上划

原因分析

可能是因为滑动时,将该提示display:none处理,导致图层之间的touch事件不连续 ​

解决方法

display:none改为opacity:0

注意

考虑一下会不会遮挡后面图层的元素 ​

结语

前端开发就是逆天而行,共勉。