React Native 重新渲染组件时的意外行为
Unexpected behaviour when React Native re-renders component
我在我的 react-native 应用程序中使用 react-native-card-stack-swiper 库。我在后端使用 firebase 实时数据库。
预期行为:
this.state.peopleList 是 { personA, personB }
此列表来自 firebase 实时数据库 people/peopleList.
这里是Home.js
import React, {Component} from 'react';
import {FlatList, View, Text, ActivityIndicator, Alert, StyleSheet, TouchableOpacity, Dimensions} from 'react-native';
import { addToAccepted, getHiddenPosts} from "../lib/firebaseUtils";
import firebase from 'react-native-firebase';
import { Button, ListItem, Card, Icon as IconElements } from 'react-native-elements';
import ActionSheet from 'react-native-actionsheet'
import Icon from 'react-native-vector-icons/FontAwesome';
import MaterialComIcon from 'react-native-vector-icons/MaterialCommunityIcons';
import * as _ from 'lodash';
import CardStack, { Card as SwipableCard } from 'react-native-card-stack-swiper';
import OfflineNotice from './OfflineNotice';
let uid;
class HomeScreen extends Component {
constructor(props) {
super(props);
this.state = {
peopleList: [],
hiddenPosts: [],
fetching: false,
};
this.getPeopleList = this.getPeopleList.bind(this);
}
componentDidMount(){
this._isMounted = true;
this.setState({fetching:true});
let user = firebase.auth().currentUser;
if (user != null) {
uid = user.uid;
} else {
this.props.navigation.navigate('Login')
}
getHiddenPosts(uid).then(hiddenPosts => {
this.setState({hiddenPosts});
})
}
componentWillUnmount()
{
this._isMounted = false;
}
/*
* get all the task requests that this user can perform
* */
getPeopleList = () => {
let networkId = this.state.networkId;
let livePostsRef = firebase.database().ref('people/peopleList')
livePostsRef.on('child_added', (snapshot) => {
let request = snapshot.val()
// Check if it is not already decided upon by this user
if(!_.includes(this.state.hiddenPosts, request.id))
{
this.setState({peopleList:[request].concat(this.state.peopleList) , fetching: false});
}
if(this.state.fetching) this.setState({fetching:false});
})
if(this._isMounted) this.setState({fetching:false});
livePostsRef.on('child_removed', (snapshot) => {
this.setState({peopleList: this.state.peopleList.filter(item => item.id !== snapshot.key)});
})
}
// The user has decided on this card and hence add this card to the user's hidden tasks list so that the app won't show it again next time it launches
decideOnPost = (id) =>
{
this.setState({peopleList: this.state.peopleList.filter(item => item.id !== id)});
if(uid) appendHiddenPosts(uid, id);
}
acceptPerson = (item) =>
{
addToAccepted(item).then(res => {
appendHiddenPosts(uid, id).then(finRes => {
this.setState({peopleList: this.state.peopleList.filter(item => item.id !== id)});
}
}
}
swipableRender() {
const {peopleList} = this.state;
console.log('swipableRender: peopleList is ', peopleList)
return peopleList.map((item) => {
const {name, photo, bio, id} = item;
console.log('swipableRender return: item.name is ', item.name)
return (
<SwipableCard key={id} onSwipedLeft={() => this.decideOnPost(id)} onSwipedRight={() => this.acceptPerson(item)}>
<View>
{console.log('swipableRender return return: customTitle is ', customTitle)}
<Card image={{uri: bgImage}} featuredTitle={customTitle} featuredTitleStyle={adourStyle.listItemText} >
<Text style={adourStyle.cardText}>{details}</Text>
</Card>
</View>
</SwipableCard>
)
})
}
render() {
const {fetching, peopleList} = this.state
console.log('*** RENDERING *** peopleList: ', peopleList)
return (
<View style={styles.mainContainer}>
<CardStack
renderNoMoreCards={() => <View style={{marginTop: 50}}>
{fetching && <ActivityIndicator color={BRAND_COLOR_ONE} size={'large'}/>}
{!fetching &&
<View style={styles.cardOverText}>
<Text style={{marginBottom: 8}}>Check back later</Text>
</View>
}
</View>}
disableBottomSwipe={true}
disableTopSwipe={true}
ref={swiper => {
this.swiper = swiper
}}
>
{this.swipableRender()}
</CardStack>
</View>
)
}
}
export default HomeScreen;
卡片堆最初按预期呈现。换句话说,当应用程序启动时,我看到一堆卡片,其中包含 personA 卡片和 personB 卡片。这些卡都可以按预期刷。
如果我刷掉所有卡片:personA 和 personB,我会按预期留下文本 'Please check back later'。
但是,如果我在屏幕上,同时一个新的 personC 对象被添加到 firebase 实时数据库 people/peopleList,我希望 firebase.database().ref().on listener将检测数据库中的更改,对状态 peopleList 执行 setState 并因此重新渲染组件。因此,我希望看到 personC 出现在我的屏幕上。
然而,实际上,
firebase 侦听器按预期检测到数据库中的更改
然后我们做一个 setState 来更新 this.state.peopleList 的值
然后 React 按预期重新渲染组件(使用 console.log 验证)
* 但是我在屏幕上看到了 personA 这很奇怪*
此外,如果我仍在屏幕上并且另一个对象被添加到 firebase 数据库:personD,同样的事情也会发生。我看到的不是 personD 卡片,而是 personB 卡片。
作为用户,我已经向左滑动了 personA 和 personB,留下了一个空白屏幕。当我在屏幕上添加新对象时,我没有看到新对象,而是再次看到旧对象。
如果我完全关闭应用程序并重新启动它,则会发生正确的行为:我看到了 personC 和 personD。直到我在 personC 和 personD 上向左滑动并且问题再次出现,除非我重新启动应用程序。
我很确定问题出在这个 Home.js 文件或库 react-native-card-stack-swiper 中。该库不再得到其开发人员的支持,因此如果该库有任何问题,我将不得不修复它。
react-native-card-stack-swiper
这里是 react-native-card-stack-swiper(您也可以在其 GitHub 页面找到源代码:https://github.com/lhandel/react-native-card-stack-swiper
这是 Card.js(作为 SwipableCard 导入到我的代码中)
import React, { Component } from 'react';
import PropTypes from 'prop-types'
import {
View,
} from 'react-native';
const Card = ({ style, children }) => (
<View style={style} >
{children}
</View>);
Card.propTypes = {
children: PropTypes.oneOfType([PropTypes.array, PropTypes.object]).isRequired,
style: PropTypes.oneOfType([PropTypes.number, PropTypes.object, PropTypes.array]),
onSwipedLeft: PropTypes.func,
onSwipedRight:PropTypes.func,
onSwipedTop: PropTypes.func,
onSwipedBottom: PropTypes.func,
onSwiped: PropTypes.func,
}
Card.defaultProps = {
style:{},
onSwiped: () => {},
onSwipedLeft: () => {},
onSwipedRight: () => {},
onSwipedTop: () => {},
onSwipedBottom: () => {},
}
export default Card;
这是CardStack.js
import React, { Component } from 'react';
import PropTypes from 'prop-types'
import {
StyleSheet,
View,
Animated,
PanResponder,
Dimensions,
Text,
Platform
} from 'react-native';
const { height, width } = Dimensions.get('window');
export default class CardStack extends Component {
static distance(x, y) {
const a = Math.abs(x);
const b = Math.abs(y);
const c = Math.sqrt((a * a) + (b * b));
return c;
}
constructor(props) {
super(props);
this.state ={
drag: new Animated.ValueXY({x: 0, y: 0}),
dragDistance: new Animated.Value(0),
sindex: 0, // index to the next card to be renderd mod card.length
cardA: null,
cardB: null,
topCard: 'cardA',
cards: [],
touchStart: 0,
};
this.distance = this.constructor.distance;
}
componentWillMount() {
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => false,
onStartShouldSetPanResponderCapture: (evt, gestureState) => false,
onMoveShouldSetPanResponder: (evt, gestureState) => {
const isVerticalSwipe = Math.sqrt(
Math.pow(gestureState.dx, 2) < Math.pow(gestureState.dy, 2)
)
if (!this.props.verticalSwipe && isVerticalSwipe) {
return false
}
return Math.sqrt(Math.pow(gestureState.dx, 2) + Math.pow(gestureState.dy, 2)) > 10
}, //(parseInt(gestureState.dx) !== 0 && parseInt(gestureState.dy) !== 0),
onMoveShouldSetPanResponderCapture: (evt, gestureState) => {
const isVerticalSwipe = Math.sqrt(
Math.pow(gestureState.dx, 2) < Math.pow(gestureState.dy, 2)
)
if (!this.props.verticalSwipe && isVerticalSwipe) {
return false
}
return Math.sqrt(Math.pow(gestureState.dx, 2) + Math.pow(gestureState.dy, 2)) > 10
}, //(parseInt(gestureState.dx) !== 0 && parseInt(gestureState.dy) !== 0),
onPanResponderGrant: (evt, gestureState) => {
this.props.onSwipeStart();
this.setState({ touchStart: new Date().getTime() });
},
onPanResponderMove: (evt, gestureState) => {
const { verticalSwipe, horizontalSwipe } = this.props;
const { verticalThreshold, horizontalThreshold } = this.props
const dragDistance = this.distance((horizontalSwipe) ? gestureState.dx : 0, (verticalSwipe) ? gestureState.dy : 0 );
this.state.dragDistance.setValue(dragDistance);
this.state.drag.setValue({x: (horizontalSwipe) ? gestureState.dx : 0, y: (verticalSwipe) ? gestureState.dy : 0});
},
onPanResponderTerminationRequest: (evt, gestureState) => true,
onPanResponderRelease: (evt, gestureState) => {
this.props.onSwipeEnd();
const currentTime = new Date().getTime();
const swipeDuration = currentTime-this.state.touchStart;
const { sindex } = this.state;
const { verticalThreshold,
horizontalThreshold,
disableTopSwipe,
disableLeftSwipe,
disableRightSwipe,
disableBottomSwipe,
} = this.props;
if (((Math.abs(gestureState.dy) > verticalThreshold) ||
( Math.abs(gestureState.dy) > verticalThreshold*0.8 &&
swipeDuration < 150)
) && this.props.verticalSwipe)
{
const swipeDirection = (gestureState.dy < 0) ? height * -1 : height;
if(swipeDirection < 0 && !disableTopSwipe)
{
this._nextCard('top', gestureState.dx, swipeDirection, this.props.duration);
}
else if (swipeDirection > 0 && !disableBottomSwipe)
{
this._nextCard('bottom', gestureState.dx, swipeDirection, this.props.duration);
}
else
{
this._resetCard();
}
}else if (((Math.abs(gestureState.dx) > horizontalThreshold) ||
(Math.abs(gestureState.dx) > horizontalThreshold*0.6 &&
swipeDuration < 150)
) && this.props.horizontalSwipe) {
const swipeDirection = (gestureState.dx < 0) ? width * -1 : width;
if (swipeDirection < 0 && !disableLeftSwipe)
{
this._nextCard('left', swipeDirection, gestureState.dy, this.props.duration);
}
else if(swipeDirection > 0 && !disableRightSwipe)
{
this._nextCard('right', swipeDirection, gestureState.dy, this.props.duration);
}
else
{
this._resetCard();
}
}
else
{
this._resetCard();
}
},
onPanResponderTerminate: (evt, gestureState) => {
},
onShouldBlockNativeResponder: (evt, gestureState) => {
return true;
},
});
}
componentDidMount(){
this.initDeck();
}
componentWillReceiveProps(nextProps){
if (nextProps.children !== this.props.children) {
this.setState({
cards: nextProps.children,
cardA: nextProps.children[(this.state.topCard=='cardA')? this.state.sindex-2 : this.state.sindex-1],
cardB: nextProps.children[(this.state.topCard=='cardB')? this.state.sindex-2 : this.state.sindex-1]
});
}
}
initDeck() {
// check if we only have 1 child
if(typeof this.props.children !== 'undefined' && !Array.isArray(this.props.children)){
this.setState({
cards: [this.props.children],
cardA: this.props.children,
cardB: null,
sindex: 2,
});
}else if(Array.isArray(this.props.children)){
this.setState({
cards: this.props.children,
cardA: this.props.children[0],
cardB: this.props.children[1],
sindex: 2,
});
}
}
_resetCard(){
Animated.timing(
this.state.dragDistance,
{
toValue: 0,
duration: this.props.duration,
}
).start();
Animated.spring(
this.state.drag,
{
toValue: {x: 0, y: 0},
duration: this.props.duration,
}
).start();
}
goBackFromTop(){
this._goBack('top');
}
goBackFromRight(){
this._goBack('right');
}
goBackFromLeft(){
this._goBack('left');
}
goBackFromBottom(){
this._goBack('bottom');
}
mod(n, m) {
return ((n % m) + m) % m;
}
_goBack(direction){
const {cardA, cardB, cards, sindex, topCard} = this.state;
if((sindex-3) < 0 && !this.props.loop) return;
const previusCardIndex = this.mod(sindex-3, cards.length)
let update = {};
if(topCard === 'cardA'){
update = {
...update,
cardB: cards[previusCardIndex]
}
}else{
update = {
...update,
cardA: cards[previusCardIndex],
}
}
this.setState({
...update,
topCard: (topCard === 'cardA') ? 'cardB' : 'cardA',
sindex: sindex-1
}, () => {
switch (direction) {
case 'top':
this.state.drag.setValue({x: 0, y: -height});
this.state.dragDistance.setValue(height);
break;
case 'left':
this.state.drag.setValue({x: -width, y: 0});
this.state.dragDistance.setValue(width);
break;
case 'right':
this.state.drag.setValue({x: width, y: 0});
this.state.dragDistance.setValue(width);
break;
case 'bottom':
this.state.drag.setValue({x: 0, y: height});
this.state.dragDistance.setValue(width);
break;
default:
}
Animated.spring(
this.state.dragDistance,
{
toValue: 0,
duration: this.props.duration,
}
).start();
Animated.spring(
this.state.drag,
{
toValue: {x: 0, y: 0},
duration: this.props.duration,
}
).start();
})
}
swipeTop(duration){
this._nextCard('top', 0, -height, duration);
}
swipeBottom(duration){
this._nextCard('bottom', 0, height, duration);
}
swipeRight(duration){
this._nextCard('right', width, 0, duration);
}
swipeLeft(duration){
this._nextCard('left', -width, 0, duration);
}
_nextCard(direction, x, y, duration=400){
const { verticalSwipe, horizontalSwipe, loop } = this.props;
const { sindex, cards, topCard } = this.state;
// index for the next card to be renderd
const nextCard = (loop) ? (Math.abs(sindex) % cards.length) : sindex;
// index of the swiped card
const index = (loop) ? this.mod(nextCard-2, cards.length) : nextCard - 2;
if (index === cards.length-1){
this.props.onSwipedAll();
}
if((sindex-2 < cards.length) || (loop) ){
Animated.spring(
this.state.dragDistance,
{
toValue: 220,
duration,
}
).start();
Animated.timing(
this.state.drag,
{
toValue: { x: (horizontalSwipe) ? x : 0, y: (verticalSwipe) ? y : 0 },
duration,
}
).start(() => {
const newTopCard = (topCard === 'cardA') ? 'cardB' : 'cardA';
let update = {};
if(newTopCard === 'cardA') {
update = {
...update,
cardB: cards[nextCard]
};
}
if(newTopCard === 'cardB') {
update = {
...update,
cardA: cards[nextCard],
};
}
this.state.drag.setValue({x: 0, y: 0});
this.state.dragDistance.setValue(0);
this.setState({
...update,
topCard: newTopCard,
sindex: nextCard+1
});
this.props.onSwiped(index);
switch (direction) {
case 'left':
this.props.onSwipedLeft(index);
this.state.cards[index].props.onSwipedLeft();
break;
case 'right':
this.props.onSwipedRight(index);
this.state.cards[index].props.onSwipedRight();
break;
case 'top':
this.props.onSwipedTop(index);
this.state.cards[index].props.onSwipedTop();
break;
case 'bottom':
this.props.onSwipedBottom(index);
this.state.cards[index].props.onSwipedBottom();
break;
default:
}
});
}
}
/**
* @description CardB’s click feature is trigger the CardA on the card stack. (Solved on Android)
* @see https://facebook.github.io/react-native/docs/view#pointerevents
*/
_setPointerEvents(topCard, topCardName) {
return { pointerEvents: topCard === topCardName ? "auto" : "none" }
}
render() {
const { secondCardZoom } = this.props;
const { drag, dragDistance, cardA, cardB, topCard, sindex } = this.state;
const SC = dragDistance.interpolate({
inputRange: [0,10, 220],
outputRange: [secondCardZoom,secondCardZoom,1],
extrapolate: 'clamp',
});
const rotate = drag.x.interpolate({
inputRange: [-320,0,320],
outputRange: this.props.outputRotationRange,
extrapolate: 'clamp',
});
return (
<View {...this._panResponder.panHandlers} style={[{position:'relative'},this.props.style]}>
{this.props.renderNoMoreCards()}
<Animated.View
{...this._setPointerEvents(topCard, 'cardB')}
style={{
position: 'absolute',
zIndex: (topCard === 'cardB') ? 3 : 2,
...Platform.select({
android: {
elevation: (topCard === 'cardB') ? 3 : 2,
}
}),
transform: [
{ rotate: (topCard === 'cardB') ? rotate: '0deg' },
{translateX: (topCard === 'cardB') ? drag.x: 0},
{translateY: (topCard === 'cardB') ? drag.y: 0},
{ scale: (topCard === 'cardB') ? 1 : SC},
]
}}>
{cardB}
</Animated.View>
<Animated.View
{...this._setPointerEvents(topCard, 'cardA')}
style={{
position: 'absolute',
zIndex: (topCard === 'cardA') ? 3 : 2,
...Platform.select({
android: {
elevation: (topCard === 'cardA') ? 3 : 2,
}
}),
transform: [
{ rotate: (topCard === 'cardA') ? rotate: '0deg' },
{translateX: (topCard === 'cardA') ? drag.x: 0},
{translateY: (topCard === 'cardA') ? drag.y: 0},
{ scale: (topCard === 'cardA') ? 1 : SC},
]
}}>
{cardA}
</Animated.View>
</View>
);
}
}
CardStack.propTypes = {
children: PropTypes.oneOfType([PropTypes.array, PropTypes.object]).isRequired,
style: PropTypes.oneOfType([PropTypes.number, PropTypes.object, PropTypes.array]),
secondCardZoom: PropTypes.number,
loop: PropTypes.bool,
renderNoMoreCards: PropTypes.func,
onSwipeStart: PropTypes.func,
onSwipeEnd: PropTypes.func,
onSwiped: PropTypes.func,
onSwipedLeft: PropTypes.func,
onSwipedRight:PropTypes.func,
onSwipedTop: PropTypes.func,
onSwipedBottom: PropTypes.func,
onSwiped: PropTypes.func,
onSwipedAll: PropTypes.func,
disableBottomSwipe: PropTypes.bool,
disableLeftSwipe: PropTypes.bool,
disableRightSwipe: PropTypes.bool,
disableTopSwipe: PropTypes.bool,
verticalSwipe: PropTypes.bool,
verticalThreshold: PropTypes.number,
horizontalSwipe: PropTypes.bool,
horizontalThreshold: PropTypes.number,
outputRotationRange: PropTypes.array,
duration: PropTypes.number
}
CardStack.defaultProps = {
style:{},
secondCardZoom: 0.95,
loop: false,
renderNoMoreCards: () => { return (<Text>No More Cards</Text>)},
onSwipeStart: () => null,
onSwipeEnd: () => null,
onSwiped: () => {},
onSwipedLeft: () => {},
onSwipedRight: () => {},
onSwipedTop: () => {},
onSwipedBottom: () => {},
onSwipedAll: async () => {
console.log('onSwipedAll')
},
disableBottomSwipe: false,
disableLeftSwipe: false,
disableRightSwipe: false,
disableTopSwipe: false,
verticalSwipe: true,
verticalThreshold: height/4,
horizontalSwipe: true,
horizontalThreshold: width/2,
outputRotationRange: ['-15deg','0deg','15deg'],
duration: 200
}
你可以看看这个问题,关于这个问题 react-native-card-stack-swiper/issues/43 - 希望他们会在某个时候修复它。
问题是滑动器在变化时不看children,所以我们必须强制它识别变化,在你的例子中,你可以简单地使用人的长度,或者来自 firebase 的最后一条消息的 ID。
<CardStack {...props} key={this.peopleList.length} > {renderCards()} </CardStack>
我在我的 react-native 应用程序中使用 react-native-card-stack-swiper 库。我在后端使用 firebase 实时数据库。
预期行为:
this.state.peopleList 是 { personA, personB } 此列表来自 firebase 实时数据库 people/peopleList.
这里是Home.js
import React, {Component} from 'react';
import {FlatList, View, Text, ActivityIndicator, Alert, StyleSheet, TouchableOpacity, Dimensions} from 'react-native';
import { addToAccepted, getHiddenPosts} from "../lib/firebaseUtils";
import firebase from 'react-native-firebase';
import { Button, ListItem, Card, Icon as IconElements } from 'react-native-elements';
import ActionSheet from 'react-native-actionsheet'
import Icon from 'react-native-vector-icons/FontAwesome';
import MaterialComIcon from 'react-native-vector-icons/MaterialCommunityIcons';
import * as _ from 'lodash';
import CardStack, { Card as SwipableCard } from 'react-native-card-stack-swiper';
import OfflineNotice from './OfflineNotice';
let uid;
class HomeScreen extends Component {
constructor(props) {
super(props);
this.state = {
peopleList: [],
hiddenPosts: [],
fetching: false,
};
this.getPeopleList = this.getPeopleList.bind(this);
}
componentDidMount(){
this._isMounted = true;
this.setState({fetching:true});
let user = firebase.auth().currentUser;
if (user != null) {
uid = user.uid;
} else {
this.props.navigation.navigate('Login')
}
getHiddenPosts(uid).then(hiddenPosts => {
this.setState({hiddenPosts});
})
}
componentWillUnmount()
{
this._isMounted = false;
}
/*
* get all the task requests that this user can perform
* */
getPeopleList = () => {
let networkId = this.state.networkId;
let livePostsRef = firebase.database().ref('people/peopleList')
livePostsRef.on('child_added', (snapshot) => {
let request = snapshot.val()
// Check if it is not already decided upon by this user
if(!_.includes(this.state.hiddenPosts, request.id))
{
this.setState({peopleList:[request].concat(this.state.peopleList) , fetching: false});
}
if(this.state.fetching) this.setState({fetching:false});
})
if(this._isMounted) this.setState({fetching:false});
livePostsRef.on('child_removed', (snapshot) => {
this.setState({peopleList: this.state.peopleList.filter(item => item.id !== snapshot.key)});
})
}
// The user has decided on this card and hence add this card to the user's hidden tasks list so that the app won't show it again next time it launches
decideOnPost = (id) =>
{
this.setState({peopleList: this.state.peopleList.filter(item => item.id !== id)});
if(uid) appendHiddenPosts(uid, id);
}
acceptPerson = (item) =>
{
addToAccepted(item).then(res => {
appendHiddenPosts(uid, id).then(finRes => {
this.setState({peopleList: this.state.peopleList.filter(item => item.id !== id)});
}
}
}
swipableRender() {
const {peopleList} = this.state;
console.log('swipableRender: peopleList is ', peopleList)
return peopleList.map((item) => {
const {name, photo, bio, id} = item;
console.log('swipableRender return: item.name is ', item.name)
return (
<SwipableCard key={id} onSwipedLeft={() => this.decideOnPost(id)} onSwipedRight={() => this.acceptPerson(item)}>
<View>
{console.log('swipableRender return return: customTitle is ', customTitle)}
<Card image={{uri: bgImage}} featuredTitle={customTitle} featuredTitleStyle={adourStyle.listItemText} >
<Text style={adourStyle.cardText}>{details}</Text>
</Card>
</View>
</SwipableCard>
)
})
}
render() {
const {fetching, peopleList} = this.state
console.log('*** RENDERING *** peopleList: ', peopleList)
return (
<View style={styles.mainContainer}>
<CardStack
renderNoMoreCards={() => <View style={{marginTop: 50}}>
{fetching && <ActivityIndicator color={BRAND_COLOR_ONE} size={'large'}/>}
{!fetching &&
<View style={styles.cardOverText}>
<Text style={{marginBottom: 8}}>Check back later</Text>
</View>
}
</View>}
disableBottomSwipe={true}
disableTopSwipe={true}
ref={swiper => {
this.swiper = swiper
}}
>
{this.swipableRender()}
</CardStack>
</View>
)
}
}
export default HomeScreen;
卡片堆最初按预期呈现。换句话说,当应用程序启动时,我看到一堆卡片,其中包含 personA 卡片和 personB 卡片。这些卡都可以按预期刷。
如果我刷掉所有卡片:personA 和 personB,我会按预期留下文本 'Please check back later'。
但是,如果我在屏幕上,同时一个新的 personC 对象被添加到 firebase 实时数据库 people/peopleList,我希望 firebase.database().ref().on listener将检测数据库中的更改,对状态 peopleList 执行 setState 并因此重新渲染组件。因此,我希望看到 personC 出现在我的屏幕上。
然而,实际上, firebase 侦听器按预期检测到数据库中的更改 然后我们做一个 setState 来更新 this.state.peopleList 的值 然后 React 按预期重新渲染组件(使用 console.log 验证) * 但是我在屏幕上看到了 personA 这很奇怪*
此外,如果我仍在屏幕上并且另一个对象被添加到 firebase 数据库:personD,同样的事情也会发生。我看到的不是 personD 卡片,而是 personB 卡片。
作为用户,我已经向左滑动了 personA 和 personB,留下了一个空白屏幕。当我在屏幕上添加新对象时,我没有看到新对象,而是再次看到旧对象。
如果我完全关闭应用程序并重新启动它,则会发生正确的行为:我看到了 personC 和 personD。直到我在 personC 和 personD 上向左滑动并且问题再次出现,除非我重新启动应用程序。
我很确定问题出在这个 Home.js 文件或库 react-native-card-stack-swiper 中。该库不再得到其开发人员的支持,因此如果该库有任何问题,我将不得不修复它。
react-native-card-stack-swiper
这里是 react-native-card-stack-swiper(您也可以在其 GitHub 页面找到源代码:https://github.com/lhandel/react-native-card-stack-swiper
这是 Card.js(作为 SwipableCard 导入到我的代码中)
import React, { Component } from 'react';
import PropTypes from 'prop-types'
import {
View,
} from 'react-native';
const Card = ({ style, children }) => (
<View style={style} >
{children}
</View>);
Card.propTypes = {
children: PropTypes.oneOfType([PropTypes.array, PropTypes.object]).isRequired,
style: PropTypes.oneOfType([PropTypes.number, PropTypes.object, PropTypes.array]),
onSwipedLeft: PropTypes.func,
onSwipedRight:PropTypes.func,
onSwipedTop: PropTypes.func,
onSwipedBottom: PropTypes.func,
onSwiped: PropTypes.func,
}
Card.defaultProps = {
style:{},
onSwiped: () => {},
onSwipedLeft: () => {},
onSwipedRight: () => {},
onSwipedTop: () => {},
onSwipedBottom: () => {},
}
export default Card;
这是CardStack.js
import React, { Component } from 'react';
import PropTypes from 'prop-types'
import {
StyleSheet,
View,
Animated,
PanResponder,
Dimensions,
Text,
Platform
} from 'react-native';
const { height, width } = Dimensions.get('window');
export default class CardStack extends Component {
static distance(x, y) {
const a = Math.abs(x);
const b = Math.abs(y);
const c = Math.sqrt((a * a) + (b * b));
return c;
}
constructor(props) {
super(props);
this.state ={
drag: new Animated.ValueXY({x: 0, y: 0}),
dragDistance: new Animated.Value(0),
sindex: 0, // index to the next card to be renderd mod card.length
cardA: null,
cardB: null,
topCard: 'cardA',
cards: [],
touchStart: 0,
};
this.distance = this.constructor.distance;
}
componentWillMount() {
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => false,
onStartShouldSetPanResponderCapture: (evt, gestureState) => false,
onMoveShouldSetPanResponder: (evt, gestureState) => {
const isVerticalSwipe = Math.sqrt(
Math.pow(gestureState.dx, 2) < Math.pow(gestureState.dy, 2)
)
if (!this.props.verticalSwipe && isVerticalSwipe) {
return false
}
return Math.sqrt(Math.pow(gestureState.dx, 2) + Math.pow(gestureState.dy, 2)) > 10
}, //(parseInt(gestureState.dx) !== 0 && parseInt(gestureState.dy) !== 0),
onMoveShouldSetPanResponderCapture: (evt, gestureState) => {
const isVerticalSwipe = Math.sqrt(
Math.pow(gestureState.dx, 2) < Math.pow(gestureState.dy, 2)
)
if (!this.props.verticalSwipe && isVerticalSwipe) {
return false
}
return Math.sqrt(Math.pow(gestureState.dx, 2) + Math.pow(gestureState.dy, 2)) > 10
}, //(parseInt(gestureState.dx) !== 0 && parseInt(gestureState.dy) !== 0),
onPanResponderGrant: (evt, gestureState) => {
this.props.onSwipeStart();
this.setState({ touchStart: new Date().getTime() });
},
onPanResponderMove: (evt, gestureState) => {
const { verticalSwipe, horizontalSwipe } = this.props;
const { verticalThreshold, horizontalThreshold } = this.props
const dragDistance = this.distance((horizontalSwipe) ? gestureState.dx : 0, (verticalSwipe) ? gestureState.dy : 0 );
this.state.dragDistance.setValue(dragDistance);
this.state.drag.setValue({x: (horizontalSwipe) ? gestureState.dx : 0, y: (verticalSwipe) ? gestureState.dy : 0});
},
onPanResponderTerminationRequest: (evt, gestureState) => true,
onPanResponderRelease: (evt, gestureState) => {
this.props.onSwipeEnd();
const currentTime = new Date().getTime();
const swipeDuration = currentTime-this.state.touchStart;
const { sindex } = this.state;
const { verticalThreshold,
horizontalThreshold,
disableTopSwipe,
disableLeftSwipe,
disableRightSwipe,
disableBottomSwipe,
} = this.props;
if (((Math.abs(gestureState.dy) > verticalThreshold) ||
( Math.abs(gestureState.dy) > verticalThreshold*0.8 &&
swipeDuration < 150)
) && this.props.verticalSwipe)
{
const swipeDirection = (gestureState.dy < 0) ? height * -1 : height;
if(swipeDirection < 0 && !disableTopSwipe)
{
this._nextCard('top', gestureState.dx, swipeDirection, this.props.duration);
}
else if (swipeDirection > 0 && !disableBottomSwipe)
{
this._nextCard('bottom', gestureState.dx, swipeDirection, this.props.duration);
}
else
{
this._resetCard();
}
}else if (((Math.abs(gestureState.dx) > horizontalThreshold) ||
(Math.abs(gestureState.dx) > horizontalThreshold*0.6 &&
swipeDuration < 150)
) && this.props.horizontalSwipe) {
const swipeDirection = (gestureState.dx < 0) ? width * -1 : width;
if (swipeDirection < 0 && !disableLeftSwipe)
{
this._nextCard('left', swipeDirection, gestureState.dy, this.props.duration);
}
else if(swipeDirection > 0 && !disableRightSwipe)
{
this._nextCard('right', swipeDirection, gestureState.dy, this.props.duration);
}
else
{
this._resetCard();
}
}
else
{
this._resetCard();
}
},
onPanResponderTerminate: (evt, gestureState) => {
},
onShouldBlockNativeResponder: (evt, gestureState) => {
return true;
},
});
}
componentDidMount(){
this.initDeck();
}
componentWillReceiveProps(nextProps){
if (nextProps.children !== this.props.children) {
this.setState({
cards: nextProps.children,
cardA: nextProps.children[(this.state.topCard=='cardA')? this.state.sindex-2 : this.state.sindex-1],
cardB: nextProps.children[(this.state.topCard=='cardB')? this.state.sindex-2 : this.state.sindex-1]
});
}
}
initDeck() {
// check if we only have 1 child
if(typeof this.props.children !== 'undefined' && !Array.isArray(this.props.children)){
this.setState({
cards: [this.props.children],
cardA: this.props.children,
cardB: null,
sindex: 2,
});
}else if(Array.isArray(this.props.children)){
this.setState({
cards: this.props.children,
cardA: this.props.children[0],
cardB: this.props.children[1],
sindex: 2,
});
}
}
_resetCard(){
Animated.timing(
this.state.dragDistance,
{
toValue: 0,
duration: this.props.duration,
}
).start();
Animated.spring(
this.state.drag,
{
toValue: {x: 0, y: 0},
duration: this.props.duration,
}
).start();
}
goBackFromTop(){
this._goBack('top');
}
goBackFromRight(){
this._goBack('right');
}
goBackFromLeft(){
this._goBack('left');
}
goBackFromBottom(){
this._goBack('bottom');
}
mod(n, m) {
return ((n % m) + m) % m;
}
_goBack(direction){
const {cardA, cardB, cards, sindex, topCard} = this.state;
if((sindex-3) < 0 && !this.props.loop) return;
const previusCardIndex = this.mod(sindex-3, cards.length)
let update = {};
if(topCard === 'cardA'){
update = {
...update,
cardB: cards[previusCardIndex]
}
}else{
update = {
...update,
cardA: cards[previusCardIndex],
}
}
this.setState({
...update,
topCard: (topCard === 'cardA') ? 'cardB' : 'cardA',
sindex: sindex-1
}, () => {
switch (direction) {
case 'top':
this.state.drag.setValue({x: 0, y: -height});
this.state.dragDistance.setValue(height);
break;
case 'left':
this.state.drag.setValue({x: -width, y: 0});
this.state.dragDistance.setValue(width);
break;
case 'right':
this.state.drag.setValue({x: width, y: 0});
this.state.dragDistance.setValue(width);
break;
case 'bottom':
this.state.drag.setValue({x: 0, y: height});
this.state.dragDistance.setValue(width);
break;
default:
}
Animated.spring(
this.state.dragDistance,
{
toValue: 0,
duration: this.props.duration,
}
).start();
Animated.spring(
this.state.drag,
{
toValue: {x: 0, y: 0},
duration: this.props.duration,
}
).start();
})
}
swipeTop(duration){
this._nextCard('top', 0, -height, duration);
}
swipeBottom(duration){
this._nextCard('bottom', 0, height, duration);
}
swipeRight(duration){
this._nextCard('right', width, 0, duration);
}
swipeLeft(duration){
this._nextCard('left', -width, 0, duration);
}
_nextCard(direction, x, y, duration=400){
const { verticalSwipe, horizontalSwipe, loop } = this.props;
const { sindex, cards, topCard } = this.state;
// index for the next card to be renderd
const nextCard = (loop) ? (Math.abs(sindex) % cards.length) : sindex;
// index of the swiped card
const index = (loop) ? this.mod(nextCard-2, cards.length) : nextCard - 2;
if (index === cards.length-1){
this.props.onSwipedAll();
}
if((sindex-2 < cards.length) || (loop) ){
Animated.spring(
this.state.dragDistance,
{
toValue: 220,
duration,
}
).start();
Animated.timing(
this.state.drag,
{
toValue: { x: (horizontalSwipe) ? x : 0, y: (verticalSwipe) ? y : 0 },
duration,
}
).start(() => {
const newTopCard = (topCard === 'cardA') ? 'cardB' : 'cardA';
let update = {};
if(newTopCard === 'cardA') {
update = {
...update,
cardB: cards[nextCard]
};
}
if(newTopCard === 'cardB') {
update = {
...update,
cardA: cards[nextCard],
};
}
this.state.drag.setValue({x: 0, y: 0});
this.state.dragDistance.setValue(0);
this.setState({
...update,
topCard: newTopCard,
sindex: nextCard+1
});
this.props.onSwiped(index);
switch (direction) {
case 'left':
this.props.onSwipedLeft(index);
this.state.cards[index].props.onSwipedLeft();
break;
case 'right':
this.props.onSwipedRight(index);
this.state.cards[index].props.onSwipedRight();
break;
case 'top':
this.props.onSwipedTop(index);
this.state.cards[index].props.onSwipedTop();
break;
case 'bottom':
this.props.onSwipedBottom(index);
this.state.cards[index].props.onSwipedBottom();
break;
default:
}
});
}
}
/**
* @description CardB’s click feature is trigger the CardA on the card stack. (Solved on Android)
* @see https://facebook.github.io/react-native/docs/view#pointerevents
*/
_setPointerEvents(topCard, topCardName) {
return { pointerEvents: topCard === topCardName ? "auto" : "none" }
}
render() {
const { secondCardZoom } = this.props;
const { drag, dragDistance, cardA, cardB, topCard, sindex } = this.state;
const SC = dragDistance.interpolate({
inputRange: [0,10, 220],
outputRange: [secondCardZoom,secondCardZoom,1],
extrapolate: 'clamp',
});
const rotate = drag.x.interpolate({
inputRange: [-320,0,320],
outputRange: this.props.outputRotationRange,
extrapolate: 'clamp',
});
return (
<View {...this._panResponder.panHandlers} style={[{position:'relative'},this.props.style]}>
{this.props.renderNoMoreCards()}
<Animated.View
{...this._setPointerEvents(topCard, 'cardB')}
style={{
position: 'absolute',
zIndex: (topCard === 'cardB') ? 3 : 2,
...Platform.select({
android: {
elevation: (topCard === 'cardB') ? 3 : 2,
}
}),
transform: [
{ rotate: (topCard === 'cardB') ? rotate: '0deg' },
{translateX: (topCard === 'cardB') ? drag.x: 0},
{translateY: (topCard === 'cardB') ? drag.y: 0},
{ scale: (topCard === 'cardB') ? 1 : SC},
]
}}>
{cardB}
</Animated.View>
<Animated.View
{...this._setPointerEvents(topCard, 'cardA')}
style={{
position: 'absolute',
zIndex: (topCard === 'cardA') ? 3 : 2,
...Platform.select({
android: {
elevation: (topCard === 'cardA') ? 3 : 2,
}
}),
transform: [
{ rotate: (topCard === 'cardA') ? rotate: '0deg' },
{translateX: (topCard === 'cardA') ? drag.x: 0},
{translateY: (topCard === 'cardA') ? drag.y: 0},
{ scale: (topCard === 'cardA') ? 1 : SC},
]
}}>
{cardA}
</Animated.View>
</View>
);
}
}
CardStack.propTypes = {
children: PropTypes.oneOfType([PropTypes.array, PropTypes.object]).isRequired,
style: PropTypes.oneOfType([PropTypes.number, PropTypes.object, PropTypes.array]),
secondCardZoom: PropTypes.number,
loop: PropTypes.bool,
renderNoMoreCards: PropTypes.func,
onSwipeStart: PropTypes.func,
onSwipeEnd: PropTypes.func,
onSwiped: PropTypes.func,
onSwipedLeft: PropTypes.func,
onSwipedRight:PropTypes.func,
onSwipedTop: PropTypes.func,
onSwipedBottom: PropTypes.func,
onSwiped: PropTypes.func,
onSwipedAll: PropTypes.func,
disableBottomSwipe: PropTypes.bool,
disableLeftSwipe: PropTypes.bool,
disableRightSwipe: PropTypes.bool,
disableTopSwipe: PropTypes.bool,
verticalSwipe: PropTypes.bool,
verticalThreshold: PropTypes.number,
horizontalSwipe: PropTypes.bool,
horizontalThreshold: PropTypes.number,
outputRotationRange: PropTypes.array,
duration: PropTypes.number
}
CardStack.defaultProps = {
style:{},
secondCardZoom: 0.95,
loop: false,
renderNoMoreCards: () => { return (<Text>No More Cards</Text>)},
onSwipeStart: () => null,
onSwipeEnd: () => null,
onSwiped: () => {},
onSwipedLeft: () => {},
onSwipedRight: () => {},
onSwipedTop: () => {},
onSwipedBottom: () => {},
onSwipedAll: async () => {
console.log('onSwipedAll')
},
disableBottomSwipe: false,
disableLeftSwipe: false,
disableRightSwipe: false,
disableTopSwipe: false,
verticalSwipe: true,
verticalThreshold: height/4,
horizontalSwipe: true,
horizontalThreshold: width/2,
outputRotationRange: ['-15deg','0deg','15deg'],
duration: 200
}
你可以看看这个问题,关于这个问题 react-native-card-stack-swiper/issues/43 - 希望他们会在某个时候修复它。
问题是滑动器在变化时不看children,所以我们必须强制它识别变化,在你的例子中,你可以简单地使用人的长度,或者来自 firebase 的最后一条消息的 ID。
<CardStack {...props} key={this.peopleList.length} > {renderCards()} </CardStack>