import React from 'react';
import clamp from 'clamp';
import {Animated, Easing, I18nManager, PanResponder, Platform, StyleSheet, View,}
from 'react-native';
import Card from './Card';
import Header from '../Header/Header';
import NavigationActions from '../../NavigationActions';
import addNavigationHelpers from '../../addNavigationHelpers';
import getChildEventSubscriber from '../../getChildEventSubscriber';
import SceneView from '../SceneView';
import TransitionConfigs from './TransitionConfigs';
import * as ReactNativeFeatures from '../../utils/ReactNativeFeatures';
const emptyFunction = () => {
};
const EaseInOut = Easing.inOut(Easing.ease);
/**
* The max duration of the card animation in milliseconds after released gesture.
* The actual duration should be always less then that because the rest distance
* is always less then the full distance of the layout.
*/
const ANIMATION_DURATION = 500;
/**
* The gesture distance threshold to trigger the back behavior. For instance,
* `1/2` means that moving greater than 1/2 of the width of the screen will
* trigger a back action
*/
const POSITION_THRESHOLD = 1 / 2;
/**
* The threshold (in pixels) to start the gesture action.
* 拖拽距离超过此值,手势成立,才进行相应的处理
*/
const RESPOND_THRESHOLD = 20;
/**
* The distance of touch start from the edge of the screen where the gesture will be recognized
*/
// 缺省情况下,水平方向上,拖拽起始点<25,拖拽手势被认可
const GESTURE_RESPONSE_DISTANCE_HORIZONTAL = 25;
// 缺省情况下,竖直方向上,拖拽起始点坐标<135,拖拽手势被认可
const GESTURE_RESPONSE_DISTANCE_VERTICAL = 135;
const animatedSubscribeValue = animatedValue => {
if (!animatedValue.__isNative) {
return;
}
if (Object.keys(animatedValue._listeners).length === 0) {
animatedValue.addListener(emptyFunction);
}
};
/**
* Card Stack, 卡片堆栈,
* 1. 对于一个卡片 Card , 你可以把它理解成正好适合屏幕大小一样的一张卡片;
* 2. 每张卡片用于渲染某一个开发人员提供的路由场景屏幕组件(也就是对应到路由设置中的一项)和头部;
* 3. 同一张卡片上不会同时出现两个路由场景屏幕的内容;
* 4. Card Stack 会把所有的这些卡片渲染出来,渲染过程是根据卡片相应场景屏幕在导航路由栈中的 index
* 从小到大的顺序在 z 轴上渲染出每张卡片。因此你可以把一个 Card Stack 想象成一个在垂直于手机屏幕
* 平面方向上堆叠起来的一沓名片,用户早期访问的卡片距离客户较远,用户最近访问的卡片距离客户更近。
* 5. Card Stack 同时也负责了手势识别关闭当前屏幕的工作 :
* - 1. 如果 navigationOptions 中指定了 boolean 类型的 gesturesEnabled, 根据它的值决定是否支持手势关屏,
* - 2. 否则如果没有指定这样一个选项,但平台是 iOS, 则支持手势关屏 ,
* - 3. 否则,也就是平台是 Android, 则不支持手势关屏.
*/
class CardStack extends React.Component {
/**
* Used to identify the starting point of the position when the gesture starts, such that it
* can be updated according to its relative position. This means that a card can effectively
* be "caught"- If a gesture starts while a card is animating, the card does not jump into a
* corresponding location for the touch.
*/
_gestureStartValue = 0;
// tracks if a touch is currently happening
_isResponding = false;
/**
* immediateIndex is used to represent the expected index that we will be on after a
* transition. To achieve a smooth animation when swiping back, the action to go back
* doesn't actually fire until the transition completes. The immediateIndex is used during
* the transition so that gestures can be handled correctly. This is a work-around for
* cases when the user quickly swipes back several times.
*
* _immediateIndex 用于记录动画结束后的目标场景屏幕 index。为了滑动返回动画能够平滑进行,
* 返回所对应的 action 会在动画结束时才派发去改变导航状态。在此过程中,导航状态并不发生变化,
* _immediateIndex 也不变,用来确保手势能得到正确地处理。
*
* 这是一个用户快速多次滑动返回时的 work-around (应急解决方案)。
*/
_immediateIndex = null;
/**
* 保存每个场景的屏幕详情信息,可以理解成是一个Map,
* key 是 scene.key, value 是一个 screenDetails 对象,
* 关于 screenDetails 对象,可以参考函数实现 : this._getScreenDetails(scene) ,
*
* @type {{}}
* @private
*/
_screenDetails = {};
_childEventSubscribers = {};
componentWillReceiveProps(props) {
if (props.screenProps !== this.props.screenProps) {
this._screenDetails = {};
}
props.transitionProps.scenes.forEach(newScene => {
if (
this._screenDetails[newScene.key] &&
this._screenDetails[newScene.key].state !== newScene.route
) {
this._screenDetails[newScene.key] = null;
}
});
}
componentDidUpdate() {
const activeKeys = this.props.transitionProps.navigation.state.routes.map(
route => route.key
);
Object.keys(this._childEventSubscribers).forEach(key => {
if (!activeKeys.includes(key)) {
delete this._childEventSubscribers[key];
}
});
}
/**
* 判断指定路由 route 是否当前焦点 (当前活跃,用户当前正在操作的屏幕, 这里都可认为是相同语义)
* @param route
* @return {boolean}
* @private
*/
_isRouteFocused = route => {
const {transitionProps: {navigation: {state}}} = this.props;
const focusedRoute = state.routes[state.index];
return route === focusedRoute;
};
/**
* 获取指定场景 scene 的屏幕详情信息对象
*
* screenDetails 对象的一个例子 :
* @param scene
* @return {*}
* @private
*/
_getScreenDetails = scene => {
const {screenProps, transitionProps: {navigation}, router} = this.props;
let screenDetails = this._screenDetails[scene.key];
if (!screenDetails || screenDetails.state !== scene.route) {
if (!this._childEventSubscribers[scene.route.key]) {
this._childEventSubscribers[scene.route.key] = getChildEventSubscriber(
navigation.addListener,
scene.route.key
);
}
const screenNavigation = addNavigationHelpers({
dispatch: navigation.dispatch,
state: scene.route,
isFocused: () => this._isRouteFocused(scene.route),
addListener: this._childEventSubscribers[scene.route.key],
});
screenDetails = {
state: scene.route,
navigation: screenNavigation,
options: router.getScreenOptions(screenNavigation, screenProps),
};
this._screenDetails[scene.key] = screenDetails;
}
return screenDetails;
};
/**
* 渲染屏幕头部组件
* @param scene
* @param headerMode
* @return {*}
* @private
*/
_renderHeader(scene, headerMode) {
// 获取为当前场景指定的头部组件 :
// 1.可能是一个一般组件 : 头部组件,可以直接返回使用 ;
// 2.也可能是个函数 : 头部渲染函数 ;
const {header} = this._getScreenDetails(scene).options;
if (typeof header !== 'undefined' && typeof header !== 'function') {
// 外部指定了自定义头部组件,并且该头部组件不是一个函数
return header;
}
// 准备真正要使用的头部渲染函数 renderHeader
const renderHeader = header || (props => <Header {...props} />);
const {
headerLeftInterpolator,
headerTitleInterpolator,
headerRightInterpolator,
} = this._getTransitionConfig();
const {
mode,
transitionProps,
prevTransitionProps,
...passProps
} = this.props;
return renderHeader({
...passProps,
...transitionProps,
scene,
mode: headerMode,
transitionPreset: this._getHeaderTransitionPreset(),
getScreenDetails: this._getScreenDetails,
leftInterpolator: headerLeftInterpolator,
titleInterpolator: headerTitleInterpolator,
rightInterpolator: headerRightInterpolator,
});
}
// eslint-disable-next-line class-methods-use-this
_animatedSubscribe(props) {
// Hack to make this work with native driven animations. We add a single listener
// so the JS value of the following animated values gets updated. We rely on
// some Animated private APIs and not doing so would require using a bunch of
// value listeners but we'd have to remove them to not leak and I'm not sure
// when we'd do that with the current structure we have. `stopAnimation` callback
// is also broken with native animated values that have no listeners so if we
// want to remove this we have to fix this too.
animatedSubscribeValue(props.transitionProps.layout.width);
animatedSubscribeValue(props.transitionProps.layout.height);
animatedSubscribeValue(props.transitionProps.position);
}
/**
* 复位到指定索引 resetToIndex 的卡片 , 有动画,动画效果和 _goBack 类似
* 1.Android 使用 Animated.timing + EaseInOut
* 2.iOS 使用 Animated.spring
* @param resetToIndex 目标卡片的索引
* @param duration 动画时间 , 仅在 Android上有效, iOS上使用了 spring, 不使用此参数
* @private
*/
_reset(resetToIndex, duration) {
if (
Platform.OS === 'ios' &&
ReactNativeFeatures.supportsImprovedSpringAnimation()
) {
Animated.spring(this.props.transitionProps.position, {
toValue: resetToIndex,
stiffness: 5000,
damping: 600,
mass: 3,
useNativeDriver: this.props.transitionProps.position.__isNative,
}).start();
} else {
Animated.timing(this.props.transitionProps.position, {
toValue: resetToIndex,
duration,
easing: EaseInOut,
useNativeDriver: this.props.transitionProps.position.__isNative,
}).start();
}
}
/**
* 从指定索引的场景屏幕返回 , 有动画,动画效果和 _reset 类似
* 1.Android 使用 Animated.timing + EaseInOut
* 2.iOS 使用 Animated.spring
* @param backFromIndex 返回动作开始时的场景屏幕的索引
* @param duration 动画时间 , 仅在 Android上有效, iOS上使用了 spring, 不使用此参数
* @private
*/
_goBack(backFromIndex, duration) {
const {navigation, position, scenes} = this.props.transitionProps;
// 用于记录目标场景屏幕的索引
const toValue = Math.max(backFromIndex - 1, 0);
// set temporary index for gesture handler to respect until the action is
// dispatched at the end of the transition.
// 目标场景屏幕的索引记录到当前实例属性 _immediateIndex 上
this._immediateIndex = toValue;
// 下面代码定义了返回动画和动画完成时的回调函数,然后启动了动画,
// 需要注意 :
// 1. 动画过程中 this._immediateIndex 保持不变,指向返回的目标场景屏幕;
// 2. 动画结束时 this._immediateIndex 复位为 null;
// 3. 动画结束时,才真正派发 BACK action 去改变路由导航状态;
// 动画结束时的回调函数
const onCompleteAnimation = () => {
this._immediateIndex = null;
// 此时此刻,界面展现的场景屏幕已经是屏幕返回动作的目标场景屏幕,
// 但是路由导航状态中当前场景屏幕仍然是返回动作的起始场景屏幕,
// 需要向 navigation 派发 BACK action 从而消除这一不一致现象
const backFromScene = scenes.find(s => s.index === toValue + 1);
if (!this._isResponding && backFromScene) {
// 如果现在没有在响应手势,并且找到了返回动作起始场景屏幕,
// 则派发从该场景屏幕开始的一个 BACK action, 对路由导航状态
// 进行相应的更新,从而保持界面展现和路由导航状态对象的一致
navigation.dispatch(
NavigationActions.back({
key: backFromScene.route.key,
immediate: true,
})
);
}
};
// 开始动画
if (
Platform.OS === 'ios' &&
ReactNativeFeatures.supportsImprovedSpringAnimation()
) {
Animated.spring(position, {
toValue,
stiffness: 5000,
damping: 600,
mass: 3,
useNativeDriver: position.__isNative,
}).start(onCompleteAnimation);
} else {
Animated.timing(position, {
toValue,
duration,
easing: EaseInOut,
useNativeDriver: position.__isNative,
}).start(onCompleteAnimation);
}
}
/**
* 渲染 CarkStack,
* 元素层次结构 :
* View (flex:1,column-reverse):
* View (flex:1): (假设有 N 个scene,则会有 N 张Card)
* Card 0
* Card 1
* Card ...
* Card n-1
* FloatingHeader (仅在headerMode为float时出现,参考 this._renderHeader)
* @return {*}
*/
render() {
// 这里对 float 头部进行渲染处理
// screen 的头部不在这里处理,是在 this._renderCard -> this._renderInnerScene 中进行处理
let floatingHeader = null;
const headerMode = this._getHeaderMode();
if (headerMode === 'float') {
floatingHeader = this._renderHeader(
this.props.transitionProps.scene,
headerMode
);
}
// 获取屏幕切换过渡动画属性 transitionProps 和屏幕模式 mode
const {
transitionProps: {navigation, position, layout, scene, scenes},
mode,
} = this.props;
// 获取当前路由/屏幕/场景的索引
const {index} = navigation.state;
// 如果当前屏幕模式为 modal, 则将会处理手势竖直方向(y)变化,否则会处理水平方向(x)变化
const isVertical = mode === 'modal';
// 获取 navigationOptions,该信息是导航器上的 navigationOptions 和 屏幕组件中的
// navigationOptions 的合并(后者优先级更高)
const {options} = this._getScreenDetails(scene);
// 手势方向是否要逆转
// 缺省情况下,是不逆转,意味着:
// 1. x 轴上,向左滑动,滑动距离(gesture['dx'])是负值;向右滑动,滑动距离(gesture['dx'])是正值;
// 2. y 轴上,向上滑动,滑动距离(gesture['dy'])是负值;向下滑动,滑动距离(gesture['dy'])是正值;
const gestureDirectionInverted = options.gestureDirection === 'inverted';
// 检查是否开启了手势 gesturesEnabled
// 1. 如果 navigationOptions 中指定了 boolean 类型的 gesturesEnabled,
就使用它作为 gesturesEnabled ,
// 2. 否则如果没有指定这样一个选项,但平台是 iOS, 则 gesturesEnabled 使用 true ,
// 3. 否则,也就是平台是 Android, gesturesEnabled 使用 false .
const gesturesEnabled =
typeof options.gesturesEnabled === 'boolean'
? options.gesturesEnabled
: Platform.OS === 'ios';
// 构造手势处理器 responder
// 如果 gesturesEnabled 为 false, responder 指定为 null,表示不处理手势,
const responder = !gesturesEnabled
? null
: PanResponder.create({
// 关于 PanResponder 全面介绍,可以参考官方文档 :
// https://facebook.github.io/react-native/docs/panresponder.html
// 另一个组件已经成为了新的响应者,当前手势被取消时的处理逻辑
onPanResponderTerminate: () => {
// 相应手势标记设置为 false, 表示不在响应手势过程中了
this._isResponding = false;
// 立即返回到手势开始时那个场景屏幕
this._reset(index, 0);
},
// 一旦成为触摸事件响应者时的处理逻辑
onPanResponderGrant: () => {
// 停止目前进行中的 postion 动画,并
// 1. 标记响应手势事件开始 _isResponding
// 2. 记录手势事件处理开始时的起始信息 _gestureStartValue
position.stopAnimation(value => {
this._isResponding = true;
this._gestureStartValue = value;
});
},
/**
* 触摸进行过程中愿不愿意成为响应者
* @param event
* @param gesture
* @return {boolean}
*/
onMoveShouldSetPanResponder: (event, gesture) => {
if (index !== scene.index) {
// 如果要渲染的当前 scene 和 导航路由状态中的当前路由 index 不一致,
// 则返回 false 表明不愿意响应该手势
return false;
}
// 愿意成为手势响应者
// 触摸移动过程中获取当前操作的场景屏幕组件的 index
const immediateIndex =
this._immediateIndex == null ? index : this._immediateIndex;
// 当前拖拽距离
const currentDragDistance = gesture[isVertical ? 'dy' : 'dx'];
// 当前拖拽位置
const currentDragPosition =
event.nativeEvent[isVertical ? 'pageY' : 'pageX'];
// 获取拖拽轴方向屏幕的长度
const axisLength = isVertical
? layout.height.__getValue()
: layout.width.__getValue();
const axisHasBeenMeasured = !!axisLength;
// Measure the distance from the touch to the edge of the screen
// 测量手势开始时的触碰位置距离屏幕边缘的距离(使用那一点的x/y轴坐标表示)
const screenEdgeDistance = gestureDirectionInverted
? axisLength - (currentDragPosition - currentDragDistance)
: currentDragPosition - currentDragDistance;
// Compare to the gesture distance relavant to card or modal
// 获取用户通过 navigationOptions 自定义的手势识别距离
const {
gestureResponseDistance: userGestureResponseDistance = {},
} = this._getScreenDetails(scene).options;
// 确定最终要在手势识别轴上使用的手势识别距离
const gestureResponseDistance = isVertical
? userGestureResponseDistance.vertical ||
GESTURE_RESPONSE_DISTANCE_VERTICAL
: userGestureResponseDistance.horizontal ||
GESTURE_RESPONSE_DISTANCE_HORIZONTAL;
// GESTURE_RESPONSE_DISTANCE is about 25 or 30. Or 135 for modals
if (screenEdgeDistance > gestureResponseDistance) {
// Reject touches that started in the middle of the screen
// 仅接受触摸起始位置在gestureResponseDistance以内的手势
return false;
}
// 拖拽距离足够大了吗 ?
const hasDraggedEnough =
Math.abs(currentDragDistance) > RESPOND_THRESHOLD;
// 是否在卡片栈中最下面一张卡片上 ?
const isOnFirstCard = immediateIndex === 0;
// 是否要执行响应逻辑 ?
const shouldSetResponder =
hasDraggedEnough && axisHasBeenMeasured && !isOnFirstCard;
return shouldSetResponder;
},
// 手势响应过程中当前卡片跟随触摸移动
onPanResponderMove: (event, gesture) => {
// Handle the moving touches for our granted responder
const startValue = this._gestureStartValue;
const axis = isVertical ? 'dy' : 'dx';
const axisDistance = isVertical
? layout.height.__getValue()
: layout.width.__getValue();
const currentValue =
(I18nManager.isRTL && axis === 'dx') !== gestureDirectionInverted
? startValue + gesture[axis] / axisDistance
: startValue - gesture[axis] / axisDistance;
// 计算目标位置 position ,一定要介于[currentValue,index] 之间的某个小数
// 缺省情况下,比如是正在处理水平方向上的滑动,越靠近屏幕左边, position
// 越小,也就是从屏幕左边向右边的整个滑动过程中, position 是从 index 开始逐渐
// 减小,在贴近屏幕右边缘的时候逼近 index -1, 这里对应的设计是让自左向右滑动
// 关闭当前屏幕,返回上一屏幕
const value = clamp(index - 1, currentValue, index);
position.setValue(value);
},
onPanResponderTerminationRequest: () =>
// Returning false will prevent other views from becoming responder while
// the navigation view is the responder (mid-gesture)
false,
// 当前视图正在处理的手势操作已经完成
onPanResponderRelease: (event, gesture) => {
if (!this._isResponding) {
// 怎么会走到这里 ? 一定是出了什么问题。这里什么都不做。
return;
}
// 应该总是走到这里
// 复位手势响应中标志为 false
this._isResponding = false;
// 触摸移动结束时获取当前操作的场景屏幕组件的 index
const immediateIndex =
this._immediateIndex == null ? index : this._immediateIndex;
// Calculate animate duration according to gesture speed and moved distance
// 手势轴方向屏幕尺寸
const axisDistance = isVertical
? layout.height.__getValue()
: layout.width.__getValue();
// 运动方向
const movementDirection = gestureDirectionInverted ? -1 : 1;
// 手势轴方向手势移动距离
const movedDistance =
movementDirection * gesture[isVertical ? 'dy' : 'dx'];
// 手势轴方向手势运动速度
const gestureVelocity =
movementDirection * gesture[isVertical ? 'vy' : 'vx'];
// 缺省速度
const defaultVelocity = axisDistance / ANIMATION_DURATION;
// 用于计算reset或者goBack切换动画需要的时间的速度
const velocity = Math.max(
Math.abs(gestureVelocity),
defaultVelocity
);
// 手势结束时,不管是要切换屏幕还是复位到原来的屏幕,
// 都计算出来相应的需要的时间用于稍后播放动画需要
const resetDuration = gestureDirectionInverted
? (axisDistance - movedDistance) / velocity
: movedDistance / velocity;
const goBackDuration = gestureDirectionInverted
? movedDistance / velocity
: (axisDistance - movedDistance) / velocity;
// To asyncronously get the current animated value, we need to run
// stopAnimation:
// 手势滑动动作结束时,停止当前 position 动画,并根据手势速度或者手势移动距离决定
// 是复位到手势开始时屏(_reset),还是返回当前屏幕之前的屏幕(_goBack)
position.stopAnimation(value => {
// If the speed of the gesture release is significant, use that as
// the indication of intent
if (gestureVelocity < -0.5) {
this._reset(immediateIndex, resetDuration);
return;
}
if (gestureVelocity > 0.5) {
this._goBack(immediateIndex, goBackDuration);
return;
}
// Then filter based on the distance the screen was moved. Over a third
// of the way swiped, and the back will happen.
// 1. 缺省情况下,比如是正在处理水平方向上的滑动,越靠近屏幕左边, position
// 越小,也就是从屏幕左边向右边的整个滑动过程中, position 是从 index 开始逐渐
// 减小,在贴近屏幕右边缘的时候逼近 index -1, 这里对应的设计是让自左向右滑动
// 关闭当前屏幕,返回上一屏幕
// 2. POSITION_THRESHOLD 位置切屏参考值常量设计为 0.5 (对应屏幕正中间竖直线为
// 边界),
// 基于以上逻辑实现如下 :
// 1. 如果拖拽到屏幕右半边,则关闭当前屏幕返回上一屏幕;
// 2. 如果拖拽到屏幕左半边,则回复到手势开始时的屏幕;
if (value <= index - POSITION_THRESHOLD) {
// 1. 如果拖拽到屏幕右半边,则关闭当前屏幕返回上一屏幕;
this._goBack(immediateIndex, goBackDuration);
} else {
// 2. 如果拖拽到屏幕左半边,则回复到手势开始时的屏幕;
this._reset(immediateIndex, resetDuration);
}
});
},
});
// 决定最重要是用的
const handlers = gesturesEnabled ? responder.panHandlers : {};
// 构建容器使用的样式:缺省定义 + 自定义 transitionConfig.containerStyle
const containerStyle = [
styles.container,
this._getTransitionConfig().containerStyle,
];
// 注意 :
// 下面的代码是将 CardStack 中所有的 Card 按照 index 从小到大的顺序都渲染了出来,
// 但是由于 Card 本身的样式定义是绝对定位,所以它们是一张张在同一位置叠放在一起的
return (
<View {...handlers} style={containerStyle}>
<View style={styles.scenes}>
{scenes.map(s => this._renderCard(s))}
</View>
{floatingHeader}
</View>
);
}
/**
* 获取头部模式 headerMode :
* 1. 如果组件属性中指定了头部模式 headerMode,直接使用 ;
* 2. 否则 ,
* 2.1 如果是 Android, 或者 iOS + modal 屏幕模式, 返回 screen ;
* 2.2 其他情况 (iOS + card 屏幕模式),返回 float ;
* @return {string}
* @private
*/
_getHeaderMode() {
if (this.props.headerMode) {
return this.props.headerMode;
}
if (Platform.OS === 'android' || this.props.mode === 'modal') {
return 'screen';
}
return 'float';
}
/**
* 获取头部过渡预定义样式
* 1. Android, 或者 iOS + screen 头部模式, 返回 fade-in-place ;
* 2. 否则, 如果组件属性指定了 headerTransitionPreset, 使用它 ;
* 3. 否则, 使用 fade-in-place .
* @return {string}
* @private
*/
_getHeaderTransitionPreset() {
// On Android or with header mode screen, we always just use in-place,
// we ignore the option entirely (at least until we have other presets)
if (Platform.OS === 'android' || this._getHeaderMode() === 'screen') {
return 'fade-in-place';
}
// TODO: validations: 'fade-in-place' or 'uikit' are valid
if (this.props.headerTransitionPreset) {
return this.props.headerTransitionPreset;
} else {
return 'fade-in-place';
}
}
/**
* 在某个卡片 Card 渲染时,渲染该卡片上的场景屏幕组件,
* 元素层次结构 :
* 1. headerMode 是 screen 的情况
* View (column-reverse):
* View (flex:1):
* SceneView :
* SceneComponent (navigation)
* Header
* 2. headerMode 不是 screen 的情况(比如 float)
* SceneView :
* SceneComponent (navigation)
*
* @param SceneComponent 路由对应的场景屏幕组件
* @param scene
* @return {*}
* @private
*/
_renderInnerScene(SceneComponent, scene) {
const {navigation} = this._getScreenDetails(scene);
const {screenProps} = this.props;
const headerMode = this._getHeaderMode();
if (headerMode === 'screen') {
return (
<View style={styles.container}>
<View style={{flex: 1}}>
<SceneView
screenProps={screenProps}
navigation={navigation}
component={SceneComponent}
/>
</View>
{this._renderHeader(scene, headerMode)}
</View>
);
}
return (
<SceneView
screenProps={this.props.screenProps}
navigation={navigation}
component={SceneComponent}
/>
);
}
/**
* 根据当前组件属性中的信息计算出屏幕场景切换过渡动画设置对象,
* 所基于的信息 :
* 1. 屏幕模式 mode : isModal , true ==> modal , false ==> card
* 2. 自定义的过渡设置函数 : this.props.transitionConfig , 可以不提供
* 3. 目标屏幕的过渡属性 : this.props.transitionProps
* 4. 起始屏幕的过渡属性 : this.props.prevTransitionProps
* @return {{}}
* @private
*/
_getTransitionConfig = () => {
const isModal = this.props.mode === 'modal';
return TransitionConfigs.getTransitionConfig(
this.props.transitionConfig,
this.props.transitionProps,
this.props.prevTransitionProps,
isModal
);
};
/**
* 渲染针对某个场景 scene 的屏幕组件的卡片组件 card ,
* 元素层次结构 :
* Card :
* InnerScene (参考 this._renderInnerScene())
* @param scene
* @return {*}
* @private
*/
_renderCard = scene => {
// 从屏幕切换过渡动画设置中获取屏幕差值样式定义函数 screenInterpolator
const {screenInterpolator} = this._getTransitionConfig();
// 构造将要使用的屏幕差值样式 style
const style =
screenInterpolator &&
screenInterpolator({...this.props.transitionProps, scene});
// 获取当前场景的路由对应的场景组件,记录到 SceneComponent
const SceneComponent = this.props.router.getComponentForRouteName(
scene.route.routeName
);
const {transitionProps, ...props} = this.props;
// 注意 :
// 下面渲染 Card 时候,将传递外部指定属性 this.props.cardStyle 到 Card 的属性 style ;
return (
<Card
{...props}
{...transitionProps}
key={`card_${scene.key}`}
style={[style, this.props.cardStyle]}
scene={scene}
>
{this._renderInnerScene(SceneComponent, scene)}
</Card>
);
};
}
const styles = StyleSheet.create({
container: {
flex: 1,
// Header is physically rendered after scenes so that Header won't be
// covered by the shadows of the scenes.
// That said, we'd have use `flexDirection: 'column-reverse'` to move
// Header above the scenes.
flexDirection: 'column-reverse',
},
scenes: {
flex: 1,
},
});
export default CardStack;
React Navigation源代码阅读 : views/CardStack/CardStack.js
猜你喜欢
转载自blog.csdn.net/andy_zhang2007/article/details/80512573
今日推荐
周排行