2017-09-09 13:48:10

React Native路由: 页面间的触摸穿透特效

在我写的PSNINE中,回复页面有一个很好玩的特效:

本文介绍一下如何配合react-navigation做出同样的效果

立个Flag

这种触摸穿透是路由中两个页面之间的,并不是Modal,因此需要一个Flag来控制是否可以点击穿透页面。这里博主选择了使用如下的标识:

this.props.navigation.navigate({{name}}, {
  shouldSeebackground: true
})

还原底层路由的透明度

react-navigation底层的路由页面的透明度是0,我们需要在此时将其还原为正常透明度,语句如下:

export function transitionConfig() {
  return {
    transitionSpec: {
      duration: 350,
      /* tslint:disable */
      easing: Easing.out(Easing.poly(5)), // decelerate
      /* tslint:enable */
      timing: Animated.timing
    },
    screenInterpolator: function (props) {
      const {
        layout,
        position,
        scene,
        navigation
      } = props

      if (!layout.isMeasured) {
        return forInitial(props)
      }

      let index = scene.index

      let prev = hack.prevTransitionProps.scenes && hack.prevTransitionProps.scenes[index + 1]

      if (scene && prev && prev.route) {
        const shouldSet = prev.route.params && prev.route.params.shouldSeeBackground === true
        if (scene.isActive && shouldSet) {
          return {
            opacity: 1
          }
        }
      }

      const params = (navigation.state.routes[navigation.state.index] || {}).params || {}
      if (params.shouldSeeBackground === true) {
        if (scene.index + 1 >= navigation.state.index) {
          return {
            opacity: 1
          }
        }
      }

      const inputRange = [index - 1, index, index + 0.99, index + 1]

      const opacity = position.interpolate({
        inputRange,
        outputRange: ([0, 1, 1, 0])
      })

      const translateY = position.interpolate({
        inputRange,
        outputRange: ([SCREEN_WIDTH / 2, 0, 0, 0])
      })

      return {
        opacity,
        transform: [{ translateX: translateY }, { translateY: 0 }]
      }
    }
  }
}

注意上面的代码中使用hack.prevTransitionProps来拿到了上一步的路由状态,很遗憾react-navigation没有将这个状态当做参数传给自定义过渡函数,不过它暴露了一个onTransitionStart,我们可以做一次手动保留状态:

function forInitial(props) {
  const {
    navigation,
    scene
  } = props

  const focused = navigation.state.index === scene.index
  const opacity = focused ? 1 : 0
  // If not focused, move the scene far away.
  const translate = focused ? 0 : 1000000
  return {
    opacity,
    transform: [{ translateX: translate }, { translateY: translate }]
  }
}

const hack: any = {
  prevTransitionProps: {}
}

export function onTransitionStart(transitionProps?: any): any {
  hack.prevTransitionProps = transitionProps
}

最后在StackNavigator中注册上面导出的两个函数即可。

控制触摸事件

做到上面的两步时,已经可以从上层路由上看到底层路由的页面路由了,然而此时并不能触摸下面的页面。react-navigation并没有导出抽象触摸事件组件的函数,因此只能改源码了。

修改react-navigation/src/views/PointerEventsContainer.js文件,将_computePointerEvents修改为如下函数:

    _computePointerEvents(): string {
      const {
        navigation,
        position,
        scene,
      } = this.props;
      const params = (navigation.state.routes[navigation.state.index] || {}).params || {}

      if (params.shouldSeeBackground === true) {
        if (scene.index === navigation.state.index)
          return 'box-none'
        else if (scene.index + 1 == navigation.state.index)
          return 'auto'
      }
      if (scene.isStale || navigation.state.index !== scene.index) {
        // The scene isn't focused.
        return scene.index > navigation.state.index ? 'box-only' : 'none';
      }

      const offset = position.__getAnimatedValue() - navigation.state.index;
      if (Math.abs(offset) > MIN_POSITION_OFFSET) {
        return 'auto'
      }

      return 'auto';
    }

现在,已经可以触摸下层的路由了。

其他

本文开头的增长动画完全由Animated.Value + useNativeDriver编写,注意一点是border-radius对ToolbarAndroid控件无效,因此博主自己用手动撸了一个纯JS实现的ToolbarAndroid,以支持圆形增长动画。

另外,在早期RN还没有废弃掉安卓Navigator组件时,博主也同样实现了本文的特效。比较Navigator和react-navigation,可以明显看出废弃Navigator是相当理智的选择,它的实现(一个文件)和抽象(没有任何暴露的接口)实在是太烂了。

本文链接:https://smallpath.me/post/react-native-touch-through-effect

-- EOF --