diff --git a/lib/FullScreenContainer.js b/lib/FullScreenContainer.js index b22020b..399f9e0 100644 --- a/lib/FullScreenContainer.js +++ b/lib/FullScreenContainer.js @@ -3,9 +3,11 @@ import PropTypes from 'prop-types'; import { DeviceEventEmitter, Dimensions, - FlatList, + ListView, View, + ViewPagerAndroid, StyleSheet, + Platform, StatusBar, TouchableWithoutFeedback, ViewPropTypes @@ -19,17 +21,18 @@ export default class FullScreenContainer extends React.Component { static propTypes = { style: ViewPropTypes.style, + dataSource: PropTypes.instanceOf(ListView.DataSource).isRequired, mediaList: PropTypes.array.isRequired, /* * opens grid view */ onGridButtonTap: PropTypes.func, - + /* * Display top bar */ displayTopBar: PropTypes.bool, - + /* * updates top bar title */ @@ -60,14 +63,8 @@ export default class FullScreenContainer extends React.Component { onActionButton: PropTypes.func, onPhotoLongPress: PropTypes.func, delayLongPress: PropTypes.number, - - /** - * Use a custom button in the bottom bar to the left of the Share button, - * without having to recreate the entire bottom bar and pass it in with the - * `bottomBarComponent` prop. The visibility of the Share button can still - * be controlled with `displayActionButton`. - */ - customBottomBarButton: PropTypes.element, + enablePinchToZoom: PropTypes.bool, + customBottomBarButton: PropTypes.element, }; static defaultProps = { @@ -80,14 +77,16 @@ export default class FullScreenContainer extends React.Component { onGridButtonTap: () => {}, onPhotoLongPress: () => {}, delayLongPress: 1000, + enablePinchToZoom: true, }; constructor(props, context) { super(props, context); - this._renderItem = this._renderItem.bind(this); + this._renderRow = this._renderRow.bind(this); this._toggleControls = this._toggleControls.bind(this); this._onScroll = this._onScroll.bind(this); + this._onPageSelected = this._onPageSelected.bind(this); this._onNextButtonTapped = this._onNextButtonTapped.bind(this); this._onPhotoLongPress = this._onPhotoLongPress.bind(this); this._onPreviousButtonTapped = this._onPreviousButtonTapped.bind(this); @@ -111,14 +110,19 @@ export default class FullScreenContainer extends React.Component { } openPage(index, animated) { - if (!this.flatListView) { + if (!this.scrollView) { return; } - this.flatListView.scrollToIndex({ - index, + if (Platform.OS === 'ios') { + const screenWidth = Dimensions.get('window').width; + this.scrollView.scrollTo({ + x: index * screenWidth, animated, }); + } else { + this.scrollView.setPageWithoutAnimation(index); + } this._updatePageIndex(index); } @@ -130,10 +134,7 @@ export default class FullScreenContainer extends React.Component { }, () => { this._triggerPhotoLoad(index); - const { customTitle, mediaList } = this.props; - - const rowCount = mediaList.length; - const newTitle = customTitle ? customTitle(index + 1, rowCount) : `${index + 1} of ${rowCount}`; + const newTitle = `${index + 1} of ${this.props.dataSource.getRowCount()}`; this.props.updateTitle(newTitle); }); } @@ -166,7 +167,7 @@ export default class FullScreenContainer extends React.Component { _onNextButtonTapped() { let nextIndex = this.state.currentIndex + 1; // go back to the first item when there is no more next item - if (nextIndex > this.props.mediaList.length - 1) { + if (nextIndex > this.props.dataSource.getRowCount() - 1) { nextIndex = 0; } this.openPage(nextIndex, false); @@ -176,7 +177,7 @@ export default class FullScreenContainer extends React.Component { let prevIndex = this.state.currentIndex - 1; // go to the last item when there is no more previous item if (prevIndex < 0) { - prevIndex = this.props.mediaList.length - 1; + prevIndex = this.props.dataSource.getRowCount() - 1; } this.openPage(prevIndex, false); } @@ -204,6 +205,11 @@ export default class FullScreenContainer extends React.Component { const { currentIndex } = this.state; let newIndex = page; + // handle ViewPagerAndroid argument + if (typeof newIndex === 'object') { + newIndex = newIndex.nativeEvent.position; + } + if (currentIndex !== newIndex) { this._updatePageIndex(newIndex); @@ -212,14 +218,13 @@ export default class FullScreenContainer extends React.Component { } } } - _onPhotoLongPress() { const onPhotoLongPress = this.props.onPhotoLongPress; const { currentMedia, currentIndex } = this.state; onPhotoLongPress(currentMedia, currentIndex); } - _renderItem({ item, index }) { + _renderRow(media: Object, sectionID: number, rowID: number) { const { displaySelectionButtons, onMediaSelection, @@ -227,53 +232,57 @@ export default class FullScreenContainer extends React.Component { } = this.props; return ( - - + this.photoRefs[rowID] = ref} + lazyLoad + useCircleProgress={useCircleProgress} + uri={media.photo} + displaySelectionButtons={displaySelectionButtons} + selected={media.selected} + enablePinchToZoom={this.props.enablePinchToZoom} + onSelection={(isSelected) => { + onMediaSelection(rowID, isSelected); + }} onPress={this._toggleControls} onLongPress={this._onPhotoLongPress} - delayLongPress={this.props.delayLongPress}> - this.photoRefs[index] = ref} - lazyLoad - useCircleProgress={useCircleProgress} - uri={item.photo} - displaySelectionButtons={displaySelectionButtons} - selected={item.selected} - onSelection={(isSelected) => onMediaSelection(item, index, isSelected)} - /> - + delayLongPress={this.props.delayLongPress} + /> ); } - getItemLayout = (data, index) => ( - { length: Dimensions.get('window').width, offset: Dimensions.get('window').width * index, index } - ) - _renderScrollableContent() { - const { mediaList } = this.props; + const { dataSource, mediaList } = this.props; + + if (Platform.OS === 'android') { + return ( + this.scrollView = scrollView} + onPageSelected={this._onPageSelected} + > + {mediaList.map((child, idx) => this._renderRow(child, 0, idx))} + + ); + } return ( - this.flatListView = flatListView} - data={mediaList} - renderItem={this._renderItem} + this.scrollView = scrollView} + dataSource={dataSource} + renderRow={this._renderRow} onScroll={this._onScroll} - keyExtractor={this._keyExtractor} horizontal pagingEnabled showsHorizontalScrollIndicator={false} showsVerticalScrollIndicator={false} directionalLockEnabled scrollEventThrottle={16} - getItemLayout={this.getItemLayout} - initialScrollIndex={this.state.currentIndex} /> ); } - _keyExtractor = item => item.id || item.thumb || item.photo; - render() { const { displayNavArrows, @@ -281,7 +290,7 @@ export default class FullScreenContainer extends React.Component { displayActionButton, onGridButtonTap, enableGrid, - customBottomBarButton, + customBottomBarButton, } = this.props; const { controlsDisplayed, currentMedia } = this.state; const BottomBarComponent = this.props.bottomBarComponent || BottomBar; @@ -289,11 +298,7 @@ export default class FullScreenContainer extends React.Component { return ( ); } + } const styles = StyleSheet.create({ diff --git a/lib/GridContainer.js b/lib/GridContainer.js index 4209fb9..0b0820e 100755 --- a/lib/GridContainer.js +++ b/lib/GridContainer.js @@ -2,15 +2,13 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Dimensions, - FlatList, + ListView, TouchableHighlight, View, StyleSheet, ViewPropTypes } from 'react-native'; -import { ifIphoneX } from 'react-native-iphone-x-helper'; - import Constants from './constants'; import { Photo } from './media'; @@ -21,8 +19,8 @@ export default class GridContainer extends React.Component { static propTypes = { style: ViewPropTypes.style, - mediaList: PropTypes.array.isRequired, square: PropTypes.bool, + dataSource: PropTypes.instanceOf(ListView.DataSource).isRequired, displaySelectionButtons: PropTypes.bool, onPhotoTap: PropTypes.func, itemPerRow: PropTypes.number, @@ -44,9 +42,15 @@ export default class GridContainer extends React.Component { itemPerRow: 3, }; - keyExtractor = item => item.id || item.thumb || item.photo; + constructor(props, context) { + super(props, context); + + this._renderRow = this._renderRow.bind(this); - renderItem = ({ item, index }) => { + this.state = {}; + } + + _renderRow(media: Object, sectionID: number, rowID: number) { const { displaySelectionButtons, onPhotoTap, @@ -59,7 +63,7 @@ export default class GridContainer extends React.Component { const photoWidth = (screenWidth / itemPerRow) - (ITEM_MARGIN * 2); return ( - onPhotoTap(index)}> + onPhotoTap(parseInt(rowID, 10))}> onMediaSelection(item, index, isSelected)} + uri={media.thumb || media.photo} + selected={media.selected} + enablePinchToZoom={false} + onSelection={(isSelected) => { + onMediaSelection(rowID, isSelected); + }} /> @@ -78,18 +85,18 @@ export default class GridContainer extends React.Component { } render() { - const { mediaList } = this.props; + const { dataSource } = this.props; return ( - ); @@ -100,9 +107,14 @@ export default class GridContainer extends React.Component { const styles = StyleSheet.create({ container: { flex: 1, - paddingTop: ifIphoneX(18, 0), paddingBottom: Constants.TOOLBAR_HEIGHT, }, + list: { + justifyContent: 'flex-start', + alignItems: 'flex-start', + flexDirection: 'row', + flexWrap: 'wrap', + }, row: { justifyContent: 'center', margin: 1, diff --git a/lib/bar/BarContainer.js b/lib/bar/BarContainer.js index a881af2..7ae9c41 100644 --- a/lib/bar/BarContainer.js +++ b/lib/bar/BarContainer.js @@ -7,8 +7,6 @@ import { ViewPropTypes } from 'react-native'; -import { getBottomSpace } from 'react-native-iphone-x-helper'; - const BAR_POSITIONS = { TOP: 'top', BOTTOM: 'bottom', @@ -37,9 +35,9 @@ class BarContainer extends Component { }; } - componentDidUpdate() { + componentWillReceiveProps(nextProps) { Animated.timing(this.state.animation, { - toValue: this.props.displayed ? 1 : 0, + toValue: nextProps.displayed ? 1 : 0, duration: 300, }).start(); } @@ -55,7 +53,7 @@ class BarContainer extends Component { styles.container, isBottomBar ? styles.bottomBar : styles.topBar, { - height: height + (isBottomBar ? getBottomSpace() : 0), + height, opacity: this.state.animation, transform: [{ translateY: this.state.animation.interpolate({ @@ -86,7 +84,6 @@ const styles = StyleSheet.create({ }, bottomBar: { bottom: 0, - paddingBottom: getBottomSpace(), }, }); diff --git a/lib/bar/BottomBar.js b/lib/bar/BottomBar.js index 57e269c..2fe31f9 100644 --- a/lib/bar/BottomBar.js +++ b/lib/bar/BottomBar.js @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; + import { Image, Text, @@ -7,6 +8,7 @@ import { TouchableOpacity, View, } from 'react-native'; +import { Icon } from 'native-base'; import { BarContainer, BAR_POSITIONS } from './BarContainer'; @@ -64,10 +66,7 @@ export default class BottomBar extends React.Component { style={styles.button} onPress={onPrev} > - + ); const rightArrow = ( @@ -76,10 +75,7 @@ export default class BottomBar extends React.Component { style={styles.button} onPress={onNext} > - + ); @@ -100,11 +96,8 @@ export default class BottomBar extends React.Component { if (displayGridButton) { return ( - - + + ); } @@ -123,7 +116,7 @@ export default class BottomBar extends React.Component { if (displayActionButton) { components.push( - + ); } @@ -198,4 +191,4 @@ const styles = StyleSheet.create({ flex: 0, paddingTop: 8, } -}); +}); \ No newline at end of file diff --git a/lib/bar/TopBar.js b/lib/bar/TopBar.js index 2af3e8b..c0daca2 100644 --- a/lib/bar/TopBar.js +++ b/lib/bar/TopBar.js @@ -8,8 +8,6 @@ import { Platform, } from 'react-native'; -import { ifIphoneX } from 'react-native-iphone-x-helper'; - import { BarContainer } from './BarContainer'; export default class TopBar extends React.Component { @@ -60,7 +58,7 @@ export default class TopBar extends React.Component { {this.renderBackButton()} {title} @@ -74,7 +72,7 @@ const styles = StyleSheet.create({ flex: 1, flexDirection: 'row', justifyContent: 'center', - paddingTop: ifIphoneX(48, 30), + paddingTop: 30, }, text: { fontSize: 18, @@ -84,7 +82,7 @@ const styles = StyleSheet.create({ position: 'absolute', flexDirection: 'row', left: 0, - top: 16 + ifIphoneX(18, 0), + top: 16, }, backText: { paddingTop: 14, diff --git a/lib/index.js b/lib/index.js index 6b3078b..46ab02b 100755 --- a/lib/index.js +++ b/lib/index.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { Animated, Dimensions, + ListView, View, StyleSheet, ViewPropTypes @@ -112,14 +113,13 @@ export default class PhotoBrowser extends React.Component { */ onPhotoLongPress: PropTypes.func, delayPhotoLongPress: PropTypes.number, + - /** - * Use a custom button in the bottom bar to the left of the Share button, - * without having to recreate the entire bottom bar and pass it in with the - * `bottomBarComponent` prop. The visibility of the Share button can still - * be controlled with `displayActionButton`. + /* + * Whether pinch to zoom is enabled or disabled */ - customBottomBarButton: PropTypes.element, + enablePinchToZoom: PropTypes.bool, + customBottomBarButton: PropTypes.element, }; static defaultProps = { @@ -138,6 +138,7 @@ export default class PhotoBrowser extends React.Component { displayTopBar: true, onPhotoLongPress: () => {}, delayPhotoLongPress: 1000, + enablePinchToZoom: true, gridOffset: 0, }; @@ -146,12 +147,15 @@ export default class PhotoBrowser extends React.Component { this._onGridPhotoTap = this._onGridPhotoTap.bind(this); this._onGridButtonTap = this._onGridButtonTap.bind(this); + this._onMediaSelection = this._onMediaSelection.bind(this); this._updateTitle = this._updateTitle.bind(this); this._toggleTopBar = this._toggleTopBar.bind(this); - const { startOnGrid, initialIndex } = props; + const { mediaList, startOnGrid, initialIndex } = props; this.state = { + dataSource: this._createDataSource(mediaList), + mediaList, isFullScreen: !startOnGrid, fullScreenAnim: new Animated.Value(startOnGrid ? 0 : 1), currentIndex: initialIndex, @@ -159,6 +163,21 @@ export default class PhotoBrowser extends React.Component { }; } + componentWillReceiveProps(nextProps) { + const mediaList = nextProps.mediaList; + this.setState({ + dataSource: this._createDataSource(mediaList), + mediaList, + }); + } + + _createDataSource(list) { + const dataSource = new ListView.DataSource({ + rowHasChanged: (r1, r2) => r1 !== r2, + }); + return dataSource.cloneWithRows(list); + } + _onGridPhotoTap(index) { this.refs.fullScreenContainer.openPage(index, false); this._toggleFullScreen(true); @@ -168,6 +187,25 @@ export default class PhotoBrowser extends React.Component { this._toggleFullScreen(false); } + _onMediaSelection(index, isSelected) { + const { + mediaList: oldMediaList, + dataSource, + } = this.state; + const newMediaList = oldMediaList.slice(); + const selectedMedia = { + ...oldMediaList[index], + selected: isSelected, + }; + newMediaList[index] = selectedMedia; + + this.setState({ + dataSource: dataSource.cloneWithRows(newMediaList), + mediaList: newMediaList, + }); + this.props.onSelectionChanged(selectedMedia, index, isSelected); + } + _updateTitle(title) { this.setState({ title }); } @@ -195,7 +233,6 @@ export default class PhotoBrowser extends React.Component { render() { const { - mediaList, alwaysShowControls, displayNavArrows, alwaysDisplayStatusBar, @@ -209,10 +246,10 @@ export default class PhotoBrowser extends React.Component { style, square, gridOffset, - customTitle, - onSelectionChanged } = this.props; const { + dataSource, + mediaList, isFullScreen, fullScreenAnim, currentIndex, @@ -238,10 +275,10 @@ export default class PhotoBrowser extends React.Component { @@ -251,6 +288,7 @@ export default class PhotoBrowser extends React.Component { fullScreenContainer = ( ); } @@ -286,7 +325,7 @@ export default class PhotoBrowser extends React.Component { diff --git a/lib/media/Photo.js b/lib/media/Photo.js index 2f6c2c9..916aee8 100644 --- a/lib/media/Photo.js +++ b/lib/media/Photo.js @@ -8,6 +8,8 @@ import { TouchableWithoutFeedback, ActivityIndicator, Platform, + ScrollView, + Text, } from 'react-native'; import * as Progress from 'react-native-progress'; @@ -75,6 +77,17 @@ export default class Photo extends Component { * iOS only */ useCircleProgress: PropTypes.bool, + + /* + * Whether or not the user can pinch to zoom in on a photo + */ + enablePinchToZoom: PropTypes.bool, + + onPress: PropTypes.func, + + onLongPress: PropTypes.func, + + delayLongPress: PropTypes.number, }; static defaultProps = { @@ -82,6 +95,7 @@ export default class Photo extends Component { thumbnail: false, lazyLoad: false, selected: false, + enablePinchToZoom: true, }; constructor(props) { @@ -92,13 +106,12 @@ export default class Photo extends Component { this._onLoad = this._onLoad.bind(this); this._toggleSelection = this._toggleSelection.bind(this); - const { lazyLoad, uri, selected } = props; + const { lazyLoad, uri } = props; this.state = { uri: lazyLoad ? null : uri, progress: 0, error: false, - selected, }; } @@ -133,8 +146,9 @@ export default class Photo extends Component { } _toggleSelection() { - this.props.onSelection(!this.state.selected); - this.setState(prevState => ({ selected: !prevState.selected})) + // onSelection is resolved in index.js + // and refreshes the dataSource with new media object + this.props.onSelection(!this.props.selected); } _renderProgressIndicator() { @@ -175,8 +189,8 @@ export default class Photo extends Component { } _renderSelectionButton() { - const { selected, progress } = this.state; - const { displaySelectionButtons, thumbnail } = this.props; + const { progress } = this.state; + const { displaySelectionButtons, selected, thumbnail } = this.props; // do not display selection before image is loaded if (!displaySelectionButtons || progress < 1) { @@ -236,19 +250,34 @@ export default class Photo extends Component { width: width || screen.width, height: height || screen.height, }; + + const props = { + ...this.props, + style: [styles.image, sizeStyle], + source, + onProgress: this._onProgress, + onError: this._onError, + onLoad: this._onLoad, + resizeMode + }; return ( {error ? this._renderErrorIcon() : this._renderProgressIndicator()} - + { + this.props.enablePinchToZoom ? + + + + + + : + + } {this._renderSelectionButton()} );