Authored by 张丽霞

收藏的商品优化,review by Redding

... ... @@ -80,6 +80,13 @@ class MetroListView extends React.Component {
getListRef() {
return this._listRef;
}
setNativeProps(props: Object) {
if (this._listRef) {
this._listRef.setNativeProps(props);
}
}
static defaultProps: DefaultProps = {
keyExtractor: (item, index) => item.key || String(index),
renderScrollComponent: (props: Props) => {
... ...
... ... @@ -266,6 +266,12 @@ class SectionList<SectionT: SectionBase<any>>
this._wrapperListRef.scrollToLocation(params);
}
setNativeProps(props: Object) {
if (this._wrapperListRef) {
this._wrapperListRef.setNativeProps(props);
}
}
/**
* Tells the list an interaction has occured, which should trigger viewability calculations, e.g.
* if `waitForInteractions` is true and the user has not scrolled. This is typically called by
... ...
... ... @@ -252,6 +252,12 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
}
}
setNativeProps(props: Object) {
if (this._scrollRef) {
this._scrollRef.setNativeProps(props);
}
}
static defaultProps = {
disableVirtualization: false,
getItem: (data: any, index: number) => data[index],
... ...
... ... @@ -135,6 +135,12 @@ class VirtualizedSectionList<SectionT: SectionBase>
state: State;
setNativeProps(props: Object) {
if (this._listRef) {
this._listRef.setNativeProps(props);
}
}
static defaultProps: DefaultProps = {
...VirtualizedList.defaultProps,
data: [],
... ...
... ... @@ -15,14 +15,14 @@ import LoadingIndicator from '../../../common/components/LoadingIndicator';
import ProductCell from './ProductCell';
import CategorySelector from './CategorySelector';
import NoDataView from '../product/NoDataView'
import { SwipeListView } from 'react-native-swipe-list-view';
import SwipeListView from '../product/SwipeListView'
export default class Browse extends Component {
constructor(props) {
super(props);
this.renderRow = this.renderRow.bind(this);
this.renderItem = this.renderItem.bind(this);
this.renderSectionHeader = this.renderSectionHeader.bind(this);
this.deleteRow = this.deleteRow.bind(this);
this.dataSource = new ListView.DataSource({
... ... @@ -63,8 +63,10 @@ export default class Browse extends Component {
return rightButtons;
}
renderRow(rowData, sectionID, rowID) {
renderItem(item) {
let rowData = item.item;
let rowID = item.index;
return (
<ProductCell
key={'row' + rowID}
... ... @@ -75,9 +77,10 @@ export default class Browse extends Component {
);
}
deleteRow(data, secId, rowId, rowMap) {
rowMap[`${secId}${rowId}`].closeRow();
this.props.onPressDelete && this.props.onPressDelete(data, rowId);
deleteRow(data, rowMap) {
let rowId = data.index;
rowMap[`${rowId}`].closeRow();
this.props.onPressDelete && this.props.onPressDelete(data.item, rowId);
}
... ... @@ -88,7 +91,7 @@ export default class Browse extends Component {
data={categoryList}
selectedCategoryIndex={selectedCategoryIndex}
onPressCategory={(rowData, rowID) => {
this.listView && this.listView.scrollTo({x: 0, y: 0, animated: false});
this.listView && this.listView.scrollToLocation({itemIndex: 0,sectionIndex: 0, viewOffset: 44, animated: false});
this.props.onPressCategory && this.props.onPressCategory(rowData, rowID);
}}
/>
... ... @@ -108,19 +111,22 @@ export default class Browse extends Component {
scrollEnabled={!this.state.isSwiping}
enableEmptySections={true}
disableRightSwipe={true}
dataSource={this.dataSource.cloneWithRows(selectedProductList.toArray())}
renderRow={this.renderRow}
renderSectionHeader={this.renderSectionHeader}renderHiddenRow={ (data, secId, rowId, rowMap) => (
<View style={styles.rowBack}>
<Text></Text>
<Text>
<TouchableOpacity activeOpacity={1} style={styles.tailContainer} onPress={ _ => this.deleteRow(data, secId, rowId, rowMap)}>
<Text style={styles.tailText}>删除</Text>
</TouchableOpacity>
</Text>
</View>
)}
disableRightSwipe={true}
sections={[{data:selectedProductList.toArray(),key:'browse'}]}
renderItem={this.renderItem}
renderSectionHeader={this.renderSectionHeader}
renderHiddenRow={ (data, rowMap) => (
<View style={styles.rowBack}>
<Text></Text>
<Text>
<TouchableOpacity activeOpacity={1} style={styles.tailContainer} onPress={ _ => this.deleteRow(data, rowMap)}>
<Text style={styles.tailText}>删除</Text>
</TouchableOpacity>
</Text>
</View>
)}
rightOpenValue={-70}
categoryId={selectedCategoryIndex}
/> : null}
{showEmpty ? <NoDataView type={'browse'} onPressGuangGuang={this.props.onPressGuangGuang}/> : null}
... ...
... ... @@ -36,6 +36,9 @@ export default class ProductCell extends Component {
if (this.props.editedRow !== nextProps.editedRow) {
return true;
}
if (this.props.editing != nextProps.editing) {
return true;
};
return false;
}
... ... @@ -104,7 +107,6 @@ export default class ProductCell extends Component {
let showLeft = editing && editedRow != rowID;
let showTail = editing && editedRow == rowID;
let marginLeft = showTail ? -70 : 0;
Animated.timing(this.state.marginLeft, {
toValue: marginLeft,
duration: 150,
... ... @@ -166,6 +168,13 @@ export default class ProductCell extends Component {
</View>
</TouchableOpacity>
</Animated.View>
{showTail
? <TouchableOpacity activeOpacity={1} style={styles.tailContainer} onPress={() => {
this.props.onPressDelete && this.props.onPressDelete(data, rowID);
}}>
<Text style={styles.tailText}>删除</Text>
</TouchableOpacity>
: null}
</View>
);
}
... ...
... ... @@ -16,19 +16,20 @@ import ProductCell from '../browse/ProductCell';
import CategorySelector from '../browse/CategorySelector';
import TabHeader from './TabHeader'
import NoDataView from './NoDataView'
import { SwipeListView } from 'react-native-swipe-list-view';
import SwipeListView from './SwipeListView'
export default class Product extends Component {
constructor(props) {
super(props);
this.renderRow = this.renderRow.bind(this);
this.renderItem = this.renderItem.bind(this);
this.renderHeader = this.renderHeader.bind(this);
this.handleScroll = this.handleScroll.bind(this);
this.renderSectionHeader = this.renderSectionHeader.bind(this);
this.renderFooter = this.renderFooter.bind(this);
this.deleteRow = this.deleteRow.bind(this);
this.showDelete = this.showDelete.bind(this);
this.dataSource = new ListView.DataSource({
rowHasChanged: (r1, r2) => !Immutable.is(r1, r2),
... ... @@ -40,6 +41,7 @@ export default class Product extends Component {
this.listView = null;
this.swipeable = {};
this.rowMap = {};
}
handleScroll() {
... ... @@ -70,14 +72,15 @@ export default class Product extends Component {
return rightButtons;
}
renderRow(rowData, sectionID, rowID) {
renderItem(item) {
let {
commonProduct,
globalProduct,
editing,
currentTab,
} = this.props.data;
let rowData = item.item;
let rowID = item.index;
let editedRow = -1;
if (currentTab == 'common') {
let {editedIndex} = commonProduct;
... ... @@ -88,7 +91,6 @@ export default class Product extends Component {
editedRow = editedIndex;
}
let buttons = editing?null: this.rightButtons(rowData, rowID);
return (
<ProductCell
key={'row' + rowID}
... ... @@ -98,6 +100,7 @@ export default class Product extends Component {
editing={editing}
editedRow={editedRow}
rowID={parseInt(rowID)}
showDelete={this.showDelete}
setEditedIndex={this.props.setEditedIndex}
onPressDelete={this.props.onPressDelete}
currentTab={currentTab}
... ... @@ -105,9 +108,15 @@ export default class Product extends Component {
);
}
deleteRow(data, secId, rowId, rowMap) {
rowMap[`${secId}${rowId}`].closeRow();
this.props.onPressDelete && this.props.onPressDelete(data, rowId);
showDelete(rowID) {
this.rowMap[`${rowID}`].rightOpenRow();
}
deleteRow(data, rowMap) {
this.rowMap = rowMap;
let rowId = data.index;
rowMap[`${rowId}`].closeRow();
this.props.onPressDelete && this.props.onPressDelete(data.item, data.index);
}
renderFooter() {
... ... @@ -162,7 +171,11 @@ export default class Product extends Component {
if (currentTab == 'global') {
return null;
}
let realHeight = 44;
let {productList} = globalProduct;
if (productList.size != 0) {
realHeight += 45;
}
if (currentTab == 'common') {
let {isFetching, selectedProductList, categoryList, selectedCategoryIndex} = commonProduct;
if (selectedProductList.size == 0) {
... ... @@ -173,7 +186,7 @@ export default class Product extends Component {
data={categoryList}
selectedCategoryIndex={selectedCategoryIndex}
onPressCategory={(rowData, rowID) => {
this.listView && this.listView.scrollTo({x: 0, y: 0, animated: false});
this.sectionList && this.sectionList.scrollToLocation({itemIndex: 0,sectionIndex: 0, viewOffset: realHeight, animated: false});
this.props.onPressCategory && this.props.onPressCategory(rowData, rowID);
}}
/>
... ... @@ -195,8 +208,10 @@ export default class Product extends Component {
let isLoading = false;
let showList = (commonProduct.get('productList').size && !commonProduct.get('isFetching')) || (globalProduct.get('productList').size && !commonProduct.get('isFetching'));
let showEmpty = commonProduct.get('showEmpty') && globalProduct.get('showEmpty');
let categoryId = 0;
if (currentTab == 'common') {
let {isFetching, selectedProductList, categoryList, selectedCategoryIndex, isDeleting} = commonProduct;
categoryId = selectedCategoryIndex;
isLoading = (selectedProductList.size == 0 && isFetching) || isDeleting;
dataArray = selectedProductList.toArray();
}
... ... @@ -206,36 +221,38 @@ export default class Product extends Component {
isLoading = (productList.size == 0 && isFetching) || isDeleting;
dataArray = productList.toArray();
}
return (
<View style={styles.container}>
{showList ? <SwipeListView
listViewRef={ ref => this.listView = ref }
enableEmptySections={true}
dataSource={this.dataSource.cloneWithRows(dataArray)}
renderRow={this.renderRow}
renderHeader={this.renderHeader}
sections={[{data:dataArray,key:'latest'}]}
renderItem={this.renderItem}
ListHeaderComponent={this.renderHeader}
renderSectionHeader={this.renderSectionHeader}
onScroll={this.handleScroll}
disableRightSwipe={true}
disableLeftSwipe={editing}
renderFooter={this.renderFooter}
bounces={dataArray.length > 0}
scrollEnabled={!this.state.isSwiping}
onScroll={this.handleScroll}
onContentSizeChange={(contentWidth, contentHeight) => {
this.setState({showFooter: contentHeight >= height - 64})
}}
renderHiddenRow={ (data, secId, rowId, rowMap) => (
renderHiddenRow={ (data, rowMap) => {
this.rowMap = rowMap;
return(
<View style={styles.rowBack}>
<Text></Text>
<Text>
<TouchableOpacity activeOpacity={1} style={styles.tailContainer} onPress={_ => this.deleteRow(data, secId, rowId, rowMap)}>
<TouchableOpacity activeOpacity={1} style={styles.tailContainer} onPress={_ => this.deleteRow(data,rowMap)}>
<Text style={styles.tailText}>删除</Text>
</TouchableOpacity>
</Text>
</View>
)}
)}}
rightOpenValue={-70}
categoryId={categoryId}
/> : null}
{showEmpty ? <NoDataView type={'product'} onPressGuangGuang={this.props.onPressGuangGuang}/> : null}
... ... @@ -249,7 +266,6 @@ export default class Product extends Component {
let {width, height} = Dimensions.get('window');
let styles = StyleSheet.create({
container: {
flex: 1,
... ...
'use strict';
import React, {Component} from 'react';
import ReactNative, {
View,
Text,
Image,
ListView,
TouchableOpacity,
StyleSheet,
Dimensions,
Animated,
Easing,
Platform
} from 'react-native';
import Immutable, {Map, List} from 'immutable';
import YH_Image from '../../../common/components/YH_Image';
import DeleteLineText from '../../../common/components/DeleteLineText';
export default class ProductCell extends Component {
constructor(props) {
super(props);
this.renderBottomView = this.renderBottomView.bind(this);
this.state = {
marginLeft: new Animated.Value(0)
}
}
shouldComponentUpdate(nextProps, nextState) {
if (this.props.data !== nextProps.data) {
return true;
}
if (this.props.rowID !== nextProps.rowID) {
return true;
}
return false;
}
renderBottomView() {
let data = this.props.data;
let showSoldOut = data.get('storage', 0) == 0 ? true : false;
if (showSoldOut) {
return (
<View style={[styles.bottomView, styles.soldOutContainer]}>
<Text style={styles.soldOut}>{'已售罄'}</Text>
</View>
);
}
let showOffShelf = data.get('status', 0) == 0 ? true : false;
let showReduce = data.get('price_down', 0) == 0 ? false : true;
if (!showOffShelf && showReduce) {
let reduceStr = '¥' + (parseFloat(data.get('price_down'))).toFixed(2);
return (
<View style={[styles.bottomView, styles.reduceContainer]}>
<Image style={styles.reduceImage} source={require('../../images/browse/mine_goods_low_icon.png')}/>
<Text style={styles.reduceText} numberOfLines={1}>
{'已降'}
<Text style={{
color: '#d0021b'
}}>{reduceStr}</Text>
</Text>
</View>
);
}
return null;
}
render() {
let data = this.props.data;
let prdImage = YH_Image.getSlicedUrl(data.get('image', ''), 76, 102, 2);
let salePrice = 0; // 售卖价
let originPrice = 0; // 原价
let salePriceStr = ''; // 拼接的售卖价
let originPriceStr = ''; // 拼接的原价
let showOriginPrice = true; // 是否显示原价
let salePriceColor = '#d0021b'; // 不显示原价时,售卖价颜色
salePrice = parseFloat(data.get('sales_price'));
originPrice = parseFloat(data.get('market_price'));
salePriceStr = '¥' + salePrice.toFixed(2);
originPriceStr = '¥' + originPrice.toFixed(2);
if (!originPrice || (salePrice == originPrice)) {
showOriginPrice = false;
salePriceColor = '#444444';
}
// let priceBottom = showOriginPrice ? 30 : 40;
let {editing, editedRow, rowID} = this.props;
let showLeft = editing && editedRow != rowID;
let showTail = editing && editedRow == rowID;
let marginLeft = showTail ? -70 : 0;
Animated.timing(this.state.marginLeft, {
toValue: marginLeft,
duration: 150,
easing: Easing.linear
}).start();
return (
<View style={styles.fatherContainer}>
{showLeft
? <TouchableOpacity activeOpacity={1} style={styles.deleteContainer} onPress={() => {
this.props.setEditedIndex && this.props.setEditedIndex(rowID);
}}>
<View style={styles.deleteCircle}>
<View style={styles.deleteBar}/>
</View>
</TouchableOpacity>
: null}
<Animated.View style={[
styles.container, {
marginLeft: this.state.marginLeft
}
]}>
<TouchableOpacity activeOpacity={1} style={[styles.container]} onPress={() => {
this.props.onPressProduct && this.props.onPressProduct(data);
}}>
<View style={styles.container}>
<View style={styles.leftContainer}>
<YH_Image style={styles.prdImage} url={prdImage}/>
<View>
<Text style={styles.prdName} numberOfLines={2}>{data.get('product_name', '')}</Text>
</View>
</View>
<Image style={styles.arrow} source={require('../../images/browse/shared_next_icon.png')}/>
<View style={styles.priceContainer}>
<Text style={[
styles.nowPrice, {
color: salePriceColor
}
]} numberOfLines={1}>{salePriceStr}</Text>
{showOriginPrice
? <DeleteLineText style={styles.oldPriceContainer} textStyle={styles.oldPrice} lineStyle={styles.deleteLine} text={originPriceStr}/>
: null}
</View>
{this.renderBottomView()}
{this.props.currentTab != 'global'
? <TouchableOpacity activeOpacity={1} style={styles.similar} onPress={() => {
this.props.onPressFindSimilar && this.props.onPressFindSimilar(data);
}}>
<Image source={require('../../images/browse/shopcart_findResemblance.png')}/>
</TouchableOpacity>
: null}
<View style={styles.separator}/>
</View>
</TouchableOpacity>
</Animated.View>
</View>
);
}
};
let {width} = Dimensions.get('window');
const DEVICE_WIDTH_RATIO = width / 320;
let imageWidth = Math.floor(width / 2);
let imageHeight = Math.floor(imageWidth * 180 / 375);
let nameWidth = width - 15 - 76 - 10 - 55;
let topLeftWidth = width - Math.ceil(84 * DEVICE_WIDTH_RATIO);
let styles = StyleSheet.create({
fatherContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'white'
},
deleteContainer: {
width: 40,
height: 70,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'white',
paddingLeft: 5
},
deleteCircle: {
width: 22,
height: 22,
borderRadius: 11,
backgroundColor: 'red',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center'
},
deleteBar: {
width: 11,
height: 1.5,
backgroundColor: 'white'
},
tailContainer: {
width: 70,
height: 122,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'red'
},
tailText: {
color: 'white',
fontSize: 17,
backgroundColor: 'red'
},
container: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
width: width,
backgroundColor: 'white'
},
leftContainer: {
flexDirection: 'row'
},
prdImage: {
marginLeft: 15,
marginTop: 10,
marginBottom: 10,
width: 76,
height: 102
},
prdName: {
fontFamily: 'HelveticaNeue-Light',
fontSize: 12,
marginTop: 22,
marginLeft: 10,
width: nameWidth
},
priceContainer: {
position: 'absolute',
flexDirection: 'row',
marginTop: 20,
left: 91,
bottom: 40
},
nowPrice: {
fontSize: 14,
color: '#d0021b',
marginLeft: 10
},
oldPriceContainer: {
flexDirection: 'row',
marginLeft: 5,
marginTop: 2
},
oldPrice: {
fontSize: 12,
color: '#b0b0b0',
height: 16
},
deleteLine: {
position: 'absolute',
top: (16 / 2) - 1,
left: 0,
right: 0,
height: 1,
backgroundColor: '#b0b0b0'
},
bottomView: {
position: 'absolute',
left: 100,
bottom: 11,
width: nameWidth,
},
soldOutContainer: {
width: 45,
height: 15,
borderRadius: 7.5,
backgroundColor: 'gray',
justifyContent: 'center'
},
soldOut: {
fontSize: 12,
color: 'white',
textAlign: 'center',
textAlignVertical:'center',
width: 45,
height: 15,
paddingTop: Platform.OS === 'ios' ? 1.0:-0.5,
backgroundColor: 'transparent'
},
reduceContainer: {
flexDirection: 'row',
},
reduceImage: {
width: 15,
height: 15
},
reduceText: {
marginLeft: 2,
fontSize: 12,
color: '#b0b0b0',
width: nameWidth - 15 - 2
},
similar: {
position: 'absolute',
width: 56,
height: 25,
right: 15,
bottom: 12,
},
separator: {
position: 'absolute',
bottom: 0,
left: 96,
width,
height: 1,
backgroundColor: '#f0f0f0'
},
arrow: {
marginRight: 15,
width: 9,
height: 15
}
});
'use strict';
import React, {
Component,
PropTypes,
} from 'react';
import {
SectionList,
Text,
View,
} from 'react-native';
import SwipeRow from './SwipeRow';
/**
* ListView that renders SwipeRows.
*/
class SwipeListView extends Component {
constructor(props){
super(props);
this._rows = {};
this.openCellId = null;
this.state = {
scrollState: true,
}
}
componentWillReceiveProps(nextProps) {
if (this.props.categoryId != nextProps.categoryId) {
this.safeCloseOpenRow();
}
if (nextProps.disableLeftSwipe) {
this.safeCloseOpenRow();
};
}
setScrollEnabled(enable) {
this._listView.setNativeProps({scrollEnabled: enable})
}
safeCloseOpenRow() {
// if the openCellId is stale due to deleting a row this could be undefined
if (this._rows[this.openCellId]) {
this._rows[this.openCellId].closeRow();
}
}
rowSwipeGestureBegan(id) {
if (this.props.closeOnRowBeginSwipe && this.openCellId && this.openCellId !== id) {
this.safeCloseOpenRow();
}
}
onRowOpen(rowId, rowMap) {
const cellIdentifier = `${rowId}`;
if (this.openCellId && this.openCellId !== cellIdentifier) {
this.safeCloseOpenRow();
}
this.openCellId = cellIdentifier;
this.props.onRowOpen && this.props.onRowOpen(rowId, rowMap);
}
onRowPress(id) {
if (this.openCellId) {
if (this.props.closeOnRowPress) {
this.safeCloseOpenRow();
this.openCellId = null;
}
}
}
onScroll(event) {
if (this.openCellId) {
if (this.props.closeOnScroll) {
this.safeCloseOpenRow();
this.openCellId = null;
}
}
this.props.onScroll && this.props.onScroll(event);
}
setRefs(ref) {
this._listView = ref;
this.props.listViewRef && this.props.listViewRef(ref);
}
renderItem(item, rowMap) {
const Component = this.props.renderItem(item, rowMap);
let rowId = item.index;
if (!this.props.renderHiddenRow) {
return React.cloneElement(
Component,
{
...Component.props,
ref: row => this._rows[`${rowId}`] = row,
onRowOpen: _ => this.onRowOpen(rowId, this._rows),
onRowDidOpen: _ => this.props.onRowDidOpen && this.props.onRowDidOpen(rowId, this._rows),
onRowClose: _ => this.props.onRowClose && this.props.onRowClose(rowId, this._rows),
onRowDidClose: _ => this.props.onRowDidClose && this.props.onRowDidClose(rowId, this._rows),
onRowPress: _ => this.onRowPress(`${rowId}`),
setScrollEnabled: enable => this.setScrollEnabled(enable),
swipeGestureBegan: _ => this.rowSwipeGestureBegan(`${rowId}`)
}
);
} else {
const previewRowId = this.props.dataSource && this.props.dataSource.getRowIDForFlatIndex(this.props.previewRowIndex || 0);
return (
<SwipeRow
ref={row => this._rows[`${rowId}`] = row}
swipeGestureBegan={ _ => this.rowSwipeGestureBegan(`${rowId}`) }
onRowOpen={ _ => this.onRowOpen(rowId, this._rows) }
onRowDidOpen={ _ => this.props.onRowDidOpen && this.props.onRowDidOpen(rowId, this._rows)}
onRowClose={ _ => this.props.onRowClose && this.props.onRowClose(rowId, this._rows) }
onRowDidClose={ _ => this.props.onRowDidClose && this.props.onRowDidClose(rowId, this._rows) }
onRowPress={ _ => this.onRowPress(`${rowId}`) }
setScrollEnabled={ (enable) => this.setScrollEnabled(enable) }
leftOpenValue={this.props.leftOpenValue}
rightOpenValue={this.props.rightOpenValue}
closeOnRowPress={this.props.closeOnRowPress}
disableLeftSwipe={this.props.disableLeftSwipe}
disableRightSwipe={this.props.disableRightSwipe}
stopLeftSwipe={this.props.stopLeftSwipe}
stopRightSwipe={this.props.stopRightSwipe}
recalculateHiddenLayout={this.props.recalculateHiddenLayout}
style={this.props.swipeRowStyle}
preview={(this.props.previewFirstRow || this.props.previewRowIndex) && rowId === previewRowId}
previewDuration={this.props.previewDuration}
previewOpenValue={this.props.previewOpenValue}
tension={this.props.tension}
friction={this.props.friction}
directionalDistanceChangeThreshold={this.props.directionalDistanceChangeThreshold}
swipeToOpenPercent={this.props.swipeToOpenPercent}
>
{this.props.renderHiddenRow(item, this._rows)}
{this.props.renderItem(item, this._rows)}
</SwipeRow>
);
}
}
render() {
return (
<SectionList
{...this.props}
ref={ c => this.setRefs(c) }
renderItem={(item) => this.renderItem(item, this._rows)}
onScroll={ e => this.onScroll(e) }
/>
)
}
}
SwipeListView.propTypes = {
/**
* How to render a row. Should return a valid React Element.
*/
renderItem: PropTypes.func.isRequired,
/**
* How to render a hidden row (renders behind the row). Should return a valid React Element.
* This is required unless renderRow is passing a SwipeRow.
*/
renderHiddenRow: PropTypes.func,
/**
* TranslateX value for opening the row to the left (positive number)
*/
leftOpenValue: PropTypes.number,
/**
* TranslateX value for opening the row to the right (negative number)
*/
rightOpenValue: PropTypes.number,
/**
* TranslateX value for stop the row to the left (positive number)
*/
stopLeftSwipe: PropTypes.number,
/**
* TranslateX value for stop the row to the right (negative number)
*/
stopRightSwipe: PropTypes.number,
/**
* Should open rows be closed when the listView begins scrolling
*/
closeOnScroll: PropTypes.bool,
/**
* Should open rows be closed when a row is pressed
*/
closeOnRowPress: PropTypes.bool,
/**
* Should open rows be closed when a row begins to swipe open
*/
closeOnRowBeginSwipe: PropTypes.bool,
/**
* Disable ability to swipe rows left
*/
disableLeftSwipe: PropTypes.bool,
/**
* Disable ability to swipe rows right
*/
disableRightSwipe: PropTypes.bool,
/**
* Enable hidden row onLayout calculations to run always.
*
* By default, hidden row size calculations are only done on the first onLayout event
* for performance reasons.
* Passing ```true``` here will cause calculations to run on every onLayout event.
* You may want to do this if your rows' sizes can change.
* One case is a SwipeListView with rows of different heights and an options to delete rows.
*/
recalculateHiddenLayout: PropTypes.bool,
/**
* Called when a swipe row is animating open
*/
onRowOpen: PropTypes.func,
/**
* Called when a swipe row has animated open
*/
onRowDidOpen: PropTypes.func,
/**
* Called when a swipe row is animating closed
*/
onRowClose: PropTypes.func,
/**
* Called when a swipe row has animated closed
*/
onRowDidClose: PropTypes.func,
/**
* Styles for the parent wrapper View of the SwipeRow
*/
swipeRowStyle: View.propTypes.style,
/**
* Called when the ListView ref is set and passes a ref to the ListView
* e.g. listViewRef={ ref => this._swipeListViewRef = ref }
*/
listViewRef: PropTypes.func,
/**
* Should the first SwipeRow do a slide out preview to show that the list is swipeable
*/
previewFirstRow: PropTypes.bool,
/**
* Should the specified rowId do a slide out preview to show that the list is swipeable
* Note: This ID will be passed to this function to get the correct row index
* https://facebook.github.io/react-native/docs/listviewdatasource.html#getrowidforflatindex
*/
previewRowIndex: PropTypes.number,
/**
* Duration of the slide out preview animation (milliseconds)
*/
previewDuration: PropTypes.number,
/**
* TranslateX value for the slide out preview animation
* Default: 0.5 * props.rightOpenValue
*/
previewOpenValue: PropTypes.number,
/**
* Friction for the open / close animation
*/
friction: PropTypes.number,
/**
* Tension for the open / close animation
*/
tension: PropTypes.number,
/**
* The dx value used to detect when a user has begun a swipe gesture
*/
directionalDistanceChangeThreshold: PropTypes.number,
/**
* What % of the left/right openValue does the user need to swipe
* past to trigger the row opening.
*/
swipeToOpenPercent: PropTypes.number,
}
SwipeListView.defaultProps = {
leftOpenValue: 0,
rightOpenValue: 0,
closeOnRowBeginSwipe: false,
closeOnScroll: true,
closeOnRowPress: true,
disableLeftSwipe: false,
disableRightSwipe: false,
recalculateHiddenLayout: false,
previewFirstRow: false,
directionalDistanceChangeThreshold: 2,
swipeToOpenPercent: 50
}
export default SwipeListView;
... ...
'use strict';
import React, {
Component,
PropTypes,
} from 'react';
import {
Animated,
PanResponder,
Platform,
StyleSheet,
TouchableOpacity,
View
} from 'react-native';
const PREVIEW_OPEN_DELAY = 700;
const PREVIEW_CLOSE_DELAY = 300;
/**
* Row that is generally used in a SwipeListView.
* If you are rendering a SwipeRow explicitly you must pass the SwipeRow exactly two children.
* The first will be rendered behind the second.
* e.g.
<SwipeRow>
<View style={hiddenRowStyle} />
<View style={visibleRowStyle} />
</SwipeRow>
*/
class SwipeRow extends React.PureComponent {
constructor(props) {
super(props);
this.horizontalSwipeGestureBegan = false;
this.swipeInitialX = null;
this.parentScrollEnabled = true;
this.ranPreview = false;
this.state = {
dimensionsSet: false,
hiddenHeight: 0,
hiddenWidth: 0
};
this._translateX = new Animated.Value(0);
}
componentWillMount() {
this._panResponder = PanResponder.create({
onMoveShouldSetPanResponder: (e, gs) => this.handleOnMoveShouldSetPanResponder(e, gs),
onPanResponderMove: (e, gs) => this.handlePanResponderMove(e, gs),
onPanResponderRelease: (e, gs) => this.handlePanResponderEnd(e, gs),
onPanResponderTerminate: (e, gs) => this.handlePanResponderEnd(e, gs),
onShouldBlockNativeResponder: _ => false,
});
}
getPreviewAnimation(toValue, delay) {
return Animated.timing(
this._translateX,
{ duration: this.props.previewDuration, toValue, delay }
);
}
onContentLayout(e) {
this.setState({
dimensionsSet: !this.props.recalculateHiddenLayout,
hiddenHeight: e.nativeEvent.layout.height,
hiddenWidth: e.nativeEvent.layout.width,
});
if (this.props.preview && !this.ranPreview) {
this.ranPreview = true;
let previewOpenValue = this.props.previewOpenValue || this.props.rightOpenValue * 0.5;
this.getPreviewAnimation(previewOpenValue, PREVIEW_OPEN_DELAY)
.start( _ => {
this.getPreviewAnimation(0, PREVIEW_CLOSE_DELAY).start();
});
}
}
onRowPress() {
if (this.props.onRowPress) {
this.props.onRowPress();
} else {
if (this.props.closeOnRowPress) {
this.closeRow();
}
}
}
handleOnMoveShouldSetPanResponder(e, gs) {
const { dx } = gs;
return Math.abs(dx) > this.props.directionalDistanceChangeThreshold;
}
handlePanResponderMove(e, gestureState) {
const { dx, dy } = gestureState;
const absDx = Math.abs(dx);
const absDy = Math.abs(dy);
// this check may not be necessary because we don't capture the move until we pass the threshold
// just being extra safe here
if (absDx > this.props.directionalDistanceChangeThreshold || absDy > this.props.directionalDistanceChangeThreshold) {
// we have enough to determine direction
if (absDy > absDx && !this.horizontalSwipeGestureBegan) {
// user is moving vertically, do nothing, listView will handle
return;
}
// user is moving horizontally
if (this.parentScrollEnabled) {
// disable scrolling on the listView parent
this.parentScrollEnabled = false;
this.props.setScrollEnabled && this.props.setScrollEnabled(false);
}
if (this.swipeInitialX === null) {
// set tranlateX value when user started swiping
this.swipeInitialX = this._translateX._value
}
if (!this.horizontalSwipeGestureBegan) {
this.horizontalSwipeGestureBegan = true;
this.props.swipeGestureBegan && this.props.swipeGestureBegan();
}
let newDX = this.swipeInitialX + dx;
if (this.props.disableLeftSwipe && newDX < 0) { newDX = 0; }
if (this.props.disableRightSwipe && newDX > 0) { newDX = 0; }
if (this.props.stopLeftSwipe && newDX > this.props.stopLeftSwipe) { newDX = this.props.stopLeftSwipe; }
if (this.props.stopRightSwipe && newDX < this.props.stopRightSwipe) { newDX = this.props.stopRightSwipe; }
this._translateX.setValue(newDX);
}
}
handlePanResponderEnd(e, gestureState) {
// finish up the animation
let toValue = 0;
if (this._translateX._value >= 0) {
// trying to open right
if (this._translateX._value > this.props.leftOpenValue * (this.props.swipeToOpenPercent/100)) {
// we're more than halfway
toValue = this.props.leftOpenValue;
}
} else {
// trying to open left
if (this._translateX._value < this.props.rightOpenValue * (this.props.swipeToOpenPercent/100)) {
// we're more than halfway
toValue = this.props.rightOpenValue
}
}
this.manuallySwipeRow(toValue);
// re-enable scrolling on listView parent
if (!this.parentScrollEnabled) {
this.parentScrollEnabled = true;
this.props.setScrollEnabled && this.props.setScrollEnabled(true);
}
}
/*
* This method is called by SwipeListView
*/
closeRow() {
this.manuallySwipeRow(0);
}
rightOpenRow() {
this.manuallySwipeRow(this.props.rightOpenValue);
}
manuallySwipeRow(toValue) {
Animated.spring(
this._translateX,
{
toValue,
friction: this.props.friction,
tension: this.props.tension
}
).start( _ => {
if (toValue === 0) {
this.props.onRowDidClose && this.props.onRowDidClose();
} else {
this.props.onRowDidOpen && this.props.onRowDidOpen();
}
})
if (toValue === 0) {
this.props.onRowClose && this.props.onRowClose();
} else {
this.props.onRowOpen && this.props.onRowOpen(toValue);
}
// reset everything
this.swipeInitialX = null;
this.horizontalSwipeGestureBegan = false;
}
renderVisibleContent() {
// handle touchables
const onPress = this.props.children[1].props.onPress;
if (onPress) {
const newOnPress = _ => {
this.onRowPress();
onPress();
}
return React.cloneElement(
this.props.children[1],
{
...this.props.children[1].props,
onPress: newOnPress
}
);
}
return (
<TouchableOpacity
activeOpacity={1}
onPress={ _ => this.onRowPress() }
>
{this.props.children[1]}
</TouchableOpacity>
)
}
renderRowContent() {
// We do this annoying if statement for performance.
// We don't want the onLayout func to run after it runs once.
if (this.state.dimensionsSet) {
return (
<Animated.View
{...this._panResponder.panHandlers}
style={{
transform: [
{translateX: this._translateX}
]
}}
>
{this.renderVisibleContent()}
</Animated.View>
);
} else {
return (
<Animated.View
{...this._panResponder.panHandlers}
onLayout={ (e) => this.onContentLayout(e) }
style={{
transform: [
{translateX: this._translateX}
]
}}
>
{this.renderVisibleContent()}
</Animated.View>
);
}
}
render() {
return (
<View style={this.props.style ? this.props.style : styles.container}>
<View style={[
styles.hidden,
{
height: this.state.hiddenHeight,
width: this.state.hiddenWidth,
}
]}>
{this.props.children[0]}
</View>
{this.renderRowContent()}
</View>
);
}
}
const styles = StyleSheet.create({
container: {
// As of RN 0.29 flex: 1 is causing all rows to be the same height
// flex: 1
},
hidden: {
bottom: 0,
left: 0,
overflow: 'hidden',
position: 'absolute',
right: 0,
top: 0,
},
});
SwipeRow.propTypes = {
/**
* Used by the SwipeListView to close rows on scroll events.
* You shouldn't need to use this prop explicitly.
*/
setScrollEnabled: PropTypes.func,
/**
* Called when it has been detected that a row should be swiped open.
*/
swipeGestureBegan: PropTypes.func,
/**
* Called when a swipe row is animating open. Used by the SwipeListView
* to keep references to open rows.
*/
onRowOpen: PropTypes.func,
/**
* Called when a swipe row has animated open.
*/
onRowDidOpen: PropTypes.func,
/**
* TranslateX value for opening the row to the left (positive number)
*/
leftOpenValue: PropTypes.number,
/**
* TranslateX value for opening the row to the right (negative number)
*/
rightOpenValue: PropTypes.number,
/**
* TranslateX value for stop the row to the left (positive number)
*/
stopLeftSwipe: PropTypes.number,
/**
* TranslateX value for stop the row to the right (negative number)
*/
stopRightSwipe: PropTypes.number,
/**
* Friction for the open / close animation
*/
friction: PropTypes.number,
/**
* Tension for the open / close animation
*/
tension: PropTypes.number,
/**
* Should the row be closed when it is tapped
*/
closeOnRowPress: PropTypes.bool,
/**
* Disable ability to swipe the row left
*/
disableLeftSwipe: PropTypes.bool,
/**
* Disable ability to swipe the row right
*/
disableRightSwipe: PropTypes.bool,
/**
* Enable hidden row onLayout calculations to run always
*/
recalculateHiddenLayout: PropTypes.bool,
/**
* Called when a swipe row is animating closed
*/
onRowClose: PropTypes.func,
/**
* Called when a swipe row has animated closed
*/
onRowDidClose: PropTypes.func,
/**
* Styles for the parent wrapper View of the SwipeRow
*/
style: View.propTypes.style,
/**
* Should the row do a slide out preview to show that it is swipeable
*/
preview: PropTypes.bool,
/**
* Duration of the slide out preview animation
*/
previewDuration: PropTypes.number,
/**
* TranslateX value for the slide out preview animation
* Default: 0.5 * props.rightOpenValue
*/
previewOpenValue: PropTypes.number,
/**
* The dx value used to detect when a user has begun a swipe gesture
*/
directionalDistanceChangeThreshold: PropTypes.number,
/**
* What % of the left/right openValue does the user need to swipe
* past to trigger the row opening.
*/
swipeToOpenPercent: PropTypes.number,
};
SwipeRow.defaultProps = {
leftOpenValue: 0,
rightOpenValue: 0,
closeOnRowPress: true,
disableLeftSwipe: false,
disableRightSwipe: false,
recalculateHiddenLayout: false,
preview: false,
previewDuration: 300,
directionalDistanceChangeThreshold: 2,
swipeToOpenPercent: 30
};
export default SwipeRow;
... ...