diff --git a/Examples/Movies/MovieCell.js b/Examples/Movies/MovieCell.js index 62062cb..453a0b4 100644 --- a/Examples/Movies/MovieCell.js +++ b/Examples/Movies/MovieCell.js @@ -16,6 +16,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { Image, PixelRatio, @@ -31,7 +32,7 @@ var getStyleFromScore = require('./getStyleFromScore'); var getImageSource = require('./getImageSource'); var getTextFromScore = require('./getTextFromScore'); -var MovieCell = React.createClass({ +var MovieCell = createClass({ render: function() { var criticsScore = this.props.movie.ratings.critics_score; var TouchableElement = TouchableHighlight; diff --git a/Examples/Movies/MovieScreen.js b/Examples/Movies/MovieScreen.js index 09d6544..2377106 100644 --- a/Examples/Movies/MovieScreen.js +++ b/Examples/Movies/MovieScreen.js @@ -16,6 +16,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { Image, PixelRatio, @@ -29,7 +30,7 @@ var getImageSource = require('./getImageSource'); var getStyleFromScore = require('./getStyleFromScore'); var getTextFromScore = require('./getTextFromScore'); -var MovieScreen = React.createClass({ +var MovieScreen = createClass({ render: function() { return ( @@ -63,7 +64,7 @@ var MovieScreen = React.createClass({ }, }); -var Ratings = React.createClass({ +var Ratings = createClass({ render: function() { var criticsScore = this.props.ratings.critics_score; var audienceScore = this.props.ratings.audience_score; @@ -87,7 +88,7 @@ var Ratings = React.createClass({ }, }); -var Cast = React.createClass({ +var Cast = createClass({ render: function() { if (!this.props.actors) { return null; diff --git a/Examples/Movies/MoviesApp.android.js b/Examples/Movies/MoviesApp.android.js index 5dfb57f..8103270 100644 --- a/Examples/Movies/MoviesApp.android.js +++ b/Examples/Movies/MoviesApp.android.js @@ -17,6 +17,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { AppRegistry, BackAndroid, @@ -64,7 +65,7 @@ var RouteMapper = function(route, navigationOperations, onComponentRef) { } }; -var MoviesApp = React.createClass({ +var MoviesApp = createClass({ render: function() { var initialRoute = {name: 'search'}; return ( diff --git a/Examples/Movies/MoviesApp.ios.js b/Examples/Movies/MoviesApp.ios.js index 1c6fc4b..228ad28 100644 --- a/Examples/Movies/MoviesApp.ios.js +++ b/Examples/Movies/MoviesApp.ios.js @@ -17,6 +17,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { AppRegistry, NavigatorIOS, @@ -25,7 +26,7 @@ var { var SearchScreen = require('./SearchScreen'); -var MoviesApp = React.createClass({ +var MoviesApp = createClass({ render: function() { return ( = 21; -var SearchBar = React.createClass({ +var SearchBar = createClass({ render: function() { var loadingView; if (this.props.isLoading) { diff --git a/Examples/Movies/SearchBar.ios.js b/Examples/Movies/SearchBar.ios.js index f4a2354..eecf212 100644 --- a/Examples/Movies/SearchBar.ios.js +++ b/Examples/Movies/SearchBar.ios.js @@ -17,6 +17,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { ActivityIndicatorIOS, TextInput, @@ -24,7 +25,7 @@ var { View, } = React; -var SearchBar = React.createClass({ +var SearchBar = createClass({ render: function() { return ( diff --git a/Examples/Movies/SearchBar.web.js b/Examples/Movies/SearchBar.web.js index ea524e8..5a141dd 100644 --- a/Examples/Movies/SearchBar.web.js +++ b/Examples/Movies/SearchBar.web.js @@ -17,6 +17,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { ActivityIndicatorIOS, TextInput, @@ -24,7 +25,7 @@ var { View, } = React; -var SearchBar = React.createClass({ +var SearchBar = createClass({ render: function() { return ( diff --git a/Examples/Movies/SearchScreen.js b/Examples/Movies/SearchScreen.js index 1633e02..7af85a9 100644 --- a/Examples/Movies/SearchScreen.js +++ b/Examples/Movies/SearchScreen.js @@ -16,6 +16,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { ActivityIndicatorIOS, ListView, @@ -59,7 +60,7 @@ var resultsCache = { var LOADING = {}; -var SearchScreen = React.createClass({ +var SearchScreen = createClass({ mixins: [TimerMixin], timeoutID: (null: any), @@ -335,7 +336,7 @@ var SearchScreen = React.createClass({ }, }); -var NoMovies = React.createClass({ +var NoMovies = createClass({ render: function() { var text = ''; if (this.props.filter) { diff --git a/Examples/TicTacToe/TicTacToeApp.js b/Examples/TicTacToe/TicTacToeApp.js index 51f7e67..466c018 100644 --- a/Examples/TicTacToe/TicTacToeApp.js +++ b/Examples/TicTacToe/TicTacToeApp.js @@ -17,6 +17,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { AppRegistry, StyleSheet, @@ -95,7 +96,7 @@ class Board { } } -var Cell = React.createClass({ +var Cell = createClass({ cellStyle() { switch (this.props.player) { case 1: @@ -145,7 +146,7 @@ var Cell = React.createClass({ } }); -var GameEndOverlay = React.createClass({ +var GameEndOverlay = createClass({ render() { var board = this.props.board; @@ -178,7 +179,7 @@ var GameEndOverlay = React.createClass({ } }); -var TicTacToeApp = React.createClass({ +var TicTacToeApp = createClass({ getInitialState() { return { board: new Board(), player: 1 }; }, diff --git a/Examples/UIExplorer/AccessibilityAndroidExample.android.js b/Examples/UIExplorer/AccessibilityAndroidExample.android.js index 3df94c6..69b565c 100644 --- a/Examples/UIExplorer/AccessibilityAndroidExample.android.js +++ b/Examples/UIExplorer/AccessibilityAndroidExample.android.js @@ -15,6 +15,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { StyleSheet, Text, @@ -28,7 +29,7 @@ var UIExplorerPage = require('./UIExplorerPage'); var importantForAccessibilityValues = ['auto', 'yes', 'no', 'no-hide-descendants']; -var AccessibilityAndroidExample = React.createClass({ +var AccessibilityAndroidExample = createClass({ statics: { title: 'Accessibility', diff --git a/Examples/UIExplorer/AccessibilityIOSExample.js b/Examples/UIExplorer/AccessibilityIOSExample.js index d543d72..42e1a58 100644 --- a/Examples/UIExplorer/AccessibilityIOSExample.js +++ b/Examples/UIExplorer/AccessibilityIOSExample.js @@ -16,12 +16,13 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { Text, View, } = React; -var AccessibilityIOSExample = React.createClass({ +var AccessibilityIOSExample = createClass({ render() { return ( diff --git a/Examples/UIExplorer/ActionSheetIOSExample.js b/Examples/UIExplorer/ActionSheetIOSExample.js index 31aedca..44225ca 100644 --- a/Examples/UIExplorer/ActionSheetIOSExample.js +++ b/Examples/UIExplorer/ActionSheetIOSExample.js @@ -16,6 +16,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { ActionSheetIOS, StyleSheet, @@ -33,7 +34,7 @@ var BUTTONS = [ var DESTRUCTIVE_INDEX = 3; var CANCEL_INDEX = 4; -var ActionSheetExample = React.createClass({ +var ActionSheetExample = createClass({ getInitialState() { return { clicked: 'none', @@ -65,7 +66,7 @@ var ActionSheetExample = React.createClass({ } }); -var ShareActionSheetExample = React.createClass({ +var ShareActionSheetExample = createClass({ getInitialState() { return { text: '' diff --git a/Examples/UIExplorer/ActivityIndicatorIOSExample.js b/Examples/UIExplorer/ActivityIndicatorIOSExample.js index 9655d68..135836a 100644 --- a/Examples/UIExplorer/ActivityIndicatorIOSExample.js +++ b/Examples/UIExplorer/ActivityIndicatorIOSExample.js @@ -16,6 +16,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { ActivityIndicatorIOS, StyleSheet, @@ -23,7 +24,7 @@ var { } = React; var TimerMixin = require('react-timer-mixin'); -var ToggleAnimatingActivityIndicator = React.createClass({ +var ToggleAnimatingActivityIndicator = createClass({ mixins: [TimerMixin], getInitialState: function() { diff --git a/Examples/UIExplorer/AdSupportIOSExample.js b/Examples/UIExplorer/AdSupportIOSExample.js index 1626249..b05f24a 100644 --- a/Examples/UIExplorer/AdSupportIOSExample.js +++ b/Examples/UIExplorer/AdSupportIOSExample.js @@ -16,6 +16,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { AdSupportIOS, StyleSheet, @@ -36,7 +37,7 @@ exports.examples = [ } ]; -var AdSupportIOSExample = React.createClass({ +var AdSupportIOSExample = createClass({ getInitialState: function() { return { deviceID: 'No IDFA yet', diff --git a/Examples/UIExplorer/AppStateIOSExample.js b/Examples/UIExplorer/AppStateIOSExample.js index 9cf15fa..5f07dc0 100644 --- a/Examples/UIExplorer/AppStateIOSExample.js +++ b/Examples/UIExplorer/AppStateIOSExample.js @@ -17,13 +17,14 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { AppStateIOS, Text, View } = React; -var AppStateSubscription = React.createClass({ +var AppStateSubscription = createClass({ getInitialState() { return { appState: AppStateIOS.currentState, diff --git a/Examples/UIExplorer/AssetScaledImageExample.js b/Examples/UIExplorer/AssetScaledImageExample.js index dbfe7af..ec963b5 100644 --- a/Examples/UIExplorer/AssetScaledImageExample.js +++ b/Examples/UIExplorer/AssetScaledImageExample.js @@ -16,6 +16,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { Image, StyleSheet, @@ -23,7 +24,7 @@ var { ScrollView } = React; -var AssetScaledImageExample = React.createClass({ +var AssetScaledImageExample = createClass({ getInitialState() { return { diff --git a/Examples/UIExplorer/AsyncStorageExample.js b/Examples/UIExplorer/AsyncStorageExample.js index 8a6a45b..1ea2740 100644 --- a/Examples/UIExplorer/AsyncStorageExample.js +++ b/Examples/UIExplorer/AsyncStorageExample.js @@ -16,6 +16,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { AsyncStorage, PickerIOS, @@ -27,7 +28,7 @@ var PickerItemIOS = PickerIOS.Item; var STORAGE_KEY = '@AsyncStorageExample:key'; var COLORS = ['red', 'orange', 'yellow', 'green', 'blue']; -var BasicStorageExample = React.createClass({ +var BasicStorageExample = createClass({ componentDidMount() { this._loadInitialState().done(); }, diff --git a/Examples/UIExplorer/CameraRollExample.ios.js b/Examples/UIExplorer/CameraRollExample.ios.js index d783d9d..3756e07 100644 --- a/Examples/UIExplorer/CameraRollExample.ios.js +++ b/Examples/UIExplorer/CameraRollExample.ios.js @@ -16,6 +16,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { CameraRoll, Image, @@ -32,7 +33,7 @@ var AssetScaledImageExampleView = require('./AssetScaledImageExample'); var CAMERA_ROLL_VIEW = 'camera_roll_view'; -var CameraRollExample = React.createClass({ +var CameraRollExample = createClass({ getInitialState() { return { diff --git a/Examples/UIExplorer/CameraRollView.ios.js b/Examples/UIExplorer/CameraRollView.ios.js index 74507aa..072a5c0 100644 --- a/Examples/UIExplorer/CameraRollView.ios.js +++ b/Examples/UIExplorer/CameraRollView.ios.js @@ -17,6 +17,8 @@ 'use strict'; var React = require('react-native'); +var PropTypes = require('prop-types'); +var createClass = require('create-react-class') var { ActivityIndicatorIOS, CameraRoll, @@ -35,7 +37,7 @@ var propTypes = { * values are 'Album', 'All', 'Event', 'Faces', 'Library', 'PhotoStream' * and SavedPhotos. */ - groupTypes: React.PropTypes.oneOf([ + groupTypes: PropTypes.oneOf([ 'Album', 'All', 'Event', @@ -48,22 +50,22 @@ var propTypes = { /** * Number of images that will be fetched in one page. */ - batchSize: React.PropTypes.number, + batchSize: PropTypes.number, /** * A function that takes a single image as a parameter and renders it. */ - renderImage: React.PropTypes.func, + renderImage: PropTypes.func, /** * imagesPerRow: Number of images to be shown in each row. */ - imagesPerRow: React.PropTypes.number, + imagesPerRow: PropTypes.number, /** * The asset type, one of 'Photos', 'Videos' or 'All' */ - assetType: React.PropTypes.oneOf([ + assetType: PropTypes.oneOf([ 'Photos', 'Videos', 'All', @@ -71,7 +73,7 @@ var propTypes = { }; -var CameraRollView = React.createClass({ +var CameraRollView = createClass({ propTypes: propTypes, getDefaultProps: function(): Object { diff --git a/Examples/UIExplorer/DatePickerIOSExample.js b/Examples/UIExplorer/DatePickerIOSExample.js index fc76868..642c64e 100644 --- a/Examples/UIExplorer/DatePickerIOSExample.js +++ b/Examples/UIExplorer/DatePickerIOSExample.js @@ -16,6 +16,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { DatePickerIOS, StyleSheet, @@ -24,7 +25,7 @@ var { View, } = React; -var DatePickerExample = React.createClass({ +var DatePickerExample = createClass({ getDefaultProps: function () { return { date: new Date(), @@ -98,7 +99,7 @@ var DatePickerExample = React.createClass({ }, }); -var WithLabel = React.createClass({ +var WithLabel = createClass({ render: function() { return ( @@ -113,7 +114,7 @@ var WithLabel = React.createClass({ } }); -var Heading = React.createClass({ +var Heading = createClass({ render: function() { return ( diff --git a/Examples/UIExplorer/DrawerLayoutExample.web.js b/Examples/UIExplorer/DrawerLayoutExample.web.js index 731009c..b933fce 100644 --- a/Examples/UIExplorer/DrawerLayoutExample.web.js +++ b/Examples/UIExplorer/DrawerLayoutExample.web.js @@ -1,6 +1,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { AppRegistry, StyleSheet, @@ -9,7 +10,7 @@ var { DrawerLayoutAndroid, } = React; -var DrawerLayoutExample = React.createClass({ +var DrawerLayoutExample = createClass({ statics: { title: '', description: 'DrawerLayout', diff --git a/Examples/UIExplorer/GeolocationExample.js b/Examples/UIExplorer/GeolocationExample.js index d9dd4e8..20da092 100644 --- a/Examples/UIExplorer/GeolocationExample.js +++ b/Examples/UIExplorer/GeolocationExample.js @@ -18,6 +18,7 @@ var React = require('react-native'); +var createClass = require('create-react-class'); var { StyleSheet, Text, @@ -37,7 +38,7 @@ exports.examples = [ } ]; -var GeolocationExample = React.createClass({ +var GeolocationExample = createClass({ watchID: (null: ?number), getInitialState: function() { diff --git a/Examples/UIExplorer/ImageCapInsetsExample.js b/Examples/UIExplorer/ImageCapInsetsExample.js index 8e17ac9..0083eb8 100644 --- a/Examples/UIExplorer/ImageCapInsetsExample.js +++ b/Examples/UIExplorer/ImageCapInsetsExample.js @@ -17,6 +17,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { Image, StyleSheet, @@ -24,7 +25,7 @@ var { View, } = React; -var ImageCapInsetsExample = React.createClass({ +var ImageCapInsetsExample = createClass({ render: function() { return ( diff --git a/Examples/UIExplorer/ImageExample.js b/Examples/UIExplorer/ImageExample.js index b29d7b9..d8cf2e9 100644 --- a/Examples/UIExplorer/ImageExample.js +++ b/Examples/UIExplorer/ImageExample.js @@ -16,6 +16,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { Image, StyleSheet, @@ -28,7 +29,7 @@ var base64Icon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAABLCAQAAACS // var ImageCapInsetsExample = require('./ImageCapInsetsExample'); -var NetworkImageExample = React.createClass({ +var NetworkImageExample = createClass({ watchID: (null: ?number), getInitialState: function() { diff --git a/Examples/UIExplorer/LayoutEventsExample.js b/Examples/UIExplorer/LayoutEventsExample.js index 368cee8..66ffe45 100644 --- a/Examples/UIExplorer/LayoutEventsExample.js +++ b/Examples/UIExplorer/LayoutEventsExample.js @@ -16,6 +16,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { Image, StyleSheet, @@ -34,7 +35,7 @@ type LayoutEvent = { }; }; -var LayoutEventExample = React.createClass({ +var LayoutEventExample = createClass({ getInitialState: function() { return { viewStyle: { diff --git a/Examples/UIExplorer/LayoutExample.js b/Examples/UIExplorer/LayoutExample.js index ef9b1c6..eb4fc86 100644 --- a/Examples/UIExplorer/LayoutExample.js +++ b/Examples/UIExplorer/LayoutExample.js @@ -16,6 +16,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { StyleSheet, Text, @@ -25,7 +26,7 @@ var { var UIExplorerBlock = require('./UIExplorerBlock'); var UIExplorerPage = require('./UIExplorerPage'); -var Circle = React.createClass({ +var Circle = createClass({ render: function() { var size = this.props.size || 20; return ( @@ -42,7 +43,7 @@ var Circle = React.createClass({ } }); -var CircleBlock = React.createClass({ +var CircleBlock = createClass({ render: function() { var circleStyle = { flexDirection: 'row', @@ -59,7 +60,7 @@ var CircleBlock = React.createClass({ } }); -var LayoutExample = React.createClass({ +var LayoutExample = createClass({ statics: { title: 'Layout - Flexbox', description: 'Examples of using the flexbox API to layout views.', diff --git a/Examples/UIExplorer/ListViewExample.js b/Examples/UIExplorer/ListViewExample.js index 4b711a0..808f7a2 100644 --- a/Examples/UIExplorer/ListViewExample.js +++ b/Examples/UIExplorer/ListViewExample.js @@ -16,6 +16,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { Image, ListView, @@ -27,7 +28,7 @@ var { var UIExplorerPage = require('./UIExplorerPage'); -var ListViewSimpleExample = React.createClass({ +var ListViewSimpleExample = createClass({ statics: { title: ' - Simple', description: 'Performant, scrollable list of data.' diff --git a/Examples/UIExplorer/ListViewGridLayoutExample.js b/Examples/UIExplorer/ListViewGridLayoutExample.js index 272bdde..7b96c2f 100644 --- a/Examples/UIExplorer/ListViewGridLayoutExample.js +++ b/Examples/UIExplorer/ListViewGridLayoutExample.js @@ -16,6 +16,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { Image, ListView, @@ -40,7 +41,7 @@ var THUMB_URLS = [ 'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-prn1/t39.1997/p128x128/851583_767334573292952_1519550680_n.png', ]; -var ListViewGridLayoutExample = React.createClass({ +var ListViewGridLayoutExample = createClass({ statics: { title: ' - Grid Layout', diff --git a/Examples/UIExplorer/ListViewPagingExample.js b/Examples/UIExplorer/ListViewPagingExample.js index f51d299..1871490 100644 --- a/Examples/UIExplorer/ListViewPagingExample.js +++ b/Examples/UIExplorer/ListViewPagingExample.js @@ -16,6 +16,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { Image, // LayoutAnimation, @@ -35,7 +36,7 @@ var THUMB_URLS = ['https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-ash3/t39.1997/ var NUM_SECTIONS = 100; var NUM_ROWS_PER_SECTION = 10; -var Thumb = React.createClass({ +var Thumb = createClass({ getInitialState: function() { return {thumbIndex: this._getThumbIdx(), dir: 'row'}; }, @@ -74,7 +75,7 @@ var Thumb = React.createClass({ } }); -var ListViewPagingExample = React.createClass({ +var ListViewPagingExample = createClass({ statics: { title: ' - Paging', description: 'Floating headers & layout animations.' diff --git a/Examples/UIExplorer/MapViewExample.js b/Examples/UIExplorer/MapViewExample.js index 5720175..9e37c01 100644 --- a/Examples/UIExplorer/MapViewExample.js +++ b/Examples/UIExplorer/MapViewExample.js @@ -16,6 +16,8 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); +var PropTypes = require('prop-types'); var { MapView, StyleSheet, @@ -31,16 +33,16 @@ var regionText = { longitudeDelta: '0', }; -var MapRegionInput = React.createClass({ +var MapRegionInput = createClass({ propTypes: { - region: React.PropTypes.shape({ - latitude: React.PropTypes.number.isRequired, - longitude: React.PropTypes.number.isRequired, - latitudeDelta: React.PropTypes.number.isRequired, - longitudeDelta: React.PropTypes.number.isRequired, + region: PropTypes.shape({ + latitude: PropTypes.number.isRequired, + longitude: PropTypes.number.isRequired, + latitudeDelta: PropTypes.number.isRequired, + longitudeDelta: PropTypes.number.isRequired, }), - onChange: React.PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, }, getInitialState: function() { @@ -145,7 +147,7 @@ var MapRegionInput = React.createClass({ }); -var MapViewExample = React.createClass({ +var MapViewExample = createClass({ getInitialState() { return { diff --git a/Examples/UIExplorer/ModalExample.js b/Examples/UIExplorer/ModalExample.js index 3202cb6..8547c8e 100644 --- a/Examples/UIExplorer/ModalExample.js +++ b/Examples/UIExplorer/ModalExample.js @@ -16,6 +16,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { Modal, StyleSheet, @@ -30,7 +31,7 @@ exports.framework = 'React'; exports.title = ''; exports.description = 'Component for presenting modal views.'; -var Button = React.createClass({ +var Button = createClass({ getInitialState() { return { active: false, @@ -62,7 +63,7 @@ var Button = React.createClass({ } }); -var ModalExample = React.createClass({ +var ModalExample = createClass({ getInitialState() { return { animated: true, diff --git a/Examples/UIExplorer/Navigator/BreadcrumbNavSample.js b/Examples/UIExplorer/Navigator/BreadcrumbNavSample.js index 62196e5..81405b0 100644 --- a/Examples/UIExplorer/Navigator/BreadcrumbNavSample.js +++ b/Examples/UIExplorer/Navigator/BreadcrumbNavSample.js @@ -14,6 +14,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { PixelRatio, Navigator, @@ -43,7 +44,7 @@ class NavButton extends React.Component { } } -var BreadcrumbNavSample = React.createClass({ +var BreadcrumbNavSample = createClass({ componentWillMount: function() { this._navBarRouteMapper = { diff --git a/Examples/UIExplorer/Navigator/JumpingNavSample.js b/Examples/UIExplorer/Navigator/JumpingNavSample.js index 707ea4f..826badd 100644 --- a/Examples/UIExplorer/Navigator/JumpingNavSample.js +++ b/Examples/UIExplorer/Navigator/JumpingNavSample.js @@ -14,6 +14,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { Navigator, PixelRatio, @@ -101,7 +102,7 @@ class JumpingNavBar extends React.Component { } } -var JumpingNavSample = React.createClass({ +var JumpingNavSample = createClass({ render: function() { return ( ', diff --git a/Examples/UIExplorer/NavigatorIOSColorsExample.js b/Examples/UIExplorer/NavigatorIOSColorsExample.js index 4078cac..e9788cd 100644 --- a/Examples/UIExplorer/NavigatorIOSColorsExample.js +++ b/Examples/UIExplorer/NavigatorIOSColorsExample.js @@ -14,6 +14,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { NavigatorIOS, StatusBarIOS, @@ -22,7 +23,7 @@ var { View } = React; -var EmptyPage = React.createClass({ +var EmptyPage = createClass({ render: function() { return ( @@ -36,7 +37,7 @@ var EmptyPage = React.createClass({ }); -var NavigatorIOSColors = React.createClass({ +var NavigatorIOSColors = createClass({ statics: { title: ' - Custom', diff --git a/Examples/UIExplorer/NavigatorIOSExample.js b/Examples/UIExplorer/NavigatorIOSExample.js index 4a2011a..0dd0c53 100644 --- a/Examples/UIExplorer/NavigatorIOSExample.js +++ b/Examples/UIExplorer/NavigatorIOSExample.js @@ -16,6 +16,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var ViewExample = require('./ViewExample'); var createExamplePage = require('./createExamplePage'); var { @@ -28,7 +29,7 @@ var { View, } = React; -var EmptyPage = React.createClass({ +var EmptyPage = createClass({ render: function() { return ( @@ -42,7 +43,7 @@ var EmptyPage = React.createClass({ }); -var NavigatorIOSExample = React.createClass({ +var NavigatorIOSExample = createClass({ statics: { title: '', diff --git a/Examples/UIExplorer/NetInfoExample.js b/Examples/UIExplorer/NetInfoExample.js index 6ab1805..302e9f3 100644 --- a/Examples/UIExplorer/NetInfoExample.js +++ b/Examples/UIExplorer/NetInfoExample.js @@ -16,13 +16,14 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { NetInfo, Text, View } = React; -var ReachabilitySubscription = React.createClass({ +var ReachabilitySubscription = createClass({ getInitialState() { return { reachabilityHistory: [], @@ -56,7 +57,7 @@ var ReachabilitySubscription = React.createClass({ } }); -var ReachabilityCurrent = React.createClass({ +var ReachabilityCurrent = createClass({ getInitialState() { return { reachability: null, @@ -91,7 +92,7 @@ var ReachabilityCurrent = React.createClass({ } }); -var IsConnected = React.createClass({ +var IsConnected = createClass({ getInitialState() { return { isConnected: null, diff --git a/Examples/UIExplorer/PanResponderExample.js b/Examples/UIExplorer/PanResponderExample.js index dc96b53..8a6af64 100644 --- a/Examples/UIExplorer/PanResponderExample.js +++ b/Examples/UIExplorer/PanResponderExample.js @@ -16,6 +16,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { PanResponder, StyleSheet, @@ -28,7 +29,7 @@ var CIRCLE_SIZE = 80; var CIRCLE_COLOR = 'blue'; var CIRCLE_HIGHLIGHT_COLOR = 'green'; -var PanResponderExample = React.createClass({ +var PanResponderExample = createClass({ statics: { title: 'PanResponder Sample', diff --git a/Examples/UIExplorer/PickerIOSExample.js b/Examples/UIExplorer/PickerIOSExample.js index 31c81cc..37a9d09 100644 --- a/Examples/UIExplorer/PickerIOSExample.js +++ b/Examples/UIExplorer/PickerIOSExample.js @@ -16,6 +16,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { PickerIOS, Text, @@ -65,7 +66,7 @@ var CAR_MAKES_AND_MODELS = { }, }; -var PickerExample = React.createClass({ +var PickerExample = createClass({ getInitialState: function() { return { carMake: 'cadillac', diff --git a/Examples/UIExplorer/PointerEventsExample.js b/Examples/UIExplorer/PointerEventsExample.js index 735a595..1ce2339 100644 --- a/Examples/UIExplorer/PointerEventsExample.js +++ b/Examples/UIExplorer/PointerEventsExample.js @@ -16,13 +16,14 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { StyleSheet, Text, View, } = React; -var ExampleBox = React.createClass({ +var ExampleBox = createClass({ getInitialState: function() { return { log: [], @@ -61,7 +62,7 @@ var ExampleBox = React.createClass({ }); -var NoneExample = React.createClass({ +var NoneExample = createClass({ render: function() { return ( @@ -107,7 +108,7 @@ var DemoText = React.createClass({ } }); -var BoxNoneExample = React.createClass({ +var BoxNoneExample = createClass({ render: function() { return ( ', diff --git a/Examples/UIExplorer/ProgressViewIOSExample.js b/Examples/UIExplorer/ProgressViewIOSExample.js index 0020d64..54053ae 100644 --- a/Examples/UIExplorer/ProgressViewIOSExample.js +++ b/Examples/UIExplorer/ProgressViewIOSExample.js @@ -16,6 +16,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { ProgressViewIOS, StyleSheet, @@ -23,7 +24,7 @@ var { } = React; var TimerMixin = require('react-timer-mixin'); -var ProgressViewExample = React.createClass({ +var ProgressViewExample = createClass({ mixins: [TimerMixin], getInitialState() { diff --git a/Examples/UIExplorer/PushNotificationIOSExample.js b/Examples/UIExplorer/PushNotificationIOSExample.js index bd6109f..4c1f17d 100644 --- a/Examples/UIExplorer/PushNotificationIOSExample.js +++ b/Examples/UIExplorer/PushNotificationIOSExample.js @@ -16,6 +16,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { AlertIOS, PushNotificationIOS, @@ -25,7 +26,7 @@ var { View, } = React; -var Button = React.createClass({ +var Button = createClass({ render: function() { return ( ', description: 'Component that enables scrolling through child components.' diff --git a/Examples/UIExplorer/SegmentedControlIOSExample.js b/Examples/UIExplorer/SegmentedControlIOSExample.js index a0794d1..2cf8013 100644 --- a/Examples/UIExplorer/SegmentedControlIOSExample.js +++ b/Examples/UIExplorer/SegmentedControlIOSExample.js @@ -16,6 +16,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { SegmentedControlIOS, Text, @@ -23,7 +24,7 @@ var { StyleSheet } = React; -var BasicSegmentedControlExample = React.createClass({ +var BasicSegmentedControlExample = createClass({ render() { return ( @@ -38,7 +39,7 @@ var BasicSegmentedControlExample = React.createClass({ } }); -var PreSelectedSegmentedControlExample = React.createClass({ +var PreSelectedSegmentedControlExample = createClass({ render() { return ( @@ -50,7 +51,7 @@ var PreSelectedSegmentedControlExample = React.createClass({ } }); -var MomentarySegmentedControlExample = React.createClass({ +var MomentarySegmentedControlExample = createClass({ render() { return ( @@ -62,7 +63,7 @@ var MomentarySegmentedControlExample = React.createClass({ } }); -var DisabledSegmentedControlExample = React.createClass({ +var DisabledSegmentedControlExample = createClass({ render() { return ( @@ -74,7 +75,7 @@ var DisabledSegmentedControlExample = React.createClass({ }, }); -var ColorSegmentedControlExample = React.createClass({ +var ColorSegmentedControlExample = createClass({ render() { return ( @@ -89,7 +90,7 @@ var ColorSegmentedControlExample = React.createClass({ }, }); -var EventSegmentedControlExample = React.createClass({ +var EventSegmentedControlExample = createClass({ getInitialState() { return { values: ['One', 'Two', 'Three'], diff --git a/Examples/UIExplorer/SliderIOSExample.js b/Examples/UIExplorer/SliderIOSExample.js index f19b71e..c5ff477 100644 --- a/Examples/UIExplorer/SliderIOSExample.js +++ b/Examples/UIExplorer/SliderIOSExample.js @@ -16,6 +16,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { SliderIOS, Text, @@ -23,7 +24,7 @@ var { View, } = React; -var SliderExample = React.createClass({ +var SliderExample = createClass({ getInitialState() { return { value: 0.2, diff --git a/Examples/UIExplorer/SwitchAndroidExample.android.js b/Examples/UIExplorer/SwitchAndroidExample.android.js index 7b6f38e..6ef2cc8 100644 --- a/Examples/UIExplorer/SwitchAndroidExample.android.js +++ b/Examples/UIExplorer/SwitchAndroidExample.android.js @@ -4,13 +4,14 @@ 'use strict'; var React = require('React'); +var createClass = require('create-react-class'); var SwitchAndroid = require('SwitchAndroid'); var Text = require('Text'); var UIExplorerBlock = require('UIExplorerBlock'); var UIExplorerPage = require('UIExplorerPage'); -var SwitchAndroidExample = React.createClass({ +var SwitchAndroidExample = createClass({ statics: { title: '', description: 'Standard Android two-state toggle component.' diff --git a/Examples/UIExplorer/SwitchIOSExample.js b/Examples/UIExplorer/SwitchIOSExample.js index feedfbf..ecc4049 100644 --- a/Examples/UIExplorer/SwitchIOSExample.js +++ b/Examples/UIExplorer/SwitchIOSExample.js @@ -16,13 +16,14 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { SwitchIOS, Text, View } = React; -var BasicSwitchExample = React.createClass({ +var BasicSwitchExample = createClass({ getInitialState() { return { trueSwitchIsOn: true, @@ -44,7 +45,7 @@ var BasicSwitchExample = React.createClass({ } }); -var DisabledSwitchExample = React.createClass({ +var DisabledSwitchExample = createClass({ render() { return ( @@ -60,7 +61,7 @@ var DisabledSwitchExample = React.createClass({ }, }); -var ColorSwitchExample = React.createClass({ +var ColorSwitchExample = createClass({ getInitialState() { return { colorTrueSwitchIsOn: true, @@ -88,7 +89,7 @@ var ColorSwitchExample = React.createClass({ }, }); -var EventSwitchExample = React.createClass({ +var EventSwitchExample = createClass({ getInitialState() { return { eventSwitchIsOn: false, diff --git a/Examples/UIExplorer/TabBarIOSExample.js b/Examples/UIExplorer/TabBarIOSExample.js index eeec28e..8a399be 100644 --- a/Examples/UIExplorer/TabBarIOSExample.js +++ b/Examples/UIExplorer/TabBarIOSExample.js @@ -16,6 +16,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { StyleSheet, TabBarIOS, @@ -25,7 +26,7 @@ var { var base64Icon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAABLCAQAAACSR7JhAAADtUlEQVR4Ac3YA2Bj6QLH0XPT1Fzbtm29tW3btm3bfLZtv7e2ObZnms7d8Uw098tuetPzrxv8wiISrtVudrG2JXQZ4VOv+qUfmqCGGl1mqLhoA52oZlb0mrjsnhKpgeUNEs91Z0pd1kvihA3ULGVHiQO2narKSHKkEMulm9VgUyE60s1aWoMQUbpZOWE+kaqs4eLEjdIlZTcFZB0ndc1+lhB1lZrIuk5P2aib1NBpZaL+JaOGIt0ls47SKzLC7CqrlGF6RZ09HGoNy1lYl2aRSWL5GuzqWU1KafRdoRp0iOQEiDzgZPnG6DbldcomadViflnl/cL93tOoVbsOLVM2jylvdWjXolWX1hmfZbGR/wjypDjFLSZIRov09BgYmtUqPQPlQrPapecLgTIy0jMgPKtTeob2zWtrGH3xvjUkPCtNg/tm1rjwrMa+mdUkPd3hWbH0jArPGiU9ufCsNNWFZ40wpwn+62/66R2RUtoso1OB34tnLOcy7YB1fUdc9e0q3yru8PGM773vXsuZ5YIZX+5xmHwHGVvlrGPN6ZSiP1smOsMMde40wKv2VmwPPVXNut4sVpUreZiLBHi0qln/VQeI/LTMYXpsJtFiclUN+5HVZazim+Ky+7sAvxWnvjXrJFneVtLWLyPJu9K3cXLWeOlbMTlrIelbMDlrLenrjEQOtIF+fuI9xRp9ZBFp6+b6WT8RrxEpdK64BuvHgDk+vUy+b5hYk6zfyfs051gRoNO1usU12WWRWL73/MMEy9pMi9qIrR4ZpV16Rrvduxazmy1FSvuFXRkqTnE7m2kdb5U8xGjLw/spRr1uTov4uOgQE+0N/DvFrG/Jt7i/FzwxbA9kDanhf2w+t4V97G8lrT7wc08aA2QNUkuTfW/KimT01wdlfK4yEw030VfT0RtZbzjeMprNq8m8tnSTASrTLti64oBNdpmMQm0eEwvfPwRbUBywG5TzjPCsdwk3IeAXjQblLCoXnDVeoAz6SfJNk5TTzytCNZk/POtTSV40NwOFWzw86wNJRpubpXsn60NJFlHeqlYRbslqZm2jnEZ3qcSKgm0kTli3zZVS7y/iivZTweYXJ26Y+RTbV1zh3hYkgyFGSTKPfRVbRqWWVReaxYeSLarYv1Qqsmh1s95S7G+eEWK0f3jYKTbV6bOwepjfhtafsvUsqrQvrGC8YhmnO9cSCk3yuY984F1vesdHYhWJ5FvASlacshUsajFt2mUM9pqzvKGcyNJW0arTKN1GGGzQlH0tXwLDgQTurS8eIQAAAABJRU5ErkJggg=='; -var TabBarExample = React.createClass({ +var TabBarExample = createClass({ statics: { title: '', description: 'Tab-based navigation.', diff --git a/Examples/UIExplorer/TextExample.android.js b/Examples/UIExplorer/TextExample.android.js index 42d17ed..401661f 100644 --- a/Examples/UIExplorer/TextExample.android.js +++ b/Examples/UIExplorer/TextExample.android.js @@ -16,6 +16,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { StyleSheet, Text, @@ -24,7 +25,7 @@ var { var UIExplorerBlock = require('./UIExplorerBlock'); var UIExplorerPage = require('./UIExplorerPage'); -var Entity = React.createClass({ +var Entity = createClass({ render: function() { return ( @@ -34,7 +35,7 @@ var Entity = React.createClass({ } }); -var AttributeToggler = React.createClass({ +var AttributeToggler = createClass({ getInitialState: function() { return {fontWeight: 'bold', fontSize: 15}; }, @@ -70,7 +71,7 @@ var AttributeToggler = React.createClass({ } }); -var TextExample = React.createClass({ +var TextExample = createClass({ statics: { title: '', description: 'Base component for rendering styled text.', diff --git a/Examples/UIExplorer/TextExample.ios.js b/Examples/UIExplorer/TextExample.ios.js index 23111f8..47562f9 100644 --- a/Examples/UIExplorer/TextExample.ios.js +++ b/Examples/UIExplorer/TextExample.ios.js @@ -16,6 +16,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { Image, StyleSheet, @@ -23,7 +24,7 @@ var { View, } = React; -var Entity = React.createClass({ +var Entity = createClass({ render: function() { return ( @@ -33,7 +34,7 @@ var Entity = React.createClass({ } }); -var AttributeToggler = React.createClass({ +var AttributeToggler = createClass({ getInitialState: function() { return {fontWeight: 'bold', fontSize: 15}; }, diff --git a/Examples/UIExplorer/TextInputExample.android.js b/Examples/UIExplorer/TextInputExample.android.js index b70f2d9..b026a7e 100644 --- a/Examples/UIExplorer/TextInputExample.android.js +++ b/Examples/UIExplorer/TextInputExample.android.js @@ -16,6 +16,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { Text, TextInput, @@ -23,7 +24,7 @@ var { StyleSheet, } = React; -var TextEventsExample = React.createClass({ +var TextEventsExample = createClass({ getInitialState: function() { return { curText: '', diff --git a/Examples/UIExplorer/TextInputExample.ios.js b/Examples/UIExplorer/TextInputExample.ios.js index d51a95e..e29cb9d 100644 --- a/Examples/UIExplorer/TextInputExample.ios.js +++ b/Examples/UIExplorer/TextInputExample.ios.js @@ -16,6 +16,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { Text, TextInput, @@ -23,7 +24,7 @@ var { StyleSheet, } = React; -var WithLabel = React.createClass({ +var WithLabel = createClass({ render: function() { return ( @@ -36,7 +37,7 @@ var WithLabel = React.createClass({ }, }); -var TextEventsExample = React.createClass({ +var TextEventsExample = createClass({ getInitialState: function() { return { curText: '', diff --git a/Examples/UIExplorer/TimerExample.js b/Examples/UIExplorer/TimerExample.js index 55272a8..6bdd2c1 100644 --- a/Examples/UIExplorer/TimerExample.js +++ b/Examples/UIExplorer/TimerExample.js @@ -16,6 +16,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { AlertIOS, Platform, @@ -25,7 +26,7 @@ var { var TimerMixin = require('react-timer-mixin'); var UIExplorerButton = require('./UIExplorerButton'); -var TimerTester = React.createClass({ +var TimerTester = createClass({ mixins: [TimerMixin], _ii: 0, @@ -149,7 +150,7 @@ exports.examples = [ description: 'Execute function fn every t milliseconds until cancelled ' + 'or component is unmounted.', render: function(): ReactElement { - var IntervalExample = React.createClass({ + var IntervalExample = createClass({ getInitialState: function() { return { showTimer: true, diff --git a/Examples/UIExplorer/ToastAndroidExample.android.js b/Examples/UIExplorer/ToastAndroidExample.android.js index 7f9cedf..b414249 100644 --- a/Examples/UIExplorer/ToastAndroidExample.android.js +++ b/Examples/UIExplorer/ToastAndroidExample.android.js @@ -17,6 +17,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { StyleSheet, Text, @@ -27,7 +28,7 @@ var { var UIExplorerBlock = require('UIExplorerBlock'); var UIExplorerPage = require('UIExplorerPage'); -var ToastExample = React.createClass({ +var ToastExample = createClass({ statics: { title: 'Toast Example', diff --git a/Examples/UIExplorer/ToolbarAndroidExample.android.js b/Examples/UIExplorer/ToolbarAndroidExample.android.js index 769737a..2285f0b 100644 --- a/Examples/UIExplorer/ToolbarAndroidExample.android.js +++ b/Examples/UIExplorer/ToolbarAndroidExample.android.js @@ -16,6 +16,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { StyleSheet, Text, @@ -27,7 +28,7 @@ var UIExplorerPage = require('./UIExplorerPage'); var SwitchAndroid = require('SwitchAndroid'); var ToolbarAndroid = require('ToolbarAndroid'); -var ToolbarAndroidExample = React.createClass({ +var ToolbarAndroidExample = createClass({ statics: { title: '', description: 'Examples of using the Android toolbar.' diff --git a/Examples/UIExplorer/TouchableExample.js b/Examples/UIExplorer/TouchableExample.js index f8ced26..5fe47b1 100644 --- a/Examples/UIExplorer/TouchableExample.js +++ b/Examples/UIExplorer/TouchableExample.js @@ -16,6 +16,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { PixelRatio, Image, @@ -87,7 +88,7 @@ exports.examples = [ }, }]; -var TextOnPressBox = React.createClass({ +var TextOnPressBox = createClass({ getInitialState: function() { return { timesPressed: 0, @@ -123,7 +124,7 @@ var TextOnPressBox = React.createClass({ } }); -var TouchableFeedbackEvents = React.createClass({ +var TouchableFeedbackEvents = createClass({ getInitialState: function() { return { eventLog: [], @@ -159,7 +160,7 @@ var TouchableFeedbackEvents = React.createClass({ }, }); -var TouchableDelayEvents = React.createClass({ +var TouchableDelayEvents = createClass({ getInitialState: function() { return { eventLog: [], diff --git a/Examples/UIExplorer/TransformExample.js b/Examples/UIExplorer/TransformExample.js index 7da9eb5..9dcd909 100644 --- a/Examples/UIExplorer/TransformExample.js +++ b/Examples/UIExplorer/TransformExample.js @@ -14,6 +14,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { Animated, StyleSheet, @@ -21,7 +22,7 @@ var { View, } = React; -var Flip = React.createClass({ +var Flip = createClass({ getInitialState() { return { theta: new Animated.Value(45), diff --git a/Examples/UIExplorer/UIExplorerApp.android.js b/Examples/UIExplorer/UIExplorerApp.android.js index c9bd241..4186119 100644 --- a/Examples/UIExplorer/UIExplorerApp.android.js +++ b/Examples/UIExplorer/UIExplorerApp.android.js @@ -17,6 +17,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { AppRegistry, BackAndroid, @@ -30,7 +31,7 @@ var UIExplorerList = require('./UIExplorerList.android'); var DRAWER_WIDTH_LEFT = 56; -var UIExplorerApp = React.createClass({ +var UIExplorerApp = createClass({ getInitialState: function() { return { example: this._getUIExplorerHome(), @@ -82,7 +83,7 @@ var UIExplorerApp = React.createClass({ _renderHome: function() { var onSelectExample = this.onSelectExample; - return React.createClass({ + return createClass({ render: function() { return ( { if (Example.displayName) { - var Snapshotter = React.createClass({ + var Snapshotter = createClass({ render: function() { var Renderable = UIExplorerListBase.makeRenderable(Example); return ( diff --git a/Examples/UIExplorer/UIExplorerPage.js b/Examples/UIExplorer/UIExplorerPage.js index 2c74497..49edc5a 100644 --- a/Examples/UIExplorer/UIExplorerPage.js +++ b/Examples/UIExplorer/UIExplorerPage.js @@ -17,6 +17,8 @@ 'use strict'; var React = require('react-native'); +var PropTypes = require('prop-types'); +var createClass = require('create-react-class'); var { ScrollView, StyleSheet, @@ -25,12 +27,12 @@ var { var UIExplorerTitle = require('./UIExplorerTitle'); -var UIExplorerPage = React.createClass({ +var UIExplorerPage = createClass({ propTypes: { - keyboardShouldPersistTaps: React.PropTypes.bool, - noScroll: React.PropTypes.bool, - noSpacer: React.PropTypes.bool, + keyboardShouldPersistTaps: PropTypes.bool, + noScroll: PropTypes.bool, + noSpacer: PropTypes.bool, }, render: function() { diff --git a/Examples/UIExplorer/UIExplorerTitle.js b/Examples/UIExplorer/UIExplorerTitle.js index 1652498..87d8ec0 100644 --- a/Examples/UIExplorer/UIExplorerTitle.js +++ b/Examples/UIExplorer/UIExplorerTitle.js @@ -17,13 +17,14 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { StyleSheet, Text, View, } = React; -var UIExplorerTitle = React.createClass({ +var UIExplorerTitle = createClass({ render: function() { return ( diff --git a/Examples/UIExplorer/ViewExample.js b/Examples/UIExplorer/ViewExample.js index b5bb18c..0d46178 100644 --- a/Examples/UIExplorer/ViewExample.js +++ b/Examples/UIExplorer/ViewExample.js @@ -16,6 +16,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { StyleSheet, Text, @@ -32,7 +33,7 @@ var styles = StyleSheet.create({ } }); -var ViewBorderStyleExample = React.createClass({ +var ViewBorderStyleExample = createClass({ getInitialState() { return { showBorder: true diff --git a/Examples/UIExplorer/ViewPagerAndroidExample.android.js b/Examples/UIExplorer/ViewPagerAndroidExample.android.js index 51a1312..2308f65 100644 --- a/Examples/UIExplorer/ViewPagerAndroidExample.android.js +++ b/Examples/UIExplorer/ViewPagerAndroidExample.android.js @@ -15,6 +15,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { Image, StyleSheet, @@ -35,7 +36,7 @@ var IMAGE_URIS = [ 'http://apod.nasa.gov/apod/image/1510/lunareclipse_27Sep_beletskycrop4.jpg', ]; -var LikeCount = React.createClass({ +var LikeCount = createClass({ getInitialState: function() { return { likes: 7, @@ -61,7 +62,7 @@ var LikeCount = React.createClass({ }, }); -var Button = React.createClass({ +var Button = createClass({ _handlePress: function() { if (this.props.enabled && this.props.onPress) { this.props.onPress(); @@ -78,7 +79,7 @@ var Button = React.createClass({ } }); -var ProgressBar = React.createClass({ +var ProgressBar = createClass({ render: function() { var fractionalPosition = (this.props.progress.position + this.props.progress.offset); var progressBarSize = (fractionalPosition / (PAGES - 1)) * this.props.size; @@ -90,7 +91,7 @@ var ProgressBar = React.createClass({ } }); -var ViewPagerAndroidExample = React.createClass({ +var ViewPagerAndroidExample = createClass({ statics: { title: '', description: 'Container that allows to flip left and right between child views.' diff --git a/Examples/UIExplorer/WebViewExample.js b/Examples/UIExplorer/WebViewExample.js index 41f6b4d..2abc49c 100644 --- a/Examples/UIExplorer/WebViewExample.js +++ b/Examples/UIExplorer/WebViewExample.js @@ -16,6 +16,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { StyleSheet, Text, @@ -33,7 +34,7 @@ var TEXT_INPUT_REF = 'urlInput'; var WEBVIEW_REF = 'webview'; var DEFAULT_URL = 'https://m.facebook.com'; -var WebViewExample = React.createClass({ +var WebViewExample = createClass({ getInitialState: function() { return { diff --git a/Examples/UIExplorer/createExamplePage.ios.js b/Examples/UIExplorer/createExamplePage.ios.js index 4106881..193ec68 100644 --- a/Examples/UIExplorer/createExamplePage.ios.js +++ b/Examples/UIExplorer/createExamplePage.ios.js @@ -17,6 +17,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { Platform, } = React; @@ -32,7 +33,7 @@ var createExamplePage = function(title: ?string, exampleModule: ExampleModule) : ReactClass { invariant(!!exampleModule.examples, 'The module must have examples'); - var ExamplePage = React.createClass({ + var ExamplePage = createClass({ statics: { title: exampleModule.title, description: exampleModule.description, diff --git a/Examples/UIExplorer/createExamplePage.web.js b/Examples/UIExplorer/createExamplePage.web.js index 6698be1..5a3ddfb 100644 --- a/Examples/UIExplorer/createExamplePage.web.js +++ b/Examples/UIExplorer/createExamplePage.web.js @@ -17,6 +17,7 @@ 'use strict'; var React = require('react-native'); +var createClass = require('create-react-class'); var { Platform, } = React; @@ -28,7 +29,7 @@ import type { Example, ExampleModule } from 'ExampleTypes'; var createExamplePage = function(title: ?string, exampleModule: ExampleModule) : ReactClass { - var ExamplePage = React.createClass({ + var ExamplePage = createClass({ statics: { title: exampleModule.title, description: exampleModule.description, diff --git a/Libraries/ActivityIndicator/ActivityIndicator.web.js b/Libraries/ActivityIndicator/ActivityIndicator.web.js index 462aa82..74bbf8e 100644 --- a/Libraries/ActivityIndicator/ActivityIndicator.web.js +++ b/Libraries/ActivityIndicator/ActivityIndicator.web.js @@ -9,7 +9,8 @@ */ 'use strict'; -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; import View from 'ReactView'; import StyleSheet from 'ReactStyleSheet'; import assign from 'domkit/appendVendorPrefix'; diff --git a/Libraries/Alert/Alert.web.js b/Libraries/Alert/Alert.web.js index e977cbd..0f4ac61 100644 --- a/Libraries/Alert/Alert.web.js +++ b/Libraries/Alert/Alert.web.js @@ -66,10 +66,10 @@ class AlertIOS { const confirmCallback = callbacks.pop() || noop; const cancelCallback = callbacks.pop() || noop; if (buttons.length === 1) { - alert(title); + alert(title + '\n' + message); confirmCallback(); } else if (buttons.length === 2) { - if (confirm(title)) { + if (confirm(title + '\n' + message)) { confirmCallback(); } else { cancelCallback(); diff --git a/Libraries/Animated/Animated.web.js b/Libraries/Animated/Animated.web.js deleted file mode 100644 index 1809763..0000000 --- a/Libraries/Animated/Animated.web.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright (c) 2015-present, Alibaba Group Holding Limited. - * All rights reserved. - * - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * @providesModule ReactAnimated - * @flow - */ -'use strict'; - -import Animated from 'animated'; -import CSSPropertyOperations from 'react-dom/lib/CSSPropertyOperations'; - -import flattenStyle from 'ReactFlattenStyle'; -import Image from 'ReactImage'; -import Text from 'ReactText'; -import View from 'ReactView'; - -// { scale: 2 } => 'scale(2)' -function mapTransform(t) { - var k = Object.keys(t)[0]; - return `${k}(${t[k]})`; -} - -// NOTE(lmr): -// Since this is a hot code path, right now this is mutative... -// As far as I can tell, this shouldn't cause any unexpected behavior. -function mapStyle(style) { - if (style && style.transform && typeof style.transform !== 'string') { - // TODO(lmr): this doesn't attempt to use vendor prefixed styles - style.transform = style.transform.map(mapTransform).join(' '); - } - return style; -} - -function ApplyAnimatedValues(instance, props) { - if (instance.setNativeProps) { - instance.setNativeProps(props); - } else if (instance.nodeType && instance.setAttribute !== undefined) { - CSSPropertyOperations.setValueForStyles(instance, mapStyle(props.style)); - } else { - return false; - } -} - -/* eslint-disable */ -Animated.inject.ApplyAnimatedValues(ApplyAnimatedValues); -Animated.inject.FlattenStyle(flattenStyle); -/* eslint-enable */ - -export default { - ...Animated, - View: Animated.createAnimatedComponent(View), - Text: Animated.createAnimatedComponent(Text), - Image: Animated.createAnimatedComponent(Image), -}; diff --git a/Libraries/Animated/src/Animated.js b/Libraries/Animated/src/Animated.js new file mode 100644 index 0000000..d5b1284 --- /dev/null +++ b/Libraries/Animated/src/Animated.js @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ReactAnimated + * @flow + */ +'use strict'; + + +var AnimatedImplementation = require('AnimatedImplementation'); +import Image from 'ReactImage'; +import Text from 'ReactText'; +import View from 'ReactView'; + +let AnimatedScrollView; + +const Animated = { + View: AnimatedImplementation.createAnimatedComponent(View), + Text: AnimatedImplementation.createAnimatedComponent(Text), + Image: AnimatedImplementation.createAnimatedComponent(Image), + get ScrollView() { + // Make this lazy to avoid circular reference. + if (!AnimatedScrollView) { + AnimatedScrollView = AnimatedImplementation.createAnimatedComponent(require('ReactScrollView')); + } + return AnimatedScrollView; + }, +}; + +Object.assign((Animated: Object), AnimatedImplementation); + +module.exports = ((Animated: any): (typeof AnimatedImplementation) & typeof Animated); diff --git a/Libraries/Animated/src/AnimatedEvent.js b/Libraries/Animated/src/AnimatedEvent.js new file mode 100644 index 0000000..c6e6596 --- /dev/null +++ b/Libraries/Animated/src/AnimatedEvent.js @@ -0,0 +1,196 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule AnimatedEvent + * @flow + * @format + */ +'use strict'; + +const AnimatedValue = require('./nodes/AnimatedValue'); +const NativeAnimatedHelper = require('./NativeAnimatedHelper'); +import findNodeHandle from 'ReactfindNodeHandle'; + +const invariant = require('fbjs/lib/invariant'); +const {shouldUseNativeDriver} = require('./NativeAnimatedHelper'); + +export type Mapping = {[key: string]: Mapping} | AnimatedValue; +export type EventConfig = { + listener?: ?Function, + useNativeDriver?: boolean, +}; + +function attachNativeEvent( + viewRef: any, + eventName: string, + argMapping: Array, +) { + // Find animated values in `argMapping` and create an array representing their + // key path inside the `nativeEvent` object. Ex.: ['contentOffset', 'x']. + const eventMappings = []; + + const traverse = (value, path) => { + if (value instanceof AnimatedValue) { + value.__makeNative(); + + eventMappings.push({ + nativeEventPath: path, + animatedValueTag: value.__getNativeTag(), + }); + } else if (typeof value === 'object') { + for (const key in value) { + traverse(value[key], path.concat(key)); + } + } + }; + + invariant( + argMapping[0] && argMapping[0].nativeEvent, + 'Native driven events only support animated values contained inside `nativeEvent`.', + ); + + // Assume that the event containing `nativeEvent` is always the first argument. + traverse(argMapping[0].nativeEvent, []); + + const viewTag = findNodeHandle(viewRef); + + eventMappings.forEach(mapping => { + NativeAnimatedHelper.API.addAnimatedEventToView( + viewTag, + eventName, + mapping, + ); + }); + + return { + detach() { + eventMappings.forEach(mapping => { + NativeAnimatedHelper.API.removeAnimatedEventFromView( + viewTag, + eventName, + mapping.animatedValueTag, + ); + }); + }, + }; +} + +class AnimatedEvent { + _argMapping: Array; + _listeners: Array = []; + _callListeners: Function; + _attachedEvent: ?{ + detach: () => void, + }; + __isNative: boolean; + + constructor(argMapping: Array, config?: EventConfig = {}) { + this._argMapping = argMapping; + if (config.listener) { + this.__addListener(config.listener); + } + this._callListeners = this._callListeners.bind(this); + this._attachedEvent = null; + this.__isNative = shouldUseNativeDriver(config); + + if (__DEV__) { + this._validateMapping(); + } + } + + __addListener(callback: Function): void { + this._listeners.push(callback); + } + + __removeListener(callback: Function): void { + this._listeners = this._listeners.filter(listener => listener !== callback); + } + + __attach(viewRef: any, eventName: string) { + invariant( + this.__isNative, + 'Only native driven events need to be attached.', + ); + + this._attachedEvent = attachNativeEvent( + viewRef, + eventName, + this._argMapping, + ); + } + + __detach(viewTag: any, eventName: string) { + invariant( + this.__isNative, + 'Only native driven events need to be detached.', + ); + + this._attachedEvent && this._attachedEvent.detach(); + } + + __getHandler() { + if (this.__isNative) { + return this._callListeners; + } + + return (...args: any) => { + const traverse = (recMapping, recEvt, key) => { + if (typeof recEvt === 'number' && recMapping instanceof AnimatedValue) { + recMapping.setValue(recEvt); + } else if (typeof recMapping === 'object') { + for (const mappingKey in recMapping) { + /* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This + * comment suppresses an error when upgrading Flow's support for + * React. To see the error delete this comment and run Flow. */ + traverse(recMapping[mappingKey], recEvt[mappingKey], mappingKey); + } + } + }; + + if (!this.__isNative) { + this._argMapping.forEach((mapping, idx) => { + traverse(mapping, args[idx], 'arg' + idx); + }); + } + this._callListeners(...args); + }; + } + + _callListeners(...args) { + this._listeners.forEach(listener => listener(...args)); + } + + _validateMapping() { + const traverse = (recMapping, recEvt, key) => { + if (typeof recEvt === 'number') { + invariant( + recMapping instanceof AnimatedValue, + 'Bad mapping of type ' + + typeof recMapping + + ' for key ' + + key + + ', event value must map to AnimatedValue', + ); + return; + } + invariant( + typeof recMapping === 'object', + 'Bad mapping of type ' + typeof recMapping + ' for key ' + key, + ); + invariant( + typeof recEvt === 'object', + 'Bad event of type ' + typeof recEvt + ' for key ' + key, + ); + for (const mappingKey in recMapping) { + traverse(recMapping[mappingKey], recEvt[mappingKey], mappingKey); + } + }; + } +} + +module.exports = {AnimatedEvent, attachNativeEvent}; diff --git a/Libraries/Animated/src/AnimatedImplementation.js b/Libraries/Animated/src/AnimatedImplementation.js new file mode 100644 index 0000000..836085f --- /dev/null +++ b/Libraries/Animated/src/AnimatedImplementation.js @@ -0,0 +1,883 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule AnimatedImplementation + * @flow + * @format + * @preventMunge + */ +'use strict'; + +const {AnimatedEvent, attachNativeEvent} = require('./AnimatedEvent'); +const AnimatedAddition = require('./nodes/AnimatedAddition'); +const AnimatedDiffClamp = require('./nodes/AnimatedDiffClamp'); +const AnimatedDivision = require('./nodes/AnimatedDivision'); +const AnimatedInterpolation = require('./nodes/AnimatedInterpolation'); +const AnimatedModulo = require('./nodes/AnimatedModulo'); +const AnimatedMultiplication = require('./nodes/AnimatedMultiplication'); +const AnimatedNode = require('./nodes/AnimatedNode'); +const AnimatedProps = require('./nodes/AnimatedProps'); +const AnimatedTracking = require('./nodes/AnimatedTracking'); +const AnimatedValue = require('./nodes/AnimatedValue'); +const AnimatedValueXY = require('./nodes/AnimatedValueXY'); +const DecayAnimation = require('./animations/DecayAnimation'); +const SpringAnimation = require('./animations/SpringAnimation'); +const TimingAnimation = require('./animations/TimingAnimation'); + +const createAnimatedComponent = require('./createAnimatedComponent'); + +const {shouldUseNativeDriver} = require('./NativeAnimatedHelper'); + +import type { + AnimationConfig, + EndCallback, + EndResult, +} from './animations/Animation'; +import type {TimingAnimationConfig} from './animations/TimingAnimation'; +import type {DecayAnimationConfig} from './animations/DecayAnimation'; +import type {SpringAnimationConfig} from './animations/SpringAnimation'; +import type {Mapping, EventConfig} from './AnimatedEvent'; + +type CompositeAnimation = { + start: (callback?: ?EndCallback) => void, + stop: () => void, + reset: () => void, + _startNativeLoop: (iterations?: number) => void, + _isUsingNativeDriver: () => boolean, +}; + +const add = function( + a: AnimatedNode | number, + b: AnimatedNode | number, +): AnimatedAddition { + return new AnimatedAddition(a, b); +}; + +const divide = function( + a: AnimatedNode | number, + b: AnimatedNode | number, +): AnimatedDivision { + return new AnimatedDivision(a, b); +}; + +const multiply = function( + a: AnimatedNode | number, + b: AnimatedNode | number, +): AnimatedMultiplication { + return new AnimatedMultiplication(a, b); +}; + +const modulo = function(a: AnimatedNode, modulus: number): AnimatedModulo { + return new AnimatedModulo(a, modulus); +}; + +const diffClamp = function( + a: AnimatedNode, + min: number, + max: number, +): AnimatedDiffClamp { + return new AnimatedDiffClamp(a, min, max); +}; + +const _combineCallbacks = function( + callback: ?EndCallback, + config: AnimationConfig, +) { + if (callback && config.onComplete) { + return (...args) => { + config.onComplete && config.onComplete(...args); + callback && callback(...args); + }; + } else { + return callback || config.onComplete; + } +}; + +const maybeVectorAnim = function( + value: AnimatedValue | AnimatedValueXY, + config: Object, + anim: (value: AnimatedValue, config: Object) => CompositeAnimation, +): ?CompositeAnimation { + if (value instanceof AnimatedValueXY) { + const configX = {...config}; + const configY = {...config}; + for (const key in config) { + const {x, y} = config[key]; + if (x !== undefined && y !== undefined) { + configX[key] = x; + configY[key] = y; + } + } + const aX = anim((value: AnimatedValueXY).x, configX); + const aY = anim((value: AnimatedValueXY).y, configY); + // We use `stopTogether: false` here because otherwise tracking will break + // because the second animation will get stopped before it can update. + return parallel([aX, aY], {stopTogether: false}); + } + return null; +}; + +const spring = function( + value: AnimatedValue | AnimatedValueXY, + config: SpringAnimationConfig, +): CompositeAnimation { + const start = function( + animatedValue: AnimatedValue | AnimatedValueXY, + configuration: SpringAnimationConfig, + callback?: ?EndCallback, + ): void { + callback = _combineCallbacks(callback, configuration); + const singleValue: any = animatedValue; + const singleConfig: any = configuration; + singleValue.stopTracking(); + if (configuration.toValue instanceof AnimatedNode) { + singleValue.track( + new AnimatedTracking( + singleValue, + configuration.toValue, + SpringAnimation, + singleConfig, + callback, + ), + ); + } else { + singleValue.animate(new SpringAnimation(singleConfig), callback); + } + }; + return ( + maybeVectorAnim(value, config, spring) || { + start: function(callback?: ?EndCallback): void { + start(value, config, callback); + }, + + stop: function(): void { + value.stopAnimation(); + }, + + reset: function(): void { + value.resetAnimation(); + }, + + _startNativeLoop: function(iterations?: number): void { + const singleConfig = {...config, iterations}; + start(value, singleConfig); + }, + + _isUsingNativeDriver: function(): boolean { + return shouldUseNativeDriver(config); + }, + } + ); +}; + +const timing = function( + value: AnimatedValue | AnimatedValueXY, + config: TimingAnimationConfig, +): CompositeAnimation { + const start = function( + animatedValue: AnimatedValue | AnimatedValueXY, + configuration: TimingAnimationConfig, + callback?: ?EndCallback, + ): void { + callback = _combineCallbacks(callback, configuration); + const singleValue: any = animatedValue; + const singleConfig: any = configuration; + singleValue.stopTracking(); + if (configuration.toValue instanceof AnimatedNode) { + singleValue.track( + new AnimatedTracking( + singleValue, + configuration.toValue, + TimingAnimation, + singleConfig, + callback, + ), + ); + } else { + singleValue.animate(new TimingAnimation(singleConfig), callback); + } + }; + + return ( + maybeVectorAnim(value, config, timing) || { + start: function(callback?: ?EndCallback): void { + start(value, config, callback); + }, + + stop: function(): void { + value.stopAnimation(); + }, + + reset: function(): void { + value.resetAnimation(); + }, + + _startNativeLoop: function(iterations?: number): void { + const singleConfig = {...config, iterations}; + start(value, singleConfig); + }, + + _isUsingNativeDriver: function(): boolean { + return shouldUseNativeDriver(config); + }, + } + ); +}; + +const decay = function( + value: AnimatedValue | AnimatedValueXY, + config: DecayAnimationConfig, +): CompositeAnimation { + const start = function( + animatedValue: AnimatedValue | AnimatedValueXY, + configuration: DecayAnimationConfig, + callback?: ?EndCallback, + ): void { + callback = _combineCallbacks(callback, configuration); + const singleValue: any = animatedValue; + const singleConfig: any = configuration; + singleValue.stopTracking(); + singleValue.animate(new DecayAnimation(singleConfig), callback); + }; + + return ( + maybeVectorAnim(value, config, decay) || { + start: function(callback?: ?EndCallback): void { + start(value, config, callback); + }, + + stop: function(): void { + value.stopAnimation(); + }, + + reset: function(): void { + value.resetAnimation(); + }, + + _startNativeLoop: function(iterations?: number): void { + const singleConfig = {...config, iterations}; + start(value, singleConfig); + }, + + _isUsingNativeDriver: function(): boolean { + return shouldUseNativeDriver(config); + }, + } + ); +}; + +const sequence = function( + animations: Array, +): CompositeAnimation { + let current = 0; + return { + start: function(callback?: ?EndCallback) { + const onComplete = function(result) { + if (!result.finished) { + callback && callback(result); + return; + } + + current++; + + if (current === animations.length) { + callback && callback(result); + return; + } + + animations[current].start(onComplete); + }; + + if (animations.length === 0) { + callback && callback({finished: true}); + } else { + animations[current].start(onComplete); + } + }, + + stop: function() { + if (current < animations.length) { + animations[current].stop(); + } + }, + + reset: function() { + animations.forEach((animation, idx) => { + if (idx <= current) { + animation.reset(); + } + }); + current = 0; + }, + + _startNativeLoop: function() { + throw new Error( + 'Loops run using the native driver cannot contain Animated.sequence animations', + ); + }, + + _isUsingNativeDriver: function(): boolean { + return false; + }, + }; +}; + +type ParallelConfig = { + stopTogether?: boolean, // If one is stopped, stop all. default: true +}; +const parallel = function( + animations: Array, + config?: ?ParallelConfig, +): CompositeAnimation { + let doneCount = 0; + // Make sure we only call stop() at most once for each animation + const hasEnded = {}; + const stopTogether = !(config && config.stopTogether === false); + + const result = { + start: function(callback?: ?EndCallback) { + if (doneCount === animations.length) { + callback && callback({finished: true}); + return; + } + + animations.forEach((animation, idx) => { + const cb = function(endResult) { + hasEnded[idx] = true; + doneCount++; + if (doneCount === animations.length) { + doneCount = 0; + callback && callback(endResult); + return; + } + + if (!endResult.finished && stopTogether) { + result.stop(); + } + }; + + if (!animation) { + cb({finished: true}); + } else { + animation.start(cb); + } + }); + }, + + stop: function(): void { + animations.forEach((animation, idx) => { + !hasEnded[idx] && animation.stop(); + hasEnded[idx] = true; + }); + }, + + reset: function(): void { + animations.forEach((animation, idx) => { + animation.reset(); + hasEnded[idx] = false; + doneCount = 0; + }); + }, + + _startNativeLoop: function() { + throw new Error( + 'Loops run using the native driver cannot contain Animated.parallel animations', + ); + }, + + _isUsingNativeDriver: function(): boolean { + return false; + }, + }; + + return result; +}; + +const delay = function(time: number): CompositeAnimation { + // Would be nice to make a specialized implementation + return timing(new AnimatedValue(0), {toValue: 0, delay: time, duration: 0}); +}; + +const stagger = function( + time: number, + animations: Array, +): CompositeAnimation { + return parallel( + animations.map((animation, i) => { + return sequence([delay(time * i), animation]); + }), + ); +}; + +type LoopAnimationConfig = {iterations: number}; + +const loop = function( + animation: CompositeAnimation, + {iterations = -1}: LoopAnimationConfig = {}, +): CompositeAnimation { + let isFinished = false; + let iterationsSoFar = 0; + return { + start: function(callback?: ?EndCallback) { + const restart = function(result: EndResult = {finished: true}): void { + if ( + isFinished || + iterationsSoFar === iterations || + result.finished === false + ) { + callback && callback(result); + } else { + iterationsSoFar++; + animation.reset(); + animation.start(restart); + } + }; + if (!animation || iterations === 0) { + callback && callback({finished: true}); + } else { + if (animation._isUsingNativeDriver()) { + animation._startNativeLoop(iterations); + } else { + restart(); // Start looping recursively on the js thread + } + } + }, + + stop: function(): void { + isFinished = true; + animation.stop(); + }, + + reset: function(): void { + iterationsSoFar = 0; + isFinished = false; + animation.reset(); + }, + + _startNativeLoop: function() { + throw new Error( + 'Loops run using the native driver cannot contain Animated.loop animations', + ); + }, + + _isUsingNativeDriver: function(): boolean { + return animation._isUsingNativeDriver(); + }, + }; +}; + +function forkEvent( + event: ?AnimatedEvent | ?Function, + listener: Function, +): AnimatedEvent | Function { + if (!event) { + return listener; + } else if (event instanceof AnimatedEvent) { + event.__addListener(listener); + return event; + } else { + return (...args) => { + typeof event === 'function' && event(...args); + listener(...args); + }; + } +} + +function unforkEvent( + event: ?AnimatedEvent | ?Function, + listener: Function, +): void { + if (event && event instanceof AnimatedEvent) { + event.__removeListener(listener); + } +} + +const event = function(argMapping: Array, config?: EventConfig): any { + const animatedEvent = new AnimatedEvent(argMapping, config); + if (animatedEvent.__isNative) { + return animatedEvent; + } else { + return animatedEvent.__getHandler(); + } +}; + +/** + * The `Animated` library is designed to make animations fluid, powerful, and + * easy to build and maintain. `Animated` focuses on declarative relationships + * between inputs and outputs, with configurable transforms in between, and + * simple `start`/`stop` methods to control time-based animation execution. + * + * The simplest workflow for creating an animation is to create an + * `Animated.Value`, hook it up to one or more style attributes of an animated + * component, and then drive updates via animations using `Animated.timing()`: + * + * ```javascript + * Animated.timing( // Animate value over time + * this.state.fadeAnim, // The value to drive + * { + * toValue: 1, // Animate to final value of 1 + * } + * ).start(); // Start the animation + * ``` + * + * Refer to the [Animations](docs/animations.html#animated-api) guide to see + * additional examples of `Animated` in action. + * + * ## Overview + * + * There are two value types you can use with `Animated`: + * + * - [`Animated.Value()`](docs/animated.html#value) for single values + * - [`Animated.ValueXY()`](docs/animated.html#valuexy) for vectors + * + * `Animated.Value` can bind to style properties or other props, and can be + * interpolated as well. A single `Animated.Value` can drive any number of + * properties. + * + * ### Configuring animations + * + * `Animated` provides three types of animation types. Each animation type + * provides a particular animation curve that controls how your values animate + * from their initial value to the final value: + * + * - [`Animated.decay()`](docs/animated.html#decay) starts with an initial + * velocity and gradually slows to a stop. + * - [`Animated.spring()`](docs/animated.html#spring) provides a simple + * spring physics model. + * - [`Animated.timing()`](docs/animated.html#timing) animates a value over time + * using [easing functions](docs/easing.html). + * + * In most cases, you will be using `timing()`. By default, it uses a symmetric + * easeInOut curve that conveys the gradual acceleration of an object to full + * speed and concludes by gradually decelerating to a stop. + * + * ### Working with animations + * + * Animations are started by calling `start()` on your animation. `start()` + * takes a completion callback that will be called when the animation is done. + * If the animation finished running normally, the completion callback will be + * invoked with `{finished: true}`. If the animation is done because `stop()` + * was called on it before it could finish (e.g. because it was interrupted by a + * gesture or another animation), then it will receive `{finished: false}`. + * + * ### Using the native driver + * + * By using the native driver, we send everything about the animation to native + * before starting the animation, allowing native code to perform the animation + * on the UI thread without having to go through the bridge on every frame. + * Once the animation has started, the JS thread can be blocked without + * affecting the animation. + * + * You can use the native driver by specifying `useNativeDriver: true` in your + * animation configuration. See the + * [Animations](docs/animations.html#using-the-native-driver) guide to learn + * more. + * + * ### Animatable components + * + * Only animatable components can be animated. These special components do the + * magic of binding the animated values to the properties, and do targeted + * native updates to avoid the cost of the react render and reconciliation + * process on every frame. They also handle cleanup on unmount so they are safe + * by default. + * + * - [`createAnimatedComponent()`](docs/animated.html#createanimatedcomponent) + * can be used to make a component animatable. + * + * `Animated` exports the following animatable components using the above + * wrapper: + * + * - `Animated.Image` + * - `Animated.ScrollView` + * - `Animated.Text` + * - `Animated.View` + * + * ### Composing animations + * + * Animations can also be combined in complex ways using composition functions: + * + * - [`Animated.delay()`](docs/animated.html#delay) starts an animation after + * a given delay. + * - [`Animated.parallel()`](docs/animated.html#parallel) starts a number of + * animations at the same time. + * - [`Animated.sequence()`](docs/animated.html#sequence) starts the animations + * in order, waiting for each to complete before starting the next. + * - [`Animated.stagger()`](docs/animated.html#stagger) starts animations in + * order and in parallel, but with successive delays. + * + * Animations can also be chained together simply by setting the `toValue` of + * one animation to be another `Animated.Value`. See + * [Tracking dynamic values](docs/animations.html#tracking-dynamic-values) in + * the Animations guide. + * + * By default, if one animation is stopped or interrupted, then all other + * animations in the group are also stopped. + * + * ### Combining animated values + * + * You can combine two animated values via addition, multiplication, division, + * or modulo to make a new animated value: + * + * - [`Animated.add()`](docs/animated.html#add) + * - [`Animated.divide()`](docs/animated.html#divide) + * - [`Animated.modulo()`](docs/animated.html#modulo) + * - [`Animated.multiply()`](docs/animated.html#multiply) + * + * ### Interpolation + * + * The `interpolate()` function allows input ranges to map to different output + * ranges. By default, it will extrapolate the curve beyond the ranges given, + * but you can also have it clamp the output value. It uses lineal interpolation + * by default but also supports easing functions. + * + * - [`interpolate()`](docs/animated.html#interpolate) + * + * Read more about interpolation in the + * [Animation](docs/animations.html#interpolation) guide. + * + * ### Handling gestures and other events + * + * Gestures, like panning or scrolling, and other events can map directly to + * animated values using `Animated.event()`. This is done with a structured map + * syntax so that values can be extracted from complex event objects. The first + * level is an array to allow mapping across multiple args, and that array + * contains nested objects. + * + * - [`Animated.event()`](docs/animated.html#event) + * + * For example, when working with horizontal scrolling gestures, you would do + * the following in order to map `event.nativeEvent.contentOffset.x` to + * `scrollX` (an `Animated.Value`): + * + * ```javascript + * onScroll={Animated.event( + * // scrollX = e.nativeEvent.contentOffset.x + * [{ nativeEvent: { + * contentOffset: { + * x: scrollX + * } + * } + * }] + * )} + * ``` + * + */ +module.exports = { + /** + * Standard value class for driving animations. Typically initialized with + * `new Animated.Value(0);` + * + * See also [`AnimatedValue`](docs/animated.html#animatedvalue). + */ + Value: AnimatedValue, + /** + * 2D value class for driving 2D animations, such as pan gestures. + * + * See also [`AnimatedValueXY`](docs/animated.html#animatedvaluexy). + */ + ValueXY: AnimatedValueXY, + /** + * exported to use the Interpolation type in flow + * + * See also [`AnimatedInterpolation`](docs/animated.html#animatedinterpolation). + */ + Interpolation: AnimatedInterpolation, + /** + * Exported for ease of type checking. All animated values derive from this class. + */ + Node: AnimatedNode, + + /** + * Animates a value from an initial velocity to zero based on a decay + * coefficient. + * + * Config is an object that may have the following options: + * + * - `velocity`: Initial velocity. Required. + * - `deceleration`: Rate of decay. Default 0.997. + * - `isInteraction`: Whether or not this animation creates an "interaction handle" on the + * `InteractionManager`. Default true. + * - `useNativeDriver`: Uses the native driver when true. Default false. + */ + decay, + /** + * Animates a value along a timed easing curve. The + * [`Easing`](docs/easing.html) module has tons of predefined curves, or you + * can use your own function. + * + * Config is an object that may have the following options: + * + * - `duration`: Length of animation (milliseconds). Default 500. + * - `easing`: Easing function to define curve. + * Default is `Easing.inOut(Easing.ease)`. + * - `delay`: Start the animation after delay (milliseconds). Default 0. + * - `isInteraction`: Whether or not this animation creates an "interaction handle" on the + * `InteractionManager`. Default true. + * - `useNativeDriver`: Uses the native driver when true. Default false. + */ + timing, + /** + * Animates a value according to an analytical spring model based on + * [damped harmonic oscillation](https://en.wikipedia.org/wiki/Harmonic_oscillator#Damped_harmonic_oscillator). + * Tracks velocity state to create fluid motions as the `toValue` updates, and + * can be chained together. + * + * Config is an object that may have the following options. + * + * Note that you can only define one of bounciness/speed, tension/friction, or + * stiffness/damping/mass, but not more than one: + * + * The friction/tension or bounciness/speed options match the spring model in + * [Facebook Pop](https://github.com/facebook/pop), [Rebound](http://facebook.github.io/rebound/), + * and [Origami](http://origami.design/). + * + * - `friction`: Controls "bounciness"/overshoot. Default 7. + * - `tension`: Controls speed. Default 40. + * - `speed`: Controls speed of the animation. Default 12. + * - `bounciness`: Controls bounciness. Default 8. + * + * Specifying stiffness/damping/mass as parameters makes `Animated.spring` use an + * analytical spring model based on the motion equations of a [damped harmonic + * oscillator](https://en.wikipedia.org/wiki/Harmonic_oscillator#Damped_harmonic_oscillator). + * This behavior is slightly more precise and faithful to the physics behind + * spring dynamics, and closely mimics the implementation in iOS's + * CASpringAnimation primitive. + * + * - `stiffness`: The spring stiffness coefficient. Default 100. + * - `damping`: Defines how the spring’s motion should be damped due to the forces of friction. + * Default 10. + * - `mass`: The mass of the object attached to the end of the spring. Default 1. + * + * Other configuration options are as follows: + * + * - `velocity`: The initial velocity of the object attached to the spring. Default 0 (object + * is at rest). + * - `overshootClamping`: Boolean indiciating whether the spring should be clamped and not + * bounce. Default false. + * - `restDisplacementThreshold`: The threshold of displacement from rest below which the + * spring should be considered at rest. Default 0.001. + * - `restSpeedThreshold`: The speed at which the spring should be considered at rest in pixels + * per second. Default 0.001. + * - `delay`: Start the animation after delay (milliseconds). Default 0. + * - `isInteraction`: Whether or not this animation creates an "interaction handle" on the + * `InteractionManager`. Default true. + * - `useNativeDriver`: Uses the native driver when true. Default false. + */ + spring, + + /** + * Creates a new Animated value composed from two Animated values added + * together. + */ + add, + + /** + * Creates a new Animated value composed by dividing the first Animated value + * by the second Animated value. + */ + divide, + + /** + * Creates a new Animated value composed from two Animated values multiplied + * together. + */ + multiply, + + /** + * Creates a new Animated value that is the (non-negative) modulo of the + * provided Animated value + */ + modulo, + + /** + * Create a new Animated value that is limited between 2 values. It uses the + * difference between the last value so even if the value is far from the bounds + * it will start changing when the value starts getting closer again. + * (`value = clamp(value + diff, min, max)`). + * + * This is useful with scroll events, for example, to show the navbar when + * scrolling up and to hide it when scrolling down. + */ + diffClamp, + + /** + * Starts an animation after the given delay. + */ + delay, + /** + * Starts an array of animations in order, waiting for each to complete + * before starting the next. If the current running animation is stopped, no + * following animations will be started. + */ + sequence, + /** + * Starts an array of animations all at the same time. By default, if one + * of the animations is stopped, they will all be stopped. You can override + * this with the `stopTogether` flag. + */ + parallel, + /** + * Array of animations may run in parallel (overlap), but are started in + * sequence with successive delays. Nice for doing trailing effects. + */ + stagger, + /** + * Loops a given animation continuously, so that each time it reaches the + * end, it resets and begins again from the start. Can specify number of + * times to loop using the key `iterations` in the config. Will loop without + * blocking the UI thread if the child animation is set to `useNativeDriver: true`. + * In addition, loops can prevent `VirtualizedList`-based components from rendering + * more rows while the animation is running. You can pass `isInteraction: false` in the + * child animation config to fix this. + */ + loop, + + /** + * Takes an array of mappings and extracts values from each arg accordingly, + * then calls `setValue` on the mapped outputs. e.g. + * + *```javascript + * onScroll={Animated.event( + * [{nativeEvent: {contentOffset: {x: this._scrollX}}}], + * {listener: (event) => console.log(event)}, // Optional async listener + * )} + * ... + * onPanResponderMove: Animated.event([ + * null, // raw event arg ignored + * {dx: this._panX}, // gestureState arg + {listener: (event, gestureState) => console.log(event, gestureState)}, // Optional async listener + * ]), + *``` + * + * Config is an object that may have the following options: + * + * - `listener`: Optional async listener. + * - `useNativeDriver`: Uses the native driver when true. Default false. + */ + event, + + /** + * Make any React component Animatable. Used to create `Animated.View`, etc. + */ + createAnimatedComponent, + + /** + * Imperative API to attach an animated value to an event on a view. Prefer using + * `Animated.event` with `useNativeDrive: true` if possible. + */ + attachNativeEvent, + + /** + * Advanced imperative API for snooping on animated events that are passed in through props. Use + * values directly where possible. + */ + forkEvent, + unforkEvent, + + __PropsOnlyForTests: AnimatedProps, +}; diff --git a/Libraries/Animated/src/AnimatedWeb.js b/Libraries/Animated/src/AnimatedWeb.js new file mode 100644 index 0000000..b88acb9 --- /dev/null +++ b/Libraries/Animated/src/AnimatedWeb.js @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @flow + * @providesModule AnimatedWeb + */ +'use strict'; + +var AnimatedImplementation = require('AnimatedImplementation'); + +module.exports = { + ...AnimatedImplementation, + div: AnimatedImplementation.createAnimatedComponent('div'), + span: AnimatedImplementation.createAnimatedComponent('span'), + img: AnimatedImplementation.createAnimatedComponent('img'), +}; diff --git a/Libraries/Animated/src/Easing.js b/Libraries/Animated/src/Easing.js new file mode 100644 index 0000000..7b396ea --- /dev/null +++ b/Libraries/Animated/src/Easing.js @@ -0,0 +1,262 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule Easing + * @flow + */ +'use strict'; + +let ease; + +/** + * The `Easing` module implements common easing functions. This module is used + * by [Animate.timing()](docs/animate.html#timing) to convey physically + * believable motion in animations. + * + * You can find a visualization of some common easing functions at + * http://easings.net/ + * + * ### Predefined animations + * + * The `Easing` module provides several predefined animations through the + * following methods: + * + * - [`back`](docs/easing.html#back) provides a simple animation where the + * object goes slightly back before moving forward + * - [`bounce`](docs/easing.html#bounce) provides a bouncing animation + * - [`ease`](docs/easing.html#ease) provides a simple inertial animation + * - [`elastic`](docs/easing.html#elastic) provides a simple spring interaction + * + * ### Standard functions + * + * Three standard easing functions are provided: + * + * - [`linear`](docs/easing.html#linear) + * - [`quad`](docs/easing.html#quad) + * - [`cubic`](docs/easing.html#cubic) + * + * The [`poly`](docs/easing.html#poly) function can be used to implement + * quartic, quintic, and other higher power functions. + * + * ### Additional functions + * + * Additional mathematical functions are provided by the following methods: + * + * - [`bezier`](docs/easing.html#bezier) provides a cubic bezier curve + * - [`circle`](docs/easing.html#circle) provides a circular function + * - [`sin`](docs/easing.html#sin) provides a sinusoidal function + * - [`exp`](docs/easing.html#exp) provides an exponential function + * + * The following helpers are used to modify other easing functions. + * + * - [`in`](docs/easing.html#in) runs an easing function forwards + * - [`inOut`](docs/easing.html#inout) makes any easing function symmetrical + * - [`out`](docs/easing.html#out) runs an easing function backwards + */ +class Easing { + /** + * A stepping function, returns 1 for any positive value of `n`. + */ + static step0(n) { + return n > 0 ? 1 : 0; + } + + /** + * A stepping function, returns 1 if `n` is greater than or equal to 1. + */ + static step1(n) { + return n >= 1 ? 1 : 0; + } + + /** + * A linear function, `f(t) = t`. Position correlates to elapsed time one to + * one. + * + * http://cubic-bezier.com/#0,0,1,1 + */ + static linear(t) { + return t; + } + + /** + * A simple inertial interaction, similar to an object slowly accelerating to + * speed. + * + * http://cubic-bezier.com/#.42,0,1,1 + */ + static ease(t: number): number { + if (!ease) { + ease = Easing.bezier(0.42, 0, 1, 1); + } + return ease(t); + } + + /** + * A quadratic function, `f(t) = t * t`. Position equals the square of elapsed + * time. + * + * http://easings.net/#easeInQuad + */ + static quad(t) { + return t * t; + } + + /** + * A cubic function, `f(t) = t * t * t`. Position equals the cube of elapsed + * time. + * + * http://easings.net/#easeInCubic + */ + static cubic(t) { + return t * t * t; + } + + /** + * A power function. Position is equal to the Nth power of elapsed time. + * + * n = 4: http://easings.net/#easeInQuart + * n = 5: http://easings.net/#easeInQuint + */ + static poly(n) { + return (t) => Math.pow(t, n); + } + + /** + * A sinusoidal function. + * + * http://easings.net/#easeInSine + */ + static sin(t) { + return 1 - Math.cos(t * Math.PI / 2); + } + + /** + * A circular function. + * + * http://easings.net/#easeInCirc + */ + static circle(t) { + return 1 - Math.sqrt(1 - t * t); + } + + /** + * An exponential function. + * + * http://easings.net/#easeInExpo + */ + static exp(t) { + return Math.pow(2, 10 * (t - 1)); + } + + /** + * A simple elastic interaction, similar to a spring oscillating back and + * forth. + * + * Default bounciness is 1, which overshoots a little bit once. 0 bounciness + * doesn't overshoot at all, and bounciness of N > 1 will overshoot about N + * times. + * + * http://easings.net/#easeInElastic + */ + static elastic(bounciness: number = 1): (t: number) => number { + const p = bounciness * Math.PI; + return (t) => 1 - Math.pow(Math.cos(t * Math.PI / 2), 3) * Math.cos(t * p); + } + + /** + * Use with `Animated.parallel()` to create a simple effect where the object + * animates back slightly as the animation starts. + * + * Wolfram Plot: + * + * - http://tiny.cc/back_default (s = 1.70158, default) + */ + static back(s: number): (t: number) => number { + if (s === undefined) { + s = 1.70158; + } + return (t) => t * t * ((s + 1) * t - s); + } + + /** + * Provides a simple bouncing effect. + * + * http://easings.net/#easeInBounce + */ + static bounce(t: number): number { + if (t < 1 / 2.75) { + return 7.5625 * t * t; + } + + if (t < 2 / 2.75) { + t -= 1.5 / 2.75; + return 7.5625 * t * t + 0.75; + } + + if (t < 2.5 / 2.75) { + t -= 2.25 / 2.75; + return 7.5625 * t * t + 0.9375; + } + + t -= 2.625 / 2.75; + return 7.5625 * t * t + 0.984375; + } + + /** + * Provides a cubic bezier curve, equivalent to CSS Transitions' + * `transition-timing-function`. + * + * A useful tool to visualize cubic bezier curves can be found at + * http://cubic-bezier.com/ + */ + static bezier( + x1: number, + y1: number, + x2: number, + y2: number + ): (t: number) => number { + const _bezier = require('bezier'); + return _bezier(x1, y1, x2, y2); + } + + /** + * Runs an easing function forwards. + */ + static in( + easing: (t: number) => number, + ): (t: number) => number { + return easing; + } + + /** + * Runs an easing function backwards. + */ + static out( + easing: (t: number) => number, + ): (t: number) => number { + return (t) => 1 - easing(1 - t); + } + + /** + * Makes any easing function symmetrical. The easing function will run + * forwards for half of the duration, then backwards for the rest of the + * duration. + */ + static inOut( + easing: (t: number) => number, + ): (t: number) => number { + return (t) => { + if (t < 0.5) { + return easing(t * 2) / 2; + } + return 1 - easing((1 - t) * 2) / 2; + }; + } +} + +module.exports = Easing; diff --git a/Libraries/Animated/src/NativeAnimatedHelper.js b/Libraries/Animated/src/NativeAnimatedHelper.js new file mode 100644 index 0000000..b2324c2 --- /dev/null +++ b/Libraries/Animated/src/NativeAnimatedHelper.js @@ -0,0 +1,253 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule NativeAnimatedHelper + * @flow + * @format + */ +'use strict'; + +const NativeAnimatedModule = require('ReactNativeModules').NativeAnimatedModule; +// const NativeEventEmitter = require('NativeEventEmitter'); + +const invariant = require('fbjs/lib/invariant'); + +import type {AnimationConfig} from './animations/Animation'; +import type {EventConfig} from './AnimatedEvent'; + +let __nativeAnimatedNodeTagCount = 1; /* used for animated nodes */ +let __nativeAnimationIdCount = 1; /* used for started animations */ + +type EndResult = {finished: boolean}; +type EndCallback = (result: EndResult) => void; +type EventMapping = { + nativeEventPath: Array, + animatedValueTag: ?number, +}; + +let nativeEventEmitter; + +/** + * Simple wrappers around NativeAnimatedModule to provide flow and autocmplete support for + * the native module methods + */ +const API = { + createAnimatedNode: function(tag: ?number, config: Object): void { + assertNativeAnimatedModule(); + NativeAnimatedModule.createAnimatedNode(tag, config); + }, + startListeningToAnimatedNodeValue: function(tag: ?number) { + assertNativeAnimatedModule(); + NativeAnimatedModule.startListeningToAnimatedNodeValue(tag); + }, + stopListeningToAnimatedNodeValue: function(tag: ?number) { + assertNativeAnimatedModule(); + NativeAnimatedModule.stopListeningToAnimatedNodeValue(tag); + }, + connectAnimatedNodes: function(parentTag: ?number, childTag: ?number): void { + assertNativeAnimatedModule(); + NativeAnimatedModule.connectAnimatedNodes(parentTag, childTag); + }, + disconnectAnimatedNodes: function( + parentTag: ?number, + childTag: ?number, + ): void { + assertNativeAnimatedModule(); + NativeAnimatedModule.disconnectAnimatedNodes(parentTag, childTag); + }, + startAnimatingNode: function( + animationId: ?number, + nodeTag: ?number, + config: Object, + endCallback: EndCallback, + ): void { + assertNativeAnimatedModule(); + NativeAnimatedModule.startAnimatingNode( + animationId, + nodeTag, + config, + endCallback, + ); + }, + stopAnimation: function(animationId: ?number) { + assertNativeAnimatedModule(); + NativeAnimatedModule.stopAnimation(animationId); + }, + setAnimatedNodeValue: function(nodeTag: ?number, value: ?number): void { + assertNativeAnimatedModule(); + NativeAnimatedModule.setAnimatedNodeValue(nodeTag, value); + }, + setAnimatedNodeOffset: function(nodeTag: ?number, offset: ?number): void { + assertNativeAnimatedModule(); + NativeAnimatedModule.setAnimatedNodeOffset(nodeTag, offset); + }, + flattenAnimatedNodeOffset: function(nodeTag: ?number): void { + assertNativeAnimatedModule(); + NativeAnimatedModule.flattenAnimatedNodeOffset(nodeTag); + }, + extractAnimatedNodeOffset: function(nodeTag: ?number): void { + assertNativeAnimatedModule(); + NativeAnimatedModule.extractAnimatedNodeOffset(nodeTag); + }, + connectAnimatedNodeToView: function( + nodeTag: ?number, + viewTag: ?number, + ): void { + assertNativeAnimatedModule(); + NativeAnimatedModule.connectAnimatedNodeToView(nodeTag, viewTag); + }, + disconnectAnimatedNodeFromView: function( + nodeTag: ?number, + viewTag: ?number, + ): void { + assertNativeAnimatedModule(); + NativeAnimatedModule.disconnectAnimatedNodeFromView(nodeTag, viewTag); + }, + dropAnimatedNode: function(tag: ?number): void { + assertNativeAnimatedModule(); + NativeAnimatedModule.dropAnimatedNode(tag); + }, + addAnimatedEventToView: function( + viewTag: ?number, + eventName: string, + eventMapping: EventMapping, + ) { + assertNativeAnimatedModule(); + NativeAnimatedModule.addAnimatedEventToView( + viewTag, + eventName, + eventMapping, + ); + }, + removeAnimatedEventFromView( + viewTag: ?number, + eventName: string, + animatedNodeTag: ?number, + ) { + assertNativeAnimatedModule(); + NativeAnimatedModule.removeAnimatedEventFromView( + viewTag, + eventName, + animatedNodeTag, + ); + }, +}; + +/** + * Styles allowed by the native animated implementation. + * + * In general native animated implementation should support any numeric property that doesn't need + * to be updated through the shadow view hierarchy (all non-layout properties). + */ +const STYLES_WHITELIST = { + opacity: true, + transform: true, + /* legacy android transform properties */ + scaleX: true, + scaleY: true, + translateX: true, + translateY: true, +}; + +const TRANSFORM_WHITELIST = { + translateX: true, + translateY: true, + scale: true, + scaleX: true, + scaleY: true, + rotate: true, + rotateX: true, + rotateY: true, + perspective: true, +}; + +function validateTransform(configs: Array): void { + configs.forEach(config => { + if (!TRANSFORM_WHITELIST.hasOwnProperty(config.property)) { + throw new Error( + `Property '${config.property}' is not supported by native animated module`, + ); + } + }); +} + +function validateStyles(styles: Object): void { + for (var key in styles) { + if (!STYLES_WHITELIST.hasOwnProperty(key)) { + throw new Error( + `Style property '${key}' is not supported by native animated module`, + ); + } + } +} + +function validateInterpolation(config: Object): void { + var SUPPORTED_INTERPOLATION_PARAMS = { + inputRange: true, + outputRange: true, + extrapolate: true, + extrapolateRight: true, + extrapolateLeft: true, + }; + for (var key in config) { + if (!SUPPORTED_INTERPOLATION_PARAMS.hasOwnProperty(key)) { + throw new Error( + `Interpolation property '${key}' is not supported by native animated module`, + ); + } + } +} + +function generateNewNodeTag(): number { + return __nativeAnimatedNodeTagCount++; +} + +function generateNewAnimationId(): number { + return __nativeAnimationIdCount++; +} + +function assertNativeAnimatedModule(): void { + invariant(NativeAnimatedModule, 'Native animated module is not available'); +} + +let _warnedMissingNativeAnimated = false; + +function shouldUseNativeDriver(config: AnimationConfig | EventConfig): boolean { + if (config.useNativeDriver && !NativeAnimatedModule) { + if (!_warnedMissingNativeAnimated) { + console.warn( + 'Animated: `useNativeDriver` is not supported because the native ' + + 'animated module is missing. Falling back to JS-based animation. To ' + + 'resolve this, add `RCTAnimation` module to this app, or remove ' + + '`useNativeDriver`. ' + + 'More info: https://github.com/facebook/react-native/issues/11094#issuecomment-263240420', + ); + _warnedMissingNativeAnimated = true; + } + return false; + } + + return config.useNativeDriver || false; +} + +module.exports = { + API, + validateStyles, + validateTransform, + validateInterpolation, + generateNewNodeTag, + generateNewAnimationId, + assertNativeAnimatedModule, + shouldUseNativeDriver, + get nativeEventEmitter() { + // if (!nativeEventEmitter) { + // nativeEventEmitter = new NativeEventEmitter(NativeAnimatedModule); + // } + return nativeEventEmitter; + }, +}; diff --git a/Libraries/Animated/src/SpringConfig.js b/Libraries/Animated/src/SpringConfig.js new file mode 100644 index 0000000..e74d167 --- /dev/null +++ b/Libraries/Animated/src/SpringConfig.js @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule SpringConfig + * @flow + */ + +'use strict'; + +type SpringConfigType = { + stiffness: number, + damping: number, +}; + +function stiffnessFromOrigamiValue(oValue) { + return (oValue - 30) * 3.62 + 194; +} + +function dampingFromOrigamiValue(oValue) { + return (oValue - 8) * 3 + 25; +} + +function fromOrigamiTensionAndFriction( + tension: number, + friction: number, +): SpringConfigType { + return { + stiffness: stiffnessFromOrigamiValue(tension), + damping: dampingFromOrigamiValue(friction), + }; +} + +function fromBouncinessAndSpeed( + bounciness: number, + speed: number, +): SpringConfigType { + function normalize(value, startValue, endValue) { + return (value - startValue) / (endValue - startValue); + } + + function projectNormal(n, start, end) { + return start + (n * (end - start)); + } + + function linearInterpolation(t, start, end) { + return t * end + (1 - t) * start; + } + + function quadraticOutInterpolation(t, start, end) { + return linearInterpolation(2 * t - t * t, start, end); + } + + function b3Friction1(x) { + return (0.0007 * Math.pow(x, 3)) - + (0.031 * Math.pow(x, 2)) + 0.64 * x + 1.28; + } + + function b3Friction2(x) { + return (0.000044 * Math.pow(x, 3)) - + (0.006 * Math.pow(x, 2)) + 0.36 * x + 2; + } + + function b3Friction3(x) { + return (0.00000045 * Math.pow(x, 3)) - + (0.000332 * Math.pow(x, 2)) + 0.1078 * x + 5.84; + } + + function b3Nobounce(tension) { + if (tension <= 18) { + return b3Friction1(tension); + } else if (tension > 18 && tension <= 44) { + return b3Friction2(tension); + } else { + return b3Friction3(tension); + } + } + + var b = normalize(bounciness / 1.7, 0, 20); + b = projectNormal(b, 0, 0.8); + var s = normalize(speed / 1.7, 0, 20); + var bouncyTension = projectNormal(s, 0.5, 200); + var bouncyFriction = quadraticOutInterpolation( + b, + b3Nobounce(bouncyTension), + 0.01 + ); + + return { + stiffness: stiffnessFromOrigamiValue(bouncyTension), + damping: dampingFromOrigamiValue(bouncyFriction), + }; +} + +module.exports = { + fromOrigamiTensionAndFriction, + fromBouncinessAndSpeed, +}; diff --git a/Libraries/Animated/src/__tests__/Animated-test.js b/Libraries/Animated/src/__tests__/Animated-test.js new file mode 100644 index 0000000..6476e18 --- /dev/null +++ b/Libraries/Animated/src/__tests__/Animated-test.js @@ -0,0 +1,792 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +var Animated = require('Animated'); +describe('Animated tests', () => { + beforeEach(() => { + jest.resetModules(); + }); + + describe('Animated', () => { + + it('works end to end', () => { + var anim = new Animated.Value(0); + + var callback = jest.fn(); + + var node = new Animated.__PropsOnlyForTests({ + style: { + backgroundColor: 'red', + opacity: anim, + transform: [ + {translateX: anim.interpolate({ + inputRange: [0, 1], + outputRange: [100, 200], + })}, + {scale: anim}, + ], + shadowOffset: { + width: anim, + height: anim, + }, + } + }, callback); + + expect(anim.__getChildren().length).toBe(3); + + expect(node.__getValue()).toEqual({ + style: { + backgroundColor: 'red', + opacity: 0, + transform: [ + {translateX: 100}, + {scale: 0}, + ], + shadowOffset: { + width: 0, + height: 0, + }, + }, + }); + + anim.setValue(0.5); + + expect(callback).toBeCalled(); + + expect(node.__getValue()).toEqual({ + style: { + backgroundColor: 'red', + opacity: 0.5, + transform: [ + {translateX: 150}, + {scale: 0.5}, + ], + shadowOffset: { + width: 0.5, + height: 0.5, + }, + }, + }); + + node.__detach(); + expect(anim.__getChildren().length).toBe(0); + + anim.setValue(1); + expect(callback.mock.calls.length).toBe(1); + }); + + it('does not detach on updates', () => { + var anim = new Animated.Value(0); + anim.__detach = jest.fn(); + + var c = new Animated.View(); + c.props = { + style: { + opacity: anim, + }, + }; + c.componentWillMount(); + + expect(anim.__detach).not.toBeCalled(); + c._component = {}; + c.componentWillReceiveProps({ + style: { + opacity: anim, + }, + }); + expect(anim.__detach).not.toBeCalled(); + + c.componentWillUnmount(); + expect(anim.__detach).toBeCalled(); + }); + + + it('stops animation when detached', () => { + var anim = new Animated.Value(0); + var callback = jest.fn(); + + var c = new Animated.View(); + c.props = { + style: { + opacity: anim, + }, + }; + c.componentWillMount(); + + Animated.timing(anim, {toValue: 10, duration: 1000}).start(callback); + c._component = {}; + c.componentWillUnmount(); + + expect(callback).toBeCalledWith({finished: false}); + expect(callback).toBeCalledWith({finished: false}); + }); + + it('triggers callback when spring is at rest', () => { + var anim = new Animated.Value(0); + var callback = jest.fn(); + Animated.spring(anim, {toValue: 0, velocity: 0}).start(callback); + expect(callback).toBeCalled(); + }); + + it('send toValue when an underdamped spring stops', () => { + var anim = new Animated.Value(0); + var listener = jest.fn(); + anim.addListener(listener); + Animated.spring(anim, {toValue: 15}).start(); + jest.runAllTimers(); + var lastValue = listener.mock.calls[listener.mock.calls.length - 2][0].value; + expect(lastValue).not.toBe(15); + expect(lastValue).toBeCloseTo(15); + expect(anim.__getValue()).toBe(15); + }); + + it('send toValue when a critically damped spring stops', () => { + var anim = new Animated.Value(0); + var listener = jest.fn(); + anim.addListener(listener); + Animated.spring(anim, {stiffness: 8000, damping: 2000, toValue: 15}).start(); + jest.runAllTimers(); + var lastValue = listener.mock.calls[listener.mock.calls.length - 2][0].value; + expect(lastValue).not.toBe(15); + expect(lastValue).toBeCloseTo(15); + expect(anim.__getValue()).toBe(15); + }); + + it('convert to JSON', () => { + expect(JSON.stringify(new Animated.Value(10))).toBe('10'); + }); + }); + + + describe('Animated Sequence', () => { + + it('works with an empty sequence', () => { + var cb = jest.fn(); + Animated.sequence([]).start(cb); + expect(cb).toBeCalledWith({finished: true}); + }); + + it('sequences well', () => { + var anim1 = {start: jest.fn()}; + var anim2 = {start: jest.fn()}; + var cb = jest.fn(); + + var seq = Animated.sequence([anim1, anim2]); + + expect(anim1.start).not.toBeCalled(); + expect(anim2.start).not.toBeCalled(); + + seq.start(cb); + + expect(anim1.start).toBeCalled(); + expect(anim2.start).not.toBeCalled(); + expect(cb).not.toBeCalled(); + + anim1.start.mock.calls[0][0]({finished: true}); + + expect(anim2.start).toBeCalled(); + expect(cb).not.toBeCalled(); + + anim2.start.mock.calls[0][0]({finished: true}); + expect(cb).toBeCalledWith({finished: true}); + }); + + it('supports interrupting sequence', () => { + var anim1 = {start: jest.fn()}; + var anim2 = {start: jest.fn()}; + var cb = jest.fn(); + + Animated.sequence([anim1, anim2]).start(cb); + + anim1.start.mock.calls[0][0]({finished: false}); + + expect(anim1.start).toBeCalled(); + expect(anim2.start).not.toBeCalled(); + expect(cb).toBeCalledWith({finished: false}); + }); + + it('supports stopping sequence', () => { + var anim1 = {start: jest.fn(), stop: jest.fn()}; + var anim2 = {start: jest.fn(), stop: jest.fn()}; + var cb = jest.fn(); + + var seq = Animated.sequence([anim1, anim2]); + seq.start(cb); + seq.stop(); + + expect(anim1.stop).toBeCalled(); + expect(anim2.stop).not.toBeCalled(); + expect(cb).not.toBeCalled(); + + anim1.start.mock.calls[0][0]({finished: false}); + + expect(cb).toBeCalledWith({finished: false}); + }); + }); + + describe('Animated Loop', () => { + + it('loops indefinitely if config not specified', () => { + var animation = {start: jest.fn(), reset: jest.fn(), _isUsingNativeDriver: () => false}; + var cb = jest.fn(); + + var loop = Animated.loop(animation); + + expect(animation.start).not.toBeCalled(); + + loop.start(cb); + + expect(animation.start).toBeCalled(); + expect(animation.reset).toHaveBeenCalledTimes(1); + expect(cb).not.toBeCalled(); + + animation.start.mock.calls[0][0]({finished: true}); // End of loop 1 + expect(animation.reset).toHaveBeenCalledTimes(2); + expect(cb).not.toBeCalled(); + + animation.start.mock.calls[0][0]({finished: true}); // End of loop 2 + expect(animation.reset).toHaveBeenCalledTimes(3); + expect(cb).not.toBeCalled(); + + animation.start.mock.calls[0][0]({finished: true}); // End of loop 3 + expect(animation.reset).toHaveBeenCalledTimes(4); + expect(cb).not.toBeCalled(); + }); + + it('loops indefinitely if iterations is -1', () => { + var animation = {start: jest.fn(), reset: jest.fn(), _isUsingNativeDriver: () => false}; + var cb = jest.fn(); + + var loop = Animated.loop(animation, { iterations: -1 }); + + expect(animation.start).not.toBeCalled(); + + loop.start(cb); + + expect(animation.start).toBeCalled(); + expect(animation.reset).toHaveBeenCalledTimes(1); + expect(cb).not.toBeCalled(); + + animation.start.mock.calls[0][0]({finished: true}); // End of loop 1 + expect(animation.reset).toHaveBeenCalledTimes(2); + expect(cb).not.toBeCalled(); + + animation.start.mock.calls[0][0]({finished: true}); // End of loop 2 + expect(animation.reset).toHaveBeenCalledTimes(3); + expect(cb).not.toBeCalled(); + + animation.start.mock.calls[0][0]({finished: true}); // End of loop 3 + expect(animation.reset).toHaveBeenCalledTimes(4); + expect(cb).not.toBeCalled(); + }); + + it('loops indefinitely if iterations not specified', () => { + var animation = {start: jest.fn(), reset: jest.fn(), _isUsingNativeDriver: () => false}; + var cb = jest.fn(); + + var loop = Animated.loop(animation, { anotherKey: 'value' }); + + expect(animation.start).not.toBeCalled(); + + loop.start(cb); + + expect(animation.start).toBeCalled(); + expect(animation.reset).toHaveBeenCalledTimes(1); + expect(cb).not.toBeCalled(); + + animation.start.mock.calls[0][0]({finished: true}); // End of loop 1 + expect(animation.reset).toHaveBeenCalledTimes(2); + expect(cb).not.toBeCalled(); + + animation.start.mock.calls[0][0]({finished: true}); // End of loop 2 + expect(animation.reset).toHaveBeenCalledTimes(3); + expect(cb).not.toBeCalled(); + + animation.start.mock.calls[0][0]({finished: true}); // End of loop 3 + expect(animation.reset).toHaveBeenCalledTimes(4); + expect(cb).not.toBeCalled(); + }); + + it('loops three times if iterations is 3', () => { + var animation = {start: jest.fn(), reset: jest.fn(), _isUsingNativeDriver: () => false}; + var cb = jest.fn(); + + var loop = Animated.loop(animation, { iterations: 3 }); + + expect(animation.start).not.toBeCalled(); + + loop.start(cb); + + expect(animation.start).toBeCalled(); + expect(animation.reset).toHaveBeenCalledTimes(1); + expect(cb).not.toBeCalled(); + + animation.start.mock.calls[0][0]({finished: true}); // End of loop 1 + expect(animation.reset).toHaveBeenCalledTimes(2); + expect(cb).not.toBeCalled(); + + animation.start.mock.calls[0][0]({finished: true}); // End of loop 2 + expect(animation.reset).toHaveBeenCalledTimes(3); + expect(cb).not.toBeCalled(); + + animation.start.mock.calls[0][0]({finished: true}); // End of loop 3 + expect(animation.reset).toHaveBeenCalledTimes(3); + expect(cb).toBeCalledWith({finished: true}); + }); + + it('does not loop if iterations is 1', () => { + var animation = {start: jest.fn(), reset: jest.fn(), _isUsingNativeDriver: () => false}; + var cb = jest.fn(); + + var loop = Animated.loop(animation, { iterations: 1 }); + + expect(animation.start).not.toBeCalled(); + + loop.start(cb); + + expect(animation.start).toBeCalled(); + expect(cb).not.toBeCalled(); + + animation.start.mock.calls[0][0]({finished: true}); // End of loop 1 + expect(cb).toBeCalledWith({finished: true}); + }); + + it('does not animate if iterations is 0', () => { + var animation = {start: jest.fn(), reset: jest.fn(), _isUsingNativeDriver: () => false}; + var cb = jest.fn(); + + var loop = Animated.loop(animation, { iterations: 0 }); + + expect(animation.start).not.toBeCalled(); + + loop.start(cb); + + expect(animation.start).not.toBeCalled(); + expect(cb).toBeCalledWith({ finished: true }); + }); + + it('supports interrupting an indefinite loop', () => { + var animation = {start: jest.fn(), reset: jest.fn(), _isUsingNativeDriver: () => false}; + var cb = jest.fn(); + + Animated.loop(animation).start(cb); + expect(animation.start).toBeCalled(); + expect(animation.reset).toHaveBeenCalledTimes(1); + expect(cb).not.toBeCalled(); + + animation.start.mock.calls[0][0]({finished: true}); // End of loop 1 + expect(animation.reset).toHaveBeenCalledTimes(2); + expect(cb).not.toBeCalled(); + + animation.start.mock.calls[0][0]({finished: false}); // Interrupt loop + expect(animation.reset).toHaveBeenCalledTimes(2); + expect(cb).toBeCalledWith({finished: false}); + }); + + it('supports stopping loop', () => { + var animation = {start: jest.fn(), stop: jest.fn(), reset: jest.fn(), _isUsingNativeDriver: () => false}; + var cb = jest.fn(); + + var loop = Animated.loop(animation); + loop.start(cb); + loop.stop(); + + expect(animation.start).toBeCalled(); + expect(animation.reset).toHaveBeenCalledTimes(1); + expect(animation.stop).toBeCalled(); + + animation.start.mock.calls[0][0]({finished: false}); // Interrupt loop + expect(animation.reset).toHaveBeenCalledTimes(1); + expect(cb).toBeCalledWith({finished: false}); + }); + }); + + describe('Animated Parallel', () => { + + it('works with an empty parallel', () => { + var cb = jest.fn(); + Animated.parallel([]).start(cb); + expect(cb).toBeCalledWith({finished: true}); + }); + + it('works with an empty element in array', () => { + var anim1 = {start: jest.fn()}; + var cb = jest.fn(); + Animated.parallel([null, anim1]).start(cb); + + expect(anim1.start).toBeCalled(); + anim1.start.mock.calls[0][0]({finished: true}); + + expect(cb).toBeCalledWith({finished: true}); + }); + + it('parellelizes well', () => { + var anim1 = {start: jest.fn()}; + var anim2 = {start: jest.fn()}; + var cb = jest.fn(); + + var par = Animated.parallel([anim1, anim2]); + + expect(anim1.start).not.toBeCalled(); + expect(anim2.start).not.toBeCalled(); + + par.start(cb); + + expect(anim1.start).toBeCalled(); + expect(anim2.start).toBeCalled(); + expect(cb).not.toBeCalled(); + + anim1.start.mock.calls[0][0]({finished: true}); + expect(cb).not.toBeCalled(); + + anim2.start.mock.calls[0][0]({finished: true}); + expect(cb).toBeCalledWith({finished: true}); + }); + + it('supports stopping parallel', () => { + var anim1 = {start: jest.fn(), stop: jest.fn()}; + var anim2 = {start: jest.fn(), stop: jest.fn()}; + var cb = jest.fn(); + + var seq = Animated.parallel([anim1, anim2]); + seq.start(cb); + seq.stop(); + + expect(anim1.stop).toBeCalled(); + expect(anim2.stop).toBeCalled(); + expect(cb).not.toBeCalled(); + + anim1.start.mock.calls[0][0]({finished: false}); + expect(cb).not.toBeCalled(); + + anim2.start.mock.calls[0][0]({finished: false}); + expect(cb).toBeCalledWith({finished: false}); + }); + + + it('does not call stop more than once when stopping', () => { + var anim1 = {start: jest.fn(), stop: jest.fn()}; + var anim2 = {start: jest.fn(), stop: jest.fn()}; + var anim3 = {start: jest.fn(), stop: jest.fn()}; + var cb = jest.fn(); + + var seq = Animated.parallel([anim1, anim2, anim3]); + seq.start(cb); + + anim1.start.mock.calls[0][0]({finished: false}); + + expect(anim1.stop.mock.calls.length).toBe(0); + expect(anim2.stop.mock.calls.length).toBe(1); + expect(anim3.stop.mock.calls.length).toBe(1); + + anim2.start.mock.calls[0][0]({finished: false}); + + expect(anim1.stop.mock.calls.length).toBe(0); + expect(anim2.stop.mock.calls.length).toBe(1); + expect(anim3.stop.mock.calls.length).toBe(1); + + anim3.start.mock.calls[0][0]({finished: false}); + + expect(anim1.stop.mock.calls.length).toBe(0); + expect(anim2.stop.mock.calls.length).toBe(1); + expect(anim3.stop.mock.calls.length).toBe(1); + }); + }); + + describe('Animated delays', () => { + it('should call anim after delay in sequence', () => { + var anim = {start: jest.fn(), stop: jest.fn()}; + var cb = jest.fn(); + Animated.sequence([ + Animated.delay(1000), + anim, + ]).start(cb); + jest.runAllTimers(); + expect(anim.start.mock.calls.length).toBe(1); + expect(cb).not.toBeCalled(); + anim.start.mock.calls[0][0]({finished: true}); + expect(cb).toBeCalledWith({finished: true}); + }); + it('should run stagger to end', () => { + var cb = jest.fn(); + Animated.stagger(1000, [ + Animated.delay(1000), + Animated.delay(1000), + Animated.delay(1000), + ]).start(cb); + jest.runAllTimers(); + expect(cb).toBeCalledWith({finished: true}); + }); + }); + + describe('Animated Events', () => { + it('should map events', () => { + var value = new Animated.Value(0); + var handler = Animated.event( + [null, {state: {foo: value}}], + ); + handler({bar: 'ignoreBar'}, {state: {baz: 'ignoreBaz', foo: 42}}); + expect(value.__getValue()).toBe(42); + }); + it('should call listeners', () => { + var value = new Animated.Value(0); + var listener = jest.fn(); + var handler = Animated.event( + [{foo: value}], + {listener}, + ); + handler({foo: 42}); + expect(value.__getValue()).toBe(42); + expect(listener.mock.calls.length).toBe(1); + expect(listener).toBeCalledWith({foo: 42}); + }); + it('should call forked event listeners', () => { + var value = new Animated.Value(0); + var listener = jest.fn(); + var handler = Animated.event( + [{foo: value}], + {listener}, + ); + var listener2 = jest.fn(); + var forkedHandler = Animated.forkEvent(handler, listener2); + forkedHandler({foo: 42}); + expect(value.__getValue()).toBe(42); + expect(listener.mock.calls.length).toBe(1); + expect(listener).toBeCalledWith({foo: 42}); + expect(listener2.mock.calls.length).toBe(1); + expect(listener2).toBeCalledWith({foo: 42}); + }); + }); + + describe('Animated Interactions', () => { + /*eslint-disable no-shadow*/ + var Animated; + /*eslint-enable*/ + var InteractionManager; + + beforeEach(() => { + jest.mock('InteractionManager'); + Animated = require('Animated'); + InteractionManager = require('InteractionManager'); + }); + + afterEach(()=> { + jest.unmock('InteractionManager'); + }); + + it('registers an interaction by default', () => { + InteractionManager.createInteractionHandle.mockReturnValue(777); + + var value = new Animated.Value(0); + var callback = jest.fn(); + Animated.timing(value, { + toValue: 100, + duration: 100, + }).start(callback); + jest.runAllTimers(); + + expect(InteractionManager.createInteractionHandle).toBeCalled(); + expect(InteractionManager.clearInteractionHandle).toBeCalledWith(777); + expect(callback).toBeCalledWith({finished: true}); + }); + + it('does not register an interaction when specified', () => { + var value = new Animated.Value(0); + var callback = jest.fn(); + Animated.timing(value, { + toValue: 100, + duration: 100, + isInteraction: false, + }).start(callback); + jest.runAllTimers(); + + expect(InteractionManager.createInteractionHandle).not.toBeCalled(); + expect(InteractionManager.clearInteractionHandle).not.toBeCalled(); + expect(callback).toBeCalledWith({finished: true}); + }); + }); + + describe('Animated Tracking', () => { + it('should track values', () => { + var value1 = new Animated.Value(0); + var value2 = new Animated.Value(0); + Animated.timing(value2, { + toValue: value1, + duration: 0, + }).start(); + value1.setValue(42); + expect(value2.__getValue()).toBe(42); + value1.setValue(7); + expect(value2.__getValue()).toBe(7); + }); + + it('should track interpolated values', () => { + var value1 = new Animated.Value(0); + var value2 = new Animated.Value(0); + Animated.timing(value2, { + toValue: value1.interpolate({ + inputRange: [0, 2], + outputRange: [0, 1] + }), + duration: 0, + }).start(); + value1.setValue(42); + expect(value2.__getValue()).toBe(42 / 2); + }); + + it('should stop tracking when animated', () => { + var value1 = new Animated.Value(0); + var value2 = new Animated.Value(0); + Animated.timing(value2, { + toValue: value1, + duration: 0, + }).start(); + value1.setValue(42); + expect(value2.__getValue()).toBe(42); + Animated.timing(value2, { + toValue: 7, + duration: 0, + }).start(); + value1.setValue(1492); + expect(value2.__getValue()).toBe(7); + }); + }); + + describe('Animated Vectors', () => { + it('should animate vectors', () => { + var vec = new Animated.ValueXY(); + + var callback = jest.fn(); + + var node = new Animated.__PropsOnlyForTests({ + style: { + opacity: vec.x.interpolate({ + inputRange: [0, 42], + outputRange: [0.2, 0.8], + }), + transform: vec.getTranslateTransform(), + ...vec.getLayout(), + } + }, callback); + + expect(node.__getValue()).toEqual({ + style: { + opacity: 0.2, + transform: [ + {translateX: 0}, + {translateY: 0}, + ], + left: 0, + top: 0, + }, + }); + + vec.setValue({x: 42, y: 1492}); + + expect(callback.mock.calls.length).toBe(2); // once each for x, y + + expect(node.__getValue()).toEqual({ + style: { + opacity: 0.8, + transform: [ + {translateX: 42}, + {translateY: 1492}, + ], + left: 42, + top: 1492, + }, + }); + + node.__detach(); + + vec.setValue({x: 1, y: 1}); + expect(callback.mock.calls.length).toBe(2); + }); + + it('should track vectors', () => { + var value1 = new Animated.ValueXY(); + var value2 = new Animated.ValueXY(); + Animated.timing(value2, { + toValue: value1, + duration: 0, + }).start(); + value1.setValue({x: 42, y: 1492}); + expect(value2.__getValue()).toEqual({x: 42, y: 1492}); + + // Make sure tracking keeps working (see stopTogether in ParallelConfig used + // by maybeVectorAnim). + value1.setValue({x: 3, y: 4}); + expect(value2.__getValue()).toEqual({x: 3, y: 4}); + }); + + it('should track with springs', () => { + var value1 = new Animated.ValueXY(); + var value2 = new Animated.ValueXY(); + Animated.spring(value2, { + toValue: value1, + tension: 3000, // faster spring for faster test + friction: 60, + }).start(); + value1.setValue({x: 1, y: 1}); + jest.runAllTimers(); + expect(Math.round(value2.__getValue().x)).toEqual(1); + expect(Math.round(value2.__getValue().y)).toEqual(1); + value1.setValue({x: 2, y: 2}); + jest.runAllTimers(); + expect(Math.round(value2.__getValue().x)).toEqual(2); + expect(Math.round(value2.__getValue().y)).toEqual(2); + }); + }); + + describe('Animated Listeners', () => { + it('should get updates', () => { + var value1 = new Animated.Value(0); + var listener = jest.fn(); + var id = value1.addListener(listener); + value1.setValue(42); + expect(listener.mock.calls.length).toBe(1); + expect(listener).toBeCalledWith({value: 42}); + expect(value1.__getValue()).toBe(42); + value1.setValue(7); + expect(listener.mock.calls.length).toBe(2); + expect(listener).toBeCalledWith({value: 7}); + expect(value1.__getValue()).toBe(7); + value1.removeListener(id); + value1.setValue(1492); + expect(listener.mock.calls.length).toBe(2); + expect(value1.__getValue()).toBe(1492); + }); + + it('should removeAll', () => { + var value1 = new Animated.Value(0); + var listener = jest.fn(); + [1,2,3,4].forEach(() => value1.addListener(listener)); + value1.setValue(42); + expect(listener.mock.calls.length).toBe(4); + expect(listener).toBeCalledWith({value: 42}); + value1.removeAllListeners(); + value1.setValue(7); + expect(listener.mock.calls.length).toBe(4); + }); + }); + + describe('Animated Diff Clamp', () => { + it('should get the proper value', () => { + const inputValues = [0, 20, 40, 30, 0, -40, -10, -20, 0]; + const expectedValues = [0, 20, 20, 10, 0, 0, 20, 10, 20]; + const value = new Animated.Value(0); + const diffClampValue = Animated.diffClamp(value, 0, 20); + for (let i = 0; i < inputValues.length; i++) { + value.setValue(inputValues[i]); + expect(diffClampValue.__getValue()).toBe(expectedValues[i]); + } + }); + }); +}); diff --git a/Libraries/Animated/src/__tests__/AnimatedNative-test.js b/Libraries/Animated/src/__tests__/AnimatedNative-test.js new file mode 100644 index 0000000..324c02f --- /dev/null +++ b/Libraries/Animated/src/__tests__/AnimatedNative-test.js @@ -0,0 +1,700 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +jest + .clearAllMocks() + .setMock('Text', {}) + .setMock('View', {}) + .setMock('Image', {}) + .setMock('React', {Component: class {}}) + .setMock('NativeModules', { + NativeAnimatedModule: {}, + }) + .mock('NativeEventEmitter') + // findNodeHandle is imported from ReactNative so mock that whole module. + .setMock('ReactNative', {findNodeHandle: () => 1}); + +const Animated = require('Animated'); +const NativeAnimatedHelper = require('NativeAnimatedHelper'); + +function createAndMountComponent(ComponentClass, props) { + const component = new ComponentClass(); + component.props = props; + component.componentWillMount(); + // Simulate that refs were set. + component._component = {}; + component.componentDidMount(); + return component; +} + +describe('Native Animated', () => { + + const nativeAnimatedModule = require('NativeModules').NativeAnimatedModule; + + beforeEach(() => { + nativeAnimatedModule.addAnimatedEventToView = jest.fn(); + nativeAnimatedModule.connectAnimatedNodes = jest.fn(); + nativeAnimatedModule.connectAnimatedNodeToView = jest.fn(); + nativeAnimatedModule.createAnimatedNode = jest.fn(); + nativeAnimatedModule.disconnectAnimatedNodeFromView = jest.fn(); + nativeAnimatedModule.disconnectAnimatedNodes = jest.fn(); + nativeAnimatedModule.dropAnimatedNode = jest.fn(); + nativeAnimatedModule.extractAnimatedNodeOffset = jest.fn(); + nativeAnimatedModule.flattenAnimatedNodeOffset = jest.fn(); + nativeAnimatedModule.removeAnimatedEventFromView = jest.fn(); + nativeAnimatedModule.setAnimatedNodeOffset = jest.fn(); + nativeAnimatedModule.setAnimatedNodeValue = jest.fn(); + nativeAnimatedModule.startAnimatingNode = jest.fn(); + nativeAnimatedModule.startListeningToAnimatedNodeValue = jest.fn(); + nativeAnimatedModule.stopAnimation = jest.fn(); + nativeAnimatedModule.stopListeningToAnimatedNodeValue = jest.fn(); + }); + + describe('Animated Value', () => { + it('proxies `setValue` correctly', () => { + const anim = new Animated.Value(0); + Animated.timing(anim, {toValue: 10, duration: 1000, useNativeDriver: true}).start(); + + const c = createAndMountComponent(Animated.View, { + style: { + opacity: anim, + }, + }); + + // We expect `setValue` not to propagate down to `setNativeProps`, otherwise it may try to access `setNativeProps` + // via component refs table that we override here. + c.refs = { + node: { + setNativeProps: jest.genMockFunction(), + }, + }; + + anim.setValue(0.5); + + expect(nativeAnimatedModule.setAnimatedNodeValue).toBeCalledWith(jasmine.any(Number), 0.5); + expect(c.refs.node.setNativeProps).not.toHaveBeenCalled(); + }); + + it('should set offset', () => { + const anim = new Animated.Value(0); + anim.setOffset(10); + anim.__makeNative(); + createAndMountComponent(Animated.View, { + style: { + opacity: anim, + }, + }); + + expect(nativeAnimatedModule.createAnimatedNode).toBeCalledWith( + jasmine.any(Number), + {type: 'value', value: 0, offset: 10}, + ); + anim.setOffset(20); + expect(nativeAnimatedModule.setAnimatedNodeOffset) + .toBeCalledWith(jasmine.any(Number), 20); + }); + + it('should flatten offset', () => { + const anim = new Animated.Value(0); + anim.__makeNative(); + createAndMountComponent(Animated.View, { + style: { + opacity: anim, + }, + }); + + expect(nativeAnimatedModule.createAnimatedNode).toBeCalledWith( + jasmine.any(Number), + {type: 'value', value: 0, offset: 0}, + ); + anim.flattenOffset(); + expect(nativeAnimatedModule.flattenAnimatedNodeOffset) + .toBeCalledWith(jasmine.any(Number)); + }); + + it('should extract offset', () => { + const anim = new Animated.Value(0); + anim.__makeNative(); + createAndMountComponent(Animated.View, { + style: { + opacity: anim, + }, + }); + + expect(nativeAnimatedModule.createAnimatedNode).toBeCalledWith( + jasmine.any(Number), + {type: 'value', value: 0, offset: 0}, + ); + anim.extractOffset(); + expect(nativeAnimatedModule.extractAnimatedNodeOffset) + .toBeCalledWith(jasmine.any(Number)); + }); + }); + + describe('Animated Listeners', () => { + it('should get updates', () => { + const value1 = new Animated.Value(0); + value1.__makeNative(); + const listener = jest.fn(); + const id = value1.addListener(listener); + expect(nativeAnimatedModule.startListeningToAnimatedNodeValue) + .toHaveBeenCalledWith(value1.__getNativeTag()); + + NativeAnimatedHelper.nativeEventEmitter.emit( + 'onAnimatedValueUpdate', + {value: 42, tag: value1.__getNativeTag()}, + ); + expect(listener).toHaveBeenCalledTimes(1); + expect(listener).toBeCalledWith({value: 42}); + expect(value1.__getValue()).toBe(42); + + NativeAnimatedHelper.nativeEventEmitter.emit( + 'onAnimatedValueUpdate', + {value: 7, tag: value1.__getNativeTag()}, + ); + expect(listener).toHaveBeenCalledTimes(2); + expect(listener).toBeCalledWith({value: 7}); + expect(value1.__getValue()).toBe(7); + + value1.removeListener(id); + expect(nativeAnimatedModule.stopListeningToAnimatedNodeValue) + .toHaveBeenCalledWith(value1.__getNativeTag()); + + NativeAnimatedHelper.nativeEventEmitter.emit( + 'onAnimatedValueUpdate', + {value: 1492, tag: value1.__getNativeTag()}, + ); + expect(listener).toHaveBeenCalledTimes(2); + expect(value1.__getValue()).toBe(7); + }); + + it('should removeAll', () => { + const value1 = new Animated.Value(0); + value1.__makeNative(); + const listener = jest.fn(); + [1,2,3,4].forEach(() => value1.addListener(listener)); + expect(nativeAnimatedModule.startListeningToAnimatedNodeValue) + .toHaveBeenCalledWith(value1.__getNativeTag()); + + NativeAnimatedHelper.nativeEventEmitter.emit( + 'onAnimatedValueUpdate', + {value: 42, tag: value1.__getNativeTag()}, + ); + expect(listener).toHaveBeenCalledTimes(4); + expect(listener).toBeCalledWith({value: 42}); + + value1.removeAllListeners(); + expect(nativeAnimatedModule.stopListeningToAnimatedNodeValue) + .toHaveBeenCalledWith(value1.__getNativeTag()); + + NativeAnimatedHelper.nativeEventEmitter.emit( + 'onAnimatedValueUpdate', + {value: 7, tag: value1.__getNativeTag()}, + ); + expect(listener).toHaveBeenCalledTimes(4); + }); + }); + + describe('Animated Events', () => { + it('should map events', () => { + const value = new Animated.Value(0); + value.__makeNative(); + const event = Animated.event( + [{nativeEvent: {state: {foo: value}}}], + {useNativeDriver: true}, + ); + const c = createAndMountComponent(Animated.View, {onTouchMove: event}); + expect(nativeAnimatedModule.addAnimatedEventToView).toBeCalledWith( + jasmine.any(Number), + 'onTouchMove', + {nativeEventPath: ['state', 'foo'], animatedValueTag: value.__getNativeTag()}, + ); + + c.componentWillUnmount(); + expect(nativeAnimatedModule.removeAnimatedEventFromView).toBeCalledWith( + jasmine.any(Number), + 'onTouchMove', + value.__getNativeTag(), + ); + }); + + it('should throw on invalid event path', () => { + const value = new Animated.Value(0); + value.__makeNative(); + const event = Animated.event( + [{notNativeEvent: {foo: value}}], + {useNativeDriver: true}, + ); + expect(() => createAndMountComponent(Animated.View, {onTouchMove: event})) + .toThrowError(/nativeEvent/); + expect(nativeAnimatedModule.addAnimatedEventToView).not.toBeCalled(); + }); + + it('should call listeners', () => { + const value = new Animated.Value(0); + value.__makeNative(); + const listener = jest.fn(); + const event = Animated.event( + [{nativeEvent: {foo: value}}], + {useNativeDriver: true, listener}, + ); + const handler = event.__getHandler(); + handler({foo: 42}); + expect(listener).toHaveBeenCalledTimes(1); + expect(listener).toBeCalledWith({foo: 42}); + }); + }); + + describe('Animated Graph', () => { + it('creates and detaches nodes', () => { + const anim = new Animated.Value(0); + const c = createAndMountComponent(Animated.View, { + style: { + opacity: anim, + }, + }); + + Animated.timing(anim, {toValue: 10, duration: 1000, useNativeDriver: true}).start(); + + c.componentWillUnmount(); + + expect(nativeAnimatedModule.createAnimatedNode).toHaveBeenCalledTimes(3); + expect(nativeAnimatedModule.connectAnimatedNodes).toHaveBeenCalledTimes(2); + + expect(nativeAnimatedModule.startAnimatingNode).toBeCalledWith( + jasmine.any(Number), + jasmine.any(Number), + {type: 'frames', frames: jasmine.any(Array), toValue: jasmine.any(Number), iterations: 1}, + jasmine.any(Function) + ); + + expect(nativeAnimatedModule.disconnectAnimatedNodes).toHaveBeenCalledTimes(2); + expect(nativeAnimatedModule.dropAnimatedNode).toHaveBeenCalledTimes(3); + }); + + it('sends a valid description for value, style and props nodes', () => { + const anim = new Animated.Value(0); + createAndMountComponent(Animated.View, { + style: { + opacity: anim, + }, + }); + + Animated.timing(anim, {toValue: 10, duration: 1000, useNativeDriver: true}).start(); + + expect(nativeAnimatedModule.createAnimatedNode) + .toBeCalledWith(jasmine.any(Number), {type: 'value', value: 0, offset: 0}); + expect(nativeAnimatedModule.createAnimatedNode) + .toBeCalledWith(jasmine.any(Number), {type: 'style', style: {opacity: jasmine.any(Number)}}); + expect(nativeAnimatedModule.createAnimatedNode) + .toBeCalledWith(jasmine.any(Number), {type: 'props', props: {style: jasmine.any(Number)}}); + }); + + it('sends a valid graph description for Animated.add nodes', () => { + const first = new Animated.Value(1); + const second = new Animated.Value(2); + first.__makeNative(); + second.__makeNative(); + + createAndMountComponent(Animated.View, { + style: { + opacity: Animated.add(first, second), + }, + }); + + expect(nativeAnimatedModule.createAnimatedNode).toBeCalledWith( + jasmine.any(Number), + {type: 'addition', input: jasmine.any(Array)}, + ); + const additionCalls = nativeAnimatedModule.createAnimatedNode.mock.calls.filter( + (call) => call[1].type === 'addition' + ); + expect(additionCalls.length).toBe(1); + const additionCall = additionCalls[0]; + const additionNodeTag = additionCall[0]; + const additionConnectionCalls = nativeAnimatedModule.connectAnimatedNodes.mock.calls.filter( + (call) => call[1] === additionNodeTag + ); + expect(additionConnectionCalls.length).toBe(2); + expect(nativeAnimatedModule.createAnimatedNode) + .toBeCalledWith(additionCall[1].input[0], {type: 'value', value: 1, offset: 0}); + expect(nativeAnimatedModule.createAnimatedNode) + .toBeCalledWith(additionCall[1].input[1], {type: 'value', value: 2, offset: 0}); + }); + + it('sends a valid graph description for Animated.multiply nodes', () => { + const first = new Animated.Value(2); + const second = new Animated.Value(1); + first.__makeNative(); + second.__makeNative(); + + createAndMountComponent(Animated.View, { + style: { + opacity: Animated.multiply(first, second), + }, + }); + + expect(nativeAnimatedModule.createAnimatedNode).toBeCalledWith( + jasmine.any(Number), + {type: 'multiplication', input: jasmine.any(Array)}, + ); + const multiplicationCalls = nativeAnimatedModule.createAnimatedNode.mock.calls.filter( + (call) => call[1].type === 'multiplication' + ); + expect(multiplicationCalls.length).toBe(1); + const multiplicationCall = multiplicationCalls[0]; + const multiplicationNodeTag = multiplicationCall[0]; + const multiplicationConnectionCalls = nativeAnimatedModule.connectAnimatedNodes.mock.calls.filter( + (call) => call[1] === multiplicationNodeTag + ); + expect(multiplicationConnectionCalls.length).toBe(2); + expect(nativeAnimatedModule.createAnimatedNode) + .toBeCalledWith(multiplicationCall[1].input[0], {type: 'value', value: 2, offset: 0}); + expect(nativeAnimatedModule.createAnimatedNode) + .toBeCalledWith(multiplicationCall[1].input[1], {type: 'value', value: 1, offset: 0}); + }); + + it('sends a valid graph description for Animated.divide nodes', () => { + const first = new Animated.Value(4); + const second = new Animated.Value(2); + first.__makeNative(); + second.__makeNative(); + + createAndMountComponent(Animated.View, { + style: { + opacity: Animated.divide(first, second), + }, + }); + + expect(nativeAnimatedModule.createAnimatedNode) + .toBeCalledWith(jasmine.any(Number), {type: 'division', input: jasmine.any(Array)}); + const divisionCalls = nativeAnimatedModule.createAnimatedNode.mock.calls.filter( + (call) => call[1].type === 'division' + ); + expect(divisionCalls.length).toBe(1); + const divisionCall = divisionCalls[0]; + const divisionNodeTag = divisionCall[0]; + const divisionConnectionCalls = nativeAnimatedModule.connectAnimatedNodes.mock.calls.filter( + (call) => call[1] === divisionNodeTag + ); + expect(divisionConnectionCalls.length).toBe(2); + expect(nativeAnimatedModule.createAnimatedNode) + .toBeCalledWith(divisionCall[1].input[0], {type: 'value', value: 4, offset: 0}); + expect(nativeAnimatedModule.createAnimatedNode) + .toBeCalledWith(divisionCall[1].input[1], {type: 'value', value: 2, offset: 0}); + }); + + it('sends a valid graph description for Animated.modulo nodes', () => { + const value = new Animated.Value(4); + value.__makeNative(); + + createAndMountComponent(Animated.View, { + style: { + opacity: Animated.modulo(value, 4), + }, + }); + + expect(nativeAnimatedModule.createAnimatedNode).toBeCalledWith( + jasmine.any(Number), + {type: 'modulus', modulus: 4, input: jasmine.any(Number)}, + ); + const moduloCalls = nativeAnimatedModule.createAnimatedNode.mock.calls.filter( + (call) => call[1].type === 'modulus' + ); + expect(moduloCalls.length).toBe(1); + const moduloCall = moduloCalls[0]; + const moduloNodeTag = moduloCall[0]; + const moduloConnectionCalls = nativeAnimatedModule.connectAnimatedNodes.mock.calls.filter( + (call) => call[1] === moduloNodeTag + ); + expect(moduloConnectionCalls.length).toBe(1); + expect(nativeAnimatedModule.createAnimatedNode) + .toBeCalledWith(moduloCall[1].input, {type: 'value', value: 4, offset: 0}); + }); + + it('sends a valid graph description for interpolate() nodes', () => { + const value = new Animated.Value(10); + value.__makeNative(); + + createAndMountComponent(Animated.View, { + style: { + opacity: value.interpolate({ + inputRange: [10, 20], + outputRange: [0, 1], + }), + }, + }); + + expect(nativeAnimatedModule.createAnimatedNode).toBeCalledWith( + jasmine.any(Number), + {type: 'value', value: 10, offset: 0} + ); + expect(nativeAnimatedModule.createAnimatedNode) + .toBeCalledWith(jasmine.any(Number), { + type: 'interpolation', + inputRange: [10, 20], + outputRange: [0, 1], + extrapolateLeft: 'extend', + extrapolateRight: 'extend', + }); + const interpolationNodeTag = nativeAnimatedModule.createAnimatedNode.mock.calls.find( + (call) => call[1].type === 'interpolation' + )[0]; + const valueNodeTag = nativeAnimatedModule.createAnimatedNode.mock.calls.find( + (call) => call[1].type === 'value' + )[0]; + expect(nativeAnimatedModule.connectAnimatedNodes).toBeCalledWith(valueNodeTag, interpolationNodeTag); + }); + + it('sends a valid graph description for transform nodes', () => { + const value = new Animated.Value(0); + value.__makeNative(); + + createAndMountComponent(Animated.View, { + style: { + transform: [{translateX: value}, {scale: 2}], + }, + }); + + expect(nativeAnimatedModule.createAnimatedNode).toBeCalledWith( + jasmine.any(Number), + { + type: 'transform', + transforms: [{ + nodeTag: jasmine.any(Number), + property: 'translateX', + type: 'animated', + }, { + value: 2, + property: 'scale', + type: 'static', + }], + }, + ); + }); + + it('sends a valid graph description for Animated.diffClamp nodes', () => { + const value = new Animated.Value(2); + value.__makeNative(); + + createAndMountComponent(Animated.View, { + style: { + opacity: Animated.diffClamp(value, 0, 20), + }, + }); + + expect(nativeAnimatedModule.createAnimatedNode).toBeCalledWith( + jasmine.any(Number), + {type: 'diffclamp', input: jasmine.any(Number), max: 20, min: 0}, + ); + const diffClampCalls = nativeAnimatedModule.createAnimatedNode.mock.calls.filter( + (call) => call[1].type === 'diffclamp' + ); + expect(diffClampCalls.length).toBe(1); + const diffClampCall = diffClampCalls[0]; + const diffClampNodeTag = diffClampCall[0]; + const diffClampConnectionCalls = nativeAnimatedModule.connectAnimatedNodes.mock.calls.filter( + (call) => call[1] === diffClampNodeTag + ); + expect(diffClampConnectionCalls.length).toBe(1); + expect(nativeAnimatedModule.createAnimatedNode) + .toBeCalledWith(diffClampCall[1].input, {type: 'value', value: 2, offset: 0}); + }); + + it('doesn\'t call into native API if useNativeDriver is set to false', () => { + const anim = new Animated.Value(0); + + const c = createAndMountComponent(Animated.View, { + style: { + opacity: anim, + }, + }); + + Animated.timing(anim, {toValue: 10, duration: 1000, useNativeDriver: false}).start(); + + c.componentWillUnmount(); + + expect(nativeAnimatedModule.createAnimatedNode).not.toBeCalled(); + }); + + it('fails when trying to run non-native animation on native node', () => { + const anim = new Animated.Value(0); + + createAndMountComponent(Animated.View, { + style: { + opacity: anim, + }, + }); + + Animated.timing(anim, {toValue: 10, duration: 50, useNativeDriver: true}).start(); + jest.runAllTimers(); + + Animated.timing(anim, {toValue: 4, duration: 500, useNativeDriver: false}).start(); + expect(jest.runAllTimers).toThrow(); + }); + + it('fails for unsupported styles', () => { + const anim = new Animated.Value(0); + + createAndMountComponent(Animated.View, { + style: { + left: anim, + }, + }); + + const animation = Animated.timing(anim, {toValue: 10, duration: 50, useNativeDriver: true}); + expect(animation.start).toThrowError(/left/); + }); + + it('works for any `static` props and styles', () => { + // Passing "unsupported" props should work just fine as long as they are not animated + const value = new Animated.Value(0); + value.__makeNative(); + + createAndMountComponent(Animated.View, { + style: { + left: 10, + top: 20, + opacity: value, + }, + removeClippedSubviews: true, + }); + + expect(nativeAnimatedModule.createAnimatedNode) + .toBeCalledWith(jasmine.any(Number), { type: 'style', style: { opacity: jasmine.any(Number) }}); + expect(nativeAnimatedModule.createAnimatedNode) + .toBeCalledWith(jasmine.any(Number), { type: 'props', props: { style: jasmine.any(Number) }}); + }); + }); + + describe('Animations', () => { + it('sends a valid timing animation description', () => { + const anim = new Animated.Value(0); + Animated.timing(anim, {toValue: 10, duration: 1000, useNativeDriver: true}).start(); + + expect(nativeAnimatedModule.startAnimatingNode).toBeCalledWith( + jasmine.any(Number), + jasmine.any(Number), + {type: 'frames', frames: jasmine.any(Array), toValue: jasmine.any(Number), iterations: 1}, + jasmine.any(Function) + ); + }); + + it('sends a valid spring animation description', () => { + const anim = new Animated.Value(0); + Animated.spring(anim, {toValue: 10, friction: 5, tension: 164, useNativeDriver: true}).start(); + expect(nativeAnimatedModule.startAnimatingNode).toBeCalledWith( + jasmine.any(Number), + jasmine.any(Number), + { + type: 'spring', + stiffness: 679.08, + damping: 16, + mass: 1, + initialVelocity: 0, + overshootClamping: false, + restDisplacementThreshold: 0.001, + restSpeedThreshold: 0.001, + toValue: 10, + iterations: 1, + }, + jasmine.any(Function) + ); + + Animated.spring(anim, { + toValue: 10, + stiffness: 1000, + damping: 500, + mass: 3, + useNativeDriver: true + }).start(); + expect(nativeAnimatedModule.startAnimatingNode).toBeCalledWith( + jasmine.any(Number), + jasmine.any(Number), + { + type: 'spring', + stiffness: 1000, + damping: 500, + mass: 3, + initialVelocity: 0, + overshootClamping: false, + restDisplacementThreshold: 0.001, + restSpeedThreshold: 0.001, + toValue: 10, + iterations: 1, + }, + jasmine.any(Function) + ); + + Animated.spring(anim, {toValue: 10, bounciness: 8, speed: 10, useNativeDriver: true}).start(); + expect(nativeAnimatedModule.startAnimatingNode).toBeCalledWith( + jasmine.any(Number), + jasmine.any(Number), + { + type: 'spring', + damping: 23.05223140901191, + initialVelocity: 0, + overshootClamping: false, + restDisplacementThreshold: 0.001, + restSpeedThreshold: 0.001, + stiffness: 299.61882352941177, + mass: 1, + toValue: 10, + iterations: 1, + }, + jasmine.any(Function) + ); + }); + + it('sends a valid decay animation description', () => { + const anim = new Animated.Value(0); + Animated.decay(anim, {velocity: 10, deceleration: 0.1, useNativeDriver: true}).start(); + + expect(nativeAnimatedModule.startAnimatingNode).toBeCalledWith( + jasmine.any(Number), + jasmine.any(Number), + {type: 'decay', deceleration: 0.1, velocity: 10, iterations: 1}, + jasmine.any(Function) + ); + }); + + it('works with Animated.loop', () => { + const anim = new Animated.Value(0); + Animated.loop( + Animated.decay(anim, {velocity: 10, deceleration: 0.1, useNativeDriver: true}), + { iterations: 10 }, + ).start(); + + expect(nativeAnimatedModule.startAnimatingNode).toBeCalledWith( + jasmine.any(Number), + jasmine.any(Number), + {type: 'decay', deceleration: 0.1, velocity: 10, iterations: 10}, + jasmine.any(Function) + ); + }); + + it('sends stopAnimation command to native', () => { + const value = new Animated.Value(0); + const animation = Animated.timing(value, {toValue: 10, duration: 50, useNativeDriver: true}); + + animation.start(); + expect(nativeAnimatedModule.startAnimatingNode).toBeCalledWith( + jasmine.any(Number), + jasmine.any(Number), + {type: 'frames', frames: jasmine.any(Array), toValue: jasmine.any(Number), iterations: 1}, + jasmine.any(Function) + ); + const animationId = nativeAnimatedModule.startAnimatingNode.mock.calls[0][0]; + + animation.stop(); + expect(nativeAnimatedModule.stopAnimation).toBeCalledWith(animationId); + }); + }); +}); diff --git a/Libraries/Animated/src/__tests__/Easing-test.js b/Libraries/Animated/src/__tests__/Easing-test.js new file mode 100644 index 0000000..aedc168 --- /dev/null +++ b/Libraries/Animated/src/__tests__/Easing-test.js @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +var Easing = require('Easing'); +describe('Easing', () => { + it('should work with linear', () => { + var easing = Easing.linear; + + expect(easing(0)).toBe(0); + expect(easing(0.5)).toBe(0.5); + expect(easing(0.8)).toBe(0.8); + expect(easing(1)).toBe(1); + }); + + it('should work with ease in linear', () => { + var easing = Easing.in(Easing.linear); + expect(easing(0)).toBe(0); + expect(easing(0.5)).toBe(0.5); + expect(easing(0.8)).toBe(0.8); + expect(easing(1)).toBe(1); + }); + + it('should work with easy out linear', () => { + var easing = Easing.out(Easing.linear); + expect(easing(0)).toBe(0); + expect(easing(0.5)).toBe(0.5); + expect(easing(0.6)).toBe(0.6); + expect(easing(1)).toBe(1); + }); + + it('should work with ease in quad', () => { + function easeInQuad(t) { + return t * t; + } + var easing = Easing.in(Easing.quad); + for (var t = -0.5; t < 1.5; t += 0.1) { + expect(easing(t)).toBe(easeInQuad(t)); + } + }); + + it('should work with ease out quad', () => { + function easeOutQuad(t) { + return -t * (t - 2); + } + var easing = Easing.out(Easing.quad); + for (var t = 0; t <= 1; t += 0.1) { + expect(easing(1)).toBe(easeOutQuad(1)); + } + }); + + it('should work with ease in-out quad', () => { + function easeInOutQuad(t) { + t = t * 2; + if (t < 1) { + return 0.5 * t * t; + } + return -((t - 1) * (t - 3) - 1) / 2; + } + var easing = Easing.inOut(Easing.quad); + for (var t = -0.5; t < 1.5; t += 0.1) { + expect(easing(t)).toBeCloseTo(easeInOutQuad(t), 4); + } + }); + + it('should satisfy boundary conditions with elastic', () => { + for (var b = 0; b < 4; b += 0.3) { + var easing = Easing.elastic(b); + expect(easing(0)).toBe(0); + expect(easing(1)).toBe(1); + } + }); + + function sampleEasingFunction(easing) { + var DURATION = 300; + var tickCount = Math.round(DURATION * 60 / 1000); + var samples = []; + for (var i = 0; i <= tickCount; i++) { + samples.push(easing(i / tickCount)); + } + return samples; + } + + var Samples = { + in_quad: [0,0.0030864197530864196,0.012345679012345678,0.027777777777777776,0.04938271604938271,0.0771604938271605,0.1111111111111111,0.15123456790123457,0.19753086419753085,0.25,0.308641975308642,0.37345679012345684,0.4444444444444444,0.5216049382716049,0.6049382716049383,0.6944444444444445,0.7901234567901234,0.8919753086419753,1], + out_quad: [0,0.10802469135802469,0.20987654320987653,0.3055555555555555,0.3950617283950617,0.47839506172839513,0.5555555555555556,0.6265432098765432,0.691358024691358,0.75,0.8024691358024691,0.8487654320987654,0.888888888888889,0.9228395061728394,0.9506172839506174,0.9722222222222221,0.9876543209876543,0.9969135802469136,1], + inOut_quad: [0,0.006172839506172839,0.024691358024691357,0.05555555555555555,0.09876543209876543,0.154320987654321,0.2222222222222222,0.30246913580246915,0.3950617283950617,0.5,0.6049382716049383,0.697530864197531,0.7777777777777777,0.845679012345679,0.9012345679012346,0.9444444444444444,0.9753086419753086,0.9938271604938271,1], + in_cubic: [0,0.00017146776406035664,0.0013717421124828531,0.004629629629629629,0.010973936899862825,0.021433470507544586,0.037037037037037035,0.05881344307270234,0.0877914951989026,0.125,0.1714677640603567,0.22822359396433475,0.2962962962962963,0.37671467764060357,0.4705075445816187,0.5787037037037038,0.7023319615912208,0.8424211248285322,1], + out_cubic: [0,0.15757887517146785,0.2976680384087792,0.42129629629629617,0.5294924554183813,0.6232853223593964,0.7037037037037036,0.7717764060356652,0.8285322359396433,0.875,0.9122085048010974,0.9411865569272977,0.9629629629629629,0.9785665294924554,0.9890260631001372,0.9953703703703703,0.9986282578875172,0.9998285322359396,1], + inOut_cubic: [0,0.0006858710562414266,0.0054869684499314125,0.018518518518518517,0.0438957475994513,0.08573388203017834,0.14814814814814814,0.23525377229080935,0.3511659807956104,0.5,0.6488340192043895,0.7647462277091908,0.8518518518518519,0.9142661179698217,0.9561042524005487,0.9814814814814815,0.9945130315500685,0.9993141289437586,1], + in_sin: [0,0.003805301908254455,0.01519224698779198,0.03407417371093169,0.06030737921409157,0.09369221296335006,0.1339745962155613,0.1808479557110082,0.233955556881022,0.2928932188134524,0.35721239031346064,0.42642356364895384,0.4999999999999999,0.5773817382593005,0.6579798566743311,0.7411809548974793,0.8263518223330696,0.9128442572523416,0.9999999999999999], + out_sin: [0,0.08715574274765817,0.17364817766693033,0.25881904510252074,0.3420201433256687,0.42261826174069944,0.49999999999999994,0.573576436351046,0.6427876096865393,0.7071067811865475,0.766044443118978,0.8191520442889918,0.8660254037844386,0.9063077870366499,0.9396926207859083,0.9659258262890683,0.984807753012208,0.9961946980917455,1], + inOut_sin: [0,0.00759612349389599,0.030153689607045786,0.06698729810778065,0.116977778440511,0.17860619515673032,0.24999999999999994,0.32898992833716556,0.4131759111665348,0.49999999999999994,0.5868240888334652,0.6710100716628343,0.7499999999999999,0.8213938048432696,0.883022221559489,0.9330127018922194,0.9698463103929542,0.9924038765061041,1], + in_exp: [0,0.0014352875901128893,0.002109491677524035,0.0031003926796253885,0.004556754060844206,0.006697218616039631,0.009843133202303688,0.014466792379488908,0.021262343752724643,0.03125,0.045929202883612456,0.06750373368076916,0.09921256574801243,0.1458161299470146,0.2143109957132682,0.31498026247371835,0.46293735614364506,0.6803950000871883,1], + out_exp: [0,0.31960499991281155,0.5370626438563548,0.6850197375262816,0.7856890042867318,0.8541838700529854,0.9007874342519875,0.9324962663192309,0.9540707971163875,0.96875,0.9787376562472754,0.9855332076205111,0.9901568667976963,0.9933027813839603,0.9954432459391558,0.9968996073203746,0.9978905083224759,0.9985647124098871,1], + inOut_exp: [0,0.0010547458387620175,0.002278377030422103,0.004921566601151844,0.010631171876362321,0.022964601441806228,0.049606282874006216,0.1071554978566341,0.23146867807182253,0.5,0.7685313219281775,0.892844502143366,0.9503937171259937,0.9770353985581938,0.9893688281236377,0.9950784333988482,0.9977216229695779,0.998945254161238,1], + in_circle: [0,0.0015444024660317135,0.006192010000093506,0.013986702816730645,0.025003956956430873,0.03935464078941209,0.057190958417936644,0.07871533601238889,0.10419358352238339,0.1339745962155614,0.1685205807169019,0.20845517506805522,0.2546440075000701,0.3083389112228482,0.37146063894529113,0.4472292016074334,0.5418771527091488,0.6713289009389102,1], + out_circle: [0,0.3286710990610898,0.45812284729085123,0.5527707983925666,0.6285393610547089,0.6916610887771518,0.7453559924999298,0.7915448249319448,0.8314794192830981,0.8660254037844386,0.8958064164776166,0.9212846639876111,0.9428090415820634,0.9606453592105879,0.9749960430435691,0.9860132971832694,0.9938079899999065,0.9984555975339683,1], + inOut_circle: [0,0.003096005000046753,0.012501978478215436,0.028595479208968322,0.052096791761191696,0.08426029035845095,0.12732200375003505,0.18573031947264557,0.2709385763545744,0.5,0.7290614236454256,0.8142696805273546,0.8726779962499649,0.915739709641549,0.9479032082388084,0.9714045207910317,0.9874980215217846,0.9969039949999532,1], + in_back_: [0,-0.004788556241426612,-0.017301289437585736,-0.0347587962962963,-0.05438167352537723,-0.07339051783264748,-0.08900592592592595,-0.09844849451303156,-0.0989388203017833,-0.08769750000000004,-0.06194513031550073,-0.018902307956104283,0.044210370370370254,0.13017230795610413,0.2417629080932785,0.3817615740740742,0.5529477091906719,0.7581007167352535,0.9999999999999998], + out_back_: [2.220446049250313e-16,0.24189928326474652,0.44705229080932807,0.6182384259259258,0.7582370919067215,0.8698276920438959,0.9557896296296297,1.0189023079561044,1.0619451303155008,1.0876975,1.0989388203017834,1.0984484945130315,1.089005925925926,1.0733905178326475,1.0543816735253773,1.0347587962962963,1.0173012894375857,1.0047885562414267,1], + }; + + Object.keys(Samples).forEach(function(type) { + it('should ease ' + type, function() { + var [modeName, easingName, isFunction] = type.split('_'); + var easing = Easing[easingName]; + if (isFunction !== undefined) { + easing = easing(); + } + var computed = sampleEasingFunction(Easing[modeName](easing)); + var samples = Samples[type]; + + computed.forEach((value, key) => { + expect(value).toBeCloseTo(samples[key], 2); + }); + }); + }); +}); diff --git a/Libraries/Animated/src/__tests__/Interpolation-test.js b/Libraries/Animated/src/__tests__/Interpolation-test.js new file mode 100644 index 0000000..8ff4633 --- /dev/null +++ b/Libraries/Animated/src/__tests__/Interpolation-test.js @@ -0,0 +1,314 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +var AnimatedInterpolation = require('../nodes/AnimatedInterpolation'); +var Easing = require('Easing'); + +describe('Interpolation', () => { + it('should work with defaults', () => { + var interpolation = AnimatedInterpolation.__createInterpolation({ + inputRange: [0, 1], + outputRange: [0, 1], + }); + + expect(interpolation(0)).toBe(0); + expect(interpolation(0.5)).toBe(0.5); + expect(interpolation(0.8)).toBe(0.8); + expect(interpolation(1)).toBe(1); + }); + + it('should work with output range', () => { + var interpolation = AnimatedInterpolation.__createInterpolation({ + inputRange: [0, 1], + outputRange: [100, 200], + }); + + expect(interpolation(0)).toBe(100); + expect(interpolation(0.5)).toBe(150); + expect(interpolation(0.8)).toBe(180); + expect(interpolation(1)).toBe(200); + }); + + it('should work with input range', () => { + var interpolation = AnimatedInterpolation.__createInterpolation({ + inputRange: [100, 200], + outputRange: [0, 1], + }); + + expect(interpolation(100)).toBe(0); + expect(interpolation(150)).toBe(0.5); + expect(interpolation(180)).toBe(0.8); + expect(interpolation(200)).toBe(1); + }); + + it('should throw for non monotonic input ranges', () => { + expect(() => + AnimatedInterpolation.__createInterpolation({ + inputRange: [0, 2, 1], + outputRange: [0, 1, 2], + }), + ).toThrow(); + + expect(() => + AnimatedInterpolation.__createInterpolation({ + inputRange: [0, 1, 2], + outputRange: [0, 3, 1], + }), + ).not.toThrow(); + }); + + it('should work with empty input range', () => { + var interpolation = AnimatedInterpolation.__createInterpolation({ + inputRange: [0, 10, 10], + outputRange: [1, 2, 3], + extrapolate: 'extend', + }); + + expect(interpolation(0)).toBe(1); + expect(interpolation(5)).toBe(1.5); + expect(interpolation(10)).toBe(2); + expect(interpolation(10.1)).toBe(3); + expect(interpolation(15)).toBe(3); + }); + + it('should work with empty output range', () => { + var interpolation = AnimatedInterpolation.__createInterpolation({ + inputRange: [1, 2, 3], + outputRange: [0, 10, 10], + extrapolate: 'extend', + }); + + expect(interpolation(0)).toBe(-10); + expect(interpolation(1.5)).toBe(5); + expect(interpolation(2)).toBe(10); + expect(interpolation(2.5)).toBe(10); + expect(interpolation(3)).toBe(10); + expect(interpolation(4)).toBe(10); + }); + + it('should work with easing', () => { + var interpolation = AnimatedInterpolation.__createInterpolation({ + inputRange: [0, 1], + outputRange: [0, 1], + easing: Easing.quad, + }); + + expect(interpolation(0)).toBe(0); + expect(interpolation(0.5)).toBe(0.25); + expect(interpolation(0.9)).toBe(0.81); + expect(interpolation(1)).toBe(1); + }); + + it('should work with extrapolate', () => { + var interpolation = AnimatedInterpolation.__createInterpolation({ + inputRange: [0, 1], + outputRange: [0, 1], + extrapolate: 'extend', + easing: Easing.quad, + }); + + expect(interpolation(-2)).toBe(4); + expect(interpolation(2)).toBe(4); + + interpolation = AnimatedInterpolation.__createInterpolation({ + inputRange: [0, 1], + outputRange: [0, 1], + extrapolate: 'clamp', + easing: Easing.quad, + }); + + expect(interpolation(-2)).toBe(0); + expect(interpolation(2)).toBe(1); + + interpolation = AnimatedInterpolation.__createInterpolation({ + inputRange: [0, 1], + outputRange: [0, 1], + extrapolate: 'identity', + easing: Easing.quad, + }); + + expect(interpolation(-2)).toBe(-2); + expect(interpolation(2)).toBe(2); + }); + + it('should work with keyframes with extrapolate', () => { + var interpolation = AnimatedInterpolation.__createInterpolation({ + inputRange: [0, 10, 100, 1000], + outputRange: [0, 5, 50, 500], + extrapolate: true, + }); + + expect(interpolation(-5)).toBe(-2.5); + expect(interpolation(0)).toBe(0); + expect(interpolation(5)).toBe(2.5); + expect(interpolation(10)).toBe(5); + expect(interpolation(50)).toBe(25); + expect(interpolation(100)).toBe(50); + expect(interpolation(500)).toBe(250); + expect(interpolation(1000)).toBe(500); + expect(interpolation(2000)).toBe(1000); + }); + + it('should work with keyframes without extrapolate', () => { + var interpolation = AnimatedInterpolation.__createInterpolation({ + inputRange: [0, 1, 2], + outputRange: [0.2, 1, 0.2], + extrapolate: 'clamp', + }); + + expect(interpolation(5)).toBeCloseTo(0.2); + }); + + it('should throw for an infinite input range', () => { + expect(() => + AnimatedInterpolation.__createInterpolation({ + inputRange: [-Infinity, Infinity], + outputRange: [0, 1], + }), + ).toThrow(); + + expect(() => + AnimatedInterpolation.__createInterpolation({ + inputRange: [-Infinity, 0, Infinity], + outputRange: [1, 2, 3], + }), + ).not.toThrow(); + }); + + it('should work with negative infinite', () => { + var interpolation = AnimatedInterpolation.__createInterpolation({ + inputRange: [-Infinity, 0], + outputRange: [-Infinity, 0], + easing: Easing.quad, + extrapolate: 'identity', + }); + + expect(interpolation(-Infinity)).toBe(-Infinity); + expect(interpolation(-100)).toBeCloseTo(-10000); + expect(interpolation(-10)).toBeCloseTo(-100); + expect(interpolation(0)).toBeCloseTo(0); + expect(interpolation(1)).toBeCloseTo(1); + expect(interpolation(100)).toBeCloseTo(100); + }); + + it('should work with positive infinite', () => { + var interpolation = AnimatedInterpolation.__createInterpolation({ + inputRange: [5, Infinity], + outputRange: [5, Infinity], + easing: Easing.quad, + extrapolate: 'identity', + }); + + expect(interpolation(-100)).toBeCloseTo(-100); + expect(interpolation(-10)).toBeCloseTo(-10); + expect(interpolation(0)).toBeCloseTo(0); + expect(interpolation(5)).toBeCloseTo(5); + expect(interpolation(6)).toBeCloseTo(5 + 1); + expect(interpolation(10)).toBeCloseTo(5 + 25); + expect(interpolation(100)).toBeCloseTo(5 + 95 * 95); + expect(interpolation(Infinity)).toBe(Infinity); + }); + + it('should work with output ranges as string', () => { + var interpolation = AnimatedInterpolation.__createInterpolation({ + inputRange: [0, 1], + outputRange: ['rgba(0, 100, 200, 0)', 'rgba(50, 150, 250, 0.4)'], + }); + + expect(interpolation(0)).toBe('rgba(0, 100, 200, 0)'); + expect(interpolation(0.5)).toBe('rgba(25, 125, 225, 0.2)'); + expect(interpolation(1)).toBe('rgba(50, 150, 250, 0.4)'); + }); + + it('should work with output ranges as short hex string', () => { + var interpolation = AnimatedInterpolation.__createInterpolation({ + inputRange: [0, 1], + outputRange: ['#024', '#9BF'], + }); + + expect(interpolation(0)).toBe('rgba(0, 34, 68, 1)'); + expect(interpolation(0.5)).toBe('rgba(77, 111, 162, 1)'); + expect(interpolation(1)).toBe('rgba(153, 187, 255, 1)'); + }); + + it('should work with output ranges as long hex string', () => { + var interpolation = AnimatedInterpolation.__createInterpolation({ + inputRange: [0, 1], + outputRange: ['#FF9500', '#87FC70'], + }); + + expect(interpolation(0)).toBe('rgba(255, 149, 0, 1)'); + expect(interpolation(0.5)).toBe('rgba(195, 201, 56, 1)'); + expect(interpolation(1)).toBe('rgba(135, 252, 112, 1)'); + }); + + it('should work with output ranges with mixed hex and rgba strings', () => { + var interpolation = AnimatedInterpolation.__createInterpolation({ + inputRange: [0, 1], + outputRange: ['rgba(100, 120, 140, .4)', '#87FC70'], + }); + + expect(interpolation(0)).toBe('rgba(100, 120, 140, 0.4)'); + expect(interpolation(0.5)).toBe('rgba(118, 186, 126, 0.7)'); + expect(interpolation(1)).toBe('rgba(135, 252, 112, 1)'); + }); + + it('should work with negative and decimal values in string ranges', () => { + var interpolation = AnimatedInterpolation.__createInterpolation({ + inputRange: [0, 1], + outputRange: ['-100.5deg', '100deg'], + }); + + expect(interpolation(0)).toBe('-100.5deg'); + expect(interpolation(0.5)).toBe('-0.25deg'); + expect(interpolation(1)).toBe('100deg'); + }); + + it('should crash when chaining an interpolation that returns a string', () => { + var interpolation = AnimatedInterpolation.__createInterpolation({ + inputRange: [0, 1], + outputRange: [0, 1], + }); + expect(() => { + interpolation('45rad'); + }).toThrow(); + }); + + it('should support a mix of color patterns', () => { + var interpolation = AnimatedInterpolation.__createInterpolation({ + inputRange: [0, 1, 2], + outputRange: ['rgba(0, 100, 200, 0)', 'rgb(50, 150, 250)', 'red'], + }); + + expect(interpolation(0)).toBe('rgba(0, 100, 200, 0)'); + expect(interpolation(0.5)).toBe('rgba(25, 125, 225, 0.5)'); + expect(interpolation(1.5)).toBe('rgba(153, 75, 125, 1)'); + expect(interpolation(2)).toBe('rgba(255, 0, 0, 1)'); + }); + + it('should crash when defining output range with different pattern', () => { + expect(() => + AnimatedInterpolation.__createInterpolation({ + inputRange: [0, 1], + outputRange: ['20deg', '30rad'], + }), + ).toThrow(); + }); + + it('should round the alpha channel of a color to the nearest thousandth', () => { + var interpolation = AnimatedInterpolation.__createInterpolation({ + inputRange: [0, 1], + outputRange: ['rgba(0, 0, 0, 0)', 'rgba(0, 0, 0, 1)'], + }); + + expect(interpolation(1e-12)).toBe('rgba(0, 0, 0, 0)'); + expect(interpolation(2 / 3)).toBe('rgba(0, 0, 0, 0.667)'); + }); +}); diff --git a/Libraries/Animated/src/__tests__/bezier-test.js b/Libraries/Animated/src/__tests__/bezier-test.js new file mode 100644 index 0000000..59cf789 --- /dev/null +++ b/Libraries/Animated/src/__tests__/bezier-test.js @@ -0,0 +1,105 @@ +/** + * BezierEasing - use bezier curve for transition easing function + * https://github.com/gre/bezier-easing + * + * @copyright 2014-2015 Gaëtan Renaudeau. MIT License. + * @noflow + */ + +/* eslint-disable */ + +'use strict'; + +var bezier = require('bezier'); + +var identity = function (x) { return x; }; + +function assertClose (a, b, precision = 3) { + expect(a).toBeCloseTo(b, precision); +} + +function makeAssertCloseWithPrecision (precision) { + return function (a, b) { + assertClose(a, b, precision); + }; +} + +function allEquals (be1, be2, samples, assertion) { + if (!assertion) assertion = assertClose; + for (var i=0; i<=samples; ++i) { + var x = i / samples; + assertion(be1(x), be2(x)); + } +} + +function repeat (n) { + return function (f) { + for (var i=0; i void; + +export type AnimationConfig = { + isInteraction?: boolean, + useNativeDriver?: boolean, + onComplete?: ?EndCallback, + iterations?: number, +}; + +// Important note: start() and stop() will only be called at most once. +// Once an animation has been stopped or finished its course, it will +// not be reused. +class Animation { + __active: boolean; + __isInteraction: boolean; + __nativeId: number; + __onEnd: ?EndCallback; + __iterations: number; + start( + fromValue: number, + onUpdate: (value: number) => void, + onEnd: ?EndCallback, + previousAnimation: ?Animation, + animatedValue: AnimatedValue, + ): void {} + stop(): void { + if (this.__nativeId) { + NativeAnimatedHelper.API.stopAnimation(this.__nativeId); + } + } + __getNativeAnimationConfig(): any { + // Subclasses that have corresponding animation implementation done in native + // should override this method + throw new Error('This animation type cannot be offloaded to native'); + } + // Helper function for subclasses to make sure onEnd is only called once. + __debouncedOnEnd(result: EndResult): void { + const onEnd = this.__onEnd; + this.__onEnd = null; + onEnd && onEnd(result); + } + __startNativeAnimation(animatedValue: AnimatedValue): void { + animatedValue.__makeNative(); + this.__nativeId = NativeAnimatedHelper.generateNewAnimationId(); + NativeAnimatedHelper.API.startAnimatingNode( + this.__nativeId, + animatedValue.__getNativeTag(), + this.__getNativeAnimationConfig(), + this.__debouncedOnEnd.bind(this), + ); + } +} + +module.exports = Animation; diff --git a/Libraries/Animated/src/animations/DecayAnimation.js b/Libraries/Animated/src/animations/DecayAnimation.js new file mode 100644 index 0000000..652bd9c --- /dev/null +++ b/Libraries/Animated/src/animations/DecayAnimation.js @@ -0,0 +1,112 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule DecayAnimation + * @flow + * @format + */ +'use strict'; + +const Animation = require('./Animation'); + +const {shouldUseNativeDriver} = require('../NativeAnimatedHelper'); + +import type {AnimationConfig, EndCallback} from './Animation'; +import type AnimatedValue from '../nodes/AnimatedValue'; + +export type DecayAnimationConfig = AnimationConfig & { + velocity: number | {x: number, y: number}, + deceleration?: number, +}; + +export type DecayAnimationConfigSingle = AnimationConfig & { + velocity: number, + deceleration?: number, +}; + +class DecayAnimation extends Animation { + _startTime: number; + _lastValue: number; + _fromValue: number; + _deceleration: number; + _velocity: number; + _onUpdate: (value: number) => void; + _animationFrame: any; + _useNativeDriver: boolean; + + constructor(config: DecayAnimationConfigSingle) { + super(); + this._deceleration = + config.deceleration !== undefined ? config.deceleration : 0.998; + this._velocity = config.velocity; + this._useNativeDriver = shouldUseNativeDriver(config); + this.__isInteraction = + config.isInteraction !== undefined ? config.isInteraction : true; + this.__iterations = config.iterations !== undefined ? config.iterations : 1; + } + + __getNativeAnimationConfig() { + return { + type: 'decay', + deceleration: this._deceleration, + velocity: this._velocity, + iterations: this.__iterations, + }; + } + + start( + fromValue: number, + onUpdate: (value: number) => void, + onEnd: ?EndCallback, + previousAnimation: ?Animation, + animatedValue: AnimatedValue, + ): void { + this.__active = true; + this._lastValue = fromValue; + this._fromValue = fromValue; + this._onUpdate = onUpdate; + this.__onEnd = onEnd; + this._startTime = Date.now(); + if (this._useNativeDriver) { + this.__startNativeAnimation(animatedValue); + } else { + this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this)); + } + } + + onUpdate(): void { + const now = Date.now(); + + const value = + this._fromValue + + this._velocity / + (1 - this._deceleration) * + (1 - Math.exp(-(1 - this._deceleration) * (now - this._startTime))); + + this._onUpdate(value); + + if (Math.abs(this._lastValue - value) < 0.1) { + this.__debouncedOnEnd({finished: true}); + return; + } + + this._lastValue = value; + if (this.__active) { + this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this)); + } + } + + stop(): void { + super.stop(); + this.__active = false; + global.cancelAnimationFrame(this._animationFrame); + this.__debouncedOnEnd({finished: false}); + } +} + +module.exports = DecayAnimation; diff --git a/Libraries/Animated/src/animations/SpringAnimation.js b/Libraries/Animated/src/animations/SpringAnimation.js new file mode 100644 index 0000000..67dee08 --- /dev/null +++ b/Libraries/Animated/src/animations/SpringAnimation.js @@ -0,0 +1,342 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule SpringAnimation + * @flow + * @format + */ +'use strict'; + +const AnimatedValue = require('../nodes/AnimatedValue'); +const AnimatedValueXY = require('../nodes/AnimatedValueXY'); +const Animation = require('./Animation'); +const SpringConfig = require('../SpringConfig'); + +const invariant = require('fbjs/lib/invariant'); +const {shouldUseNativeDriver} = require('../NativeAnimatedHelper'); + +import type {AnimationConfig, EndCallback} from './Animation'; + +export type SpringAnimationConfig = AnimationConfig & { + toValue: number | AnimatedValue | {x: number, y: number} | AnimatedValueXY, + overshootClamping?: boolean, + restDisplacementThreshold?: number, + restSpeedThreshold?: number, + velocity?: number | {x: number, y: number}, + bounciness?: number, + speed?: number, + tension?: number, + friction?: number, + stiffness?: number, + damping?: number, + mass?: number, + delay?: number, +}; + +export type SpringAnimationConfigSingle = AnimationConfig & { + toValue: number | AnimatedValue, + overshootClamping?: boolean, + restDisplacementThreshold?: number, + restSpeedThreshold?: number, + velocity?: number, + bounciness?: number, + speed?: number, + tension?: number, + friction?: number, + stiffness?: number, + damping?: number, + mass?: number, + delay?: number, +}; + +function withDefault(value: ?T, defaultValue: T): T { + if (value === undefined || value === null) { + return defaultValue; + } + return value; +} + +class SpringAnimation extends Animation { + _overshootClamping: boolean; + _restDisplacementThreshold: number; + _restSpeedThreshold: number; + _lastVelocity: number; + _startPosition: number; + _lastPosition: number; + _fromValue: number; + _toValue: any; + _stiffness: number; + _damping: number; + _mass: number; + _initialVelocity: number; + _delay: number; + _timeout: any; + _startTime: number; + _lastTime: number; + _frameTime: number; + _onUpdate: (value: number) => void; + _animationFrame: any; + _useNativeDriver: boolean; + + constructor(config: SpringAnimationConfigSingle) { + super(); + + this._overshootClamping = withDefault(config.overshootClamping, false); + this._restDisplacementThreshold = withDefault( + config.restDisplacementThreshold, + 0.001, + ); + this._restSpeedThreshold = withDefault(config.restSpeedThreshold, 0.001); + this._initialVelocity = withDefault(config.velocity, 0); + this._lastVelocity = withDefault(config.velocity, 0); + this._toValue = config.toValue; + this._delay = withDefault(config.delay, 0); + this._useNativeDriver = shouldUseNativeDriver(config); + this.__isInteraction = + config.isInteraction !== undefined ? config.isInteraction : true; + this.__iterations = config.iterations !== undefined ? config.iterations : 1; + + if ( + config.stiffness !== undefined || + config.damping !== undefined || + config.mass !== undefined + ) { + invariant( + config.bounciness === undefined && + config.speed === undefined && + config.tension === undefined && + config.friction === undefined, + 'You can define one of bounciness/speed, tension/friction, or stiffness/damping/mass, but not more than one', + ); + this._stiffness = withDefault(config.stiffness, 100); + this._damping = withDefault(config.damping, 10); + this._mass = withDefault(config.mass, 1); + } else if (config.bounciness !== undefined || config.speed !== undefined) { + // Convert the origami bounciness/speed values to stiffness/damping + // We assume mass is 1. + invariant( + config.tension === undefined && + config.friction === undefined && + config.stiffness === undefined && + config.damping === undefined && + config.mass === undefined, + 'You can define one of bounciness/speed, tension/friction, or stiffness/damping/mass, but not more than one', + ); + const springConfig = SpringConfig.fromBouncinessAndSpeed( + withDefault(config.bounciness, 8), + withDefault(config.speed, 12), + ); + this._stiffness = springConfig.stiffness; + this._damping = springConfig.damping; + this._mass = 1; + } else { + // Convert the origami tension/friction values to stiffness/damping + // We assume mass is 1. + const springConfig = SpringConfig.fromOrigamiTensionAndFriction( + withDefault(config.tension, 40), + withDefault(config.friction, 7), + ); + this._stiffness = springConfig.stiffness; + this._damping = springConfig.damping; + this._mass = 1; + } + + invariant(this._stiffness > 0, 'Stiffness value must be greater than 0'); + invariant(this._damping > 0, 'Damping value must be greater than 0'); + invariant(this._mass > 0, 'Mass value must be greater than 0'); + } + + __getNativeAnimationConfig() { + return { + type: 'spring', + overshootClamping: this._overshootClamping, + restDisplacementThreshold: this._restDisplacementThreshold, + restSpeedThreshold: this._restSpeedThreshold, + stiffness: this._stiffness, + damping: this._damping, + mass: this._mass, + initialVelocity: withDefault(this._initialVelocity, this._lastVelocity), + toValue: this._toValue, + iterations: this.__iterations, + }; + } + + start( + fromValue: number, + onUpdate: (value: number) => void, + onEnd: ?EndCallback, + previousAnimation: ?Animation, + animatedValue: AnimatedValue, + ): void { + this.__active = true; + this._startPosition = fromValue; + this._lastPosition = this._startPosition; + + this._onUpdate = onUpdate; + this.__onEnd = onEnd; + this._lastTime = Date.now(); + this._frameTime = 0.0; + + if (previousAnimation instanceof SpringAnimation) { + const internalState = previousAnimation.getInternalState(); + this._lastPosition = internalState.lastPosition; + this._lastVelocity = internalState.lastVelocity; + // Set the initial velocity to the last velocity + this._initialVelocity = this._lastVelocity; + this._lastTime = internalState.lastTime; + } + + const start = () => { + if (this._useNativeDriver) { + this.__startNativeAnimation(animatedValue); + } else { + this.onUpdate(); + } + }; + + // If this._delay is more than 0, we start after the timeout. + if (this._delay) { + this._timeout = setTimeout(start, this._delay); + } else { + start(); + } + } + + getInternalState(): Object { + return { + lastPosition: this._lastPosition, + lastVelocity: this._lastVelocity, + lastTime: this._lastTime, + }; + } + + /** + * This spring model is based off of a damped harmonic oscillator + * (https://en.wikipedia.org/wiki/Harmonic_oscillator#Damped_harmonic_oscillator). + * + * We use the closed form of the second order differential equation: + * + * x'' + (2ζ⍵_0)x' + ⍵^2x = 0 + * + * where + * ⍵_0 = √(k / m) (undamped angular frequency of the oscillator), + * ζ = c / 2√mk (damping ratio), + * c = damping constant + * k = stiffness + * m = mass + * + * The derivation of the closed form is described in detail here: + * http://planetmath.org/sites/default/files/texpdf/39745.pdf + * + * This algorithm happens to match the algorithm used by CASpringAnimation, + * a QuartzCore (iOS) API that creates spring animations. + */ + onUpdate(): void { + // If for some reason we lost a lot of frames (e.g. process large payload or + // stopped in the debugger), we only advance by 4 frames worth of + // computation and will continue on the next frame. It's better to have it + // running at faster speed than jumping to the end. + const MAX_STEPS = 64; + let now = Date.now(); + if (now > this._lastTime + MAX_STEPS) { + now = this._lastTime + MAX_STEPS; + } + + const deltaTime = (now - this._lastTime) / 1000; + this._frameTime += deltaTime; + + const c: number = this._damping; + const m: number = this._mass; + const k: number = this._stiffness; + const v0: number = -this._initialVelocity; + + const zeta = c / (2 * Math.sqrt(k * m)); // damping ratio + const omega0 = Math.sqrt(k / m); // undamped angular frequency of the oscillator (rad/ms) + const omega1 = omega0 * Math.sqrt(1.0 - zeta * zeta); // exponential decay + const x0 = this._toValue - this._startPosition; // calculate the oscillation from x0 = 1 to x = 0 + + let position = 0.0; + let velocity = 0.0; + const t = this._frameTime; + if (zeta < 1) { + // Under damped + const envelope = Math.exp(-zeta * omega0 * t); + position = + this._toValue - + envelope * + ((v0 + zeta * omega0 * x0) / omega1 * Math.sin(omega1 * t) + + x0 * Math.cos(omega1 * t)); + // This looks crazy -- it's actually just the derivative of the + // oscillation function + velocity = + zeta * + omega0 * + envelope * + (Math.sin(omega1 * t) * (v0 + zeta * omega0 * x0) / omega1 + + x0 * Math.cos(omega1 * t)) - + envelope * + (Math.cos(omega1 * t) * (v0 + zeta * omega0 * x0) - + omega1 * x0 * Math.sin(omega1 * t)); + } else { + // Critically damped + const envelope = Math.exp(-omega0 * t); + position = this._toValue - envelope * (x0 + (v0 + omega0 * x0) * t); + velocity = + envelope * (v0 * (t * omega0 - 1) + t * x0 * (omega0 * omega0)); + } + + this._lastTime = now; + this._lastPosition = position; + this._lastVelocity = velocity; + + this._onUpdate(position); + if (!this.__active) { + // a listener might have stopped us in _onUpdate + return; + } + + // Conditions for stopping the spring animation + let isOvershooting = false; + if (this._overshootClamping && this._stiffness !== 0) { + if (this._startPosition < this._toValue) { + isOvershooting = position > this._toValue; + } else { + isOvershooting = position < this._toValue; + } + } + const isVelocity = Math.abs(velocity) <= this._restSpeedThreshold; + let isDisplacement = true; + if (this._stiffness !== 0) { + isDisplacement = + Math.abs(this._toValue - position) <= this._restDisplacementThreshold; + } + + if (isOvershooting || (isVelocity && isDisplacement)) { + if (this._stiffness !== 0) { + // Ensure that we end up with a round value + this._lastPosition = this._toValue; + this._lastVelocity = 0; + this._onUpdate(this._toValue); + } + + this.__debouncedOnEnd({finished: true}); + return; + } + this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this)); + } + + stop(): void { + super.stop(); + this.__active = false; + clearTimeout(this._timeout); + global.cancelAnimationFrame(this._animationFrame); + this.__debouncedOnEnd({finished: false}); + } +} + +module.exports = SpringAnimation; diff --git a/Libraries/Animated/src/animations/TimingAnimation.js b/Libraries/Animated/src/animations/TimingAnimation.js new file mode 100644 index 0000000..a3db014 --- /dev/null +++ b/Libraries/Animated/src/animations/TimingAnimation.js @@ -0,0 +1,155 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule TimingAnimation + * @flow + * @format + */ +'use strict'; + +const AnimatedValue = require('../nodes/AnimatedValue'); +const AnimatedValueXY = require('../nodes/AnimatedValueXY'); +const Animation = require('./Animation'); + +const {shouldUseNativeDriver} = require('../NativeAnimatedHelper'); + +import type {AnimationConfig, EndCallback} from './Animation'; + +export type TimingAnimationConfig = AnimationConfig & { + toValue: number | AnimatedValue | {x: number, y: number} | AnimatedValueXY, + easing?: (value: number) => number, + duration?: number, + delay?: number, +}; + +export type TimingAnimationConfigSingle = AnimationConfig & { + toValue: number | AnimatedValue, + easing?: (value: number) => number, + duration?: number, + delay?: number, +}; + +let _easeInOut; +function easeInOut() { + if (!_easeInOut) { + const Easing = require('Easing'); + _easeInOut = Easing.inOut(Easing.ease); + } + return _easeInOut; +} + +class TimingAnimation extends Animation { + _startTime: number; + _fromValue: number; + _toValue: any; + _duration: number; + _delay: number; + _easing: (value: number) => number; + _onUpdate: (value: number) => void; + _animationFrame: any; + _timeout: any; + _useNativeDriver: boolean; + + constructor(config: TimingAnimationConfigSingle) { + super(); + this._toValue = config.toValue; + this._easing = config.easing !== undefined ? config.easing : easeInOut(); + this._duration = config.duration !== undefined ? config.duration : 500; + this._delay = config.delay !== undefined ? config.delay : 0; + this.__iterations = config.iterations !== undefined ? config.iterations : 1; + this.__isInteraction = + config.isInteraction !== undefined ? config.isInteraction : true; + this._useNativeDriver = shouldUseNativeDriver(config); + } + + __getNativeAnimationConfig(): any { + const frameDuration = 1000.0 / 60.0; + const frames = []; + for (let dt = 0.0; dt < this._duration; dt += frameDuration) { + frames.push(this._easing(dt / this._duration)); + } + frames.push(this._easing(1)); + return { + type: 'frames', + frames, + toValue: this._toValue, + iterations: this.__iterations, + }; + } + + start( + fromValue: number, + onUpdate: (value: number) => void, + onEnd: ?EndCallback, + previousAnimation: ?Animation, + animatedValue: AnimatedValue, + ): void { + this.__active = true; + this._fromValue = fromValue; + this._onUpdate = onUpdate; + this.__onEnd = onEnd; + + const start = () => { + // Animations that sometimes have 0 duration and sometimes do not + // still need to use the native driver when duration is 0 so as to + // not cause intermixed JS and native animations. + if (this._duration === 0 && !this._useNativeDriver) { + this._onUpdate(this._toValue); + this.__debouncedOnEnd({finished: true}); + } else { + this._startTime = Date.now(); + if (this._useNativeDriver) { + this.__startNativeAnimation(animatedValue); + } else { + this._animationFrame = requestAnimationFrame( + this.onUpdate.bind(this), + ); + } + } + }; + if (this._delay) { + this._timeout = setTimeout(start, this._delay); + } else { + start(); + } + } + + onUpdate(): void { + const now = Date.now(); + if (now >= this._startTime + this._duration) { + if (this._duration === 0) { + this._onUpdate(this._toValue); + } else { + this._onUpdate( + this._fromValue + this._easing(1) * (this._toValue - this._fromValue), + ); + } + this.__debouncedOnEnd({finished: true}); + return; + } + + this._onUpdate( + this._fromValue + + this._easing((now - this._startTime) / this._duration) * + (this._toValue - this._fromValue), + ); + if (this.__active) { + this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this)); + } + } + + stop(): void { + super.stop(); + this.__active = false; + clearTimeout(this._timeout); + global.cancelAnimationFrame(this._animationFrame); + this.__debouncedOnEnd({finished: false}); + } +} + +module.exports = TimingAnimation; diff --git a/Libraries/Animated/src/bezier.js b/Libraries/Animated/src/bezier.js new file mode 100644 index 0000000..2bd5949 --- /dev/null +++ b/Libraries/Animated/src/bezier.js @@ -0,0 +1,108 @@ +/** + * BezierEasing - use bezier curve for transition easing function + * https://github.com/gre/bezier-easing + * + * @copyright 2014-2015 Gaëtan Renaudeau. MIT License. + * @providesModule bezier + * @noflow + */ +'use strict'; + + // These values are established by empiricism with tests (tradeoff: performance VS precision) + var NEWTON_ITERATIONS = 4; + var NEWTON_MIN_SLOPE = 0.001; + var SUBDIVISION_PRECISION = 0.0000001; + var SUBDIVISION_MAX_ITERATIONS = 10; + + var kSplineTableSize = 11; + var kSampleStepSize = 1.0 / (kSplineTableSize - 1.0); + + var float32ArraySupported = typeof Float32Array === 'function'; + + function A (aA1, aA2) { return 1.0 - 3.0 * aA2 + 3.0 * aA1; } + function B (aA1, aA2) { return 3.0 * aA2 - 6.0 * aA1; } + function C (aA1) { return 3.0 * aA1; } + + // Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2. + function calcBezier (aT, aA1, aA2) { return ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT; } + + // Returns dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2. + function getSlope (aT, aA1, aA2) { return 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1); } + + function binarySubdivide (aX, aA, aB, mX1, mX2) { + var currentX, currentT, i = 0; + do { + currentT = aA + (aB - aA) / 2.0; + currentX = calcBezier(currentT, mX1, mX2) - aX; + if (currentX > 0.0) { + aB = currentT; + } else { + aA = currentT; + } + } while (Math.abs(currentX) > SUBDIVISION_PRECISION && ++i < SUBDIVISION_MAX_ITERATIONS); + return currentT; + } + + function newtonRaphsonIterate (aX, aGuessT, mX1, mX2) { + for (var i = 0; i < NEWTON_ITERATIONS; ++i) { + var currentSlope = getSlope(aGuessT, mX1, mX2); + if (currentSlope === 0.0) { + return aGuessT; + } + var currentX = calcBezier(aGuessT, mX1, mX2) - aX; + aGuessT -= currentX / currentSlope; + } + return aGuessT; + } + + module.exports = function bezier (mX1, mY1, mX2, mY2) { + if (!(0 <= mX1 && mX1 <= 1 && 0 <= mX2 && mX2 <= 1)) { // eslint-disable-line yoda + throw new Error('bezier x values must be in [0, 1] range'); + } + + // Precompute samples table + var sampleValues = float32ArraySupported ? new Float32Array(kSplineTableSize) : new Array(kSplineTableSize); + if (mX1 !== mY1 || mX2 !== mY2) { + for (var i = 0; i < kSplineTableSize; ++i) { + sampleValues[i] = calcBezier(i * kSampleStepSize, mX1, mX2); + } + } + + function getTForX (aX) { + var intervalStart = 0.0; + var currentSample = 1; + var lastSample = kSplineTableSize - 1; + + for (; currentSample !== lastSample && sampleValues[currentSample] <= aX; ++currentSample) { + intervalStart += kSampleStepSize; + } + --currentSample; + + // Interpolate to provide an initial guess for t + var dist = (aX - sampleValues[currentSample]) / (sampleValues[currentSample + 1] - sampleValues[currentSample]); + var guessForT = intervalStart + dist * kSampleStepSize; + + var initialSlope = getSlope(guessForT, mX1, mX2); + if (initialSlope >= NEWTON_MIN_SLOPE) { + return newtonRaphsonIterate(aX, guessForT, mX1, mX2); + } else if (initialSlope === 0.0) { + return guessForT; + } else { + return binarySubdivide(aX, intervalStart, intervalStart + kSampleStepSize, mX1, mX2); + } + } + + return function BezierEasing (x) { + if (mX1 === mY1 && mX2 === mY2) { + return x; // linear + } + // Because JavaScript number are imprecise, we should guarantee the extremes are right. + if (x === 0) { + return 0; + } + if (x === 1) { + return 1; + } + return calcBezier(getTForX(x), mY1, mY2); + }; + }; diff --git a/Libraries/Animated/src/createAnimatedComponent.js b/Libraries/Animated/src/createAnimatedComponent.js new file mode 100644 index 0000000..1d649a0 --- /dev/null +++ b/Libraries/Animated/src/createAnimatedComponent.js @@ -0,0 +1,193 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule createAnimatedComponent + * @flow + * @format + */ +'use strict'; + +const {AnimatedEvent} = require('./AnimatedEvent'); +const AnimatedProps = require('./nodes/AnimatedProps'); +const React = require('react'); +// const ViewStylePropTypes = require('ViewStylePropTypes'); + +function createAnimatedComponent(Component: any): any { + class AnimatedComponent extends React.Component { + _component: any; + _prevComponent: any; + _propsAnimated: AnimatedProps; + _eventDetachers: Array = []; + _setComponentRef: Function; + + static __skipSetNativeProps_FOR_TESTS_ONLY = false; + + constructor(props: Object) { + super(props); + this._setComponentRef = this._setComponentRef.bind(this); + } + + componentWillUnmount() { + this._propsAnimated && this._propsAnimated.__detach(); + this._detachNativeEvents(); + } + + setNativeProps(props) { + this._component.setNativeProps(props); + } + + componentWillMount() { + this._attachProps(this.props); + } + + componentDidMount() { + this._propsAnimated.setNativeView(this._component); + this._attachNativeEvents(); + } + + _attachNativeEvents() { + // Make sure to get the scrollable node for components that implement + // `ScrollResponder.Mixin`. + const scrollableNode = this._component.getScrollableNode + ? this._component.getScrollableNode() + : this._component; + + for (const key in this.props) { + const prop = this.props[key]; + if (prop instanceof AnimatedEvent && prop.__isNative) { + prop.__attach(scrollableNode, key); + this._eventDetachers.push(() => prop.__detach(scrollableNode, key)); + } + } + } + + _detachNativeEvents() { + this._eventDetachers.forEach(remove => remove()); + this._eventDetachers = []; + } + + _attachProps(nextProps) { + const oldPropsAnimated = this._propsAnimated; + + // The system is best designed when setNativeProps is implemented. It is + // able to avoid re-rendering and directly set the attributes that + // changed. However, setNativeProps can only be implemented on leaf + // native components. If you want to animate a composite component, you + // need to re-render it. In this case, we have a fallback that uses + // forceUpdate. + const callback = () => { + if ( + !AnimatedComponent.__skipSetNativeProps_FOR_TESTS_ONLY && + this._component.setNativeProps + ) { + if (!this._propsAnimated.__isNative) { + this._component.setNativeProps( + this._propsAnimated.__getAnimatedValue(), + ); + } else { + throw new Error( + 'Attempting to run JS driven animation on animated ' + + 'node that has been moved to "native" earlier by starting an ' + + 'animation with `useNativeDriver: true`', + ); + } + } else { + this.forceUpdate(); + } + }; + + this._propsAnimated = new AnimatedProps(nextProps, callback); + + // When you call detach, it removes the element from the parent list + // of children. If it goes to 0, then the parent also detaches itself + // and so on. + // An optimization is to attach the new elements and THEN detach the old + // ones instead of detaching and THEN attaching. + // This way the intermediate state isn't to go to 0 and trigger + // this expensive recursive detaching to then re-attach everything on + // the very next operation. + oldPropsAnimated && oldPropsAnimated.__detach(); + } + + componentWillReceiveProps(newProps) { + this._attachProps(newProps); + } + + componentDidUpdate(prevProps) { + if (this._component !== this._prevComponent) { + this._propsAnimated.setNativeView(this._component); + } + if (this._component !== this._prevComponent || prevProps !== this.props) { + this._detachNativeEvents(); + this._attachNativeEvents(); + } + } + + render() { + const props = this._propsAnimated.__getValue(); + return ( + + ); + } + + _setComponentRef(c) { + this._prevComponent = this._component; + this._component = c; + } + + // A third party library can use getNode() + // to get the node reference of the decorated component + getNode() { + return this._component; + } + } + + // ReactNative `View.propTypes` have been deprecated in favor of + // `ViewPropTypes`. In their place a temporary getter has been added with a + // deprecated warning message. Avoid triggering that warning here by using + // temporary workaround, __propTypesSecretDontUseThesePlease. + // TODO (bvaughn) Revert this particular change any time after April 1 + const propTypes = + Component.__propTypesSecretDontUseThesePlease || Component.propTypes; + + AnimatedComponent.propTypes = { + style: function(props, propName, componentName) { + if (!propTypes) { + return; + } + + // for (const key in ViewStylePropTypes) { + // if (!propTypes[key] && props[key] !== undefined) { + // console.warn( + // 'You are setting the style `{ ' + + // key + + // ': ... }` as a prop. You ' + + // 'should nest it in a style object. ' + + // 'E.g. `{ style: { ' + + // key + + // ': ... } }`', + // ); + // } + // } + }, + }; + + return AnimatedComponent; +} + +module.exports = createAnimatedComponent; diff --git a/Libraries/Animated/src/nodes/AnimatedAddition.js b/Libraries/Animated/src/nodes/AnimatedAddition.js new file mode 100644 index 0000000..53d3388 --- /dev/null +++ b/Libraries/Animated/src/nodes/AnimatedAddition.js @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule AnimatedAddition + * @flow + * @format + */ +'use strict'; + +const AnimatedInterpolation = require('./AnimatedInterpolation'); +const AnimatedNode = require('./AnimatedNode'); +const AnimatedValue = require('./AnimatedValue'); +const AnimatedWithChildren = require('./AnimatedWithChildren'); + +import type {InterpolationConfigType} from './AnimatedInterpolation'; + +class AnimatedAddition extends AnimatedWithChildren { + _a: AnimatedNode; + _b: AnimatedNode; + + constructor(a: AnimatedNode | number, b: AnimatedNode | number) { + super(); + this._a = typeof a === 'number' ? new AnimatedValue(a) : a; + this._b = typeof b === 'number' ? new AnimatedValue(b) : b; + } + + __makeNative() { + this._a.__makeNative(); + this._b.__makeNative(); + super.__makeNative(); + } + + __getValue(): number { + return this._a.__getValue() + this._b.__getValue(); + } + + interpolate(config: InterpolationConfigType): AnimatedInterpolation { + return new AnimatedInterpolation(this, config); + } + + __attach(): void { + this._a.__addChild(this); + this._b.__addChild(this); + } + + __detach(): void { + this._a.__removeChild(this); + this._b.__removeChild(this); + super.__detach(); + } + + __getNativeConfig(): any { + return { + type: 'addition', + input: [this._a.__getNativeTag(), this._b.__getNativeTag()], + }; + } +} + +module.exports = AnimatedAddition; diff --git a/Libraries/Animated/src/nodes/AnimatedDiffClamp.js b/Libraries/Animated/src/nodes/AnimatedDiffClamp.js new file mode 100644 index 0000000..eac26a1 --- /dev/null +++ b/Libraries/Animated/src/nodes/AnimatedDiffClamp.js @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule AnimatedDiffClamp + * @flow + * @format + */ +'use strict'; + +const AnimatedInterpolation = require('./AnimatedInterpolation'); +const AnimatedNode = require('./AnimatedNode'); +const AnimatedWithChildren = require('./AnimatedWithChildren'); + +import type {InterpolationConfigType} from './AnimatedInterpolation'; + +class AnimatedDiffClamp extends AnimatedWithChildren { + _a: AnimatedNode; + _min: number; + _max: number; + _value: number; + _lastValue: number; + + constructor(a: AnimatedNode, min: number, max: number) { + super(); + + this._a = a; + this._min = min; + this._max = max; + this._value = this._lastValue = this._a.__getValue(); + } + + __makeNative() { + this._a.__makeNative(); + super.__makeNative(); + } + + interpolate(config: InterpolationConfigType): AnimatedInterpolation { + return new AnimatedInterpolation(this, config); + } + + __getValue(): number { + const value = this._a.__getValue(); + const diff = value - this._lastValue; + this._lastValue = value; + this._value = Math.min(Math.max(this._value + diff, this._min), this._max); + return this._value; + } + + __attach(): void { + this._a.__addChild(this); + } + + __detach(): void { + this._a.__removeChild(this); + super.__detach(); + } + + __getNativeConfig(): any { + return { + type: 'diffclamp', + input: this._a.__getNativeTag(), + min: this._min, + max: this._max, + }; + } +} + +module.exports = AnimatedDiffClamp; diff --git a/Libraries/Animated/src/nodes/AnimatedDivision.js b/Libraries/Animated/src/nodes/AnimatedDivision.js new file mode 100644 index 0000000..8d658f4 --- /dev/null +++ b/Libraries/Animated/src/nodes/AnimatedDivision.js @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule AnimatedDivision + * @flow + * @format + */ +'use strict'; + +const AnimatedInterpolation = require('./AnimatedInterpolation'); +const AnimatedNode = require('./AnimatedNode'); +const AnimatedValue = require('./AnimatedValue'); +const AnimatedWithChildren = require('./AnimatedWithChildren'); + +import type {InterpolationConfigType} from './AnimatedInterpolation'; + +class AnimatedDivision extends AnimatedWithChildren { + _a: AnimatedNode; + _b: AnimatedNode; + + constructor(a: AnimatedNode | number, b: AnimatedNode | number) { + super(); + this._a = typeof a === 'number' ? new AnimatedValue(a) : a; + this._b = typeof b === 'number' ? new AnimatedValue(b) : b; + } + + __makeNative() { + this._a.__makeNative(); + this._b.__makeNative(); + super.__makeNative(); + } + + __getValue(): number { + const a = this._a.__getValue(); + const b = this._b.__getValue(); + if (b === 0) { + console.error('Detected division by zero in AnimatedDivision'); + } + return a / b; + } + + interpolate(config: InterpolationConfigType): AnimatedInterpolation { + return new AnimatedInterpolation(this, config); + } + + __attach(): void { + this._a.__addChild(this); + this._b.__addChild(this); + } + + __detach(): void { + this._a.__removeChild(this); + this._b.__removeChild(this); + super.__detach(); + } + + __getNativeConfig(): any { + return { + type: 'division', + input: [this._a.__getNativeTag(), this._b.__getNativeTag()], + }; + } +} + +module.exports = AnimatedDivision; diff --git a/Libraries/Animated/src/nodes/AnimatedInterpolation.js b/Libraries/Animated/src/nodes/AnimatedInterpolation.js new file mode 100644 index 0000000..cfe9e70 --- /dev/null +++ b/Libraries/Animated/src/nodes/AnimatedInterpolation.js @@ -0,0 +1,388 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule AnimatedInterpolation + * @flow + * @format + */ +/* eslint no-bitwise: 0 */ +'use strict'; + +const AnimatedNode = require('./AnimatedNode'); +const AnimatedWithChildren = require('./AnimatedWithChildren'); +const NativeAnimatedHelper = require('../NativeAnimatedHelper'); + +const invariant = require('fbjs/lib/invariant'); +const normalizeColor = require('normalizeColor'); + +type ExtrapolateType = 'extend' | 'identity' | 'clamp'; + +export type InterpolationConfigType = { + inputRange: Array, + /* $FlowFixMe(>=0.38.0 site=react_native_fb,react_native_oss) - Flow error + * detected during the deployment of v0.38.0. To see the error, remove this + * comment and run flow + */ + outputRange: Array | Array, + easing?: (input: number) => number, + extrapolate?: ExtrapolateType, + extrapolateLeft?: ExtrapolateType, + extrapolateRight?: ExtrapolateType, +}; + +const linear = t => t; + +/** + * Very handy helper to map input ranges to output ranges with an easing + * function and custom behavior outside of the ranges. + */ +function createInterpolation( + config: InterpolationConfigType, +): (input: number) => number | string { + if (config.outputRange && typeof config.outputRange[0] === 'string') { + return createInterpolationFromStringOutputRange(config); + } + + const outputRange: Array = (config.outputRange: any); + checkInfiniteRange('outputRange', outputRange); + + const inputRange = config.inputRange; + checkInfiniteRange('inputRange', inputRange); + checkValidInputRange(inputRange); + + invariant( + inputRange.length === outputRange.length, + 'inputRange (' + + inputRange.length + + ') and outputRange (' + + outputRange.length + + ') must have the same length', + ); + + const easing = config.easing || linear; + + let extrapolateLeft: ExtrapolateType = 'extend'; + if (config.extrapolateLeft !== undefined) { + extrapolateLeft = config.extrapolateLeft; + } else if (config.extrapolate !== undefined) { + extrapolateLeft = config.extrapolate; + } + + let extrapolateRight: ExtrapolateType = 'extend'; + if (config.extrapolateRight !== undefined) { + extrapolateRight = config.extrapolateRight; + } else if (config.extrapolate !== undefined) { + extrapolateRight = config.extrapolate; + } + + return input => { + invariant( + typeof input === 'number', + 'Cannot interpolation an input which is not a number', + ); + + const range = findRange(input, inputRange); + return interpolate( + input, + inputRange[range], + inputRange[range + 1], + outputRange[range], + outputRange[range + 1], + easing, + extrapolateLeft, + extrapolateRight, + ); + }; +} + +function interpolate( + input: number, + inputMin: number, + inputMax: number, + outputMin: number, + outputMax: number, + easing: (input: number) => number, + extrapolateLeft: ExtrapolateType, + extrapolateRight: ExtrapolateType, +) { + let result = input; + + // Extrapolate + if (result < inputMin) { + if (extrapolateLeft === 'identity') { + return result; + } else if (extrapolateLeft === 'clamp') { + result = inputMin; + } else if (extrapolateLeft === 'extend') { + // noop + } + } + + if (result > inputMax) { + if (extrapolateRight === 'identity') { + return result; + } else if (extrapolateRight === 'clamp') { + result = inputMax; + } else if (extrapolateRight === 'extend') { + // noop + } + } + + if (outputMin === outputMax) { + return outputMin; + } + + if (inputMin === inputMax) { + if (input <= inputMin) { + return outputMin; + } + return outputMax; + } + + // Input Range + if (inputMin === -Infinity) { + result = -result; + } else if (inputMax === Infinity) { + result = result - inputMin; + } else { + result = (result - inputMin) / (inputMax - inputMin); + } + + // Easing + result = easing(result); + + // Output Range + if (outputMin === -Infinity) { + result = -result; + } else if (outputMax === Infinity) { + result = result + outputMin; + } else { + result = result * (outputMax - outputMin) + outputMin; + } + + return result; +} + +function colorToRgba(input: string): string { + let int32Color = normalizeColor(input); + if (int32Color === null) { + return input; + } + + int32Color = int32Color || 0; + + const r = (int32Color & 0xff000000) >>> 24; + const g = (int32Color & 0x00ff0000) >>> 16; + const b = (int32Color & 0x0000ff00) >>> 8; + const a = (int32Color & 0x000000ff) / 255; + + return `rgba(${r}, ${g}, ${b}, ${a})`; +} + +const stringShapeRegex = /[0-9\.-]+/g; + +/** + * Supports string shapes by extracting numbers so new values can be computed, + * and recombines those values into new strings of the same shape. Supports + * things like: + * + * rgba(123, 42, 99, 0.36) // colors + * -45deg // values with units + */ +function createInterpolationFromStringOutputRange( + config: InterpolationConfigType, +): (input: number) => string { + let outputRange: Array = (config.outputRange: any); + invariant(outputRange.length >= 2, 'Bad output range'); + outputRange = outputRange.map(colorToRgba); + checkPattern(outputRange); + + // ['rgba(0, 100, 200, 0)', 'rgba(50, 150, 250, 0.5)'] + // -> + // [ + // [0, 50], + // [100, 150], + // [200, 250], + // [0, 0.5], + // ] + /* $FlowFixMe(>=0.18.0): `outputRange[0].match()` can return `null`. Need to + * guard against this possibility. + */ + const outputRanges = outputRange[0].match(stringShapeRegex).map(() => []); + outputRange.forEach(value => { + /* $FlowFixMe(>=0.18.0): `value.match()` can return `null`. Need to guard + * against this possibility. + */ + value.match(stringShapeRegex).forEach((number, i) => { + outputRanges[i].push(+number); + }); + }); + + /* $FlowFixMe(>=0.18.0): `outputRange[0].match()` can return `null`. Need to + * guard against this possibility. + */ + const interpolations = outputRange[0] + .match(stringShapeRegex) + .map((value, i) => { + return createInterpolation({ + ...config, + outputRange: outputRanges[i], + }); + }); + + // rgba requires that the r,g,b are integers.... so we want to round them, but we *dont* want to + // round the opacity (4th column). + const shouldRound = isRgbOrRgba(outputRange[0]); + + return input => { + let i = 0; + // 'rgba(0, 100, 200, 0)' + // -> + // 'rgba(${interpolations[0](input)}, ${interpolations[1](input)}, ...' + return outputRange[0].replace(stringShapeRegex, () => { + const val = +interpolations[i++](input); + const rounded = + shouldRound && i < 4 ? Math.round(val) : Math.round(val * 1000) / 1000; + return String(rounded); + }); + }; +} + +function isRgbOrRgba(range) { + return typeof range === 'string' && range.startsWith('rgb'); +} + +function checkPattern(arr: Array) { + const pattern = arr[0].replace(stringShapeRegex, ''); + for (let i = 1; i < arr.length; ++i) { + invariant( + pattern === arr[i].replace(stringShapeRegex, ''), + 'invalid pattern ' + arr[0] + ' and ' + arr[i], + ); + } +} + +function findRange(input: number, inputRange: Array) { + let i; + for (i = 1; i < inputRange.length - 1; ++i) { + if (inputRange[i] >= input) { + break; + } + } + return i - 1; +} + +function checkValidInputRange(arr: Array) { + invariant(arr.length >= 2, 'inputRange must have at least 2 elements'); + for (let i = 1; i < arr.length; ++i) { + invariant( + arr[i] >= arr[i - 1], + /* $FlowFixMe(>=0.13.0) - In the addition expression below this comment, + * one or both of the operands may be something that doesn't cleanly + * convert to a string, like undefined, null, and object, etc. If you really + * mean this implicit string conversion, you can do something like + * String(myThing) + */ + 'inputRange must be monotonically increasing ' + arr, + ); + } +} + +function checkInfiniteRange(name: string, arr: Array) { + invariant(arr.length >= 2, name + ' must have at least 2 elements'); + invariant( + arr.length !== 2 || arr[0] !== -Infinity || arr[1] !== Infinity, + /* $FlowFixMe(>=0.13.0) - In the addition expression below this comment, + * one or both of the operands may be something that doesn't cleanly convert + * to a string, like undefined, null, and object, etc. If you really mean + * this implicit string conversion, you can do something like + * String(myThing) + */ + name + 'cannot be ]-infinity;+infinity[ ' + arr, + ); +} + +class AnimatedInterpolation extends AnimatedWithChildren { + // Export for testing. + static __createInterpolation = createInterpolation; + + _parent: AnimatedNode; + _config: InterpolationConfigType; + _interpolation: (input: number) => number | string; + + constructor(parent: AnimatedNode, config: InterpolationConfigType) { + super(); + this._parent = parent; + this._config = config; + this._interpolation = createInterpolation(config); + } + + __makeNative() { + this._parent.__makeNative(); + super.__makeNative(); + } + + __getValue(): number | string { + const parentValue: number = this._parent.__getValue(); + invariant( + typeof parentValue === 'number', + 'Cannot interpolate an input which is not a number.', + ); + return this._interpolation(parentValue); + } + + interpolate(config: InterpolationConfigType): AnimatedInterpolation { + return new AnimatedInterpolation(this, config); + } + + __attach(): void { + this._parent.__addChild(this); + } + + __detach(): void { + this._parent.__removeChild(this); + super.__detach(); + } + + __transformDataType(range: Array) { + // Change the string array type to number array + // So we can reuse the same logic in iOS and Android platform + return range.map(function(value) { + if (typeof value !== 'string') { + return value; + } + if (/deg$/.test(value)) { + const degrees = parseFloat(value) || 0; + const radians = degrees * Math.PI / 180.0; + return radians; + } else { + // Assume radians + return parseFloat(value) || 0; + } + }); + } + + __getNativeConfig(): any { + if (__DEV__) { + NativeAnimatedHelper.validateInterpolation(this._config); + } + + return { + inputRange: this._config.inputRange, + // Only the `outputRange` can contain strings so we don't need to tranform `inputRange` here + outputRange: this.__transformDataType(this._config.outputRange), + extrapolateLeft: + this._config.extrapolateLeft || this._config.extrapolate || 'extend', + extrapolateRight: + this._config.extrapolateRight || this._config.extrapolate || 'extend', + type: 'interpolation', + }; + } +} + +module.exports = AnimatedInterpolation; diff --git a/Libraries/Animated/src/nodes/AnimatedModulo.js b/Libraries/Animated/src/nodes/AnimatedModulo.js new file mode 100644 index 0000000..3ff75e7 --- /dev/null +++ b/Libraries/Animated/src/nodes/AnimatedModulo.js @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule AnimatedModulo + * @flow + * @format + */ +'use strict'; + +const AnimatedInterpolation = require('./AnimatedInterpolation'); +const AnimatedNode = require('./AnimatedNode'); +const AnimatedWithChildren = require('./AnimatedWithChildren'); + +import type {InterpolationConfigType} from './AnimatedInterpolation'; + +class AnimatedModulo extends AnimatedWithChildren { + _a: AnimatedNode; + _modulus: number; + + constructor(a: AnimatedNode, modulus: number) { + super(); + this._a = a; + this._modulus = modulus; + } + + __makeNative() { + this._a.__makeNative(); + super.__makeNative(); + } + + __getValue(): number { + return ( + (this._a.__getValue() % this._modulus + this._modulus) % this._modulus + ); + } + + interpolate(config: InterpolationConfigType): AnimatedInterpolation { + return new AnimatedInterpolation(this, config); + } + + __attach(): void { + this._a.__addChild(this); + } + + __detach(): void { + this._a.__removeChild(this); + super.__detach(); + } + + __getNativeConfig(): any { + return { + type: 'modulus', + input: this._a.__getNativeTag(), + modulus: this._modulus, + }; + } +} + +module.exports = AnimatedModulo; diff --git a/Libraries/Animated/src/nodes/AnimatedMultiplication.js b/Libraries/Animated/src/nodes/AnimatedMultiplication.js new file mode 100644 index 0000000..d30ce25 --- /dev/null +++ b/Libraries/Animated/src/nodes/AnimatedMultiplication.js @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule AnimatedMultiplication + * @flow + * @format + */ +'use strict'; + +const AnimatedInterpolation = require('./AnimatedInterpolation'); +const AnimatedNode = require('./AnimatedNode'); +const AnimatedValue = require('./AnimatedValue'); +const AnimatedWithChildren = require('./AnimatedWithChildren'); + +import type {InterpolationConfigType} from './AnimatedInterpolation'; + +class AnimatedMultiplication extends AnimatedWithChildren { + _a: AnimatedNode; + _b: AnimatedNode; + + constructor(a: AnimatedNode | number, b: AnimatedNode | number) { + super(); + this._a = typeof a === 'number' ? new AnimatedValue(a) : a; + this._b = typeof b === 'number' ? new AnimatedValue(b) : b; + } + + __makeNative() { + this._a.__makeNative(); + this._b.__makeNative(); + super.__makeNative(); + } + + __getValue(): number { + return this._a.__getValue() * this._b.__getValue(); + } + + interpolate(config: InterpolationConfigType): AnimatedInterpolation { + return new AnimatedInterpolation(this, config); + } + + __attach(): void { + this._a.__addChild(this); + this._b.__addChild(this); + } + + __detach(): void { + this._a.__removeChild(this); + this._b.__removeChild(this); + super.__detach(); + } + + __getNativeConfig(): any { + return { + type: 'multiplication', + input: [this._a.__getNativeTag(), this._b.__getNativeTag()], + }; + } +} + +module.exports = AnimatedMultiplication; diff --git a/Libraries/Animated/src/nodes/AnimatedNode.js b/Libraries/Animated/src/nodes/AnimatedNode.js new file mode 100644 index 0000000..c29f4b7 --- /dev/null +++ b/Libraries/Animated/src/nodes/AnimatedNode.js @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule AnimatedNode + * @flow + * @format + */ +'use strict'; + +const NativeAnimatedHelper = require('../NativeAnimatedHelper'); + +const invariant = require('fbjs/lib/invariant'); + +// Note(vjeux): this would be better as an interface but flow doesn't +// support them yet +class AnimatedNode { + __attach(): void {} + __detach(): void { + if (this.__isNative && this.__nativeTag != null) { + NativeAnimatedHelper.API.dropAnimatedNode(this.__nativeTag); + this.__nativeTag = undefined; + } + } + __getValue(): any {} + __getAnimatedValue(): any { + return this.__getValue(); + } + __addChild(child: AnimatedNode) {} + __removeChild(child: AnimatedNode) {} + __getChildren(): Array { + return []; + } + + /* Methods and props used by native Animated impl */ + __isNative: boolean; + __nativeTag: ?number; + __makeNative() { + if (!this.__isNative) { + throw new Error('This node cannot be made a "native" animated node'); + } + } + __getNativeTag(): ?number { + NativeAnimatedHelper.assertNativeAnimatedModule(); + invariant( + this.__isNative, + 'Attempt to get native tag from node not marked as "native"', + ); + if (this.__nativeTag == null) { + const nativeTag: ?number = NativeAnimatedHelper.generateNewNodeTag(); + NativeAnimatedHelper.API.createAnimatedNode( + nativeTag, + this.__getNativeConfig(), + ); + this.__nativeTag = nativeTag; + } + return this.__nativeTag; + } + __getNativeConfig(): Object { + throw new Error( + 'This JS animated node type cannot be used as native animated node', + ); + } + toJSON(): any { + return this.__getValue(); + } +} + +module.exports = AnimatedNode; diff --git a/Libraries/Animated/src/nodes/AnimatedProps.js b/Libraries/Animated/src/nodes/AnimatedProps.js new file mode 100644 index 0000000..7cf110a --- /dev/null +++ b/Libraries/Animated/src/nodes/AnimatedProps.js @@ -0,0 +1,167 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule AnimatedProps + * @flow + * @format + */ +'use strict'; + +const {AnimatedEvent} = require('../AnimatedEvent'); +const AnimatedNode = require('./AnimatedNode'); +const AnimatedStyle = require('./AnimatedStyle'); +const NativeAnimatedHelper = require('../NativeAnimatedHelper'); +import findNodeHandle from 'ReactfindNodeHandle'; + +const invariant = require('fbjs/lib/invariant'); + +class AnimatedProps extends AnimatedNode { + _props: Object; + _animatedView: any; + _callback: () => void; + + constructor(props: Object, callback: () => void) { + super(); + if (props.style) { + props = { + ...props, + style: new AnimatedStyle(props.style), + }; + } + this._props = props; + this._callback = callback; + this.__attach(); + } + + __getValue(): Object { + const props = {}; + for (const key in this._props) { + const value = this._props[key]; + if (value instanceof AnimatedNode) { + if (!value.__isNative || value instanceof AnimatedStyle) { + // We cannot use value of natively driven nodes this way as the value we have access from + // JS may not be up to date. + props[key] = value.__getValue(); + } + } else if (value instanceof AnimatedEvent) { + props[key] = value.__getHandler(); + } else { + props[key] = value; + } + } + return props; + } + + __getAnimatedValue(): Object { + const props = {}; + for (const key in this._props) { + const value = this._props[key]; + if (value instanceof AnimatedNode) { + props[key] = value.__getAnimatedValue(); + } + } + return props; + } + + __attach(): void { + for (const key in this._props) { + const value = this._props[key]; + if (value instanceof AnimatedNode) { + value.__addChild(this); + } + } + } + + __detach(): void { + if (this.__isNative && this._animatedView) { + this.__disconnectAnimatedView(); + } + for (const key in this._props) { + const value = this._props[key]; + if (value instanceof AnimatedNode) { + value.__removeChild(this); + } + } + super.__detach(); + } + + update(): void { + this._callback(); + } + + __makeNative(): void { + if (!this.__isNative) { + this.__isNative = true; + for (const key in this._props) { + const value = this._props[key]; + if (value instanceof AnimatedNode) { + value.__makeNative(); + } + } + if (this._animatedView) { + this.__connectAnimatedView(); + } + } + } + + setNativeView(animatedView: any): void { + if (this._animatedView === animatedView) { + return; + } + this._animatedView = animatedView; + if (this.__isNative) { + this.__connectAnimatedView(); + } + } + + __connectAnimatedView(): void { + invariant(this.__isNative, 'Expected node to be marked as "native"'); + const nativeViewTag: ?number = findNodeHandle( + this._animatedView, + ); + invariant( + nativeViewTag != null, + 'Unable to locate attached view in the native tree', + ); + NativeAnimatedHelper.API.connectAnimatedNodeToView( + this.__getNativeTag(), + nativeViewTag, + ); + } + + __disconnectAnimatedView(): void { + invariant(this.__isNative, 'Expected node to be marked as "native"'); + const nativeViewTag: ?number = findNodeHandle( + this._animatedView, + ); + invariant( + nativeViewTag != null, + 'Unable to locate attached view in the native tree', + ); + NativeAnimatedHelper.API.disconnectAnimatedNodeFromView( + this.__getNativeTag(), + nativeViewTag, + ); + } + + __getNativeConfig(): Object { + const propsConfig = {}; + for (const propKey in this._props) { + const value = this._props[propKey]; + if (value instanceof AnimatedNode) { + propsConfig[propKey] = value.__getNativeTag(); + } + } + return { + type: 'props', + props: propsConfig, + }; + } +} + +module.exports = AnimatedProps; diff --git a/Libraries/Animated/src/nodes/AnimatedStyle.js b/Libraries/Animated/src/nodes/AnimatedStyle.js new file mode 100644 index 0000000..22e5d31 --- /dev/null +++ b/Libraries/Animated/src/nodes/AnimatedStyle.js @@ -0,0 +1,127 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule AnimatedStyle + * @flow + * @format + */ +'use strict'; + +const AnimatedNode = require('./AnimatedNode'); +const AnimatedTransform = require('./AnimatedTransform'); +const AnimatedWithChildren = require('./AnimatedWithChildren'); +const NativeAnimatedHelper = require('../NativeAnimatedHelper'); + +const flattenStyle = require('ReactFlattenStyle'); + +class AnimatedStyle extends AnimatedWithChildren { + _style: Object; + + constructor(style: any) { + super(); + style = flattenStyle(style) || {}; + if (style.transform) { + style = { + ...style, + transform: new AnimatedTransform(style.transform), + }; + } + this._style = style; + } + + // Recursively get values for nested styles (like iOS's shadowOffset) + _walkStyleAndGetValues(style) { + const updatedStyle = {}; + for (const key in style) { + const value = style[key]; + if (value instanceof AnimatedNode) { + if (!value.__isNative) { + // We cannot use value of natively driven nodes this way as the value we have access from + // JS may not be up to date. + updatedStyle[key] = value.__getValue(); + } + } else if (value && !Array.isArray(value) && typeof value === 'object') { + // Support animating nested values (for example: shadowOffset.height) + updatedStyle[key] = this._walkStyleAndGetValues(value); + } else { + updatedStyle[key] = value; + } + } + return updatedStyle; + } + + __getValue(): Object { + return this._walkStyleAndGetValues(this._style); + } + + // Recursively get animated values for nested styles (like iOS's shadowOffset) + _walkStyleAndGetAnimatedValues(style) { + const updatedStyle = {}; + for (const key in style) { + const value = style[key]; + if (value instanceof AnimatedNode) { + updatedStyle[key] = value.__getAnimatedValue(); + } else if (value && !Array.isArray(value) && typeof value === 'object') { + // Support animating nested values (for example: shadowOffset.height) + updatedStyle[key] = this._walkStyleAndGetAnimatedValues(value); + } + } + return updatedStyle; + } + + __getAnimatedValue(): Object { + return this._walkStyleAndGetAnimatedValues(this._style); + } + + __attach(): void { + for (const key in this._style) { + const value = this._style[key]; + if (value instanceof AnimatedNode) { + value.__addChild(this); + } + } + } + + __detach(): void { + for (const key in this._style) { + const value = this._style[key]; + if (value instanceof AnimatedNode) { + value.__removeChild(this); + } + } + super.__detach(); + } + + __makeNative() { + super.__makeNative(); + for (const key in this._style) { + const value = this._style[key]; + if (value instanceof AnimatedNode) { + value.__makeNative(); + } + } + } + + __getNativeConfig(): Object { + const styleConfig = {}; + for (const styleKey in this._style) { + if (this._style[styleKey] instanceof AnimatedNode) { + styleConfig[styleKey] = this._style[styleKey].__getNativeTag(); + } + // Non-animated styles are set using `setNativeProps`, no need + // to pass those as a part of the node config + } + NativeAnimatedHelper.validateStyles(styleConfig); + return { + type: 'style', + style: styleConfig, + }; + } +} + +module.exports = AnimatedStyle; diff --git a/Libraries/Animated/src/nodes/AnimatedTracking.js b/Libraries/Animated/src/nodes/AnimatedTracking.js new file mode 100644 index 0000000..1a54f78 --- /dev/null +++ b/Libraries/Animated/src/nodes/AnimatedTracking.js @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule AnimatedTracking + * @flow + * @format + */ +'use strict'; + +const AnimatedValue = require('./AnimatedValue'); +const AnimatedNode = require('./AnimatedNode'); + +import type {EndCallback} from '../animations/Animation'; + +class AnimatedTracking extends AnimatedNode { + _value: AnimatedValue; + _parent: AnimatedNode; + _callback: ?EndCallback; + _animationConfig: Object; + _animationClass: any; + + constructor( + value: AnimatedValue, + parent: AnimatedNode, + animationClass: any, + animationConfig: Object, + callback?: ?EndCallback, + ) { + super(); + this._value = value; + this._parent = parent; + this._animationClass = animationClass; + this._animationConfig = animationConfig; + this._callback = callback; + this.__attach(); + } + + __getValue(): Object { + return this._parent.__getValue(); + } + + __attach(): void { + this._parent.__addChild(this); + } + + __detach(): void { + this._parent.__removeChild(this); + super.__detach(); + } + + update(): void { + this._value.animate( + new this._animationClass({ + ...this._animationConfig, + toValue: (this._animationConfig.toValue: any).__getValue(), + }), + this._callback, + ); + } +} + +module.exports = AnimatedTracking; diff --git a/Libraries/Animated/src/nodes/AnimatedTransform.js b/Libraries/Animated/src/nodes/AnimatedTransform.js new file mode 100644 index 0000000..c95aa16 --- /dev/null +++ b/Libraries/Animated/src/nodes/AnimatedTransform.js @@ -0,0 +1,123 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule AnimatedTransform + * @flow + * @format + */ +'use strict'; + +const AnimatedNode = require('./AnimatedNode'); +const AnimatedWithChildren = require('./AnimatedWithChildren'); +const NativeAnimatedHelper = require('../NativeAnimatedHelper'); + +class AnimatedTransform extends AnimatedWithChildren { + _transforms: Array; + + constructor(transforms: Array) { + super(); + this._transforms = transforms; + } + + __makeNative() { + super.__makeNative(); + this._transforms.forEach(transform => { + for (const key in transform) { + const value = transform[key]; + if (value instanceof AnimatedNode) { + value.__makeNative(); + } + } + }); + } + + __getValue(): Array { + return this._transforms.map(transform => { + const result = {}; + for (const key in transform) { + const value = transform[key]; + if (value instanceof AnimatedNode) { + result[key] = value.__getValue(); + } else { + result[key] = value; + } + } + return result; + }); + } + + __getAnimatedValue(): Array { + return this._transforms.map(transform => { + const result = {}; + for (const key in transform) { + const value = transform[key]; + if (value instanceof AnimatedNode) { + result[key] = value.__getAnimatedValue(); + } else { + // All transform components needed to recompose matrix + result[key] = value; + } + } + return result; + }); + } + + __attach(): void { + this._transforms.forEach(transform => { + for (const key in transform) { + const value = transform[key]; + if (value instanceof AnimatedNode) { + value.__addChild(this); + } + } + }); + } + + __detach(): void { + this._transforms.forEach(transform => { + for (const key in transform) { + const value = transform[key]; + if (value instanceof AnimatedNode) { + value.__removeChild(this); + } + } + }); + super.__detach(); + } + + __getNativeConfig(): any { + const transConfigs = []; + + this._transforms.forEach(transform => { + for (const key in transform) { + const value = transform[key]; + if (value instanceof AnimatedNode) { + transConfigs.push({ + type: 'animated', + property: key, + nodeTag: value.__getNativeTag(), + }); + } else { + transConfigs.push({ + type: 'static', + property: key, + value, + }); + } + } + }); + + NativeAnimatedHelper.validateTransform(transConfigs); + return { + type: 'transform', + transforms: transConfigs, + }; + } +} + +module.exports = AnimatedTransform; diff --git a/Libraries/Animated/src/nodes/AnimatedValue.js b/Libraries/Animated/src/nodes/AnimatedValue.js new file mode 100644 index 0000000..e9ef0f0 --- /dev/null +++ b/Libraries/Animated/src/nodes/AnimatedValue.js @@ -0,0 +1,309 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule AnimatedValue + * @flow + * @format + */ +'use strict'; + +const AnimatedInterpolation = require('./AnimatedInterpolation'); +const AnimatedNode = require('./AnimatedNode'); +const AnimatedWithChildren = require('./AnimatedWithChildren'); +const InteractionManager = require('ReactInteractionManager'); +const NativeAnimatedHelper = require('../NativeAnimatedHelper'); + +import type Animation, {EndCallback} from '../animations/Animation'; +import type {InterpolationConfigType} from './AnimatedInterpolation'; + +const NativeAnimatedAPI = NativeAnimatedHelper.API; + +type ValueListenerCallback = (state: {value: number}) => void; + +let _uniqueId = 1; + +/** + * Animated works by building a directed acyclic graph of dependencies + * transparently when you render your Animated components. + * + * new Animated.Value(0) + * .interpolate() .interpolate() new Animated.Value(1) + * opacity translateY scale + * style transform + * View#234 style + * View#123 + * + * A) Top Down phase + * When an Animated.Value is updated, we recursively go down through this + * graph in order to find leaf nodes: the views that we flag as needing + * an update. + * + * B) Bottom Up phase + * When a view is flagged as needing an update, we recursively go back up + * in order to build the new value that it needs. The reason why we need + * this two-phases process is to deal with composite props such as + * transform which can receive values from multiple parents. + */ +function _flush(rootNode: AnimatedValue): void { + const animatedStyles = new Set(); + function findAnimatedStyles(node) { + if (typeof node.update === 'function') { + animatedStyles.add(node); + } else { + node.__getChildren().forEach(findAnimatedStyles); + } + } + findAnimatedStyles(rootNode); + /* $FlowFixMe */ + animatedStyles.forEach(animatedStyle => animatedStyle.update()); +} + +/** + * Standard value for driving animations. One `Animated.Value` can drive + * multiple properties in a synchronized fashion, but can only be driven by one + * mechanism at a time. Using a new mechanism (e.g. starting a new animation, + * or calling `setValue`) will stop any previous ones. + */ +class AnimatedValue extends AnimatedWithChildren { + _value: number; + _startingValue: number; + _offset: number; + _animation: ?Animation; + _tracking: ?AnimatedNode; + _listeners: {[key: string]: ValueListenerCallback}; + __nativeAnimatedValueListener: ?any; + + constructor(value: number) { + super(); + this._startingValue = this._value = value; + this._offset = 0; + this._animation = null; + this._listeners = {}; + } + + __detach() { + this.stopAnimation(); + super.__detach(); + } + + __getValue(): number { + return this._value + this._offset; + } + + __makeNative() { + super.__makeNative(); + + if (Object.keys(this._listeners).length) { + this._startListeningToNativeValueUpdates(); + } + } + + /** + * Directly set the value. This will stop any animations running on the value + * and update all the bound properties. + */ + setValue(value: number): void { + if (this._animation) { + this._animation.stop(); + this._animation = null; + } + this._updateValue( + value, + !this.__isNative /* don't perform a flush for natively driven values */, + ); + if (this.__isNative) { + NativeAnimatedAPI.setAnimatedNodeValue(this.__getNativeTag(), value); + } + } + + /** + * Sets an offset that is applied on top of whatever value is set, whether via + * `setValue`, an animation, or `Animated.event`. Useful for compensating + * things like the start of a pan gesture. + */ + setOffset(offset: number): void { + this._offset = offset; + if (this.__isNative) { + NativeAnimatedAPI.setAnimatedNodeOffset(this.__getNativeTag(), offset); + } + } + + /** + * Merges the offset value into the base value and resets the offset to zero. + * The final output of the value is unchanged. + */ + flattenOffset(): void { + this._value += this._offset; + this._offset = 0; + if (this.__isNative) { + NativeAnimatedAPI.flattenAnimatedNodeOffset(this.__getNativeTag()); + } + } + + /** + * Sets the offset value to the base value, and resets the base value to zero. + * The final output of the value is unchanged. + */ + extractOffset(): void { + this._offset += this._value; + this._value = 0; + if (this.__isNative) { + NativeAnimatedAPI.extractAnimatedNodeOffset(this.__getNativeTag()); + } + } + + /** + * Adds an asynchronous listener to the value so you can observe updates from + * animations. This is useful because there is no way to + * synchronously read the value because it might be driven natively. + */ + addListener(callback: ValueListenerCallback): string { + const id = String(_uniqueId++); + this._listeners[id] = callback; + if (this.__isNative) { + this._startListeningToNativeValueUpdates(); + } + return id; + } + + removeListener(id: string): void { + delete this._listeners[id]; + if (this.__isNative && Object.keys(this._listeners).length === 0) { + this._stopListeningForNativeValueUpdates(); + } + } + + removeAllListeners(): void { + this._listeners = {}; + if (this.__isNative) { + this._stopListeningForNativeValueUpdates(); + } + } + + _startListeningToNativeValueUpdates() { + if (this.__nativeAnimatedValueListener) { + return; + } + + NativeAnimatedAPI.startListeningToAnimatedNodeValue(this.__getNativeTag()); + this.__nativeAnimatedValueListener = NativeAnimatedHelper.nativeEventEmitter.addListener( + 'onAnimatedValueUpdate', + data => { + if (data.tag !== this.__getNativeTag()) { + return; + } + this._updateValue(data.value, false /* flush */); + }, + ); + } + + _stopListeningForNativeValueUpdates() { + if (!this.__nativeAnimatedValueListener) { + return; + } + + this.__nativeAnimatedValueListener.remove(); + this.__nativeAnimatedValueListener = null; + NativeAnimatedAPI.stopListeningToAnimatedNodeValue(this.__getNativeTag()); + } + + /** + * Stops any running animation or tracking. `callback` is invoked with the + * final value after stopping the animation, which is useful for updating + * state to match the animation position with layout. + */ + stopAnimation(callback?: ?(value: number) => void): void { + this.stopTracking(); + this._animation && this._animation.stop(); + this._animation = null; + callback && callback(this.__getValue()); + } + + /** + * Stops any animation and resets the value to its original + */ + resetAnimation(callback?: ?(value: number) => void): void { + this.stopAnimation(callback); + this._value = this._startingValue; + } + + /** + * Interpolates the value before updating the property, e.g. mapping 0-1 to + * 0-10. + */ + interpolate(config: InterpolationConfigType): AnimatedInterpolation { + return new AnimatedInterpolation(this, config); + } + + /** + * Typically only used internally, but could be used by a custom Animation + * class. + */ + animate(animation: Animation, callback: ?EndCallback): void { + let handle = null; + if (animation.__isInteraction) { + handle = InteractionManager.createInteractionHandle(); + } + const previousAnimation = this._animation; + this._animation && this._animation.stop(); + this._animation = animation; + animation.start( + this._value, + value => { + // Natively driven animations will never call into that callback, therefore we can always + // pass flush = true to allow the updated value to propagate to native with setNativeProps + this._updateValue(value, true /* flush */); + }, + result => { + this._animation = null; + if (handle !== null) { + InteractionManager.clearInteractionHandle(handle); + } + callback && callback(result); + }, + previousAnimation, + this, + ); + } + + /** + * Typically only used internally. + */ + stopTracking(): void { + this._tracking && this._tracking.__detach(); + this._tracking = null; + } + + /** + * Typically only used internally. + */ + track(tracking: AnimatedNode): void { + this.stopTracking(); + this._tracking = tracking; + } + + _updateValue(value: number, flush: boolean): void { + this._value = value; + if (flush) { + _flush(this); + } + for (const key in this._listeners) { + this._listeners[key]({value: this.__getValue()}); + } + } + + __getNativeConfig(): Object { + return { + type: 'value', + value: this._value, + offset: this._offset, + }; + } +} + +module.exports = AnimatedValue; diff --git a/Libraries/Animated/src/nodes/AnimatedValueXY.js b/Libraries/Animated/src/nodes/AnimatedValueXY.js new file mode 100644 index 0000000..8d26560 --- /dev/null +++ b/Libraries/Animated/src/nodes/AnimatedValueXY.js @@ -0,0 +1,180 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule AnimatedValueXY + * @flow + * @format + */ +'use strict'; + +const AnimatedValue = require('./AnimatedValue'); +const AnimatedWithChildren = require('./AnimatedWithChildren'); + +const invariant = require('fbjs/lib/invariant'); + +type ValueXYListenerCallback = (value: {x: number, y: number}) => void; + +let _uniqueId = 1; + +/** + * 2D Value for driving 2D animations, such as pan gestures. Almost identical + * API to normal `Animated.Value`, but multiplexed. Contains two regular + * `Animated.Value`s under the hood. + * + * #### Example + * + *```javascript + * class DraggableView extends React.Component { + * constructor(props) { + * super(props); + * this.state = { + * pan: new Animated.ValueXY(), // inits to zero + * }; + * this.state.panResponder = PanResponder.create({ + * onStartShouldSetPanResponder: () => true, + * onPanResponderMove: Animated.event([null, { + * dx: this.state.pan.x, // x,y are Animated.Value + * dy: this.state.pan.y, + * }]), + * onPanResponderRelease: () => { + * Animated.spring( + * this.state.pan, // Auto-multiplexed + * {toValue: {x: 0, y: 0}} // Back to zero + * ).start(); + * }, + * }); + * } + * render() { + * return ( + * + * {this.props.children} + * + * ); + * } + * } + *``` + */ +class AnimatedValueXY extends AnimatedWithChildren { + x: AnimatedValue; + y: AnimatedValue; + _listeners: {[key: string]: {x: string, y: string}}; + + constructor( + valueIn?: ?{x: number | AnimatedValue, y: number | AnimatedValue}, + ) { + super(); + const value: any = valueIn || {x: 0, y: 0}; // @flowfixme: shouldn't need `: any` + if (typeof value.x === 'number' && typeof value.y === 'number') { + this.x = new AnimatedValue(value.x); + this.y = new AnimatedValue(value.y); + } else { + invariant( + value.x instanceof AnimatedValue && value.y instanceof AnimatedValue, + 'AnimatedValueXY must be initalized with an object of numbers or ' + + 'AnimatedValues.', + ); + this.x = value.x; + this.y = value.y; + } + this._listeners = {}; + } + + setValue(value: {x: number, y: number}) { + this.x.setValue(value.x); + this.y.setValue(value.y); + } + + setOffset(offset: {x: number, y: number}) { + this.x.setOffset(offset.x); + this.y.setOffset(offset.y); + } + + flattenOffset(): void { + this.x.flattenOffset(); + this.y.flattenOffset(); + } + + extractOffset(): void { + this.x.extractOffset(); + this.y.extractOffset(); + } + + __getValue(): {x: number, y: number} { + return { + x: this.x.__getValue(), + y: this.y.__getValue(), + }; + } + + resetAnimation(callback?: (value: {x: number, y: number}) => void): void { + this.x.resetAnimation(); + this.y.resetAnimation(); + callback && callback(this.__getValue()); + } + + stopAnimation(callback?: (value: {x: number, y: number}) => void): void { + this.x.stopAnimation(); + this.y.stopAnimation(); + callback && callback(this.__getValue()); + } + + addListener(callback: ValueXYListenerCallback): string { + const id = String(_uniqueId++); + const jointCallback = ({value: number}) => { + callback(this.__getValue()); + }; + this._listeners[id] = { + x: this.x.addListener(jointCallback), + y: this.y.addListener(jointCallback), + }; + return id; + } + + removeListener(id: string): void { + this.x.removeListener(this._listeners[id].x); + this.y.removeListener(this._listeners[id].y); + delete this._listeners[id]; + } + + removeAllListeners(): void { + this.x.removeAllListeners(); + this.y.removeAllListeners(); + this._listeners = {}; + } + + /** + * Converts `{x, y}` into `{left, top}` for use in style, e.g. + * + *```javascript + * style={this.state.anim.getLayout()} + *``` + */ + getLayout(): {[key: string]: AnimatedValue} { + return { + left: this.x, + top: this.y, + }; + } + + /** + * Converts `{x, y}` into a useable translation transform, e.g. + * + *```javascript + * style={{ + * transform: this.state.anim.getTranslateTransform() + * }} + *``` + */ + getTranslateTransform(): Array<{[key: string]: AnimatedValue}> { + return [{translateX: this.x}, {translateY: this.y}]; + } +} + +module.exports = AnimatedValueXY; diff --git a/Libraries/Animated/src/nodes/AnimatedWithChildren.js b/Libraries/Animated/src/nodes/AnimatedWithChildren.js new file mode 100644 index 0000000..4676437 --- /dev/null +++ b/Libraries/Animated/src/nodes/AnimatedWithChildren.js @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule AnimatedWithChildren + * @flow + * @format + */ +'use strict'; + +const AnimatedNode = require('./AnimatedNode'); +const NativeAnimatedHelper = require('../NativeAnimatedHelper'); + +class AnimatedWithChildren extends AnimatedNode { + _children: Array; + + constructor() { + super(); + this._children = []; + } + + __makeNative() { + if (!this.__isNative) { + this.__isNative = true; + for (const child of this._children) { + child.__makeNative(); + NativeAnimatedHelper.API.connectAnimatedNodes( + this.__getNativeTag(), + child.__getNativeTag(), + ); + } + } + } + + __addChild(child: AnimatedNode): void { + if (this._children.length === 0) { + this.__attach(); + } + this._children.push(child); + if (this.__isNative) { + // Only accept "native" animated nodes as children + child.__makeNative(); + NativeAnimatedHelper.API.connectAnimatedNodes( + this.__getNativeTag(), + child.__getNativeTag(), + ); + } + } + + __removeChild(child: AnimatedNode): void { + const index = this._children.indexOf(child); + if (index === -1) { + console.warn("Trying to remove a child that doesn't exist"); + return; + } + if (this.__isNative && child.__isNative) { + NativeAnimatedHelper.API.disconnectAnimatedNodes( + this.__getNativeTag(), + child.__getNativeTag(), + ); + } + this._children.splice(index, 1); + if (this._children.length === 0) { + this.__detach(); + } + } + + __getChildren(): Array { + return this._children; + } +} + +module.exports = AnimatedWithChildren; diff --git a/Libraries/Animated/src/polyfills/InteractionManager.js b/Libraries/Animated/src/polyfills/InteractionManager.js new file mode 100644 index 0000000..db45ede --- /dev/null +++ b/Libraries/Animated/src/polyfills/InteractionManager.js @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +'use strict'; + +module.exports = { + createInteractionHandle: function() {}, + clearInteractionHandle: function() {} +}; diff --git a/Libraries/Animated/src/polyfills/Set.js b/Libraries/Animated/src/polyfills/Set.js new file mode 100644 index 0000000..988f0b3 --- /dev/null +++ b/Libraries/Animated/src/polyfills/Set.js @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +'use strict'; + +function SetPolyfill() { + this._cache = []; +} + +SetPolyfill.prototype.add = function(e) { + if (this._cache.indexOf(e) === -1) { + this._cache.push(e); + } +}; + +SetPolyfill.prototype.forEach = function(cb) { + this._cache.forEach(cb); +}; + +module.exports = SetPolyfill; diff --git a/Libraries/Animated/src/polyfills/flattenStyle.js b/Libraries/Animated/src/polyfills/flattenStyle.js new file mode 100644 index 0000000..42a6802 --- /dev/null +++ b/Libraries/Animated/src/polyfills/flattenStyle.js @@ -0,0 +1,12 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; +module.exports = function(style) { + return style; +}; diff --git a/Libraries/AppRegistry/renderApplication.web.js b/Libraries/AppRegistry/renderApplication.web.js index ef587a5..a60f332 100644 --- a/Libraries/AppRegistry/renderApplication.web.js +++ b/Libraries/AppRegistry/renderApplication.web.js @@ -18,7 +18,6 @@ class AppContainer extends Component { let RootComponent = this.props.rootComponent; let appView = {}, + forceRTL: (forceRTL: boolean) => {}, + setRTL: (setRTL: boolean) => {}, + isRTL: boolean +}; + +let isPreferredLanguageRTL = false; +let isRTLAllowed = true; +let isRTLForced = false; + +const isRTL = () => { + if (isRTLForced) { + return true; + } + return isRTLAllowed && isPreferredLanguageRTL; +}; + +const onChange = () => { + if (ExecutionEnvironment.canUseDOM) { + document.documentElement.setAttribute('dir', isRTL() ? 'rtl' : 'ltr'); + } +}; + +const I18nManager: I18nManagerStatus = { + allowRTL(bool) { + isRTLAllowed = bool; + onChange(); + }, + forceRTL(bool) { + isRTLForced = bool; + onChange(); + }, + setPreferredLanguageRTL(bool) { + isPreferredLanguageRTL = bool; + onChange(); + }, + get isRTL() { + return isRTL(); + } +}; + +export default I18nManager; diff --git a/Libraries/Image/Image.web.js b/Libraries/Image/Image.web.js index e18a34a..6d8516d 100644 --- a/Libraries/Image/Image.web.js +++ b/Libraries/Image/Image.web.js @@ -6,7 +6,8 @@ */ 'use strict'; -import React, {Component, PropTypes} from 'react'; +import React, {Component} from 'react'; +import PropTypes from 'prop-types'; import View from 'ReactView'; import { Mixin as LayoutMixin } from 'ReactLayoutMixin'; import ImageResizeMode from './ImageResizeMode'; @@ -23,6 +24,7 @@ class Image extends Component { }), // Opaque type returned by require('./image.jpg') PropTypes.number, + PropTypes.string, // Multiple sources PropTypes.arrayOf( PropTypes.shape({ @@ -31,10 +33,14 @@ class Image extends Component { height: PropTypes.number, })) ]), + style: PropTypes.oneOfType([ + PropTypes.object, + PropTypes.array + ]) } static contextTypes = { - isInAParentText: React.PropTypes.bool + isInAParentText: PropTypes.bool } static getSize = function( @@ -68,7 +74,10 @@ class Image extends Component { render() { - let props = {...this.props}; + let props = { + ...this.props, + 'aria-label': this.props.accessibilityLabel + }; props.src = typeof props.source === 'string' ? props.source : props.source.uri; // TODO: lazyload image when not in viewport @@ -89,8 +98,16 @@ class Image extends Component { ); } else { + const { + /* eslint-disable */ + resizeMode, + source, + /* eslint-enable */ + ...noWarningProps + } = props; + return ( - + ); } } diff --git a/Libraries/Image/ImageBackground.js b/Libraries/Image/ImageBackground.js new file mode 100644 index 0000000..9aea166 --- /dev/null +++ b/Libraries/Image/ImageBackground.js @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ReactImageBackground + * @flow + * @format + */ +'use strict'; + +import Image from 'ReactImage'; +import React from 'react'; +import StyleSheet from 'ReactStyleSheet'; +import View from 'ReactView'; +import flattenStyle from 'ReactFlattenStyle'; + +/** + * Very simple drop-in replacement for which supports nesting views. + * + * ```ReactNativeWebPlayer + * import React, { Component } from 'react'; + * import { AppRegistry, View, ImageBackground, Text } from 'react-native'; + * + * class DisplayAnImageBackground extends Component { + * render() { + * return ( + * + * React + * + * ); + * } + * } + * + * // App registration and rendering + * AppRegistry.registerComponent('DisplayAnImageBackground', () => DisplayAnImageBackground); + * ``` + */ +class ImageBackground extends React.Component<$FlowFixMeProps> { + setNativeProps(props: Object) { + // Work-around flow + const viewRef = this._viewRef; + if (viewRef) { + viewRef.setNativeProps(props); + } + } + + _viewRef: ?View = null; + + _captureRef = ref => { + this._viewRef = ref; + }; + + render() { + const {children, imageStyle, imageRef, ...props} = this.props; + const style = flattenStyle(this.props.style) || {}; + + return ( + + overwrites width and height styles + // (which is not quite correct), and these styles conflict with explicitly set styles + // of and with our internal layout model here. + // So, we have to proxy/reapply these styles explicitly for actual component. + // This workaround should be removed after implementing proper support of + // intrinsic content size of the . + width: style.width, + height: style.height, + }, + imageStyle, + ]} + ref={imageRef} + /> + {children} + + ); + } +} + +export default ImageBackground; diff --git a/Libraries/Interaction/Batchinator.js b/Libraries/Interaction/Batchinator.js new file mode 100644 index 0000000..a64613e --- /dev/null +++ b/Libraries/Interaction/Batchinator.js @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule Batchinator + * @flow + */ +'use strict'; + +import InteractionManager from 'ReactInteractionManager'; + +/** + * A simple class for batching up invocations of a low-pri callback. A timeout is set to run the + * callback once after a delay, no matter how many times it's scheduled. Once the delay is reached, + * InteractionManager.runAfterInteractions is used to invoke the callback after any hi-pri + * interactions are done running. + * + * Make sure to cleanup with dispose(). Example: + * + * class Widget extends React.Component { + * _batchedSave: new Batchinator(() => this._saveState, 1000); + * _saveSate() { + * // save this.state to disk + * } + * componentDidUpdate() { + * this._batchedSave.schedule(); + * } + * componentWillUnmount() { + * this._batchedSave.dispose(); + * } + * ... + * } + */ +class Batchinator { + _callback: () => void; + _delay: number; + _taskHandle: ?{cancel: () => void}; + constructor(callback: () => void, delayMS: number) { + this._delay = delayMS; + this._callback = callback; + } + /* + * Cleanup any pending tasks. + * + * By default, if there is a pending task the callback is run immediately. Set the option abort to + * true to not call the callback if it was pending. + */ + dispose(options: {abort: boolean} = {abort: false}) { + if (this._taskHandle) { + this._taskHandle.cancel(); + if (!options.abort) { + this._callback(); + } + this._taskHandle = null; + } + } + schedule() { + if (this._taskHandle) { + return; + } + const timeoutHandle = setTimeout(() => { + this._taskHandle = InteractionManager.runAfterInteractions(() => { + // Note that we clear the handle before invoking the callback so that if the callback calls + // schedule again, it will actually schedule another task. + this._taskHandle = null; + this._callback(); + }); + }, this._delay); + this._taskHandle = {cancel: () => clearTimeout(timeoutHandle)}; + } +} + +module.exports = Batchinator; diff --git a/Libraries/Keyboard/Keyboard.js b/Libraries/Keyboard/Keyboard.js new file mode 100644 index 0000000..2d0cd53 --- /dev/null +++ b/Libraries/Keyboard/Keyboard.js @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2015-present, Alibaba Group Holding Limited. + * All rights reserved. + * + * @providesModule ReactKeyboard + */ +'use strict'; + +import dismissKeyboard from 'ReactDismissKeyboard'; + +const Keyboard = { + addListener() { + return { + remove() {} + }; + }, + dismiss() { + dismissKeyboard(); + }, + removeAllListeners() {}, + removeListener() {} +}; + +export default Keyboard; diff --git a/Libraries/Linking/Linking.web.js b/Libraries/Linking/Linking.web.js index d752e84..c390f33 100644 --- a/Libraries/Linking/Linking.web.js +++ b/Libraries/Linking/Linking.web.js @@ -18,9 +18,9 @@ var Linking = { } }, canOpenURL: (url) => { - return true; + return Promise.resolve(true); }, - getInitialURL: emptyFunction, + getInitialURL: () => Promise.resolve() }; module.exports = Linking; diff --git a/Libraries/ListView/ListView.web.js b/Libraries/ListView/ListView.web.js index b7cce7b..119fec3 100644 --- a/Libraries/ListView/ListView.web.js +++ b/Libraries/ListView/ListView.web.js @@ -5,16 +5,27 @@ * Copyright (c) 2015, Facebook, Inc. All rights reserved. * * @providesModule ReactListView + * @flow + * @format */ 'use strict'; -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; import ReactDOM from 'react-dom'; import ListViewDataSource from 'ReactListViewDataSource'; +import Platform from 'ReactPlatform'; +import findNodeHandle from 'ReactfindNodeHandle'; import ScrollView from 'ReactScrollView'; import ScrollResponder from 'ReactScrollResponder'; import StaticRenderer from 'ReactStaticRenderer'; +/* $FlowFixMe(>=0.54.0 site=react_native_oss) This comment suppresses an error + * found when Flow v0.54 was deployed. To see the error delete this comment and + * run Flow. */ import TimerMixin from 'react-timer-mixin'; +import View from 'ReactView'; +var cloneReferencedElement = require('react-clone-referenced-element'); +import isEmpty from 'fbjs/lib/isEmpty'; import mixin from 'react-mixin'; import assign from 'object-assign'; import autobind from 'autobind-decorator'; @@ -24,34 +35,42 @@ const DEFAULT_INITIAL_ROWS = 10; const DEFAULT_SCROLL_RENDER_AHEAD = 1000; const DEFAULT_END_REACHED_THRESHOLD = 1000; const DEFAULT_SCROLL_CALLBACK_THROTTLE = 50; -const SCROLLVIEW_REF = 'listviewscroll'; /** + * DEPRECATED - use one of the new list components, such as [`FlatList`](docs/flatlist.html) + * or [`SectionList`](docs/sectionlist.html) for bounded memory use, fewer bugs, + * better performance, an easier to use API, and more features. Check out this + * [blog post](https://facebook.github.io/react-native/blog/2017/03/13/better-list-views.html) + * for more details. + * * ListView - A core component designed for efficient display of vertically - * scrolling lists of changing data. The minimal API is to create a - * `ListView.DataSource`, populate it with a simple array of data blobs, and - * instantiate a `ListView` component with that data source and a `renderRow` - * callback which takes a blob from the data array and returns a renderable - * component. + * scrolling lists of changing data. The minimal API is to create a + * [`ListView.DataSource`](docs/listviewdatasource.html), populate it with a simple + * array of data blobs, and instantiate a `ListView` component with that data + * source and a `renderRow` callback which takes a blob from the data array and + * returns a renderable component. * * Minimal example: * * ``` - * getInitialState: function() { - * let ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2}); - * return { - * dataSource: ds.cloneWithRows(['row 1', 'row 2']), - * }; - * }, + * class MyComponent extends Component { + * constructor() { + * super(); + * const ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2}); + * this.state = { + * dataSource: ds.cloneWithRows(['row 1', 'row 2']), + * }; + * } * - * render: function() { - * return ( - * {rowData}} - * /> - * ); - * }, + * render() { + * return ( + * {rowData}} + * /> + * ); + * } + * } * ``` * * ListView also supports more advanced features, including sections with sticky @@ -69,11 +88,24 @@ const SCROLLVIEW_REF = 'listviewscroll'; * source data has changed - see ListViewDataSource for more details. * * * Rate-limited row rendering - By default, only one row is rendered per - * event-loop (customizable with the `pageSize` prop). This breaks up the + * event-loop (customizable with the `pageSize` prop). This breaks up the * work into smaller chunks to reduce the chance of dropping frames while * rendering rows. */ + +/* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This comment + * suppresses an error when upgrading Flow's support for React. To see the + * error delete this comment and run Flow. */ class ListView extends Component { + static displayName = 'ListView' + static _childFrames = ([]: Array) + static _sentEndForContentLength = (null: ?number) + static _scrollComponent = (null: any) + static _prevRenderedRowsCount = 0 + static _visibleRows = ({}: Object) + static scrollProperties = ({}: Object) + + static mixins = [ScrollResponder.Mixin, TimerMixin] static DataSource = ListViewDataSource @@ -86,7 +118,9 @@ class ListView extends Component { */ static propTypes = { ...ScrollView.propTypes, - + /** + * An instance of [ListView.DataSource](docs/listviewdatasource.html) to use + */ dataSource: PropTypes.instanceOf(ListViewDataSource).isRequired, /** * (sectionID, rowID, adjacentRowHighlighted) => renderable @@ -101,30 +135,31 @@ class ListView extends Component { * (rowData, sectionID, rowID, highlightRow) => renderable * * Takes a data entry from the data source and its ids and should return - * a renderable component to be rendered as the row. By default the data + * a renderable component to be rendered as the row. By default the data * is exactly what was put into the data source, but it's also possible to * provide custom extractors. ListView can be notified when a row is - * being highlighted by calling highlightRow function. The separators above and - * below will be hidden when a row is highlighted. The highlighted state of - * a row can be reset by calling highlightRow(null). + * being highlighted by calling `highlightRow(sectionID, rowID)`. This + * sets a boolean value of adjacentRowHighlighted in renderSeparator, allowing you + * to control the separators above and below the highlighted row. The highlighted + * state of a row can be reset by calling highlightRow(null). */ renderRow: PropTypes.func.isRequired, /** - * How many rows to render on initial component mount. Use this to make + * How many rows to render on initial component mount. Use this to make * it so that the first screen worth of data appears at one time instead of * over the course of multiple frames. */ - initialListSize: PropTypes.number, + initialListSize: PropTypes.number.isRequired, /** * Called when all rows have been rendered and the list has been scrolled - * to within onEndReachedThreshold of the bottom. The native scroll + * to within onEndReachedThreshold of the bottom. The native scroll * event is provided. */ onEndReached: PropTypes.func, /** * Threshold in pixels (virtual, not physical) for calling onEndReached. */ - onEndReachedThreshold: PropTypes.number, + onEndReachedThreshold: PropTypes.number.isRequired, /** * Number of rows to render per event loop. Note: if your 'rows' are actually * cells, i.e. they don't span the full width of your view (as in the @@ -132,25 +167,23 @@ class ListView extends Component { * of the number of cells per row, otherwise you're likely to see gaps at * the edge of the ListView as new pages are loaded. */ - pageSize: PropTypes.number, + pageSize: PropTypes.number.isRequired, /** * () => renderable * * The header and footer are always rendered (if these props are provided) - * on every render pass. If they are expensive to re-render, wrap them - * in StaticContainer or other mechanism as appropriate. Footer is always + * on every render pass. If they are expensive to re-render, wrap them + * in StaticContainer or other mechanism as appropriate. Footer is always * at the bottom of the list, and header at the top, on every render pass. + * In a horizontal ListView, the header is rendered on the left and the + * footer on the right. */ renderFooter: PropTypes.func, renderHeader: PropTypes.func, /** * (sectionData, sectionID) => renderable * - * If provided, a sticky header is rendered for this section. The sticky - * behavior means that it will scroll with the content at the top of the - * section until it reaches the top of the screen, at which point it will - * stick to the top until it is pushed off the screen by the next section - * header. + * If provided, a header is rendered for this section. */ renderSectionHeader: PropTypes.func, /** @@ -159,55 +192,51 @@ class ListView extends Component { * A function that returns the scrollable component in which the list rows * are rendered. Defaults to returning a ScrollView with the given props. */ - renderScrollComponent: React.PropTypes.func.isRequired, + renderScrollComponent: PropTypes.func.isRequired, /** * How early to start rendering rows before they come on screen, in * pixels. */ - scrollRenderAheadDistance: React.PropTypes.number, + scrollRenderAheadDistance: PropTypes.number.isRequired, /** * (visibleRows, changedRows) => void * - * Called when the set of visible rows changes. `visibleRows` maps + * Called when the set of visible rows changes. `visibleRows` maps * { sectionID: { rowID: true }} for all the visible rows, and * `changedRows` maps { sectionID: { rowID: true | false }} for the rows * that have changed their visibility, with true indicating visible, and * false indicating the view has moved out of view. */ - onChangeVisibleRows: React.PropTypes.func, + onChangeVisibleRows: PropTypes.func, /** * A performance optimization for improving scroll perf of * large lists, used in conjunction with overflow: 'hidden' on the row - * containers. This is enabled by default. + * containers. This is enabled by default. */ - removeClippedSubviews: React.PropTypes.bool, + removeClippedSubviews: PropTypes.bool, + /** + * Makes the sections headers sticky. The sticky behavior means that it + * will scroll with the content at the top of the section until it reaches + * the top of the screen, at which point it will stick to the top until it + * is pushed off the screen by the next section header. This property is + * not supported in conjunction with `horizontal={true}`. Only enabled by + * default on iOS because of typical platform standards. + */ + stickySectionHeadersEnabled: PropTypes.bool, /** * An array of child indices determining which children get docked to the * top of the screen when scrolling. For example, passing * `stickyHeaderIndices={[0]}` will cause the first child to be fixed to the * top of the scroll view. This property is not supported in conjunction * with `horizontal={true}`. - * @platform ios */ - stickyHeaderIndices: PropTypes.arrayOf(PropTypes.number), - } - - /** - * React life cycle hooks. - */ - - static defaultProps = { - initialListSize: DEFAULT_INITIAL_ROWS, - pageSize: DEFAULT_PAGE_SIZE, - renderScrollComponent: props => , - scrollRenderAheadDistance: DEFAULT_SCROLL_RENDER_AHEAD, - onEndReachedThreshold: DEFAULT_END_REACHED_THRESHOLD, - stickyHeaderIndices: [], - } - - state = { - curRenderedRowsCount: this.props.initialListSize, - highlightedRow: {}, + stickyHeaderIndices: PropTypes.arrayOf(PropTypes.number).isRequired, + /** + * Flag indicating whether empty section headers should be rendered. In the future release + * empty section headers will be rendered by default, and the flag will be deprecated. + * If empty sections are not desired to be rendered their indices should be excluded from sectionID object. + */ + enableEmptySections: PropTypes.bool, } /** @@ -216,7 +245,9 @@ class ListView extends Component { getMetrics() { return { contentLength: this.scrollProperties.contentLength, - totalRows: this.props.dataSource.getRowCount(), + totalRows: this.props.enableEmptySections + ? this.props.dataSource.getRowAndSectionCount() + : this.props.dataSource.getRowCount(), renderedRows: this.state.curRenderedRowsCount, visibleRows: Object.keys(this._visibleRows).length, }; @@ -224,28 +255,95 @@ class ListView extends Component { /** * Provides a handle to the underlying scroll responder. - * Note that the view in `SCROLLVIEW_REF` may not be a `ScrollView`, so we + * Note that `this._scrollComponent` might not be a `ScrollView`, so we * need to check that it responds to `getScrollResponder` before calling it. */ getScrollResponder() { - return this.refs[SCROLLVIEW_REF] && - this.refs[SCROLLVIEW_REF].getScrollResponder && - this.refs[SCROLLVIEW_REF].getScrollResponder(); + if (this._scrollComponent && this._scrollComponent.getScrollResponder) { + return this._scrollComponent.getScrollResponder(); + } + } + + getScrollableNode() { + if (this._scrollComponent && this._scrollComponent.getScrollableNode) { + return this._scrollComponent.getScrollableNode(); + } else { + return findNodeHandle(this._scrollComponent); + } + } + + /** + * Scrolls to a given x, y offset, either immediately or with a smooth animation. + * + * See `ScrollView#scrollTo`. + */ + scrollTo(...args: Array) { + if (this._scrollComponent && this._scrollComponent.scrollTo) { + this._scrollComponent.scrollTo(...args); + } + } + + /** + * If this is a vertical ListView scrolls to the bottom. + * If this is a horizontal ListView scrolls to the right. + * + * Use `scrollToEnd({animated: true})` for smooth animated scrolling, + * `scrollToEnd({animated: false})` for immediate scrolling. + * If no options are passed, `animated` defaults to true. + * + * See `ScrollView#scrollToEnd`. + */ + scrollToEnd(options?: ?{animated?: ?boolean}) { + if (this._scrollComponent) { + if (this._scrollComponent.scrollToEnd) { + this._scrollComponent.scrollToEnd(options); + } else { + console.warn( + 'The scroll component used by the ListView does not support ' + + 'scrollToEnd. Check the renderScrollComponent prop of your ListView.', + ); + } + } } - scrollTo(...args) { - this.refs[SCROLLVIEW_REF] && - this.refs[SCROLLVIEW_REF].scrollTo && - this.refs[SCROLLVIEW_REF].scrollTo(...args); + /** + * Displays the scroll indicators momentarily. + * + * @platform ios + */ + flashScrollIndicators() { + if (this._scrollComponent && this._scrollComponent.flashScrollIndicators) { + this._scrollComponent.flashScrollIndicators(); + } } - setNativeProps(props) { - this.refs[SCROLLVIEW_REF] && - this.refs[SCROLLVIEW_REF].setNativeProps(props); + setNativeProps(props: Object) { + if (this._scrollComponent) { + this._scrollComponent.setNativeProps(props); + } + } + + /** + * React life cycle hooks. + */ + + static defaultProps = { + initialListSize: DEFAULT_INITIAL_ROWS, + pageSize: DEFAULT_PAGE_SIZE, + renderScrollComponent: props => , + scrollRenderAheadDistance: DEFAULT_SCROLL_RENDER_AHEAD, + onEndReachedThreshold: DEFAULT_END_REACHED_THRESHOLD, + stickySectionHeadersEnabled: Platform.OS === 'ios', + stickyHeaderIndices: [], + } + + state = { + curRenderedRowsCount: this.props.initialListSize, + highlightedRow: ({}: Object), } getInnerViewNode() { - return this.refs[SCROLLVIEW_REF].getInnerViewNode(); + return this._scrollComponent.getInnerViewNode(); } componentWillMount() { @@ -253,7 +351,7 @@ class ListView extends Component { this.scrollProperties = { visibleLength: null, contentLength: null, - offset: 0 + offset: 0, }; this._childFrames = []; this._visibleRows = {}; @@ -269,21 +367,25 @@ class ListView extends Component { }); } - componentWillReceiveProps(nextProps) { - if (this.props.dataSource !== nextProps.dataSource || - this.props.initialListSize !== nextProps.initialListSize) { - this.setState((state, props) => { - this._prevRenderedRowsCount = 0; - return { - curRenderedRowsCount: Math.min( - Math.max( - state.curRenderedRowsCount, - props.initialListSize + componentWillReceiveProps(nextProps: Object) { + if ( + this.props.dataSource !== nextProps.dataSource || + this.props.initialListSize !== nextProps.initialListSize + ) { + this.setState( + (state, props) => { + this._prevRenderedRowsCount = 0; + return { + curRenderedRowsCount: Math.min( + Math.max(state.curRenderedRowsCount, props.initialListSize), + props.enableEmptySections + ? props.dataSource.getRowAndSectionCount() + : props.dataSource.getRowCount(), ), - props.dataSource.getRowCount() - ), - }; - }, () => this._renderMoreRowsIfNeeded()); + }; + }, + () => this._renderMoreRowsIfNeeded(), + ); } } @@ -293,7 +395,7 @@ class ListView extends Component { }); } - onRowHighlighted(sectionID, rowID) { + _onRowHighlighted(sectionID: string, rowID: string) { this.setState({highlightedRow: {sectionID, rowID}}); } @@ -303,7 +405,9 @@ class ListView extends Component { let dataSource = this.props.dataSource; let allRowIDs = dataSource.rowIdentities; let rowCount = 0; - let sectionHeaderIndices = []; + let stickySectionHeaderIndices = []; + + const {renderSectionHeader} = this.props; let header = this.props.renderHeader && this.props.renderHeader(); let footer = this.props.renderFooter && this.props.renderFooter(); @@ -313,32 +417,51 @@ class ListView extends Component { let sectionID = dataSource.sectionIdentities[sectionIdx]; let rowIDs = allRowIDs[sectionIdx]; if (rowIDs.length === 0) { - continue; + if (this.props.enableEmptySections === undefined) { + /* $FlowFixMe(>=0.54.0 site=react_native_oss) This comment suppresses + * an error found when Flow v0.54 was deployed. To see the error + * delete this comment and run Flow. */ + var warning = require('fbjs/lib/warning'); + warning( + false, + 'In next release empty section headers will be rendered.' + + " In this release you can use 'enableEmptySections' flag to render empty section headers.", + ); + continue; + } else { + var invariant = require('fbjs/lib/invariant'); + invariant( + this.props.enableEmptySections, + "In next release 'enableEmptySections' flag will be deprecated, empty section headers will always be rendered." + + ' If empty section headers are not desirable their indices should be excluded from sectionIDs object.' + + " In this release 'enableEmptySections' may only have value 'true' to allow empty section headers rendering.", + ); + } } - if (this.props.renderSectionHeader) { - let shouldUpdateHeader = rowCount >= this._prevRenderedRowsCount && - dataSource.sectionHeaderShouldUpdate(sectionIdx); - bodyComponents.push( - + if (renderSectionHeader) { + const element = renderSectionHeader( + dataSource.getSectionHeaderData(sectionIdx), + sectionID, ); - sectionHeaderIndices.push(totalIndex++); + if (element) { + bodyComponents.push( + React.cloneElement(element, {key: 's_' + sectionID}), + ); + if (this.props.stickySectionHeadersEnabled) { + stickySectionHeaderIndices.push(totalIndex); + } + totalIndex++; + } } for (let rowIdx = 0; rowIdx < rowIDs.length; rowIdx++) { let rowID = rowIDs[rowIdx]; let comboID = sectionID + '_' + rowID; - let shouldUpdateRow = rowCount >= this._prevRenderedRowsCount && + let shouldUpdateRow = + rowCount >= this._prevRenderedRowsCount && dataSource.rowShouldUpdate(sectionIdx, rowIdx); - let row = + let row = ( ; + /> + ); bodyComponents.push(row); totalIndex++; - if (this.props.renderSeparator && - (rowIdx !== rowIDs.length - 1 || sectionIdx === allRowIDs.length - 1)) { + if ( + this.props.renderSeparator && + (rowIdx !== rowIDs.length - 1 || sectionIdx === allRowIDs.length - 1) + ) { let adjacentRowHighlighted = - this.state.highlightedRow.sectionID === sectionID && ( - this.state.highlightedRow.rowID === rowID || - this.state.highlightedRow.rowID === rowIDs[rowIdx + 1] - ); + this.state.highlightedRow.sectionID === sectionID && + (this.state.highlightedRow.rowID === rowID || + this.state.highlightedRow.rowID === rowIDs[rowIdx + 1]); let separator = this.props.renderSeparator( sectionID, rowID, - adjacentRowHighlighted + adjacentRowHighlighted, ); if (separator) { - bodyComponents.push(separator); + bodyComponents.push({separator}); totalIndex++; } } @@ -379,10 +504,7 @@ class ListView extends Component { } } - let { - renderScrollComponent, - ...props, - } = this.props; + let {renderScrollComponent, ...props} = this.props; if (!props.scrollEventThrottle) { props.scrollEventThrottle = DEFAULT_SCROLL_CALLBACK_THROTTLE; } @@ -391,7 +513,9 @@ class ListView extends Component { } assign(props, { onScroll: this._onScroll, - stickyHeaderIndices: this.props.stickyHeaderIndices.concat(sectionHeaderIndices), + stickyHeaderIndices: this.props.stickyHeaderIndices.concat( + stickySectionHeaderIndices, + ), // Do not pass these events downstream to ScrollView since they will be // registered in ListView's own ScrollResponder.Mixin @@ -401,13 +525,19 @@ class ListView extends Component { onKeyboardDidHide: undefined, }); - // TODO(ide): Use function refs so we can compose with the scroll - // component's original ref instead of clobbering it - return React.cloneElement(renderScrollComponent(props), { - ref: SCROLLVIEW_REF, - onContentSizeChange: this._onContentSizeChange, - onLayout: this._onLayout, - }, header, bodyComponents, footer); + return cloneReferencedElement( + renderScrollComponent(props), + { + ref: this._setScrollComponentRef, + onContentSizeChange: this._onContentSizeChange, + onLayout: this._onLayout, + DEPRECATED_sendUpdatedChildFrames: + typeof props.onChangeVisibleRows !== undefined, + }, + header, + bodyComponents, + footer, + ); } /** @@ -422,24 +552,30 @@ class ListView extends Component { // RCTScrollViewManager.calculateChildFrames is not available on // every platform - // RCTScrollViewManager && RCTScrollViewManager.calculateChildFrames && + // RCTScrollViewManager && + // RCTScrollViewManager.calculateChildFrames && // RCTScrollViewManager.calculateChildFrames( - // React.findNodeHandle(scrollComponent), + // findNodeHandle(scrollComponent), // this._updateVisibleRows, // ); } - _onContentSizeChange(width, height) { + _setScrollComponentRef = (scrollComponent: Object) => { + this._scrollComponent = scrollComponent; + } + + _onContentSizeChange(width: number, height: number) { let contentLength = !this.props.horizontal ? height : width; if (contentLength !== this.scrollProperties.contentLength) { this.scrollProperties.contentLength = contentLength; this._updateVisibleRows(); this._renderMoreRowsIfNeeded(); } - this.props.onContentSizeChange && this.props.onContentSizeChange(width, height); + this.props.onContentSizeChange && + this.props.onContentSizeChange(width, height); } - _onLayout(event) { + _onLayout(event: Object) { let {width, height} = event.nativeEvent.layout; let visibleLength = !this.props.horizontal ? height : width; if (visibleLength !== this.scrollProperties.visibleLength) { @@ -450,11 +586,17 @@ class ListView extends Component { this.props.onLayout && this.props.onLayout(event); } - _maybeCallOnEndReached(event) { - if (this.props.onEndReached && - this.scrollProperties.contentLength !== this._sentEndForContentLength && - this._getDistanceFromEnd(this.scrollProperties) < this.props.onEndReachedThreshold && - this.state.curRenderedRowsCount === this.props.dataSource.getRowCount()) { + _maybeCallOnEndReached(event?: Object) { + if ( + this.props.onEndReached && + this.scrollProperties.contentLength !== this._sentEndForContentLength && + this._getDistanceFromEnd(this.scrollProperties) < + this.props.onEndReachedThreshold && + this.state.curRenderedRowsCount === + (this.props.enableEmptySections + ? this.props.dataSource.getRowAndSectionCount() + : this.props.dataSource.getRowCount()) + ) { this._sentEndForContentLength = this.scrollProperties.contentLength; this.props.onEndReached(event); return true; @@ -463,9 +605,14 @@ class ListView extends Component { } _renderMoreRowsIfNeeded() { - if (this.scrollProperties.contentLength === null || + if ( + this.scrollProperties.contentLength === null || this.scrollProperties.visibleLength === null || - this.state.curRenderedRowsCount === this.props.dataSource.getRowCount()) { + this.state.curRenderedRowsCount === + (this.props.enableEmptySections + ? this.props.dataSource.getRowAndSectionCount() + : this.props.dataSource.getRowCount()) + ) { this._maybeCallOnEndReached(); return; } @@ -477,31 +624,40 @@ class ListView extends Component { } _pageInNewRows() { - this.setState((state, props) => { - let rowsToRender = Math.min( - state.curRenderedRowsCount + props.pageSize, - props.dataSource.getRowCount() - ); - this._prevRenderedRowsCount = state.curRenderedRowsCount; - return { - curRenderedRowsCount: rowsToRender - }; - }, () => { - this._measureAndUpdateScrollProps(); - this._prevRenderedRowsCount = this.state.curRenderedRowsCount; - }); + this.setState( + (state, props) => { + var rowsToRender = Math.min( + state.curRenderedRowsCount + props.pageSize, + props.enableEmptySections + ? props.dataSource.getRowAndSectionCount() + : props.dataSource.getRowCount(), + ); + this._prevRenderedRowsCount = state.curRenderedRowsCount; + return { + curRenderedRowsCount: rowsToRender, + }; + }, + () => { + this._measureAndUpdateScrollProps(); + this._prevRenderedRowsCount = this.state.curRenderedRowsCount; + }, + ); } - _getDistanceFromEnd(scrollProperties) { - return scrollProperties.contentLength - scrollProperties.visibleLength - scrollProperties.offset; + _getDistanceFromEnd(scrollProperties: Object) { + return ( + scrollProperties.contentLength - + scrollProperties.visibleLength - + scrollProperties.offset + ); } - _updateVisibleRows(updatedFrames) { + _updateVisibleRows(updatedFrames?: Array) { // if (!this.props.onChangeVisibleRows) { // return; // No need to compute visible rows if there is no callback // } // if (updatedFrames) { - // updatedFrames.forEach((newFrame) => { + // updatedFrames.forEach(newFrame => { // this._childFrames[newFrame.index] = merge(newFrame); // }); // } @@ -532,12 +688,21 @@ class ListView extends Component { // let rowID = rowIDs[rowIdx]; // let frame = this._childFrames[totalIndex]; // totalIndex++; + // if ( + // this.props.renderSeparator && + // (rowIdx !== rowIDs.length - 1 || sectionIdx === allRowIDs.length - 1) + // ) { + // totalIndex++; + // } // if (!frame) { // break; // } // let rowVisible = visibleSection[rowID]; // let min = isVertical ? frame.y : frame.x; // let max = min + (isVertical ? frame.height : frame.width); + // if ((!min && !max) || min === max) { + // break; + // } // if (min > visibleMax || max < visibleMin) { // if (rowVisible) { // visibilityChanged = true; @@ -562,22 +727,20 @@ class ListView extends Component { // delete this._visibleRows[sectionID]; // } // } - // visibilityChanged && this.props.onChangeVisibleRows(this._visibleRows, changedRows); + // visibilityChanged && + // this.props.onChangeVisibleRows(this._visibleRows, changedRows); } - _onScroll(e) { + _onScroll(e: Object) { let isVertical = !this.props.horizontal; - // this.scrollProperties.visibleLength = e.nativeEvent.layoutMeasurement[ - // isVertical ? 'height' : 'width' - // ]; - // this.scrollProperties.contentLength = e.nativeEvent.contentSize[ - // isVertical ? 'height' : 'width' - // ]; - // this.scrollProperties.offset = e.nativeEvent.contentOffset[ - // isVertical ? 'y' : 'x' - // ]; - - let target = ReactDOM.findDOMNode(this.refs[SCROLLVIEW_REF]); + // this.scrollProperties.visibleLength = + // e.nativeEvent.layoutMeasurement[isVertical ? 'height' : 'width']; + // this.scrollProperties.contentLength = + // e.nativeEvent.contentSize[isVertical ? 'height' : 'width']; + // this.scrollProperties.offset = + // e.nativeEvent.contentOffset[isVertical ? 'y' : 'x']; + + let target = ReactDOM.findDOMNode(this._scrollComponent); this.scrollProperties.visibleLength = target[ isVertical ? 'offsetHeight' : 'offsetWidth' ]; @@ -592,8 +755,11 @@ class ListView extends Component { this._renderMoreRowsIfNeeded(); } - if (this.props.onEndReached && - this._getDistanceFromEnd(this.scrollProperties) > this.props.onEndReachedThreshold) { + if ( + this.props.onEndReached && + this._getDistanceFromEnd(this.scrollProperties) > + this.props.onEndReachedThreshold + ) { // Scrolled out of the end zone, so it should be able to trigger again. this._sentEndForContentLength = null; } diff --git a/Libraries/ListView/ListViewDataSource.web.js b/Libraries/ListView/ListViewDataSource.web.js index 63c3e12..150264f 100644 --- a/Libraries/ListView/ListViewDataSource.web.js +++ b/Libraries/ListView/ListViewDataSource.web.js @@ -5,36 +5,41 @@ * Copyright (c) 2015, Facebook, Inc. All rights reserved. * * @providesModule ReactListViewDataSource + * @flow + * @format */ 'use strict'; import invariant from 'fbjs/lib/invariant'; import isEmpty from 'fbjs/lib/isEmpty'; +/* $FlowFixMe(>=0.54.0 site=react_native_oss) This comment suppresses an error + * found when Flow v0.54 was deployed. To see the error delete this comment and + * run Flow. */ import warning from 'fbjs/lib/warning'; function defaultGetRowData( dataBlob: any, sectionID: number | string, - rowID: number | string + rowID: number | string, ): any { return dataBlob[sectionID][rowID]; } function defaultGetSectionHeaderData( dataBlob: any, - sectionID: number | string + sectionID: number | string, ): any { return dataBlob[sectionID]; } -type differType = (data1: any, data2: any) => bool; +type differType = (data1: any, data2: any) => boolean; type ParamType = { - rowHasChanged: differType; - getRowData: ?typeof defaultGetRowData; - sectionHeaderHasChanged: ?differType; - getSectionHeaderData: ?typeof defaultGetSectionHeaderData; -} + rowHasChanged: differType, + getRowData?: ?typeof defaultGetRowData, + sectionHeaderHasChanged?: ?differType, + getSectionHeaderData?: ?typeof defaultGetSectionHeaderData, +}; /** * Provides efficient data processing and access to the @@ -58,7 +63,7 @@ type ParamType = { * * ``` * getInitialState: function() { - * var ds = new ListViewDataSource({rowHasChanged: this._rowHasChanged}); + * var ds = new ListView.DataSource({rowHasChanged: this._rowHasChanged}); * return {ds}; * }, * _onDataArrived(newData) { @@ -71,7 +76,6 @@ type ParamType = { */ class ListViewDataSource { - /** * You can provide custom extraction and `hasChanged` functions for section * headers and rows. If absent, data will be extracted with the @@ -100,7 +104,7 @@ class ListViewDataSource { constructor(params: ParamType) { invariant( params && typeof params.rowHasChanged === 'function', - 'Must provide a rowHasChanged function.' + 'Must provide a rowHasChanged function.', ); this._rowHasChanged = params.rowHasChanged; this._getRowData = params.getRowData || defaultGetRowData; @@ -125,7 +129,7 @@ class ListViewDataSource { * construction an extractor to get the interesting information was defined * (or the default was used). * - * The `rowIdentities` is is a 2D array of identifiers for rows. + * The `rowIdentities` is a 2D array of identifiers for rows. * ie. [['a1', 'a2'], ['b1', 'b2', 'b3'], ...]. If not provided, it's * assumed that the keys of the section data are the row identities. * @@ -136,10 +140,10 @@ class ListViewDataSource { * this function as the `dataBlob`. */ cloneWithRows( - dataBlob: Array | {[key: string]: any}, - rowIdentities: ?Array - ): ListViewDataSource { - var rowIds = rowIdentities ? [rowIdentities] : null; + dataBlob: $ReadOnlyArray | {+[key: string]: any}, + rowIdentities: ?$ReadOnlyArray, + ): ListViewDataSource { + var rowIds = rowIdentities ? [[...rowIdentities]] : null; if (!this._sectionHeaderHasChanged) { this._sectionHeaderHasChanged = () => false; } @@ -151,24 +155,34 @@ class ListViewDataSource { * you also specify what your `sectionIdentities` are. If you don't care * about sections you should safely be able to use `cloneWithRows`. * - * `sectionIdentities` is an array of identifiers for sections. - * ie. ['s1', 's2', ...]. If not provided, it's assumed that the + * `sectionIdentities` is an array of identifiers for sections. + * ie. ['s1', 's2', ...]. The identifiers should correspond to the keys or array indexes + * of the data you wish to include. If not provided, it's assumed that the * keys of dataBlob are the section identities. * * Note: this returns a new object! + * + * ``` + * const dataSource = ds.cloneWithRowsAndSections({ + * addresses: ['row 1', 'row 2'], + * phone_numbers: ['data 1', 'data 2'], + * }, ['phone_numbers']); + * ``` */ cloneWithRowsAndSections( - dataBlob: any, - sectionIdentities: ?Array, - rowIdentities: ?Array> + dataBlob: any, + sectionIdentities: ?Array, + rowIdentities: ?Array>, ): ListViewDataSource { invariant( typeof this._sectionHeaderHasChanged === 'function', - 'Must provide a sectionHeaderHasChanged function with section data.' + 'Must provide a sectionHeaderHasChanged function with section data.', ); invariant( - !sectionIdentities || !rowIdentities || sectionIdentities.length === rowIdentities.length, - 'row and section ids lengths must be the same' + !sectionIdentities || + !rowIdentities || + sectionIdentities.length === rowIdentities.length, + 'row and section ids lengths must be the same', ); var newSource = new ListViewDataSource({ @@ -187,7 +201,7 @@ class ListViewDataSource { newSource.rowIdentities = rowIdentities; } else { newSource.rowIdentities = []; - newSource.sectionIdentities.forEach((sectionID) => { + newSource.sectionIdentities.forEach(sectionID => { newSource.rowIdentities.push(Object.keys(dataBlob[sectionID])); }); } @@ -196,27 +210,39 @@ class ListViewDataSource { newSource._calculateDirtyArrays( this._dataBlob, this.sectionIdentities, - this.rowIdentities + this.rowIdentities, ); return newSource; } + /** + * Returns the total number of rows in the data source. + * + * If you are specifying the rowIdentities or sectionIdentities, then `getRowCount` will return the number of rows in the filtered data source. + */ getRowCount(): number { return this._cachedRowCount; } + /** + * Returns the total number of rows in the data source (see `getRowCount` for how this is calculated) plus the number of sections in the data. + * + * If you are specifying the rowIdentities or sectionIdentities, then `getRowAndSectionCount` will return the number of rows & sections in the filtered data source. + */ getRowAndSectionCount(): number { - return (this._cachedRowCount + this.sectionIdentities.length); + return this._cachedRowCount + this.sectionIdentities.length; } /** * Returns if the row is dirtied and needs to be rerendered */ - rowShouldUpdate(sectionIndex: number, rowIndex: number): bool { + rowShouldUpdate(sectionIndex: number, rowIndex: number): boolean { var needsUpdate = this._dirtyRows[sectionIndex][rowIndex]; - warning(needsUpdate !== undefined, - 'missing dirtyBit for section, row: ' + sectionIndex + ', ' + rowIndex); + warning( + needsUpdate !== undefined, + 'missing dirtyBit for section, row: ' + sectionIndex + ', ' + rowIndex, + ); return needsUpdate; } @@ -228,7 +254,7 @@ class ListViewDataSource { var rowID = this.rowIdentities[sectionIndex][rowIndex]; warning( sectionID !== undefined && rowID !== undefined, - 'rendering invalid section, row: ' + sectionIndex + ', ' + rowIndex + 'rendering invalid section, row: ' + sectionIndex + ', ' + rowIndex, ); return this._getRowData(this._dataBlob, sectionID, rowID); } @@ -279,10 +305,12 @@ class ListViewDataSource { /** * Returns if the section header is dirtied and needs to be rerendered */ - sectionHeaderShouldUpdate(sectionIndex: number): bool { + sectionHeaderShouldUpdate(sectionIndex: number): boolean { var needsUpdate = this._dirtySections[sectionIndex]; - warning(needsUpdate !== undefined, - 'missing dirtyBit for section: ' + sectionIndex); + warning( + needsUpdate !== undefined, + 'missing dirtyBit for section: ' + sectionIndex, + ); return needsUpdate; } @@ -294,8 +322,10 @@ class ListViewDataSource { return null; } var sectionID = this.sectionIdentities[sectionIndex]; - warning(sectionID !== undefined, - 'renderSection called on invalid section: ' + sectionIndex); + warning( + sectionID !== undefined, + 'renderSection called on invalid section: ' + sectionIndex, + ); return this._getSectionHeaderData(this._dataBlob, sectionID); } @@ -309,8 +339,8 @@ class ListViewDataSource { _sectionHeaderHasChanged: ?differType; _dataBlob: any; - _dirtyRows: Array>; - _dirtySections: Array; + _dirtyRows: Array>; + _dirtySections: Array; _cachedRowCount: number; // These two 'protected' variables are accessed by ListView to iterate over @@ -321,7 +351,7 @@ class ListViewDataSource { _calculateDirtyArrays( prevDataBlob: any, prevSectionIDs: Array, - prevRowIDs: Array> + prevRowIDs: Array>, ): void { // construct a hashmap of the existing (old) id arrays var prevSectionsHash = keyedDictionaryFromArray(prevSectionIDs); @@ -330,7 +360,7 @@ class ListViewDataSource { var sectionID = prevSectionIDs[ii]; warning( !prevRowsHash[sectionID], - 'SectionID appears more than once: ' + sectionID + 'SectionID appears more than once: ' + sectionID, ); prevRowsHash[sectionID] = keyedDictionaryFromArray(prevRowIDs[ii]); } @@ -348,13 +378,17 @@ class ListViewDataSource { if (!dirty && sectionHeaderHasChanged) { dirty = sectionHeaderHasChanged( this._getSectionHeaderData(prevDataBlob, sectionID), - this._getSectionHeaderData(this._dataBlob, sectionID) + this._getSectionHeaderData(this._dataBlob, sectionID), ); } this._dirtySections.push(!!dirty); this._dirtyRows[sIndex] = []; - for (var rIndex = 0; rIndex < this.rowIdentities[sIndex].length; rIndex++) { + for ( + var rIndex = 0; + rIndex < this.rowIdentities[sIndex].length; + rIndex++ + ) { var rowID = this.rowIdentities[sIndex][rIndex]; // dirty if the section is new, row is new or _rowHasChanged is true dirty = @@ -362,7 +396,7 @@ class ListViewDataSource { !prevRowsHash[sectionID][rowID] || this._rowHasChanged( this._getRowData(prevDataBlob, sectionID, rowID), - this._getRowData(this._dataBlob, sectionID, rowID) + this._getRowData(this._dataBlob, sectionID, rowID), ); this._dirtyRows[sIndex].push(!!dirty); } diff --git a/Libraries/ListView/ScrollResponder.web.js b/Libraries/ListView/ScrollResponder.web.js index 04eb969..2487fe9 100644 --- a/Libraries/ListView/ScrollResponder.web.js +++ b/Libraries/ListView/ScrollResponder.web.js @@ -13,6 +13,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import warning from 'fbjs/lib/warning'; +import animatedScrollTo from '../Utilties/animatedScrollTo'; /** * Mixin that can be integrated in order to handle scrolling that plays well @@ -93,6 +94,7 @@ import warning from 'fbjs/lib/warning'; */ const IS_ANIMATING_TOUCH_START_THRESHOLD_MS = 16; +const ANIMATION_DURATION_MS = 300; type State = { isTouching: boolean; @@ -351,7 +353,9 @@ let ScrollResponderMixin = { */ scrollResponderScrollTo: function(offsetX: number, offsetY: number) { // TODO: Add scroll animation - this.scrollResponderScrollWithouthAnimationTo(offsetX, offsetY); + let node = ReactDOM.findDOMNode(this); + + animatedScrollTo(node, {y: offsetY, x: offsetX}, ANIMATION_DURATION_MS); }, /** @@ -361,8 +365,8 @@ let ScrollResponderMixin = { scrollResponderScrollWithouthAnimationTo: function(offsetX: number, offsetY: number) { let node = ReactDOM.findDOMNode(this); - node.offsetX = offsetX; - node.offsetY = offsetY; + node.scrollLeft = offsetX; + node.scrollTop = offsetY; }, /** diff --git a/Libraries/ListView/StaticRenderer.web.js b/Libraries/ListView/StaticRenderer.web.js index 36af765..6d87be4 100644 --- a/Libraries/ListView/StaticRenderer.web.js +++ b/Libraries/ListView/StaticRenderer.web.js @@ -6,7 +6,8 @@ */ 'use strict'; -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; class StaticRenderer extends Component { static propTypes = { diff --git a/Libraries/Lists/FillRateHelper.js b/Libraries/Lists/FillRateHelper.js new file mode 100644 index 0000000..bc119a8 --- /dev/null +++ b/Libraries/Lists/FillRateHelper.js @@ -0,0 +1,246 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule FillRateHelper + * @flow + * @format + */ + +/* eslint-disable no-console */ + +'use strict'; + +/* $FlowFixMe(>=0.54.0 site=react_native_oss) This comment suppresses an error + * found when Flow v0.54 was deployed. To see the error delete this comment and + * run Flow. */ +const performanceNow = require('fbjs/lib/performanceNow'); +/* $FlowFixMe(>=0.54.0 site=react_native_oss) This comment suppresses an error + * found when Flow v0.54 was deployed. To see the error delete this comment and + * run Flow. */ +const warning = require('fbjs/lib/warning'); + +export type FillRateInfo = Info; + +class Info { + any_blank_count = 0; + any_blank_ms = 0; + any_blank_speed_sum = 0; + mostly_blank_count = 0; + mostly_blank_ms = 0; + pixels_blank = 0; + pixels_sampled = 0; + pixels_scrolled = 0; + total_time_spent = 0; + sample_count = 0; +} + +type FrameMetrics = {inLayout?: boolean, length: number, offset: number}; + +const DEBUG = false; + +let _listeners: Array<(Info) => void> = []; +let _minSampleCount = 10; +let _sampleRate = DEBUG ? 1 : null; + +/** + * A helper class for detecting when the maximem fill rate of `VirtualizedList` is exceeded. + * By default the sampling rate is set to zero and this will do nothing. If you want to collect + * samples (e.g. to log them), make sure to call `FillRateHelper.setSampleRate(0.0-1.0)`. + * + * Listeners and sample rate are global for all `VirtualizedList`s - typical usage will combine with + * `SceneTracker.getActiveScene` to determine the context of the events. + */ +class FillRateHelper { + _anyBlankStartTime = (null: ?number); + _enabled = false; + _getFrameMetrics: (index: number) => ?FrameMetrics; + _info = new Info(); + _mostlyBlankStartTime = (null: ?number); + _samplesStartTime = (null: ?number); + + static addListener(callback: FillRateInfo => void): {remove: () => void} { + warning( + _sampleRate !== null, + 'Call `FillRateHelper.setSampleRate` before `addListener`.', + ); + _listeners.push(callback); + return { + remove: () => { + _listeners = _listeners.filter(listener => callback !== listener); + }, + }; + } + + static setSampleRate(sampleRate: number) { + _sampleRate = sampleRate; + } + + static setMinSampleCount(minSampleCount: number) { + _minSampleCount = minSampleCount; + } + + constructor(getFrameMetrics: (index: number) => ?FrameMetrics) { + this._getFrameMetrics = getFrameMetrics; + this._enabled = (_sampleRate || 0) > Math.random(); + this._resetData(); + } + + activate() { + if (this._enabled && this._samplesStartTime == null) { + DEBUG && console.debug('FillRateHelper: activate'); + this._samplesStartTime = performanceNow(); + } + } + + deactivateAndFlush() { + if (!this._enabled) { + return; + } + const start = this._samplesStartTime; // const for flow + if (start == null) { + DEBUG && + console.debug('FillRateHelper: bail on deactivate with no start time'); + return; + } + if (this._info.sample_count < _minSampleCount) { + // Don't bother with under-sampled events. + this._resetData(); + return; + } + const total_time_spent = performanceNow() - start; + const info: any = { + ...this._info, + total_time_spent, + }; + if (DEBUG) { + const derived = { + avg_blankness: this._info.pixels_blank / this._info.pixels_sampled, + avg_speed: this._info.pixels_scrolled / (total_time_spent / 1000), + avg_speed_when_any_blank: + this._info.any_blank_speed_sum / this._info.any_blank_count, + any_blank_per_min: + this._info.any_blank_count / (total_time_spent / 1000 / 60), + any_blank_time_frac: this._info.any_blank_ms / total_time_spent, + mostly_blank_per_min: + this._info.mostly_blank_count / (total_time_spent / 1000 / 60), + mostly_blank_time_frac: this._info.mostly_blank_ms / total_time_spent, + }; + for (const key in derived) { + derived[key] = Math.round(1000 * derived[key]) / 1000; + } + console.debug('FillRateHelper deactivateAndFlush: ', {derived, info}); + } + _listeners.forEach(listener => listener(info)); + this._resetData(); + } + + computeBlankness( + props: { + data: Array, + getItemCount: (data: Array) => number, + initialNumToRender: number, + }, + state: { + first: number, + last: number, + }, + scrollMetrics: { + dOffset: number, + offset: number, + velocity: number, + visibleLength: number, + }, + ): number { + if ( + !this._enabled || + props.getItemCount(props.data) === 0 || + this._samplesStartTime == null + ) { + return 0; + } + const {dOffset, offset, velocity, visibleLength} = scrollMetrics; + + // Denominator metrics that we track for all events - most of the time there is no blankness and + // we want to capture that. + this._info.sample_count++; + this._info.pixels_sampled += Math.round(visibleLength); + this._info.pixels_scrolled += Math.round(Math.abs(dOffset)); + const scrollSpeed = Math.round(Math.abs(velocity) * 1000); // px / sec + + // Whether blank now or not, record the elapsed time blank if we were blank last time. + const now = performanceNow(); + if (this._anyBlankStartTime != null) { + this._info.any_blank_ms += now - this._anyBlankStartTime; + } + this._anyBlankStartTime = null; + if (this._mostlyBlankStartTime != null) { + this._info.mostly_blank_ms += now - this._mostlyBlankStartTime; + } + this._mostlyBlankStartTime = null; + + let blankTop = 0; + let first = state.first; + let firstFrame = this._getFrameMetrics(first); + while (first <= state.last && (!firstFrame || !firstFrame.inLayout)) { + firstFrame = this._getFrameMetrics(first); + first++; + } + // Only count blankTop if we aren't rendering the first item, otherwise we will count the header + // as blank. + if (firstFrame && first > 0) { + blankTop = Math.min( + visibleLength, + Math.max(0, firstFrame.offset - offset), + ); + } + let blankBottom = 0; + let last = state.last; + let lastFrame = this._getFrameMetrics(last); + while (last >= state.first && (!lastFrame || !lastFrame.inLayout)) { + lastFrame = this._getFrameMetrics(last); + last--; + } + // Only count blankBottom if we aren't rendering the last item, otherwise we will count the + // footer as blank. + if (lastFrame && last < props.getItemCount(props.data) - 1) { + const bottomEdge = lastFrame.offset + lastFrame.length; + blankBottom = Math.min( + visibleLength, + Math.max(0, offset + visibleLength - bottomEdge), + ); + } + const pixels_blank = Math.round(blankTop + blankBottom); + const blankness = pixels_blank / visibleLength; + if (blankness > 0) { + this._anyBlankStartTime = now; + this._info.any_blank_speed_sum += scrollSpeed; + this._info.any_blank_count++; + this._info.pixels_blank += pixels_blank; + if (blankness > 0.5) { + this._mostlyBlankStartTime = now; + this._info.mostly_blank_count++; + } + } else if (scrollSpeed < 0.01 || Math.abs(dOffset) < 1) { + this.deactivateAndFlush(); + } + return blankness; + } + + enabled(): boolean { + return this._enabled; + } + + _resetData() { + this._anyBlankStartTime = null; + this._info = new Info(); + this._mostlyBlankStartTime = null; + this._samplesStartTime = null; + } +} + +module.exports = FillRateHelper; diff --git a/Libraries/Lists/FlatList.js b/Libraries/Lists/FlatList.js new file mode 100644 index 0000000..9e66b25 --- /dev/null +++ b/Libraries/Lists/FlatList.js @@ -0,0 +1,636 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ReactFlatList + * @flow + * @format + */ +'use strict'; + +const MetroListView = require('MetroListView'); // Used as a fallback legacy option +import React from 'react'; +import View from 'ReactView'; +import VirtualizedList from 'ReactVirtualizedList'; + +const invariant = require('fbjs/lib/invariant'); + +import type {StyleObj} from 'StyleSheetTypes'; +import type { + ViewabilityConfig, + ViewToken, + ViewabilityConfigCallbackPair, +} from 'ViewabilityHelper'; +import type {Props as VirtualizedListProps} from 'VirtualizedList'; + +type RequiredProps = { + /** + * Takes an item from `data` and renders it into the list. Example usage: + * + * ( + * + * )} + * data={[{title: 'Title Text', key: 'item1'}]} + * renderItem={({item, separators}) => ( + * this._onPress(item)} + * onShowUnderlay={separators.highlight} + * onHideUnderlay={separators.unhighlight}> + * + * {item.title} + * + * + * )} + * /> + * + * Provides additional metadata like `index` if you need it, as well as a more generic + * `separators.updateProps` function which let's you set whatever props you want to change the + * rendering of either the leading separator or trailing separator in case the more common + * `highlight` and `unhighlight` (which set the `highlighted: boolean` prop) are insufficient for + * your use-case. + */ + renderItem: (info: { + item: ItemT, + index: number, + separators: { + highlight: () => void, + unhighlight: () => void, + updateProps: (select: 'leading' | 'trailing', newProps: Object) => void, + }, + }) => ?React.Element, + /** + * For simplicity, data is just a plain array. If you want to use something else, like an + * immutable list, use the underlying `VirtualizedList` directly. + */ + data: ?$ReadOnlyArray, +}; +type OptionalProps = { + /** + * Rendered in between each item, but not at the top or bottom. By default, `highlighted` and + * `leadingItem` props are provided. `renderItem` provides `separators.highlight`/`unhighlight` + * which will update the `highlighted` prop, but you can also add custom props with + * `separators.updateProps`. + */ + ItemSeparatorComponent?: ?React.ComponentType, + /** + * Rendered when the list is empty. Can be a React Component Class, a render function, or + * a rendered element. + */ + ListEmptyComponent?: ?(React.ComponentType | React.Element), + /** + * Rendered at the bottom of all the items. Can be a React Component Class, a render function, or + * a rendered element. + */ + ListFooterComponent?: ?(React.ComponentType | React.Element), + /** + * Rendered at the top of all the items. Can be a React Component Class, a render function, or + * a rendered element. + */ + ListHeaderComponent?: ?(React.ComponentType | React.Element), + /** + * Optional custom style for multi-item rows generated when numColumns > 1. + */ + columnWrapperStyle?: StyleObj, + /** + * A marker property for telling the list to re-render (since it implements `PureComponent`). If + * any of your `renderItem`, Header, Footer, etc. functions depend on anything outside of the + * `data` prop, stick it here and treat it immutably. + */ + extraData?: any, + /** + * `getItemLayout` is an optional optimizations that let us skip measurement of dynamic content if + * you know the height of items a priori. `getItemLayout` is the most efficient, and is easy to + * use if you have fixed height items, for example: + * + * getItemLayout={(data, index) => ( + * {length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index} + * )} + * + * Adding `getItemLayout` can be a great performance boost for lists of several hundred items. + * Remember to include separator length (height or width) in your offset calculation if you + * specify `ItemSeparatorComponent`. + */ + getItemLayout?: ( + data: ?Array, + index: number, + ) => {length: number, offset: number, index: number}, + /** + * If true, renders items next to each other horizontally instead of stacked vertically. + */ + horizontal?: ?boolean, + /** + * How many items to render in the initial batch. This should be enough to fill the screen but not + * much more. Note these items will never be unmounted as part of the windowed rendering in order + * to improve perceived performance of scroll-to-top actions. + */ + initialNumToRender: number, + /** + * Instead of starting at the top with the first item, start at `initialScrollIndex`. This + * disables the "scroll to top" optimization that keeps the first `initialNumToRender` items + * always rendered and immediately renders the items starting at this initial index. Requires + * `getItemLayout` to be implemented. + */ + initialScrollIndex?: ?number, + /** + * Reverses the direction of scroll. Uses scale transforms of -1. + */ + inverted?: ?boolean, + /** + * Used to extract a unique key for a given item at the specified index. Key is used for caching + * and as the react key to track item re-ordering. The default extractor checks `item.key`, then + * falls back to using the index, like React does. + */ + keyExtractor: (item: ItemT, index: number) => string, + /** + * Multiple columns can only be rendered with `horizontal={false}` and will zig-zag like a + * `flexWrap` layout. Items should all be the same height - masonry layouts are not supported. + */ + numColumns: number, + /** + * Called once when the scroll position gets within `onEndReachedThreshold` of the rendered + * content. + */ + onEndReached?: ?(info: {distanceFromEnd: number}) => void, + /** + * How far from the end (in units of visible length of the list) the bottom edge of the + * list must be from the end of the content to trigger the `onEndReached` callback. + * Thus a value of 0.5 will trigger `onEndReached` when the end of the content is + * within half the visible length of the list. + */ + onEndReachedThreshold?: ?number, + /** + * If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. Make + * sure to also set the `refreshing` prop correctly. + */ + onRefresh?: ?() => void, + /** + * Called when the viewability of rows changes, as defined by the `viewabilityConfig` prop. + */ + onViewableItemsChanged?: ?(info: { + viewableItems: Array, + changed: Array, + }) => void, + /** + * Set this when offset is needed for the loading indicator to show correctly. + * @platform android + */ + progressViewOffset?: number, + legacyImplementation?: ?boolean, + /** + * Set this true while waiting for new data from a refresh. + */ + refreshing?: ?boolean, + /** + * Note: may have bugs (missing content) in some circumstances - use at your own risk. + * + * This may improve scroll performance for large lists. + */ + removeClippedSubviews?: boolean, + /** + * See `ViewabilityHelper` for flow type and further documentation. + */ + viewabilityConfig?: ViewabilityConfig, + /** + * List of ViewabilityConfig/onViewableItemsChanged pairs. A specific onViewableItemsChanged + * will be called when its corresponding ViewabilityConfig's conditions are met. + */ + viewabilityConfigCallbackPairs?: Array, +}; +export type Props = RequiredProps & + OptionalProps & + VirtualizedListProps; + +const defaultProps = { + ...VirtualizedList.defaultProps, + numColumns: 1, +}; +export type DefaultProps = typeof defaultProps; + +/** + * A performant interface for rendering simple, flat lists, supporting the most handy features: + * + * - Fully cross-platform. + * - Optional horizontal mode. + * - Configurable viewability callbacks. + * - Header support. + * - Footer support. + * - Separator support. + * - Pull to Refresh. + * - Scroll loading. + * - ScrollToIndex support. + * + * If you need section support, use [``](docs/sectionlist.html). + * + * Minimal Example: + * + * {item.key}} + * /> + * + * More complex, multi-select example demonstrating `PureComponent` usage for perf optimization and avoiding bugs. + * + * - By binding the `onPressItem` handler, the props will remain `===` and `PureComponent` will + * prevent wasteful re-renders unless the actual `id`, `selected`, or `title` props change, even + * if the components rendered in `MyListItem` did not have such optimizations. + * - By passing `extraData={this.state}` to `FlatList` we make sure `FlatList` itself will re-render + * when the `state.selected` changes. Without setting this prop, `FlatList` would not know it + * needs to re-render any items because it is also a `PureComponent` and the prop comparison will + * not show any changes. + * - `keyExtractor` tells the list to use the `id`s for the react keys instead of the default `key` property. + * + * + * class MyListItem extends React.PureComponent { + * _onPress = () => { + * this.props.onPressItem(this.props.id); + * }; + * + * render() { + * const textColor = this.props.selected ? "red" : "black"; + * return ( + * + * + * + * {this.props.title} + * + * + * + * ); + * } + * } + * + * class MultiSelectList extends React.PureComponent { + * state = {selected: (new Map(): Map)}; + * + * _keyExtractor = (item, index) => item.id; + * + * _onPressItem = (id: string) => { + * // updater functions are preferred for transactional updates + * this.setState((state) => { + * // copy the map rather than modifying state. + * const selected = new Map(state.selected); + * selected.set(id, !selected.get(id)); // toggle + * return {selected}; + * }); + * }; + * + * _renderItem = ({item}) => ( + * + * ); + * + * render() { + * return ( + * + * ); + * } + * } + * + * This is a convenience wrapper around [``](docs/virtualizedlist.html), + * and thus inherits its props (as well as those of `ScrollView`) that aren't explicitly listed + * here, along with the following caveats: + * + * - Internal state is not preserved when content scrolls out of the render window. Make sure all + * your data is captured in the item data or external stores like Flux, Redux, or Relay. + * - This is a `PureComponent` which means that it will not re-render if `props` remain shallow- + * equal. Make sure that everything your `renderItem` function depends on is passed as a prop + * (e.g. `extraData`) that is not `===` after updates, otherwise your UI may not update on + * changes. This includes the `data` prop and parent component state. + * - In order to constrain memory and enable smooth scrolling, content is rendered asynchronously + * offscreen. This means it's possible to scroll faster than the fill rate ands momentarily see + * blank content. This is a tradeoff that can be adjusted to suit the needs of each application, + * and we are working on improving it behind the scenes. + * - By default, the list looks for a `key` prop on each item and uses that for the React key. + * Alternatively, you can provide a custom `keyExtractor` prop. + * + * Also inherits [ScrollView Props](docs/scrollview.html#props), unless it is nested in another FlatList of same orientation. + */ +class FlatList extends React.PureComponent, void> { + static defaultProps: DefaultProps = defaultProps; + props: Props; + /** + * Scrolls to the end of the content. May be janky without `getItemLayout` prop. + */ + scrollToEnd(params?: ?{animated?: ?boolean}) { + this._listRef.scrollToEnd(params); + } + + /** + * Scrolls to the item at the specified index such that it is positioned in the viewable area + * such that `viewPosition` 0 places it at the top, 1 at the bottom, and 0.5 centered in the + * middle. `viewOffset` is a fixed number of pixels to offset the final target position. + * + * Note: cannot scroll to locations outside the render window without specifying the + * `getItemLayout` prop. + */ + scrollToIndex(params: { + animated?: ?boolean, + index: number, + viewOffset?: number, + viewPosition?: number, + }) { + this._listRef.scrollToIndex(params); + } + + /** + * Requires linear scan through data - use `scrollToIndex` instead if possible. + * + * Note: cannot scroll to locations outside the render window without specifying the + * `getItemLayout` prop. + */ + scrollToItem(params: { + animated?: ?boolean, + item: ItemT, + viewPosition?: number, + }) { + this._listRef.scrollToItem(params); + } + + /** + * Scroll to a specific content pixel offset in the list. + * + * Check out [scrollToOffset](docs/virtualizedlist.html#scrolltooffset) of VirtualizedList + */ + scrollToOffset(params: {animated?: ?boolean, offset: number}) { + this._listRef.scrollToOffset(params); + } + + /** + * Tells the list an interaction has occurred, which should trigger viewability calculations, e.g. + * if `waitForInteractions` is true and the user has not scrolled. This is typically called by + * taps on items or by navigation actions. + */ + recordInteraction() { + this._listRef.recordInteraction(); + } + + /** + * Displays the scroll indicators momentarily. + * + * @platform ios + */ + flashScrollIndicators() { + this._listRef.flashScrollIndicators(); + } + + /** + * Provides a handle to the underlying scroll responder. + */ + getScrollResponder() { + if (this._listRef) { + return this._listRef.getScrollResponder(); + } + } + + getScrollableNode() { + if (this._listRef) { + return this._listRef.getScrollableNode(); + } + } + + setNativeProps(props: Object) { + if (this._listRef) { + this._listRef.setNativeProps(props); + } + } + + componentWillMount() { + this._checkProps(this.props); + } + + componentWillReceiveProps(nextProps: Props) { + invariant( + nextProps.numColumns === this.props.numColumns, + 'Changing numColumns on the fly is not supported. Change the key prop on FlatList when ' + + 'changing the number of columns to force a fresh render of the component.', + ); + invariant( + nextProps.onViewableItemsChanged === this.props.onViewableItemsChanged, + 'Changing onViewableItemsChanged on the fly is not supported', + ); + invariant( + nextProps.viewabilityConfig === this.props.viewabilityConfig, + 'Changing viewabilityConfig on the fly is not supported', + ); + invariant( + nextProps.viewabilityConfigCallbackPairs === + this.props.viewabilityConfigCallbackPairs, + 'Changing viewabilityConfigCallbackPairs on the fly is not supported', + ); + + this._checkProps(nextProps); + } + + constructor(props: Props<*>) { + super(props); + if (this.props.viewabilityConfigCallbackPairs) { + this._virtualizedListPairs = this.props.viewabilityConfigCallbackPairs.map( + pair => ({ + viewabilityConfig: pair.viewabilityConfig, + onViewableItemsChanged: this._createOnViewableItemsChanged( + pair.onViewableItemsChanged, + ), + }), + ); + } else if (this.props.onViewableItemsChanged) { + this._virtualizedListPairs.push({ + viewabilityConfig: this.props.viewabilityConfig, + onViewableItemsChanged: this._createOnViewableItemsChanged( + this.props.onViewableItemsChanged, + ), + }); + } + } + + _hasWarnedLegacy = false; + _listRef: VirtualizedList; + _virtualizedListPairs: Array = []; + + _captureRef = ref => { + /* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This comment + * suppresses an error when upgrading Flow's support for React. To see the + * error delete this comment and run Flow. */ + this._listRef = ref; + }; + + _checkProps(props: Props) { + const { + getItem, + getItemCount, + horizontal, + legacyImplementation, + numColumns, + columnWrapperStyle, + onViewableItemsChanged, + viewabilityConfigCallbackPairs, + } = props; + invariant( + !getItem && !getItemCount, + 'FlatList does not support custom data formats.', + ); + if (numColumns > 1) { + invariant(!horizontal, 'numColumns does not support horizontal.'); + } else { + invariant( + !columnWrapperStyle, + 'columnWrapperStyle not supported for single column lists', + ); + } + if (legacyImplementation) { + invariant( + numColumns === 1, + 'Legacy list does not support multiple columns.', + ); + // Warning: may not have full feature parity and is meant more for debugging and performance + // comparison. + if (!this._hasWarnedLegacy) { + console.warn( + 'FlatList: Using legacyImplementation - some features not supported and performance ' + + 'may suffer', + ); + this._hasWarnedLegacy = true; + } + } + invariant( + !(onViewableItemsChanged && viewabilityConfigCallbackPairs), + 'FlatList does not support setting both onViewableItemsChanged and ' + + 'viewabilityConfigCallbackPairs.', + ); + } + + _getItem = (data: Array, index: number) => { + const {numColumns} = this.props; + if (numColumns > 1) { + const ret = []; + for (let kk = 0; kk < numColumns; kk++) { + const item = data[index * numColumns + kk]; + item && ret.push(item); + } + return ret; + } else { + return data[index]; + } + }; + + _getItemCount = (data: ?Array): number => { + return data ? Math.ceil(data.length / this.props.numColumns) : 0; + }; + + _keyExtractor = (items: ItemT | Array, index: number) => { + const {keyExtractor, numColumns} = this.props; + if (numColumns > 1) { + invariant( + Array.isArray(items), + 'FlatList: Encountered internal consistency error, expected each item to consist of an ' + + 'array with 1-%s columns; instead, received a single item.', + numColumns, + ); + return items + .map((it, kk) => keyExtractor(it, index * numColumns + kk)) + .join(':'); + } else { + return keyExtractor(items, index); + } + }; + + _pushMultiColumnViewable(arr: Array, v: ViewToken): void { + const {numColumns, keyExtractor} = this.props; + v.item.forEach((item, ii) => { + invariant(v.index != null, 'Missing index!'); + const index = v.index * numColumns + ii; + arr.push({...v, item, key: keyExtractor(item, index), index}); + }); + } + + _createOnViewableItemsChanged( + onViewableItemsChanged: ?(info: { + viewableItems: Array, + changed: Array, + }) => void, + ) { + return (info: { + viewableItems: Array, + changed: Array, + }) => { + const {numColumns} = this.props; + if (onViewableItemsChanged) { + if (numColumns > 1) { + const changed = []; + const viewableItems = []; + info.viewableItems.forEach(v => + this._pushMultiColumnViewable(viewableItems, v), + ); + info.changed.forEach(v => this._pushMultiColumnViewable(changed, v)); + onViewableItemsChanged({viewableItems, changed}); + } else { + onViewableItemsChanged(info); + } + } + }; + } + + _renderItem = (info: Object) => { + const {renderItem, numColumns, columnWrapperStyle} = this.props; + if (numColumns > 1) { + const {item, index} = info; + invariant( + Array.isArray(item), + 'Expected array of items with numColumns > 1', + ); + return ( + + {item.map((it, kk) => { + const element = renderItem({ + item: it, + index: index * numColumns + kk, + separators: info.separators, + }); + return element && React.cloneElement(element, {key: kk}); + })} + + ); + } else { + return renderItem(info); + } + }; + + render() { + if (this.props.legacyImplementation) { + return ( + + ); + } else { + return ( + + ); + } + } +} + +export default FlatList; diff --git a/Libraries/Lists/MetroListView.js b/Libraries/Lists/MetroListView.js new file mode 100644 index 0000000..db7e4e1 --- /dev/null +++ b/Libraries/Lists/MetroListView.js @@ -0,0 +1,215 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule MetroListView + * @flow + * @format + */ +'use strict'; + +import ListView from 'ReactListView'; +import React from 'react'; +import RefreshControl from 'ReactRefreshControl'; +import ScrollView from 'ReactScrollView'; + +const invariant = require('fbjs/lib/invariant'); + +type Item = any; + +type NormalProps = { + FooterComponent?: React.ComponentType<*>, + renderItem: (info: Object) => ?React.Element, + /* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This comment + * suppresses an error when upgrading Flow's support for React. To see the + * error delete this comment and run Flow. */ + renderSectionHeader?: ({section: Object}) => ?React.Element, + SeparatorComponent?: ?React.ComponentType<*>, // not supported yet + + // Provide either `items` or `sections` + items?: ?Array, // By default, an Item is assumed to be {key: string} + // $FlowFixMe - Something is a little off with the type Array + sections?: ?Array<{key: string, data: Array}>, + + /** + * If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. Make + * sure to also set the `refreshing` prop correctly. + */ + onRefresh?: ?Function, + /** + * Set this true while waiting for new data from a refresh. + */ + refreshing?: boolean, + /** + * If true, renders items next to each other horizontally instead of stacked vertically. + */ + horizontal?: ?boolean, +}; +type DefaultProps = { + keyExtractor: (item: Item, index: number) => string, +}; +/* $FlowFixMe - the renderItem passed in from SectionList is optional there but + * required here */ +type Props = NormalProps & DefaultProps; + +/** + * This is just a wrapper around the legacy ListView that matches the new API of FlatList, but with + * some section support tacked on. It is recommended to just use FlatList directly, this component + * is mostly for debugging and performance comparison. + */ +class MetroListView extends React.Component { + scrollToEnd(params?: ?{animated?: ?boolean}) { + throw new Error('scrollToEnd not supported in legacy ListView.'); + } + scrollToIndex(params: { + animated?: ?boolean, + index: number, + viewPosition?: number, + }) { + throw new Error('scrollToIndex not supported in legacy ListView.'); + } + scrollToItem(params: { + animated?: ?boolean, + item: Item, + viewPosition?: number, + }) { + throw new Error('scrollToItem not supported in legacy ListView.'); + } + scrollToLocation(params: { + animated?: ?boolean, + itemIndex: number, + sectionIndex: number, + viewOffset?: number, + viewPosition?: number, + }) { + throw new Error('scrollToLocation not supported in legacy ListView.'); + } + scrollToOffset(params: {animated?: ?boolean, offset: number}) { + const {animated, offset} = params; + this._listRef.scrollTo( + this.props.horizontal ? {x: offset, animated} : {y: offset, animated}, + ); + } + 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) => { + if (props.onRefresh) { + return ( + /* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This + * comment suppresses an error when upgrading Flow's support for + * React. To see the error delete this comment and run Flow. */ + =0.53.0 site=react_native_fb,react_native_oss) + * This comment suppresses an error when upgrading Flow's support + * for React. To see the error delete this comment and run Flow. + */ + + } + /> + ); + } else { + /* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This + * comment suppresses an error when upgrading Flow's support for React. + * To see the error delete this comment and run Flow. */ + return ; + } + }, + }; + state = this._computeState(this.props, { + ds: new ListView.DataSource({ + rowHasChanged: (itemA, itemB) => true, + sectionHeaderHasChanged: () => true, + getSectionHeaderData: (dataBlob, sectionID) => + this.state.sectionHeaderData[sectionID], + }), + sectionHeaderData: {}, + }); + componentWillReceiveProps(newProps: Props) { + this.setState(state => this._computeState(newProps, state)); + } + render() { + return ( + /* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This + * comment suppresses an error when upgrading Flow's support for React. + * To see the error delete this comment and run Flow. */ + + ); + } + _listRef: ListView; + _captureRef = ref => { + /* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This comment + * suppresses an error when upgrading Flow's support for React. To see the + * error delete this comment and run Flow. */ + this._listRef = ref; + }; + _computeState(props: Props, state) { + const sectionHeaderData = {}; + if (props.sections) { + invariant(!props.items, 'Cannot have both sections and items props.'); + const sections = {}; + props.sections.forEach((sectionIn, ii) => { + const sectionID = 's' + ii; + sections[sectionID] = sectionIn.data; + sectionHeaderData[sectionID] = sectionIn; + }); + return { + ds: state.ds.cloneWithRowsAndSections(sections), + sectionHeaderData, + }; + } else { + invariant(!props.sections, 'Cannot have both sections and items props.'); + return { + ds: state.ds.cloneWithRows(props.items), + sectionHeaderData, + }; + } + } + /* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This comment + * suppresses an error when upgrading Flow's support for React. To see the + * error delete this comment and run Flow. */ + _renderFooter = () => ; + _renderRow = (item, sectionID, rowID, highlightRow) => { + return this.props.renderItem({item, index: rowID}); + }; + _renderSectionHeader = (section, sectionID) => { + const {renderSectionHeader} = this.props; + invariant( + renderSectionHeader, + 'Must provide renderSectionHeader with sections prop', + ); + return renderSectionHeader({section}); + }; + _renderSeparator = (sID, rID) => ( + /* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This comment + * suppresses an error when upgrading Flow's support for React. To see the + * error delete this comment and run Flow. */ + + ); +} + +module.exports = MetroListView; diff --git a/Libraries/Lists/SectionList.js b/Libraries/Lists/SectionList.js new file mode 100644 index 0000000..3c40896 --- /dev/null +++ b/Libraries/Lists/SectionList.js @@ -0,0 +1,343 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ReactSectionList + * @flow + * @format + */ +'use strict'; + +const MetroListView = require('MetroListView'); +import Platform from 'ReactPlatform'; +import React from 'react'; +import ScrollView from 'ReactScrollView'; +const VirtualizedSectionList = require('VirtualizedSectionList'); + +import type {ViewToken} from 'ViewabilityHelper'; +import type {Props as VirtualizedSectionListProps} from 'VirtualizedSectionList'; + +type Item = any; + +export type SectionBase = { + /** + * The data for rendering items in this section. + */ + data: $ReadOnlyArray, + /** + * Optional key to keep track of section re-ordering. If you don't plan on re-ordering sections, + * the array index will be used by default. + */ + key?: string, + + // Optional props will override list-wide props just for this section. + renderItem?: ?(info: { + item: SectionItemT, + index: number, + section: SectionBase, + separators: { + highlight: () => void, + unhighlight: () => void, + updateProps: (select: 'leading' | 'trailing', newProps: Object) => void, + }, + }) => ?React.Element, + ItemSeparatorComponent?: ?React.ComponentType, + keyExtractor?: (item: SectionItemT) => string, + + // TODO: support more optional/override props + // onViewableItemsChanged?: ... +}; + +type RequiredProps> = { + /** + * The actual data to render, akin to the `data` prop in [``](/react-native/docs/flatlist.html). + * + * General shape: + * + * sections: $ReadOnlyArray<{ + * data: $ReadOnlyArray, + * renderItem?: ({item: SectionItem, ...}) => ?React.Element<*>, + * ItemSeparatorComponent?: ?ReactClass<{highlighted: boolean, ...}>, + * }> + */ + sections: $ReadOnlyArray, +}; + +type OptionalProps> = { + /** + * Default renderer for every item in every section. Can be over-ridden on a per-section basis. + */ + renderItem: (info: { + item: Item, + index: number, + section: SectionT, + separators: { + highlight: () => void, + unhighlight: () => void, + updateProps: (select: 'leading' | 'trailing', newProps: Object) => void, + }, + }) => ?React.Element, + /** + * Rendered in between each item, but not at the top or bottom. By default, `highlighted`, + * `section`, and `[leading/trailing][Item/Separator]` props are provided. `renderItem` provides + * `separators.highlight`/`unhighlight` which will update the `highlighted` prop, but you can also + * add custom props with `separators.updateProps`. + */ + ItemSeparatorComponent?: ?React.ComponentType, + /** + * Rendered at the very beginning of the list. Can be a React Component Class, a render function, or + * a rendered element. + */ + ListHeaderComponent?: ?(React.ComponentType | React.Element), + /** + * Rendered when the list is empty. Can be a React Component Class, a render function, or + * a rendered element. + */ + ListEmptyComponent?: ?(React.ComponentType | React.Element), + /** + * Rendered at the very end of the list. Can be a React Component Class, a render function, or + * a rendered element. + */ + ListFooterComponent?: ?(React.ComponentType | React.Element), + /** + * Rendered at the top and bottom of each section (note this is different from + * `ItemSeparatorComponent` which is only rendered between items). These are intended to separate + * sections from the headers above and below and typically have the same highlight response as + * `ItemSeparatorComponent`. Also receives `highlighted`, `[leading/trailing][Item/Separator]`, + * and any custom props from `separators.updateProps`. + */ + SectionSeparatorComponent?: ?React.ComponentType, + /** + * A marker property for telling the list to re-render (since it implements `PureComponent`). If + * any of your `renderItem`, Header, Footer, etc. functions depend on anything outside of the + * `data` prop, stick it here and treat it immutably. + */ + extraData?: any, + /** + * How many items to render in the initial batch. This should be enough to fill the screen but not + * much more. Note these items will never be unmounted as part of the windowed rendering in order + * to improve perceived performance of scroll-to-top actions. + */ + initialNumToRender: number, + /** + * Reverses the direction of scroll. Uses scale transforms of -1. + */ + inverted?: ?boolean, + /** + * Used to extract a unique key for a given item at the specified index. Key is used for caching + * and as the react key to track item re-ordering. The default extractor checks item.key, then + * falls back to using the index, like react does. Note that this sets keys for each item, but + * each overall section still needs its own key. + */ + keyExtractor: (item: Item, index: number) => string, + /** + * Called once when the scroll position gets within `onEndReachedThreshold` of the rendered + * content. + */ + onEndReached?: ?(info: {distanceFromEnd: number}) => void, + /** + * How far from the end (in units of visible length of the list) the bottom edge of the + * list must be from the end of the content to trigger the `onEndReached` callback. + * Thus a value of 0.5 will trigger `onEndReached` when the end of the content is + * within half the visible length of the list. + */ + onEndReachedThreshold?: ?number, + /** + * If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. Make + * sure to also set the `refreshing` prop correctly. + */ + onRefresh?: ?() => void, + /** + * Called when the viewability of rows changes, as defined by the + * `viewabilityConfig` prop. + */ + onViewableItemsChanged?: ?(info: { + viewableItems: Array, + changed: Array, + }) => void, + /** + * Set this true while waiting for new data from a refresh. + */ + refreshing?: ?boolean, + /** + * Note: may have bugs (missing content) in some circumstances - use at your own risk. + * + * This may improve scroll performance for large lists. + */ + removeClippedSubviews?: boolean, + /** + * Rendered at the top of each section. These stick to the top of the `ScrollView` by default on + * iOS. See `stickySectionHeadersEnabled`. + */ + renderSectionHeader?: ?(info: {section: SectionT}) => ?React.Element, + /** + * Rendered at the bottom of each section. + */ + renderSectionFooter?: ?(info: {section: SectionT}) => ?React.Element, + /** + * Makes section headers stick to the top of the screen until the next one pushes it off. Only + * enabled by default on iOS because that is the platform standard there. + */ + stickySectionHeadersEnabled?: boolean, + + legacyImplementation?: ?boolean, +}; + +export type Props = RequiredProps & + OptionalProps & + VirtualizedSectionListProps; + +const defaultProps = { + ...VirtualizedSectionList.defaultProps, + stickySectionHeadersEnabled: Platform.OS === 'ios', +}; + +type DefaultProps = typeof defaultProps; + +/** + * A performant interface for rendering sectioned lists, supporting the most handy features: + * + * - Fully cross-platform. + * - Configurable viewability callbacks. + * - List header support. + * - List footer support. + * - Item separator support. + * - Section header support. + * - Section separator support. + * - Heterogeneous data and item rendering support. + * - Pull to Refresh. + * - Scroll loading. + * + * If you don't need section support and want a simpler interface, use + * [``](/react-native/docs/flatlist.html). + * + * Simple Examples: + * + * } + * renderSectionHeader={({section}) =>
} + * sections={[ // homogeneous rendering between sections + * {data: [...], title: ...}, + * {data: [...], title: ...}, + * {data: [...], title: ...}, + * ]} + * /> + * + * + * + * This is a convenience wrapper around [``](docs/virtualizedlist.html), + * and thus inherits its props (as well as those of `ScrollView`) that aren't explicitly listed + * here, along with the following caveats: + * + * - Internal state is not preserved when content scrolls out of the render window. Make sure all + * your data is captured in the item data or external stores like Flux, Redux, or Relay. + * - This is a `PureComponent` which means that it will not re-render if `props` remain shallow- + * equal. Make sure that everything your `renderItem` function depends on is passed as a prop + * (e.g. `extraData`) that is not `===` after updates, otherwise your UI may not update on + * changes. This includes the `data` prop and parent component state. + * - In order to constrain memory and enable smooth scrolling, content is rendered asynchronously + * offscreen. This means it's possible to scroll faster than the fill rate and momentarily see + * blank content. This is a tradeoff that can be adjusted to suit the needs of each application, + * and we are working on improving it behind the scenes. + * - By default, the list looks for a `key` prop on each item and uses that for the React key. + * Alternatively, you can provide a custom `keyExtractor` prop. + * + */ +class SectionList> extends React.PureComponent< + Props, + void, +> { + props: Props; + static defaultProps: DefaultProps = defaultProps; + + /** + * Scrolls to the item at the specified `sectionIndex` and `itemIndex` (within the section) + * positioned in the viewable area such that `viewPosition` 0 places it at the top (and may be + * covered by a sticky header), 1 at the bottom, and 0.5 centered in the middle. `viewOffset` is a + * fixed number of pixels to offset the final target position, e.g. to compensate for sticky + * headers. + * + * Note: cannot scroll to locations outside the render window without specifying the + * `getItemLayout` prop. + */ + scrollToLocation(params: { + animated?: ?boolean, + itemIndex: number, + sectionIndex: number, + viewOffset?: number, + viewPosition?: number, + }) { + this._wrapperListRef.scrollToLocation(params); + } + + /** + * 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 + * taps on items or by navigation actions. + */ + recordInteraction() { + const listRef = this._wrapperListRef && this._wrapperListRef.getListRef(); + listRef && listRef.recordInteraction(); + } + + /** + * Displays the scroll indicators momentarily. + * + * @platform ios + */ + flashScrollIndicators() { + const listRef = this._wrapperListRef && this._wrapperListRef.getListRef(); + listRef && listRef.flashScrollIndicators(); + } + + /** + * Provides a handle to the underlying scroll responder. + */ + getScrollResponder(): ?ScrollView { + const listRef = this._wrapperListRef && this._wrapperListRef.getListRef(); + if (listRef) { + return listRef.getScrollResponder(); + } + } + + getScrollableNode() { + const listRef = this._wrapperListRef && this._wrapperListRef.getListRef(); + if (listRef) { + return listRef.getScrollableNode(); + } + } + + setNativeProps(props: Object) { + const listRef = this._wrapperListRef && this._wrapperListRef.getListRef(); + if (listRef) { + listRef.setNativeProps(props); + } + } + + render() { + const List = this.props.legacyImplementation + ? MetroListView + : VirtualizedSectionList; + return ; + } + + _wrapperListRef: MetroListView | VirtualizedSectionList; + _captureRef = ref => { + /* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This comment + * suppresses an error when upgrading Flow's support for React. To see the + * error delete this comment and run Flow. */ + this._wrapperListRef = ref; + }; +} + +export default SectionList; diff --git a/Libraries/Lists/ViewabilityHelper.js b/Libraries/Lists/ViewabilityHelper.js new file mode 100644 index 0000000..05f1a1b --- /dev/null +++ b/Libraries/Lists/ViewabilityHelper.js @@ -0,0 +1,314 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ViewabilityHelper + * @flow + * @format + */ +'use strict'; + +const invariant = require('fbjs/lib/invariant'); + +export type ViewToken = { + item: any, + key: string, + index: ?number, + isViewable: boolean, + section?: any, +}; + +export type ViewabilityConfigCallbackPair = { + viewabilityConfig: ViewabilityConfig, + onViewableItemsChanged: (info: { + viewableItems: Array, + changed: Array, + }) => void, +}; + +export type ViewabilityConfig = {| + /** + * Minimum amount of time (in milliseconds) that an item must be physically viewable before the + * viewability callback will be fired. A high number means that scrolling through content without + * stopping will not mark the content as viewable. + */ + minimumViewTime?: number, + + /** + * Percent of viewport that must be covered for a partially occluded item to count as + * "viewable", 0-100. Fully visible items are always considered viewable. A value of 0 means + * that a single pixel in the viewport makes the item viewable, and a value of 100 means that + * an item must be either entirely visible or cover the entire viewport to count as viewable. + */ + viewAreaCoveragePercentThreshold?: number, + + /** + * Similar to `viewAreaPercentThreshold`, but considers the percent of the item that is visible, + * rather than the fraction of the viewable area it covers. + */ + itemVisiblePercentThreshold?: number, + + /** + * Nothing is considered viewable until the user scrolls or `recordInteraction` is called after + * render. + */ + waitForInteraction?: boolean, +|}; + +/** +* A Utility class for calculating viewable items based on current metrics like scroll position and +* layout. +* +* An item is said to be in a "viewable" state when any of the following +* is true for longer than `minimumViewTime` milliseconds (after an interaction if `waitForInteraction` +* is true): +* +* - Occupying >= `viewAreaCoveragePercentThreshold` of the view area XOR fraction of the item +* visible in the view area >= `itemVisiblePercentThreshold`. +* - Entirely visible on screen +*/ +class ViewabilityHelper { + _config: ViewabilityConfig; + _hasInteracted: boolean = false; + _lastUpdateTime: number = 0; + _timers: Set = new Set(); + _viewableIndices: Array = []; + _viewableItems: Map = new Map(); + + constructor( + config: ViewabilityConfig = {viewAreaCoveragePercentThreshold: 0}, + ) { + this._config = config; + } + + /** + * Cleanup, e.g. on unmount. Clears any pending timers. + */ + dispose() { + this._timers.forEach(clearTimeout); + } + + /** + * Determines which items are viewable based on the current metrics and config. + */ + computeViewableItems( + itemCount: number, + scrollOffset: number, + viewportHeight: number, + getFrameMetrics: (index: number) => ?{length: number, offset: number}, + renderRange?: {first: number, last: number}, // Optional optimization to reduce the scan size + ): Array { + const { + itemVisiblePercentThreshold, + viewAreaCoveragePercentThreshold, + } = this._config; + const viewAreaMode = viewAreaCoveragePercentThreshold != null; + const viewablePercentThreshold = viewAreaMode + ? viewAreaCoveragePercentThreshold + : itemVisiblePercentThreshold; + invariant( + viewablePercentThreshold != null && + (itemVisiblePercentThreshold != null) !== + (viewAreaCoveragePercentThreshold != null), + 'Must set exactly one of itemVisiblePercentThreshold or viewAreaCoveragePercentThreshold', + ); + const viewableIndices = []; + if (itemCount === 0) { + return viewableIndices; + } + let firstVisible = -1; + const {first, last} = renderRange || {first: 0, last: itemCount - 1}; + invariant( + last < itemCount, + 'Invalid render range ' + JSON.stringify({renderRange, itemCount}), + ); + for (let idx = first; idx <= last; idx++) { + const metrics = getFrameMetrics(idx); + if (!metrics) { + continue; + } + const top = metrics.offset - scrollOffset; + const bottom = top + metrics.length; + if (top < viewportHeight && bottom > 0) { + firstVisible = idx; + if ( + _isViewable( + viewAreaMode, + viewablePercentThreshold, + top, + bottom, + viewportHeight, + metrics.length, + ) + ) { + viewableIndices.push(idx); + } + } else if (firstVisible >= 0) { + break; + } + } + return viewableIndices; + } + + /** + * Figures out which items are viewable and how that has changed from before and calls + * `onViewableItemsChanged` as appropriate. + */ + onUpdate( + itemCount: number, + scrollOffset: number, + viewportHeight: number, + getFrameMetrics: (index: number) => ?{length: number, offset: number}, + createViewToken: (index: number, isViewable: boolean) => ViewToken, + onViewableItemsChanged: ({ + viewableItems: Array, + changed: Array, + }) => void, + renderRange?: {first: number, last: number}, // Optional optimization to reduce the scan size + ): void { + const updateTime = Date.now(); + if (this._lastUpdateTime === 0 && itemCount > 0 && getFrameMetrics(0)) { + // Only count updates after the first item is rendered and has a frame. + this._lastUpdateTime = updateTime; + } + const updateElapsed = this._lastUpdateTime + ? updateTime - this._lastUpdateTime + : 0; + if (this._config.waitForInteraction && !this._hasInteracted) { + return; + } + let viewableIndices = []; + if (itemCount) { + viewableIndices = this.computeViewableItems( + itemCount, + scrollOffset, + viewportHeight, + getFrameMetrics, + renderRange, + ); + } + if ( + this._viewableIndices.length === viewableIndices.length && + this._viewableIndices.every((v, ii) => v === viewableIndices[ii]) + ) { + // We might get a lot of scroll events where visibility doesn't change and we don't want to do + // extra work in those cases. + return; + } + this._viewableIndices = viewableIndices; + this._lastUpdateTime = updateTime; + if ( + this._config.minimumViewTime && + updateElapsed < this._config.minimumViewTime + ) { + const handle = setTimeout(() => { + this._timers.delete(handle); + this._onUpdateSync( + viewableIndices, + onViewableItemsChanged, + createViewToken, + ); + }, this._config.minimumViewTime); + this._timers.add(handle); + } else { + this._onUpdateSync( + viewableIndices, + onViewableItemsChanged, + createViewToken, + ); + } + } + + /** + * clean-up cached _viewableIndices to evaluate changed items on next update + */ + resetViewableIndices() { + this._viewableIndices = []; + } + + /** + * Records that an interaction has happened even if there has been no scroll. + */ + recordInteraction() { + this._hasInteracted = true; + } + + _onUpdateSync( + viewableIndicesToCheck, + onViewableItemsChanged, + createViewToken, + ) { + // Filter out indices that have gone out of view since this call was scheduled. + viewableIndicesToCheck = viewableIndicesToCheck.filter(ii => + this._viewableIndices.includes(ii), + ); + const prevItems = this._viewableItems; + const nextItems = new Map( + viewableIndicesToCheck.map(ii => { + const viewable = createViewToken(ii, true); + return [viewable.key, viewable]; + }), + ); + + const changed = []; + for (const [key, viewable] of nextItems) { + if (!prevItems.has(key)) { + changed.push(viewable); + } + } + for (const [key, viewable] of prevItems) { + if (!nextItems.has(key)) { + changed.push({...viewable, isViewable: false}); + } + } + if (changed.length > 0) { + this._viewableItems = nextItems; + onViewableItemsChanged({ + viewableItems: Array.from(nextItems.values()), + changed, + viewabilityConfig: this._config, + }); + } + } +} + +function _isViewable( + viewAreaMode: boolean, + viewablePercentThreshold: number, + top: number, + bottom: number, + viewportHeight: number, + itemLength: number, +): boolean { + if (_isEntirelyVisible(top, bottom, viewportHeight)) { + return true; + } else { + const pixels = _getPixelsVisible(top, bottom, viewportHeight); + const percent = + 100 * (viewAreaMode ? pixels / viewportHeight : pixels / itemLength); + return percent >= viewablePercentThreshold; + } +} + +function _getPixelsVisible( + top: number, + bottom: number, + viewportHeight: number, +): number { + const visibleHeight = Math.min(bottom, viewportHeight) - Math.max(top, 0); + return Math.max(0, visibleHeight); +} + +function _isEntirelyVisible( + top: number, + bottom: number, + viewportHeight: number, +): boolean { + return top >= 0 && bottom <= viewportHeight && bottom > top; +} + +module.exports = ViewabilityHelper; diff --git a/Libraries/Lists/VirtualizeUtils.js b/Libraries/Lists/VirtualizeUtils.js new file mode 100644 index 0000000..91cb58e --- /dev/null +++ b/Libraries/Lists/VirtualizeUtils.js @@ -0,0 +1,208 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule VirtualizeUtils + * @flow + * @format + */ +'use strict'; + +const invariant = require('fbjs/lib/invariant'); + +/** + * Used to find the indices of the frames that overlap the given offsets. Useful for finding the + * items that bound different windows of content, such as the visible area or the buffered overscan + * area. + */ +function elementsThatOverlapOffsets( + offsets: Array, + itemCount: number, + getFrameMetrics: (index: number) => {length: number, offset: number}, +): Array { + const out = []; + for (let ii = 0; ii < itemCount; ii++) { + const frame = getFrameMetrics(ii); + const trailingOffset = frame.offset + frame.length; + for (let kk = 0; kk < offsets.length; kk++) { + if (out[kk] == null && trailingOffset >= offsets[kk]) { + out[kk] = ii; + if (kk === offsets.length - 1) { + invariant( + out.length === offsets.length, + 'bad offsets input, should be in increasing order ' + + JSON.stringify(offsets), + ); + return out; + } + } + } + } + return out; +} + +/** + * Computes the number of elements in the `next` range that are new compared to the `prev` range. + * Handy for calculating how many new items will be rendered when the render window changes so we + * can restrict the number of new items render at once so that content can appear on the screen + * faster. + */ +function newRangeCount( + prev: {first: number, last: number}, + next: {first: number, last: number}, +): number { + return ( + next.last - + next.first + + 1 - + Math.max( + 0, + 1 + Math.min(next.last, prev.last) - Math.max(next.first, prev.first), + ) + ); +} + +/** + * Custom logic for determining which items should be rendered given the current frame and scroll + * metrics, as well as the previous render state. The algorithm may evolve over time, but generally + * prioritizes the visible area first, then expands that with overscan regions ahead and behind, + * biased in the direction of scroll. + */ +function computeWindowedRenderLimits( + props: { + data: any, + getItemCount: (data: any) => number, + maxToRenderPerBatch: number, + windowSize: number, + }, + prev: {first: number, last: number}, + getFrameMetricsApprox: (index: number) => {length: number, offset: number}, + scrollMetrics: { + dt: number, + offset: number, + velocity: number, + visibleLength: number, + }, +): {first: number, last: number} { + const {data, getItemCount, maxToRenderPerBatch, windowSize} = props; + const itemCount = getItemCount(data); + if (itemCount === 0) { + return prev; + } + const {offset, velocity, visibleLength} = scrollMetrics; + + // Start with visible area, then compute maximum overscan region by expanding from there, biased + // in the direction of scroll. Total overscan area is capped, which should cap memory consumption + // too. + const visibleBegin = Math.max(0, offset); + const visibleEnd = visibleBegin + visibleLength; + const overscanLength = (windowSize - 1) * visibleLength; + + // Considering velocity seems to introduce more churn than it's worth. + const leadFactor = 0.5; // Math.max(0, Math.min(1, velocity / 25 + 0.5)); + + const fillPreference = + velocity > 1 ? 'after' : velocity < -1 ? 'before' : 'none'; + + const overscanBegin = Math.max( + 0, + visibleBegin - (1 - leadFactor) * overscanLength, + ); + const overscanEnd = Math.max(0, visibleEnd + leadFactor * overscanLength); + + // Find the indices that correspond to the items at the render boundaries we're targetting. + let [overscanFirst, first, last, overscanLast] = elementsThatOverlapOffsets( + [overscanBegin, visibleBegin, visibleEnd, overscanEnd], + props.getItemCount(props.data), + getFrameMetricsApprox, + ); + overscanFirst = overscanFirst == null ? 0 : overscanFirst; + first = first == null ? Math.max(0, overscanFirst) : first; + overscanLast = overscanLast == null ? itemCount - 1 : overscanLast; + last = + last == null + ? Math.min(overscanLast, first + maxToRenderPerBatch - 1) + : last; + const visible = {first, last}; + + // We want to limit the number of new cells we're rendering per batch so that we can fill the + // content on the screen quickly. If we rendered the entire overscan window at once, the user + // could be staring at white space for a long time waiting for a bunch of offscreen content to + // render. + let newCellCount = newRangeCount(prev, visible); + + while (true) { + if (first <= overscanFirst && last >= overscanLast) { + // If we fill the entire overscan range, we're done. + break; + } + const maxNewCells = newCellCount >= maxToRenderPerBatch; + const firstWillAddMore = first <= prev.first || first > prev.last; + const firstShouldIncrement = + first > overscanFirst && (!maxNewCells || !firstWillAddMore); + const lastWillAddMore = last >= prev.last || last < prev.first; + const lastShouldIncrement = + last < overscanLast && (!maxNewCells || !lastWillAddMore); + if (maxNewCells && !firstShouldIncrement && !lastShouldIncrement) { + // We only want to stop if we've hit maxNewCells AND we cannot increment first or last + // without rendering new items. This let's us preserve as many already rendered items as + // possible, reducing render churn and keeping the rendered overscan range as large as + // possible. + break; + } + if ( + firstShouldIncrement && + !(fillPreference === 'after' && lastShouldIncrement && lastWillAddMore) + ) { + if (firstWillAddMore) { + newCellCount++; + } + first--; + } + if ( + lastShouldIncrement && + !(fillPreference === 'before' && firstShouldIncrement && firstWillAddMore) + ) { + if (lastWillAddMore) { + newCellCount++; + } + last++; + } + } + if ( + !( + last >= first && + first >= 0 && + last < itemCount && + first >= overscanFirst && + last <= overscanLast && + first <= visible.first && + last >= visible.last + ) + ) { + throw new Error( + 'Bad window calculation ' + + JSON.stringify({ + first, + last, + itemCount, + overscanFirst, + overscanLast, + visible, + }), + ); + } + return {first, last}; +} + +const VirtualizeUtils = { + computeWindowedRenderLimits, + elementsThatOverlapOffsets, + newRangeCount, +}; + +module.exports = VirtualizeUtils; diff --git a/Libraries/Lists/VirtualizedList.js b/Libraries/Lists/VirtualizedList.js new file mode 100644 index 0000000..ec59387 --- /dev/null +++ b/Libraries/Lists/VirtualizedList.js @@ -0,0 +1,1405 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ReactVirtualizedList + * @flow + * @format + */ +'use strict'; + +const Batchinator = require('Batchinator'); +const FillRateHelper = require('FillRateHelper'); +const PropTypes = require('prop-types'); +import React from 'react'; +import ReactDOM from 'react-dom'; +import findNodeHandle from 'ReactfindNodeHandle'; +import RefreshControl from 'ReactRefreshControl'; +import ScrollView from 'ReactScrollView'; +import StyleSheet from 'ReactStyleSheet'; +import View from 'ReactView'; +const ViewabilityHelper = require('ViewabilityHelper'); + +import flattenStyle from 'ReactFlattenStyle'; +const infoLog = require('infoLog'); +const invariant = require('fbjs/lib/invariant'); +/* $FlowFixMe(>=0.54.0 site=react_native_oss) This comment suppresses an error + * found when Flow v0.54 was deployed. To see the error delete this comment and + * run Flow. */ +const warning = require('fbjs/lib/warning'); + +const {computeWindowedRenderLimits} = require('VirtualizeUtils'); + +import type {StyleObj} from 'StyleSheetTypes'; +import type { + ViewabilityConfig, + ViewToken, + ViewabilityConfigCallbackPair, +} from 'ViewabilityHelper'; + +type Item = any; + +export type renderItemType = (info: any) => ?React.Element; + +type ViewabilityHelperCallbackTuple = { + viewabilityHelper: ViewabilityHelper, + onViewableItemsChanged: (info: { + viewableItems: Array, + changed: Array, + }) => void, +}; + +type RequiredProps = { + renderItem: renderItemType, + /** + * The default accessor functions assume this is an Array<{key: string}> but you can override + * getItem, getItemCount, and keyExtractor to handle any type of index-based data. + */ + data?: any, + /** + * A generic accessor for extracting an item from any sort of data blob. + */ + getItem: (data: any, index: number) => ?Item, + /** + * Determines how many items are in the data blob. + */ + getItemCount: (data: any) => number, +}; +type OptionalProps = { + /** + * `debug` will turn on extra logging and visual overlays to aid with debugging both usage and + * implementation, but with a significant perf hit. + */ + debug?: ?boolean, + /** + * DEPRECATED: Virtualization provides significant performance and memory optimizations, but fully + * unmounts react instances that are outside of the render window. You should only need to disable + * this for debugging purposes. + */ + disableVirtualization: boolean, + /** + * A marker property for telling the list to re-render (since it implements `PureComponent`). If + * any of your `renderItem`, Header, Footer, etc. functions depend on anything outside of the + * `data` prop, stick it here and treat it immutably. + */ + extraData?: any, + getItemLayout?: ( + data: any, + index: number, + ) => {length: number, offset: number, index: number}, // e.g. height, y + horizontal?: ?boolean, + /** + * How many items to render in the initial batch. This should be enough to fill the screen but not + * much more. Note these items will never be unmounted as part of the windowed rendering in order + * to improve perceived performance of scroll-to-top actions. + */ + initialNumToRender: number, + /** + * Instead of starting at the top with the first item, start at `initialScrollIndex`. This + * disables the "scroll to top" optimization that keeps the first `initialNumToRender` items + * always rendered and immediately renders the items starting at this initial index. Requires + * `getItemLayout` to be implemented. + */ + initialScrollIndex?: ?number, + /** + * Reverses the direction of scroll. Uses scale transforms of -1. + */ + inverted?: ?boolean, + keyExtractor: (item: Item, index: number) => string, + /** + * Each cell is rendered using this element. Can be a React Component Class, + * or a render function. Defaults to using View. + */ + CellRendererComponent?: ?React.ComponentType, + /** + * Rendered when the list is empty. Can be a React Component Class, a render function, or + * a rendered element. + */ + ListEmptyComponent?: ?(React.ComponentType | React.Element), + /** + * Rendered at the bottom of all the items. Can be a React Component Class, a render function, or + * a rendered element. + */ + ListFooterComponent?: ?(React.ComponentType | React.Element), + /** + * Rendered at the top of all the items. Can be a React Component Class, a render function, or + * a rendered element. + */ + ListHeaderComponent?: ?(React.ComponentType | React.Element), + /** + * The maximum number of items to render in each incremental render batch. The more rendered at + * once, the better the fill rate, but responsiveness my suffer because rendering content may + * interfere with responding to button taps or other interactions. + */ + maxToRenderPerBatch: number, + onEndReached?: ?(info: {distanceFromEnd: number}) => void, + onEndReachedThreshold?: ?number, // units of visible length + onLayout?: ?Function, + /** + * If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. Make + * sure to also set the `refreshing` prop correctly. + */ + onRefresh?: ?Function, + /** + * Used to handle failures when scrolling to an index that has not been measured yet. Recommended + * action is to either compute your own offset and `scrollTo` it, or scroll as far as possible and + * then try again after more items have been rendered. + */ + onScrollToIndexFailed?: ?(info: { + index: number, + highestMeasuredFrameIndex: number, + averageItemLength: number, + }) => void, + /** + * Called when the viewability of rows changes, as defined by the + * `viewabilityConfig` prop. + */ + onViewableItemsChanged?: ?(info: { + viewableItems: Array, + changed: Array, + }) => void, + /** + * Set this when offset is needed for the loading indicator to show correctly. + * @platform android + */ + progressViewOffset?: number, + /** + * Set this true while waiting for new data from a refresh. + */ + refreshing?: ?boolean, + /** + * Note: may have bugs (missing content) in some circumstances - use at your own risk. + * + * This may improve scroll performance for large lists. + */ + removeClippedSubviews?: boolean, + /** + * Render a custom scroll component, e.g. with a differently styled `RefreshControl`. + */ + renderScrollComponent?: (props: Object) => React.Element, + /** + * Amount of time between low-pri item render batches, e.g. for rendering items quite a ways off + * screen. Similar fill rate/responsiveness tradeoff as `maxToRenderPerBatch`. + */ + updateCellsBatchingPeriod: number, + viewabilityConfig?: ViewabilityConfig, + /** + * List of ViewabilityConfig/onViewableItemsChanged pairs. A specific onViewableItemsChanged + * will be called when its corresponding ViewabilityConfig's conditions are met. + */ + viewabilityConfigCallbackPairs?: Array, + /** + * Determines the maximum number of items rendered outside of the visible area, in units of + * visible lengths. So if your list fills the screen, then `windowSize={21}` (the default) will + * render the visible screen area plus up to 10 screens above and 10 below the viewport. Reducing + * this number will reduce memory consumption and may improve performance, but will increase the + * chance that fast scrolling may reveal momentary blank areas of unrendered content. + */ + windowSize: number, +}; +/* $FlowFixMe - this Props seems to be missing a bunch of stuff. Remove this + * comment to see the errors */ +export type Props = RequiredProps & OptionalProps; + +let _usedIndexForKey = false; + +type State = {first: number, last: number}; + +/** + * Base implementation for the more convenient [``](/react-native/docs/flatlist.html) + * and [``](/react-native/docs/sectionlist.html) components, which are also better + * documented. In general, this should only really be used if you need more flexibility than + * `FlatList` provides, e.g. for use with immutable data instead of plain arrays. + * + * Virtualization massively improves memory consumption and performance of large lists by + * maintaining a finite render window of active items and replacing all items outside of the render + * window with appropriately sized blank space. The window adapts to scrolling behavior, and items + * are rendered incrementally with low-pri (after any running interactions) if they are far from the + * visible area, or with hi-pri otherwise to minimize the potential of seeing blank space. + * + * Some caveats: + * + * - Internal state is not preserved when content scrolls out of the render window. Make sure all + * your data is captured in the item data or external stores like Flux, Redux, or Relay. + * - This is a `PureComponent` which means that it will not re-render if `props` remain shallow- + * equal. Make sure that everything your `renderItem` function depends on is passed as a prop + * (e.g. `extraData`) that is not `===` after updates, otherwise your UI may not update on + * changes. This includes the `data` prop and parent component state. + * - In order to constrain memory and enable smooth scrolling, content is rendered asynchronously + * offscreen. This means it's possible to scroll faster than the fill rate ands momentarily see + * blank content. This is a tradeoff that can be adjusted to suit the needs of each application, + * and we are working on improving it behind the scenes. + * - By default, the list looks for a `key` prop on each item and uses that for the React key. + * Alternatively, you can provide a custom `keyExtractor` prop. + * + */ +class VirtualizedList extends React.PureComponent { + props: Props; + + // scrollToEnd may be janky without getItemLayout prop + scrollToEnd(params?: ?{animated?: ?boolean}) { + const animated = params ? params.animated : true; + const veryLast = this.props.getItemCount(this.props.data) - 1; + const frame = this._getFrameMetricsApprox(veryLast); + const offset = Math.max( + 0, + frame.offset + + frame.length + + this._footerLength - + this._scrollMetrics.visibleLength, + ); + /* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This comment + * suppresses an error when upgrading Flow's support for React. To see the + * error delete this comment and run Flow. */ + this._scrollRef.scrollTo( + /* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This + * comment suppresses an error when upgrading Flow's support for React. + * To see the error delete this comment and run Flow. */ + this.props.horizontal ? {x: offset, animated} : {y: offset, animated}, + ); + } + + // scrollToIndex may be janky without getItemLayout prop + scrollToIndex(params: { + animated?: ?boolean, + index: number, + viewOffset?: number, + viewPosition?: number, + }) { + const { + data, + horizontal, + getItemCount, + getItemLayout, + onScrollToIndexFailed, + } = this.props; + const {animated, index, viewOffset, viewPosition} = params; + invariant( + index >= 0 && index < getItemCount(data), + `scrollToIndex out of range: ${index} vs ${getItemCount(data) - 1}`, + ); + if (!getItemLayout && index > this._highestMeasuredFrameIndex) { + invariant( + !!onScrollToIndexFailed, + 'scrollToIndex should be used in conjunction with getItemLayout or onScrollToIndexFailed, ' + + 'otherwise there is no way to know the location of offscreen indices or handle failures.', + ); + onScrollToIndexFailed({ + averageItemLength: this._averageCellLength, + highestMeasuredFrameIndex: this._highestMeasuredFrameIndex, + index, + }); + return; + } + const frame = this._getFrameMetricsApprox(index); + const offset = + Math.max( + 0, + frame.offset - + (viewPosition || 0) * + (this._scrollMetrics.visibleLength - frame.length), + ) - (viewOffset || 0); + /* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This comment + * suppresses an error when upgrading Flow's support for React. To see the + * error delete this comment and run Flow. */ + this._scrollRef.scrollTo( + /* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This + * comment suppresses an error when upgrading Flow's support for React. + * To see the error delete this comment and run Flow. */ + horizontal ? {x: offset, animated} : {y: offset, animated}, + ); + } + + // scrollToItem may be janky without getItemLayout prop. Required linear scan through items - + // use scrollToIndex instead if possible. + scrollToItem(params: { + animated?: ?boolean, + item: Item, + viewPosition?: number, + }) { + const {item} = params; + const {data, getItem, getItemCount} = this.props; + const itemCount = getItemCount(data); + for (let index = 0; index < itemCount; index++) { + if (getItem(data, index) === item) { + this.scrollToIndex({...params, index}); + break; + } + } + } + + /** + * Scroll to a specific content pixel offset in the list. + * + * Param `offset` expects the offset to scroll to. + * In case of `horizontal` is true, the offset is the x-value, + * in any other case the offset is the y-value. + * + * Param `animated` (`true` by default) defines whether the list + * should do an animation while scrolling. + */ + scrollToOffset(params: {animated?: ?boolean, offset: number}) { + const {animated, offset} = params; + /* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This comment + * suppresses an error when upgrading Flow's support for React. To see the + * error delete this comment and run Flow. */ + this._scrollRef.scrollTo( + /* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This + * comment suppresses an error when upgrading Flow's support for React. + * To see the error delete this comment and run Flow. */ + this.props.horizontal ? {x: offset, animated} : {y: offset, animated}, + ); + } + + recordInteraction() { + this._viewabilityTuples.forEach(t => { + t.viewabilityHelper.recordInteraction(); + }); + this._updateViewableItems(this.props.data); + } + + flashScrollIndicators() { + /* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This comment + * suppresses an error when upgrading Flow's support for React. To see the + * error delete this comment and run Flow. */ + this._scrollRef.flashScrollIndicators(); + } + + /** + * Provides a handle to the underlying scroll responder. + * Note that `this._scrollRef` might not be a `ScrollView`, so we + * need to check that it responds to `getScrollResponder` before calling it. + */ + getScrollResponder() { + if (this._scrollRef && this._scrollRef.getScrollResponder) { + return this._scrollRef.getScrollResponder(); + } + } + + getScrollableNode() { + if (this._scrollRef && this._scrollRef.getScrollableNode) { + return this._scrollRef.getScrollableNode(); + } else { + return findNodeHandle(this._scrollRef); + } + } + + setNativeProps(props: Object) { + if (this._scrollRef) { + this._scrollRef.setNativeProps(props); + } + } + + static defaultProps = { + disableVirtualization: false, + horizontal: false, + initialNumToRender: 10, + keyExtractor: (item: Item, index: number) => { + if (item.key != null) { + return item.key; + } + _usedIndexForKey = true; + return String(index); + }, + maxToRenderPerBatch: 10, + onEndReachedThreshold: 2, // multiples of length + scrollEventThrottle: 50, + updateCellsBatchingPeriod: 50, + windowSize: 21, // multiples of length + }; + + static contextTypes = { + virtualizedList: PropTypes.shape({ + horizontal: PropTypes.bool, + }), + }; + + static childContextTypes = { + virtualizedList: PropTypes.shape({ + horizontal: PropTypes.bool, + }), + }; + + getChildContext() { + return { + virtualizedList: { + horizontal: this.props.horizontal, + // TODO: support nested virtualization and onViewableItemsChanged + }, + }; + } + + state: State; + + constructor(props: Props, context: Object) { + super(props, context); + invariant( + !props.onScroll || !props.onScroll.__isNative, + 'Components based on VirtualizedList must be wrapped with Animated.createAnimatedComponent ' + + 'to support native onScroll events with useNativeDriver', + ); + invariant( + !(this._isNestedWithSameOrientation() && props.onViewableItemsChanged), + 'Nesting lists that scroll in the same direction does not support onViewableItemsChanged' + + 'on the inner list.', + ); + + this._fillRateHelper = new FillRateHelper(this._getFrameMetrics); + this._updateCellsToRenderBatcher = new Batchinator( + this._updateCellsToRender, + this.props.updateCellsBatchingPeriod, + ); + + if (this.props.viewabilityConfigCallbackPairs) { + this._viewabilityTuples = this.props.viewabilityConfigCallbackPairs.map( + pair => ({ + viewabilityHelper: new ViewabilityHelper(pair.viewabilityConfig), + onViewableItemsChanged: pair.onViewableItemsChanged, + }), + ); + } else if (this.props.onViewableItemsChanged) { + this._viewabilityTuples.push({ + viewabilityHelper: new ViewabilityHelper(this.props.viewabilityConfig), + onViewableItemsChanged: this.props.onViewableItemsChanged, + }); + } + + this.state = { + first: this.props.initialScrollIndex || 0, + last: + Math.min( + this.props.getItemCount(this.props.data), + (this.props.initialScrollIndex || 0) + this.props.initialNumToRender, + ) - 1, + }; + } + + componentDidMount() { + if (this.props.initialScrollIndex) { + this._initialScrollIndexTimeout = setTimeout( + () => + this.scrollToIndex({ + animated: false, + index: this.props.initialScrollIndex, + }), + 0, + ); + } + } + + componentWillUnmount() { + this._updateViewableItems(null); + this._updateCellsToRenderBatcher.dispose(); + this._viewabilityTuples.forEach(tuple => { + tuple.viewabilityHelper.dispose(); + }); + this._fillRateHelper.deactivateAndFlush(); + clearTimeout(this._initialScrollIndexTimeout); + } + + componentWillReceiveProps(newProps: Props) { + const {data, extraData, getItemCount, maxToRenderPerBatch} = newProps; + // first and last could be stale (e.g. if a new, shorter items props is passed in), so we make + // sure we're rendering a reasonable range here. + this.setState({ + first: Math.max( + 0, + Math.min( + this.state.first, + getItemCount(data) - 1 - maxToRenderPerBatch, + ), + ), + last: Math.max(0, Math.min(this.state.last, getItemCount(data) - 1)), + }); + if (data !== this.props.data || extraData !== this.props.extraData) { + this._hasDataChangedSinceEndReached = true; + + // clear the viewableIndices cache to also trigger + // the onViewableItemsChanged callback with the new data + this._viewabilityTuples.forEach(tuple => { + tuple.viewabilityHelper.resetViewableIndices(); + }); + } + } + + _pushCells( + cells: Array, + stickyHeaderIndices: Array, + stickyIndicesFromProps: Set, + first: number, + last: number, + inversionStyle: ?StyleObj, + ) { + const { + CellRendererComponent, + ItemSeparatorComponent, + data, + getItem, + getItemCount, + horizontal, + keyExtractor, + } = this.props; + const stickyOffset = this.props.ListHeaderComponent ? 1 : 0; + const end = getItemCount(data) - 1; + let prevCellKey; + last = Math.min(end, last); + for (let ii = first; ii <= last; ii++) { + const item = getItem(data, ii); + const key = keyExtractor(item, ii); + if (stickyIndicesFromProps.has(ii + stickyOffset)) { + stickyHeaderIndices.push(cells.length); + } + cells.push( + this._onCellLayout(e, key, ii)} + onUnmount={this._onCellUnmount} + parentProps={this.props} + ref={ref => { + this._cellRefs[key] = ref; + }} + />, + ); + prevCellKey = key; + } + } + + _onUpdateSeparators = (keys: Array, newProps: Object) => { + keys.forEach(key => { + const ref = key != null && this._cellRefs[key]; + ref && ref.updateSeparatorProps(newProps); + }); + }; + + _isVirtualizationDisabled(): boolean { + return ( + this.props.disableVirtualization || this._isNestedWithSameOrientation() + ); + } + + _isNestedWithSameOrientation(): boolean { + const nestedContext = this.context.virtualizedList; + return !!( + nestedContext && !!nestedContext.horizontal === !!this.props.horizontal + ); + } + + render() { + if (__DEV__) { + const flatStyles = flattenStyle(this.props.contentContainerStyle); + warning( + flatStyles == null || flatStyles.flexWrap !== 'wrap', + '`flexWrap: `wrap`` is not supported with the `VirtualizedList` components.' + + 'Consider using `numColumns` with `FlatList` instead.', + ); + } + + const { + ListEmptyComponent, + ListFooterComponent, + ListHeaderComponent, + } = this.props; + const {data, horizontal} = this.props; + const isVirtualizationDisabled = this._isVirtualizationDisabled(); + const inversionStyle = this.props.inverted + ? this.props.horizontal + ? styles.horizontallyInverted + : styles.verticallyInverted + : null; + const cells = []; + const stickyIndicesFromProps = new Set(this.props.stickyHeaderIndices); + const stickyHeaderIndices = []; + if (ListHeaderComponent) { + if (stickyIndicesFromProps.has(0)) { + stickyHeaderIndices.push(0); + } + const element = React.isValidElement(ListHeaderComponent) ? ( + ListHeaderComponent + ) : ( + // $FlowFixMe + + ); + cells.push( + + {element} + , + ); + } + const itemCount = this.props.getItemCount(data); + if (itemCount > 0) { + _usedIndexForKey = false; + const spacerKey = !horizontal ? 'height' : 'width'; + const lastInitialIndex = this.props.initialScrollIndex + ? -1 + : this.props.initialNumToRender - 1; + const {first, last} = this.state; + this._pushCells( + cells, + stickyHeaderIndices, + stickyIndicesFromProps, + 0, + lastInitialIndex, + inversionStyle, + ); + const firstAfterInitial = Math.max(lastInitialIndex + 1, first); + if (!isVirtualizationDisabled && first > lastInitialIndex + 1) { + let insertedStickySpacer = false; + if (stickyIndicesFromProps.size > 0) { + const stickyOffset = ListHeaderComponent ? 1 : 0; + // See if there are any sticky headers in the virtualized space that we need to render. + for (let ii = firstAfterInitial - 1; ii > lastInitialIndex; ii--) { + if (stickyIndicesFromProps.has(ii + stickyOffset)) { + const initBlock = this._getFrameMetricsApprox(lastInitialIndex); + const stickyBlock = this._getFrameMetricsApprox(ii); + const leadSpace = + stickyBlock.offset - (initBlock.offset + initBlock.length); + cells.push( + , + ); + this._pushCells( + cells, + stickyHeaderIndices, + stickyIndicesFromProps, + ii, + ii, + inversionStyle, + ); + const trailSpace = + this._getFrameMetricsApprox(first).offset - + (stickyBlock.offset + stickyBlock.length); + cells.push( + , + ); + insertedStickySpacer = true; + break; + } + } + } + if (!insertedStickySpacer) { + const initBlock = this._getFrameMetricsApprox(lastInitialIndex); + const firstSpace = + this._getFrameMetricsApprox(first).offset - + (initBlock.offset + initBlock.length); + cells.push( + , + ); + } + } + this._pushCells( + cells, + stickyHeaderIndices, + stickyIndicesFromProps, + firstAfterInitial, + last, + inversionStyle, + ); + if (!this._hasWarned.keys && _usedIndexForKey) { + console.warn( + 'VirtualizedList: missing keys for items, make sure to specify a key property on each ' + + 'item or provide a custom keyExtractor.', + ); + this._hasWarned.keys = true; + } + if (!isVirtualizationDisabled && last < itemCount - 1) { + const lastFrame = this._getFrameMetricsApprox(last); + // Without getItemLayout, we limit our tail spacer to the _highestMeasuredFrameIndex to + // prevent the user for hyperscrolling into un-measured area because otherwise content will + // likely jump around as it renders in above the viewport. + const end = this.props.getItemLayout + ? itemCount - 1 + : Math.min(itemCount - 1, this._highestMeasuredFrameIndex); + const endFrame = this._getFrameMetricsApprox(end); + const tailSpacerLength = + endFrame.offset + + endFrame.length - + (lastFrame.offset + lastFrame.length); + cells.push( + , + ); + } + } else if (ListEmptyComponent) { + const element = React.isValidElement(ListEmptyComponent) ? ( + ListEmptyComponent + ) : ( + // $FlowFixMe + + ); + cells.push( + + {element} + , + ); + } + if (ListFooterComponent) { + const element = React.isValidElement(ListFooterComponent) ? ( + ListFooterComponent + ) : ( + // $FlowFixMe + + ); + cells.push( + + {element} + , + ); + } + const scrollProps = { + ...this.props, + onContentSizeChange: this._onContentSizeChange, + onLayout: this._onLayout, + onScroll: this._onScroll, + onScrollBeginDrag: this._onScrollBeginDrag, + onScrollEndDrag: this._onScrollEndDrag, + onMomentumScrollEnd: this._onMomentumScrollEnd, + scrollEventThrottle: this.props.scrollEventThrottle, // TODO: Android support + stickyHeaderIndices, + }; + if (inversionStyle) { + scrollProps.style = [inversionStyle, this.props.style]; + } + const ret = React.cloneElement( + (this.props.renderScrollComponent || this._defaultRenderScrollComponent)( + scrollProps, + ), + { + ref: this._captureScrollRef, + }, + cells, + ); + if (this.props.debug) { + return ( + + {ret} + {this._renderDebugOverlay()} + + ); + } else { + return ret; + } + } + + componentDidUpdate() { + this._scheduleCellsToRenderUpdate(); + } + + _averageCellLength = 0; + _cellRefs = {}; + _hasDataChangedSinceEndReached = true; + _hasWarned = {}; + _highestMeasuredFrameIndex = 0; + _headerLength = 0; + _initialScrollIndexTimeout = 0; + _fillRateHelper: FillRateHelper; + _frames = {}; + _footerLength = 0; + _scrollMetrics = { + contentLength: 0, + dOffset: 0, + dt: 10, + offset: 0, + timestamp: 0, + velocity: 0, + visibleLength: 0, + }; + _scrollRef = (null: any); + _sentEndForContentLength = 0; + _totalCellLength = 0; + _totalCellsMeasured = 0; + _updateCellsToRenderBatcher: Batchinator; + _viewabilityTuples: Array = []; + + _captureScrollRef = ref => { + this._scrollRef = ref; + }; + + _computeBlankness() { + this._fillRateHelper.computeBlankness( + this.props, + this.state, + this._scrollMetrics, + ); + } + + _defaultRenderScrollComponent = props => { + if (this._isNestedWithSameOrientation()) { + return ; + } else if (props.onRefresh) { + invariant( + typeof props.refreshing === 'boolean', + '`refreshing` prop must be set as a boolean in order to use `onRefresh`, but got `' + + JSON.stringify(props.refreshing) + + '`', + ); + return ( + /* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This + * comment suppresses an error when upgrading Flow's support for React. + * To see the error delete this comment and run Flow. */ + =0.53.0 site=react_native_fb,react_native_oss) This + * comment suppresses an error when upgrading Flow's support for + * React. To see the error delete this comment and run Flow. */ + + } + /> + ); + } else { + /* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This + * comment suppresses an error when upgrading Flow's support for React. + * To see the error delete this comment and run Flow. */ + return ; + } + }; + + _onCellLayout(e, cellKey, index) { + const layout = e.nativeEvent.layout; + const next = { + offset: this._selectOffset(layout), + length: this._selectLength(layout), + index, + inLayout: true, + }; + const curr = this._frames[cellKey]; + if ( + !curr || + next.offset !== curr.offset || + next.length !== curr.length || + index !== curr.index + ) { + this._totalCellLength += next.length - (curr ? curr.length : 0); + this._totalCellsMeasured += curr ? 0 : 1; + this._averageCellLength = + this._totalCellLength / this._totalCellsMeasured; + this._frames[cellKey] = next; + this._highestMeasuredFrameIndex = Math.max( + this._highestMeasuredFrameIndex, + index, + ); + this._scheduleCellsToRenderUpdate(); + } else { + this._frames[cellKey].inLayout = true; + } + this._computeBlankness(); + } + + _onCellUnmount = (cellKey: string) => { + const curr = this._frames[cellKey]; + if (curr) { + this._frames[cellKey] = {...curr, inLayout: false}; + } + }; + + _onLayout = (e: Object) => { + this._scrollMetrics.visibleLength = this._selectLength( + e.nativeEvent.layout, + ); + this.props.onLayout && this.props.onLayout(e); + this._scheduleCellsToRenderUpdate(); + this._maybeCallOnEndReached(); + }; + + _onLayoutEmpty = e => { + this.props.onLayout && this.props.onLayout(e); + }; + + _onLayoutFooter = e => { + this._footerLength = this._selectLength(e.nativeEvent.layout); + }; + + _onLayoutHeader = e => { + this._headerLength = this._selectLength(e.nativeEvent.layout); + }; + + _renderDebugOverlay() { + const normalize = + this._scrollMetrics.visibleLength / this._scrollMetrics.contentLength; + const framesInLayout = []; + const itemCount = this.props.getItemCount(this.props.data); + for (let ii = 0; ii < itemCount; ii++) { + const frame = this._getFrameMetricsApprox(ii); + if (frame.inLayout) { + framesInLayout.push(frame); + } + } + const windowTop = this._getFrameMetricsApprox(this.state.first).offset; + const frameLast = this._getFrameMetricsApprox(this.state.last); + const windowLen = frameLast.offset + frameLast.length - windowTop; + const visTop = this._scrollMetrics.offset; + const visLen = this._scrollMetrics.visibleLength; + const baseStyle = {position: 'absolute', top: 0, right: 0}; + return ( + + {framesInLayout.map((f, ii) => ( + + ))} + + + + ); + } + + _selectLength(metrics: {height: number, width: number}): number { + return !this.props.horizontal ? metrics.height : metrics.width; + } + + _selectOffset(metrics: {x: number, y: number}): number { + return !this.props.horizontal ? metrics.y : metrics.x; + } + + _maybeCallOnEndReached() { + const { + data, + getItemCount, + onEndReached, + onEndReachedThreshold, + } = this.props; + const {contentLength, visibleLength, offset} = this._scrollMetrics; + const distanceFromEnd = contentLength - visibleLength - offset; + if ( + onEndReached && + this.state.last === getItemCount(data) - 1 && + distanceFromEnd < onEndReachedThreshold * visibleLength && + (this._hasDataChangedSinceEndReached || + this._scrollMetrics.contentLength !== this._sentEndForContentLength) + ) { + // Only call onEndReached once for a given dataset + content length. + this._hasDataChangedSinceEndReached = false; + this._sentEndForContentLength = this._scrollMetrics.contentLength; + onEndReached({distanceFromEnd}); + } + } + + _onContentSizeChange = (width: number, height: number) => { + if (this.props.onContentSizeChange) { + this.props.onContentSizeChange(width, height); + } + this._scrollMetrics.contentLength = this._selectLength({height, width}); + this._scheduleCellsToRenderUpdate(); + this._maybeCallOnEndReached(); + }; + + _onScroll = (e: Object) => { + if (this.props.onScroll) { + this.props.onScroll(e); + } + const timestamp = e.timeStamp; + let isVertical = !this.props.horizontal; + let target = ReactDOM.findDOMNode(this._scrollRef); + const visibleLength = target[ + isVertical ? 'offsetHeight' : 'offsetWidth' + ]; + const contentLength = target[ + isVertical ? 'scrollHeight' : 'scrollWidth' + ]; + const offset = target[ + isVertical ? 'scrollTop' : 'scrollLeft' + ]; + const dt = this._scrollMetrics.timestamp + ? Math.max(1, timestamp - this._scrollMetrics.timestamp) + : 1; + if ( + dt > 500 && + this._scrollMetrics.dt > 500 && + contentLength > 5 * visibleLength && + !this._hasWarned.perf + ) { + infoLog( + 'VirtualizedList: You have a large list that is slow to update - make sure your ' + + 'renderItem function renders components that follow React performance best practices ' + + 'like PureComponent, shouldComponentUpdate, etc.', + {dt, prevDt: this._scrollMetrics.dt, contentLength}, + ); + this._hasWarned.perf = true; + } + const dOffset = offset - this._scrollMetrics.offset; + const velocity = dOffset / dt; + this._scrollMetrics = { + contentLength, + dt, + dOffset, + offset, + timestamp, + velocity, + visibleLength, + }; + this._updateViewableItems(this.props.data); + if (!this.props) { + return; + } + this._maybeCallOnEndReached(); + if (velocity !== 0) { + this._fillRateHelper.activate(); + } + this._computeBlankness(); + this._scheduleCellsToRenderUpdate(); + }; + + _scheduleCellsToRenderUpdate() { + const {first, last} = this.state; + const {offset, visibleLength, velocity} = this._scrollMetrics; + const itemCount = this.props.getItemCount(this.props.data); + let hiPri = false; + if (first > 0 || last < itemCount - 1) { + const distTop = offset - this._getFrameMetricsApprox(first).offset; + const distBottom = + this._getFrameMetricsApprox(last).offset - (offset + visibleLength); + const scrollingThreshold = + this.props.onEndReachedThreshold * visibleLength / 2; + hiPri = + Math.min(distTop, distBottom) < 0 || + (velocity < -2 && distTop < scrollingThreshold) || + (velocity > 2 && distBottom < scrollingThreshold); + } + // Only trigger high-priority updates if we've actually rendered cells, + // and with that size estimate, accurately compute how many cells we should render. + // Otherwise, it would just render as many cells as it can (of zero dimension), + // each time through attempting to render more (limited by maxToRenderPerBatch), + // starving the renderer from actually laying out the objects and computing _averageCellLength. + if (hiPri && this._averageCellLength) { + // Don't worry about interactions when scrolling quickly; focus on filling content as fast + // as possible. + this._updateCellsToRenderBatcher.dispose({abort: true}); + this._updateCellsToRender(); + return; + } else { + this._updateCellsToRenderBatcher.schedule(); + } + } + + _onScrollBeginDrag = (e): void => { + this._viewabilityTuples.forEach(tuple => { + tuple.viewabilityHelper.recordInteraction(); + }); + this.props.onScrollBeginDrag && this.props.onScrollBeginDrag(e); + }; + + _onScrollEndDrag = (e): void => { + const {velocity} = e.nativeEvent; + if (velocity) { + this._scrollMetrics.velocity = this._selectOffset(velocity); + } + this._computeBlankness(); + this.props.onScrollEndDrag && this.props.onScrollEndDrag(e); + }; + + _onMomentumScrollEnd = (e): void => { + this._scrollMetrics.velocity = 0; + this._computeBlankness(); + this.props.onMomentumScrollEnd && this.props.onMomentumScrollEnd(e); + }; + + _updateCellsToRender = () => { + const {data, getItemCount, onEndReachedThreshold} = this.props; + const isVirtualizationDisabled = this._isVirtualizationDisabled(); + this._updateViewableItems(data); + if (!data) { + return; + } + this.setState(state => { + let newState; + if (!isVirtualizationDisabled) { + // If we run this with bogus data, we'll force-render window {first: 0, last: 0}, + // and wipe out the initialNumToRender rendered elements. + // So let's wait until the scroll view metrics have been set up. And until then, + // we will trust the initialNumToRender suggestion + if (this._scrollMetrics.visibleLength) { + // If we have a non-zero initialScrollIndex and run this before we've scrolled, + // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex. + // So let's wait until we've scrolled the view to the right place. And until then, + // we will trust the initialScrollIndex suggestion. + if (!this.props.initialScrollIndex || this._scrollMetrics.offset) { + newState = computeWindowedRenderLimits( + this.props, + state, + this._getFrameMetricsApprox, + this._scrollMetrics, + ); + } + } + } else { + const {contentLength, offset, visibleLength} = this._scrollMetrics; + const distanceFromEnd = contentLength - visibleLength - offset; + const renderAhead = + distanceFromEnd < onEndReachedThreshold * visibleLength + ? this.props.maxToRenderPerBatch + : 0; + newState = { + first: 0, + last: Math.min(state.last + renderAhead, getItemCount(data) - 1), + }; + } + return newState; + }); + }; + + _createViewToken = (index: number, isViewable: boolean) => { + const {data, getItem, keyExtractor} = this.props; + const item = getItem(data, index); + return {index, item, key: keyExtractor(item, index), isViewable}; + }; + + _getFrameMetricsApprox = ( + index: number, + ): {length: number, offset: number} => { + const frame = this._getFrameMetrics(index); + if (frame && frame.index === index) { + // check for invalid frames due to row re-ordering + return frame; + } else { + const {getItemLayout} = this.props; + invariant( + !getItemLayout, + 'Should not have to estimate frames when a measurement metrics function is provided', + ); + return { + length: this._averageCellLength, + offset: this._averageCellLength * index, + }; + } + }; + + _getFrameMetrics = ( + index: number, + ): ?{ + length: number, + offset: number, + index: number, + inLayout?: boolean, + } => { + const { + data, + getItem, + getItemCount, + getItemLayout, + keyExtractor, + } = this.props; + invariant( + getItemCount(data) > index, + 'Tried to get frame for out of range index ' + index, + ); + const item = getItem(data, index); + let frame = item && this._frames[keyExtractor(item, index)]; + if (!frame || frame.index !== index) { + if (getItemLayout) { + frame = getItemLayout(data, index); + if (__DEV__) { + const frameType = PropTypes.shape({ + length: PropTypes.number.isRequired, + offset: PropTypes.number.isRequired, + index: PropTypes.number.isRequired, + }).isRequired; + PropTypes.checkPropTypes( + {frame: frameType}, + {frame}, + 'frame', + 'VirtualizedList.getItemLayout', + ); + } + } + } + return frame; + }; + + _updateViewableItems(data: any) { + const {getItemCount} = this.props; + + this._viewabilityTuples.forEach(tuple => { + tuple.viewabilityHelper.onUpdate( + getItemCount(data), + this._scrollMetrics.offset, + this._scrollMetrics.visibleLength, + this._getFrameMetrics, + this._createViewToken, + tuple.onViewableItemsChanged, + this.state, + ); + }); + } +} + +class CellRenderer extends React.Component< + { + CellRendererComponent?: ?React.ComponentType, + ItemSeparatorComponent: ?React.ComponentType<*>, + cellKey: string, + fillRateHelper: FillRateHelper, + horizontal: ?boolean, + index: number, + inversionStyle: ?StyleObj, + item: Item, + onLayout: (event: Object) => void, // This is extracted by ScrollViewStickyHeader + onUnmount: (cellKey: string) => void, + onUpdateSeparators: (cellKeys: Array, props: Object) => void, + parentProps: { + getItemLayout?: ?Function, + renderItem: renderItemType, + }, + prevCellKey: ?string, + }, + $FlowFixMeState, +> { + state = { + separatorProps: { + highlighted: false, + leadingItem: this.props.item, + }, + }; + + // TODO: consider factoring separator stuff out of VirtualizedList into FlatList since it's not + // reused by SectionList and we can keep VirtualizedList simpler. + _separators = { + highlight: () => { + const {cellKey, prevCellKey} = this.props; + this.props.onUpdateSeparators([cellKey, prevCellKey], { + highlighted: true, + }); + }, + unhighlight: () => { + const {cellKey, prevCellKey} = this.props; + this.props.onUpdateSeparators([cellKey, prevCellKey], { + highlighted: false, + }); + }, + updateProps: (select: 'leading' | 'trailing', newProps: Object) => { + const {cellKey, prevCellKey} = this.props; + this.props.onUpdateSeparators( + [select === 'leading' ? prevCellKey : cellKey], + newProps, + ); + }, + }; + + updateSeparatorProps(newProps: Object) { + this.setState(state => ({ + separatorProps: {...state.separatorProps, ...newProps}, + })); + } + + componentWillUnmount() { + this.props.onUnmount(this.props.cellKey); + } + + render() { + const { + CellRendererComponent, + ItemSeparatorComponent, + fillRateHelper, + horizontal, + item, + index, + inversionStyle, + parentProps, + } = this.props; + const {renderItem, getItemLayout} = parentProps; + invariant(renderItem, 'no renderItem!'); + const element = renderItem({ + item, + index, + separators: this._separators, + }); + const onLayout = + getItemLayout && !parentProps.debug && !fillRateHelper.enabled() + ? undefined + : this.props.onLayout; + // NOTE: that when this is a sticky header, `onLayout` will get automatically extracted and + // called explicitly by `ScrollViewStickyHeader`. + const itemSeparator = ItemSeparatorComponent && ( + + ); + const cellStyle = inversionStyle + ? horizontal + ? [{flexDirection: 'row-reverse'}, inversionStyle] + : [{flexDirection: 'column-reverse'}, inversionStyle] + : horizontal ? [{flexDirection: 'row'}, inversionStyle] : inversionStyle; + if (!CellRendererComponent) { + return ( + + {element} + {itemSeparator} + + ); + } + return ( + + {element} + {itemSeparator} + + ); + } +} + +const styles = StyleSheet.create({ + verticallyInverted: { + transform: [{scaleY: -1}], + }, + horizontallyInverted: { + transform: [{scaleX: -1}], + }, +}); + +export default VirtualizedList; diff --git a/Libraries/Lists/VirtualizedSectionList.js b/Libraries/Lists/VirtualizedSectionList.js new file mode 100644 index 0000000..016e9a6 --- /dev/null +++ b/Libraries/Lists/VirtualizedSectionList.js @@ -0,0 +1,526 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule VirtualizedSectionList + * @flow + * @format + */ +'use strict'; + +import React from 'react'; +import View from 'ReactView'; +import VirtualizedList from 'ReactVirtualizedList'; + +const invariant = require('fbjs/lib/invariant'); + +import type {ViewToken} from 'ViewabilityHelper'; +import type {Props as VirtualizedListProps} from 'VirtualizedList'; + +type Item = any; +type SectionItem = any; + +type SectionBase = { + // Must be provided directly on each section. + data: $ReadOnlyArray, + key?: string, + + // Optional props will override list-wide props just for this section. + renderItem?: ?({ + item: SectionItem, + index: number, + section: SectionBase, + separators: { + highlight: () => void, + unhighlight: () => void, + updateProps: (select: 'leading' | 'trailing', newProps: Object) => void, + }, + }) => ?React.Element, + ItemSeparatorComponent?: ?React.ComponentType<*>, + keyExtractor?: (item: SectionItem, index: ?number) => string, + + // TODO: support more optional/override props + // FooterComponent?: ?ReactClass<*>, + // HeaderComponent?: ?ReactClass<*>, + // onViewableItemsChanged?: ({viewableItems: Array, changed: Array}) => void, +}; + +type RequiredProps = { + sections: $ReadOnlyArray, +}; + +type OptionalProps = { + /** + * Rendered after the last item in the last section. + */ + ListFooterComponent?: ?(React.ComponentType<*> | React.Element), + /** + * Rendered at the very beginning of the list. + */ + ListHeaderComponent?: ?(React.ComponentType<*> | React.Element), + /** + * Default renderer for every item in every section. + */ + renderItem?: (info: { + item: Item, + index: number, + section: SectionT, + separators: { + highlight: () => void, + unhighlight: () => void, + updateProps: (select: 'leading' | 'trailing', newProps: Object) => void, + }, + }) => ?React.Element, + /** + * Rendered at the top of each section. + */ + renderSectionHeader?: ?({section: SectionT}) => ?React.Element, + /** + * Rendered at the bottom of each section. + */ + renderSectionFooter?: ?({section: SectionT}) => ?React.Element, + /** + * Rendered at the bottom of every Section, except the very last one, in place of the normal + * ItemSeparatorComponent. + */ + SectionSeparatorComponent?: ?React.ComponentType<*>, + /** + * Rendered at the bottom of every Item except the very last one in the last section. + */ + ItemSeparatorComponent?: ?React.ComponentType<*>, + /** + * Warning: Virtualization can drastically improve memory consumption for long lists, but trashes + * the state of items when they scroll out of the render window, so make sure all relavent data is + * stored outside of the recursive `renderItem` instance tree. + */ + enableVirtualization?: ?boolean, + keyExtractor: (item: Item, index: number) => string, + onEndReached?: ?({distanceFromEnd: number}) => void, + /** + * If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. Make + * sure to also set the `refreshing` prop correctly. + */ + onRefresh?: ?Function, + /** + * Called when the viewability of rows changes, as defined by the + * `viewabilityConfig` prop. + */ + onViewableItemsChanged?: ?({ + viewableItems: Array, + changed: Array, + }) => void, + /** + * Set this true while waiting for new data from a refresh. + */ + refreshing?: ?boolean, +}; + +export type Props = RequiredProps & + OptionalProps & + VirtualizedListProps; + +type DefaultProps = typeof VirtualizedList.defaultProps & { + data: $ReadOnlyArray, +}; +type State = {childProps: VirtualizedListProps}; + +/** + * Right now this just flattens everything into one list and uses VirtualizedList under the + * hood. The only operation that might not scale well is concatting the data arrays of all the + * sections when new props are received, which should be plenty fast for up to ~10,000 items. + */ +class VirtualizedSectionList extends React.PureComponent< + Props, + State, +> { + props: Props; + + state: State; + + static defaultProps: DefaultProps = { + ...VirtualizedList.defaultProps, + data: [], + }; + + scrollToLocation(params: { + animated?: ?boolean, + itemIndex: number, + sectionIndex: number, + viewPosition?: number, + }) { + let index = params.itemIndex + 1; + for (let ii = 0; ii < params.sectionIndex; ii++) { + index += this.props.sections[ii].data.length + 2; + } + const toIndexParams = { + ...params, + index, + }; + this._listRef.scrollToIndex(toIndexParams); + } + + getListRef(): VirtualizedList { + return this._listRef; + } + + _keyExtractor = (item: Item, index: number) => { + const info = this._subExtractor(index); + return (info && info.key) || String(index); + }; + + _subExtractor( + index: number, + ): ?{ + section: SectionT, + key: string, // Key of the section or combined key for section + item + index: ?number, // Relative index within the section + header?: ?boolean, // True if this is the section header + leadingItem?: ?Item, + leadingSection?: ?SectionT, + trailingItem?: ?Item, + trailingSection?: ?SectionT, + } { + let itemIndex = index; + const defaultKeyExtractor = this.props.keyExtractor; + for (let ii = 0; ii < this.props.sections.length; ii++) { + const section = this.props.sections[ii]; + const key = section.key || String(ii); + itemIndex -= 1; // The section adds an item for the header + if (itemIndex >= section.data.length + 1) { + itemIndex -= section.data.length + 1; // The section adds an item for the footer. + } else if (itemIndex === -1) { + return { + section, + key: key + ':header', + index: null, + header: true, + trailingSection: this.props.sections[ii + 1], + }; + } else if (itemIndex === section.data.length) { + return { + section, + key: key + ':footer', + index: null, + header: false, + trailingSection: this.props.sections[ii + 1], + }; + } else { + const keyExtractor = section.keyExtractor || defaultKeyExtractor; + return { + section, + key: key + ':' + keyExtractor(section.data[itemIndex], itemIndex), + index: itemIndex, + leadingItem: section.data[itemIndex - 1], + leadingSection: this.props.sections[ii - 1], + trailingItem: section.data[itemIndex + 1], + trailingSection: this.props.sections[ii + 1], + }; + } + } + } + + _convertViewable = (viewable: ViewToken): ?ViewToken => { + invariant(viewable.index != null, 'Received a broken ViewToken'); + const info = this._subExtractor(viewable.index); + if (!info) { + return null; + } + const keyExtractor = info.section.keyExtractor || this.props.keyExtractor; + return { + ...viewable, + index: info.index, + key: keyExtractor(viewable.item, info.index), + section: info.section, + }; + }; + + _onViewableItemsChanged = ({ + viewableItems, + changed, + }: { + viewableItems: Array, + changed: Array, + }) => { + if (this.props.onViewableItemsChanged) { + this.props.onViewableItemsChanged({ + viewableItems: viewableItems + .map(this._convertViewable, this) + .filter(Boolean), + changed: changed.map(this._convertViewable, this).filter(Boolean), + }); + } + }; + + _renderItem = ({item, index}: {item: Item, index: number}) => { + const info = this._subExtractor(index); + if (!info) { + return null; + } + const infoIndex = info.index; + if (infoIndex == null) { + const {section} = info; + if (info.header === true) { + const {renderSectionHeader} = this.props; + return renderSectionHeader ? renderSectionHeader({section}) : null; + } else { + const {renderSectionFooter} = this.props; + return renderSectionFooter ? renderSectionFooter({section}) : null; + } + } else { + const renderItem = info.section.renderItem || this.props.renderItem; + const SeparatorComponent = this._getSeparatorComponent(index, info); + invariant(renderItem, 'no renderItem!'); + return ( + { + this._cellRefs[info.key] = ref; + }} + renderItem={renderItem} + section={info.section} + trailingItem={info.trailingItem} + trailingSection={info.trailingSection} + /> + ); + } + }; + + _onUpdateSeparator = (key: string, newProps: Object) => { + const ref = this._cellRefs[key]; + ref && ref.updateSeparatorProps(newProps); + }; + + _getSeparatorComponent( + index: number, + info?: ?Object, + ): ?React.ComponentType<*> { + info = info || this._subExtractor(index); + if (!info) { + return null; + } + const ItemSeparatorComponent = + info.section.ItemSeparatorComponent || this.props.ItemSeparatorComponent; + const {SectionSeparatorComponent} = this.props; + const isLastItemInList = index === this.state.childProps.getItemCount() - 1; + const isLastItemInSection = info.index === info.section.data.length - 1; + if (SectionSeparatorComponent && isLastItemInSection) { + return SectionSeparatorComponent; + } + if (ItemSeparatorComponent && !isLastItemInSection && !isLastItemInList) { + return ItemSeparatorComponent; + } + return null; + } + + _computeState(props: Props): State { + const offset = props.ListHeaderComponent ? 1 : 0; + const stickyHeaderIndices = []; + const itemCount = props.sections.reduce((v, section) => { + stickyHeaderIndices.push(v + offset); + return v + section.data.length + 2; // Add two for the section header and footer. + }, 0); + + return { + childProps: { + ...props, + renderItem: this._renderItem, + ItemSeparatorComponent: undefined, // Rendered with renderItem + data: props.sections, + getItemCount: () => itemCount, + getItem, + keyExtractor: this._keyExtractor, + onViewableItemsChanged: props.onViewableItemsChanged + ? this._onViewableItemsChanged + : undefined, + stickyHeaderIndices: props.stickySectionHeadersEnabled + ? stickyHeaderIndices + : undefined, + }, + }; + } + + constructor(props: Props, context: Object) { + super(props, context); + this.state = this._computeState(props); + } + + componentWillReceiveProps(nextProps: Props) { + this.setState(this._computeState(nextProps)); + } + + render() { + return ( + + ); + } + + _cellRefs = {}; + _listRef: VirtualizedList; + _captureRef = ref => { + /* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This comment + * suppresses an error when upgrading Flow's support for React. To see the + * error delete this comment and run Flow. */ + this._listRef = ref; + }; +} + +type ItemWithSeparatorProps = { + LeadingSeparatorComponent: ?React.ComponentType<*>, + SeparatorComponent: ?React.ComponentType<*>, + cellKey: string, + index: number, + item: Item, + onUpdateSeparator: (cellKey: string, newProps: Object) => void, + prevCellKey?: ?string, + renderItem: Function, + section: Object, + leadingItem: ?Item, + leadingSection: ?Object, + trailingItem: ?Item, + trailingSection: ?Object, +}; + +class ItemWithSeparator extends React.Component< + ItemWithSeparatorProps, + $FlowFixMeState, +> { + state = { + separatorProps: { + highlighted: false, + leadingItem: this.props.item, + leadingSection: this.props.leadingSection, + section: this.props.section, + trailingItem: this.props.trailingItem, + trailingSection: this.props.trailingSection, + }, + leadingSeparatorProps: { + highlighted: false, + leadingItem: this.props.leadingItem, + leadingSection: this.props.leadingSection, + section: this.props.section, + trailingItem: this.props.item, + trailingSection: this.props.trailingSection, + }, + }; + + _separators = { + highlight: () => { + ['leading', 'trailing'].forEach(s => + this._separators.updateProps(s, {highlighted: true}), + ); + }, + unhighlight: () => { + ['leading', 'trailing'].forEach(s => + this._separators.updateProps(s, {highlighted: false}), + ); + }, + updateProps: (select: 'leading' | 'trailing', newProps: Object) => { + const {LeadingSeparatorComponent, cellKey, prevCellKey} = this.props; + if (select === 'leading' && LeadingSeparatorComponent) { + this.setState(state => ({ + leadingSeparatorProps: {...state.leadingSeparatorProps, ...newProps}, + })); + } else { + this.props.onUpdateSeparator( + (select === 'leading' && prevCellKey) || cellKey, + newProps, + ); + } + }, + }; + + componentWillReceiveProps(props: ItemWithSeparatorProps) { + this.setState(state => ({ + separatorProps: { + ...this.state.separatorProps, + leadingItem: props.item, + leadingSection: props.leadingSection, + section: props.section, + trailingItem: props.trailingItem, + trailingSection: props.trailingSection, + }, + leadingSeparatorProps: { + ...this.state.leadingSeparatorProps, + leadingItem: props.leadingItem, + leadingSection: props.leadingSection, + section: props.section, + trailingItem: props.item, + trailingSection: props.trailingSection, + }, + })); + } + + updateSeparatorProps(newProps: Object) { + this.setState(state => ({ + separatorProps: {...state.separatorProps, ...newProps}, + })); + } + + render() { + const { + LeadingSeparatorComponent, + SeparatorComponent, + item, + index, + section, + } = this.props; + const element = this.props.renderItem({ + item, + index, + section, + separators: this._separators, + }); + const leadingSeparator = LeadingSeparatorComponent && ( + + ); + const separator = SeparatorComponent && ( + + ); + return leadingSeparator || separator ? ( + + {leadingSeparator} + {element} + {separator} + + ) : ( + element + ); + } +} + +function getItem(sections: ?$ReadOnlyArray, index: number): ?Item { + if (!sections) { + return null; + } + let itemIdx = index - 1; + for (let ii = 0; ii < sections.length; ii++) { + if (itemIdx === -1 || itemIdx === sections[ii].data.length) { + // We intend for there to be overflow by one on both ends of the list. + // This will be for headers and footers. When returning a header or footer + // item the section itself is the item. + return sections[ii]; + } else if (itemIdx < sections[ii].data.length) { + // If we are in the bounds of the list's data then return the item. + return sections[ii].data[itemIdx]; + } else { + itemIdx -= sections[ii].data.length + 2; // Add two for the header and footer + } + } + return null; +} + +module.exports = VirtualizedSectionList; diff --git a/Libraries/Modal/Modal.web.js b/Libraries/Modal/Modal.web.js index b98fcfa..82b8a01 100644 --- a/Libraries/Modal/Modal.web.js +++ b/Libraries/Modal/Modal.web.js @@ -6,7 +6,8 @@ */ 'use strict'; -import React, { PropTypes, Component } from 'react'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; import StyleSheet from 'ReactStyleSheet'; import View from 'ReactView'; diff --git a/Libraries/Navigator/Navigator.web.js b/Libraries/Navigator/Navigator.web.js index 3228139..e0d5912 100644 --- a/Libraries/Navigator/Navigator.web.js +++ b/Libraries/Navigator/Navigator.web.js @@ -9,7 +9,8 @@ /* eslint-disable no-extra-boolean-cast*/ 'use strict'; -import React, { PropTypes } from 'react'; +import React, {Component} from 'react'; +import PropTypes from 'prop-types'; import Dimensions from 'ReactDimensions'; import InteractionMixin from 'ReactInteractionMixin'; import Map from 'core-js/library/fn/map'; @@ -21,6 +22,8 @@ import PanResponder from 'ReactPanResponder'; import StyleSheet from 'ReactStyleSheet'; import Subscribable from './polyfills/Subscribable'; import TimerMixin from 'react-timer-mixin'; +import mixin from 'react-mixin'; +import autobind from 'autobind-decorator'; import View from 'ReactView'; import clamp from './polyfills/clamp'; import flattenStyle from 'ReactFlattenStyle'; @@ -165,9 +168,9 @@ const GESTURE_ACTIONS = [ * other scene * */ -let Navigator = React.createClass({ +class Navigator extends Component { - propTypes: { + static propTypes = { /** * Optional function that allows configuration about scene animations and * gestures. Will be invoked with the route and should return a scene @@ -237,42 +240,37 @@ let Navigator = React.createClass({ * Styles to apply to the container of each scene */ sceneStyle: View.propTypes.style, - }, - - statics: { - BreadcrumbNavigationBar: NavigatorBreadcrumbNavigationBar, - NavigationBar: NavigatorNavigationBar, - SceneConfigs: NavigatorSceneConfigs, - }, + }; - mixins: [TimerMixin, InteractionMixin, Subscribable.Mixin], + static BreadcrumbNavigationBar = NavigatorBreadcrumbNavigationBar; + static NavigationBar = NavigatorNavigationBar; + static SceneConfigs = NavigatorSceneConfigs; - getDefaultProps: function() { - return { - configureScene: () => NavigatorSceneConfigs.PushFromRight, - sceneStyle: styles.defaultSceneStyle, - }; - }, + static defaultProps = { + configureScene: () => NavigatorSceneConfigs.PushFromRight, + sceneStyle: styles.defaultSceneStyle, + }; - getInitialState: function() { + constructor(props) { + super(props); this._renderedSceneMap = new Map(); - let routeStack = this.props.initialRouteStack || [this.props.initialRoute]; + let routeStack = props.initialRouteStack || [props.initialRoute]; invariant( routeStack.length >= 1, 'Navigator requires props.initialRoute or props.initialRouteStack.' ); let initialRouteIndex = routeStack.length - 1; - if (this.props.initialRoute) { - initialRouteIndex = routeStack.indexOf(this.props.initialRoute); + if (props.initialRoute) { + initialRouteIndex = routeStack.indexOf(props.initialRoute); invariant( initialRouteIndex !== -1, 'initialRoute is not in initialRouteStack.' ); } - return { + this.state = { sceneConfigStack: routeStack.map( - (route) => this.props.configureScene(route) + (route) => props.configureScene(route) ), routeStack, presentedIndex: initialRouteIndex, @@ -281,9 +279,9 @@ let Navigator = React.createClass({ pendingGestureProgress: null, transitionQueue: [], }; - }, + } - componentWillMount: function() { + componentWillMount() { // TODO(t7489503): Don't need this once ES6 Class landed. this.__defineGetter__('navigationContext', this._getNavigationContext); @@ -317,9 +315,9 @@ let Navigator = React.createClass({ this._interactionHandle = null; this._emitWillFocus(this.state.routeStack[this.state.presentedIndex]); this.hashChanged = false; - }, + } - componentDidMount: function() { + componentDidMount() { this._handleSpringUpdate(); this._emitDidFocus(this.state.routeStack[this.state.presentedIndex]); @@ -336,9 +334,9 @@ let Navigator = React.createClass({ this.hashChanged = false; } }.bind(this)); - }, + } - componentWillUnmount: function() { + componentWillUnmount() { if (this._navigationContext) { this._navigationContext.dispose(); this._navigationContext = null; @@ -347,7 +345,7 @@ let Navigator = React.createClass({ // When you're finished, stop the listener. _unlisten(); - }, + } /** * @param {RouteStack} nextRouteStack Next route stack to reinitialize. This @@ -356,7 +354,7 @@ let Navigator = React.createClass({ * not animate, immediately replaces and rerenders navigation bar and stack * items. */ - immediatelyResetRouteStack: function(nextRouteStack) { + immediatelyResetRouteStack(nextRouteStack) { let destIndex = nextRouteStack.length - 1; this.setState({ routeStack: nextRouteStack, @@ -370,9 +368,9 @@ let Navigator = React.createClass({ }, () => { this._handleSpringUpdate(); }); - }, + } - _transitionTo: function(destIndex, velocity, jumpSpringTo, cb) { + _transitionTo(destIndex, velocity, jumpSpringTo, cb) { if (destIndex === this.state.presentedIndex) { return; } @@ -405,13 +403,13 @@ let Navigator = React.createClass({ this.spring.getSpringConfig().tension = sceneConfig.springTension; this.spring.setVelocity(velocity || sceneConfig.defaultTransitionVelocity); this.spring.setEndValue(1); - }, + } /** * This happens for each frame of either a gesture or a transition. If both are * happening, we only set values for the transition and the gesture will catch up later */ - _handleSpringUpdate: function() { + _handleSpringUpdate() { // Prioritize handling transition in progress over a gesture: if (this.state.transitionFromIndex != null) { this._transitionBetween( @@ -427,12 +425,12 @@ let Navigator = React.createClass({ this.spring.getCurrentValue() ); } - }, + } /** * This happens at the end of a transition started by transitionTo, and when the spring catches up to a pending gesture */ - _completeTransition: function() { + _completeTransition() { if (this.spring.getCurrentValue() !== 1 && this.spring.getCurrentValue() !== 0) { // The spring has finished catching up to a gesture in progress. Remove the pending progress // and we will be in a normal activeGesture state @@ -478,17 +476,17 @@ let Navigator = React.createClass({ queuedTransition.cb ); } - }, + } - _emitDidFocus: function(route) { + _emitDidFocus(route) { this.navigationContext.emit('didfocus', {route: route}); if (this.props.onDidFocus) { this.props.onDidFocus(route); } - }, + } - _emitWillFocus: function(route) { + _emitWillFocus(route) { this.navigationContext.emit('willfocus', {route: route}); let navBar = this._navBar; @@ -498,12 +496,12 @@ let Navigator = React.createClass({ if (this.props.onWillFocus) { this.props.onWillFocus(route); } - }, + } /** * Hides all scenes that we are not currently on, gesturing to, or transitioning from */ - _hideScenes: function() { + _hideScenes() { let gesturingToIndex = null; if (this.state.activeGesture) { gesturingToIndex = this.state.presentedIndex + this._deltaForGestureAction(this.state.activeGesture); @@ -516,20 +514,20 @@ let Navigator = React.createClass({ } this._disableScene(i); } - }, + } /** * Push a scene off the screen, so that opacity:0 scenes will not block touches sent to the presented scenes */ - _disableScene: function(sceneIndex) { - this.refs['scene_' + sceneIndex] && - this.refs['scene_' + sceneIndex].setNativeProps(SCENE_DISABLED_NATIVE_PROPS); - }, + _disableScene(sceneIndex) { + this._sceneRefs[sceneIndex] && + this._sceneRefs[sceneIndex].setNativeProps(SCENE_DISABLED_NATIVE_PROPS); + } /** * Put the scene back into the state as defined by props.sceneStyle, so transitions can happen normally */ - _enableScene: function(sceneIndex) { + _enableScene(sceneIndex) { // First, determine what the defined styles are for scenes in this navigator let sceneStyle = flattenStyle([styles.baseScene, this.props.sceneStyle]); // Then restore the pointer events and top value for this scene @@ -546,11 +544,11 @@ let Navigator = React.createClass({ // to prevent the enabled scene from flashing over the presented scene enabledSceneNativeProps.style.opacity = 0; } - this.refs['scene_' + sceneIndex] && - this.refs['scene_' + sceneIndex].setNativeProps(enabledSceneNativeProps); - }, + this._sceneRefs[sceneIndex] && + this._sceneRefs[sceneIndex].setNativeProps(enabledSceneNativeProps); + } - _onAnimationStart: function() { + _onAnimationStart() { let fromIndex = this.state.presentedIndex; let toIndex = this.state.presentedIndex; if (this.state.transitionFromIndex != null) { @@ -564,9 +562,9 @@ let Navigator = React.createClass({ if (navBar && navBar.onAnimationStart) { navBar.onAnimationStart(fromIndex, toIndex); } - }, + } - _onAnimationEnd: function() { + _onAnimationEnd() { let max = this.state.routeStack.length - 1; for (let index = 0; index <= max; index++) { this._setRenderSceneToHardwareTextureAndroid(index, false); @@ -576,38 +574,38 @@ let Navigator = React.createClass({ if (navBar && navBar.onAnimationEnd) { navBar.onAnimationEnd(); } - }, + } - _setRenderSceneToHardwareTextureAndroid: function(sceneIndex, shouldRenderToHardwareTexture) { - let viewAtIndex = this.refs['scene_' + sceneIndex]; + _setRenderSceneToHardwareTextureAndroid(sceneIndex, shouldRenderToHardwareTexture) { + let viewAtIndex = this._sceneRefs[sceneIndex]; if (viewAtIndex === null || viewAtIndex === undefined) { return; } viewAtIndex.setNativeProps( {renderToHardwareTextureAndroid: shouldRenderToHardwareTexture}); - }, + } - _handleTouchStart: function() { + _handleTouchStart() { this._eligibleGestures = GESTURE_ACTIONS; - }, + } - _handleMoveShouldSetPanResponder: function(e, gestureState) { + _handleMoveShouldSetPanResponder(e, gestureState) { let sceneConfig = this.state.sceneConfigStack[this.state.presentedIndex]; if (!sceneConfig) { return false; } this._expectingGestureGrant = this._matchGestureAction(this._eligibleGestures, sceneConfig.gestures, gestureState); return !!this._expectingGestureGrant; - }, + } - _doesGestureOverswipe: function(gestureName) { + _doesGestureOverswipe(gestureName) { let wouldOverswipeBack = this.state.presentedIndex <= 0 && (gestureName === 'pop' || gestureName === 'jumpBack'); let wouldOverswipeForward = this.state.presentedIndex >= this.state.routeStack.length - 1 && gestureName === 'jumpForward'; return wouldOverswipeForward || wouldOverswipeBack; - }, + } - _handlePanResponderGrant: function(e, gestureState) { + _handlePanResponderGrant(e, gestureState) { invariant( this._expectingGestureGrant, 'Responder granted unexpectedly.' @@ -615,9 +613,9 @@ let Navigator = React.createClass({ this._attachGesture(this._expectingGestureGrant); this._onAnimationStart(); this._expectingGestureGrant = null; - }, + } - _deltaForGestureAction: function(gestureAction) { + _deltaForGestureAction(gestureAction) { switch (gestureAction) { case 'pop': case 'jumpBack': @@ -628,9 +626,9 @@ let Navigator = React.createClass({ invariant(false, 'Unsupported gesture action ' + gestureAction); return; } - }, + } - _handlePanResponderRelease: function(e, gestureState) { + _handlePanResponderRelease(e, gestureState) { let sceneConfig = this.state.sceneConfigStack[this.state.presentedIndex]; let releaseGestureAction = this.state.activeGesture; if (!releaseGestureAction) { @@ -690,9 +688,9 @@ let Navigator = React.createClass({ ); } this._detachGesture(); - }, + } - _handlePanResponderTerminate: function(e, gestureState) { + _handlePanResponderTerminate(e, gestureState) { if (this.state.activeGesture == null) { return; } @@ -706,21 +704,21 @@ let Navigator = React.createClass({ null, 1 - this.spring.getCurrentValue() ); - }, + } - _attachGesture: function(gestureId) { + _attachGesture(gestureId) { this.state.activeGesture = gestureId; let gesturingToIndex = this.state.presentedIndex + this._deltaForGestureAction(this.state.activeGesture); this._enableScene(gesturingToIndex); - }, + } - _detachGesture: function() { + _detachGesture() { this.state.activeGesture = null; this.state.pendingGestureProgress = null; this._hideScenes(); - }, + } - _handlePanResponderMove: function(e, gestureState) { + _handlePanResponderMove(e, gestureState) { let sceneConfig = this.state.sceneConfigStack[this.state.presentedIndex]; if (this.state.activeGesture) { let gesture = sceneConfig.gestures[this.state.activeGesture]; @@ -730,9 +728,9 @@ let Navigator = React.createClass({ if (matchedGesture) { this._attachGesture(matchedGesture); } - }, + } - _moveAttachedGesture: function(gesture, gestureState) { + _moveAttachedGesture(gesture, gestureState) { let isTravelVertical = gesture.direction === 'top-to-bottom' || gesture.direction === 'bottom-to-top'; let isTravelInverted = gesture.direction === 'right-to-left' || gesture.direction === 'bottom-to-top'; let distance = isTravelVertical ? gestureState.dy : gestureState.dx; @@ -763,9 +761,9 @@ let Navigator = React.createClass({ } else { this.spring.setCurrentValue(nextProgress); } - }, + } - _matchGestureAction: function(eligibleGestures, gestures, gestureState) { + _matchGestureAction(eligibleGestures, gestures, gestureState) { if (!gestures) { return null; } @@ -812,10 +810,10 @@ let Navigator = React.createClass({ } }); return matchedGesture; - }, + } - _transitionSceneStyle: function(fromIndex, toIndex, progress, index) { - let viewAtIndex = this.refs['scene_' + index]; + _transitionSceneStyle(fromIndex, toIndex, progress, index) { + let viewAtIndex = this._sceneRefs[index]; if (viewAtIndex === null || viewAtIndex === undefined) { return; } @@ -835,22 +833,22 @@ let Navigator = React.createClass({ if (didChange) { viewAtIndex.setNativeProps({style: styleToUse}); } - }, + } - _transitionBetween: function(fromIndex, toIndex, progress) { + _transitionBetween(fromIndex, toIndex, progress) { this._transitionSceneStyle(fromIndex, toIndex, progress, fromIndex); this._transitionSceneStyle(fromIndex, toIndex, progress, toIndex); let navBar = this._navBar; if (navBar && navBar.updateProgress && toIndex >= 0 && fromIndex >= 0) { navBar.updateProgress(progress, fromIndex, toIndex); } - }, + } - _handleResponderTerminationRequest: function() { + _handleResponderTerminationRequest() { return false; - }, + } - _getDestIndexWithinBounds: function(n) { + _getDestIndexWithinBounds(n) { let currentIndex = this.state.presentedIndex; let destIndex = currentIndex + n; invariant( @@ -863,9 +861,9 @@ let Navigator = React.createClass({ 'Cannot jump past the last route.' ); return destIndex; - }, + } - _jumpN: function(n) { + _jumpN(n) { let destIndex = this._getDestIndexWithinBounds(n); this._enableScene(destIndex); this._emitWillFocus(this.state.routeStack[destIndex]); @@ -882,26 +880,26 @@ let Navigator = React.createClass({ // __uid should be non-negative __uid = Math.max(__uid + n, 0); } - }, + } - jumpTo: function(route) { + jumpTo(route) { let destIndex = this.state.routeStack.indexOf(route); invariant( destIndex !== -1, 'Cannot jump to route that is not in the route stack' ); this._jumpN(destIndex - this.state.presentedIndex); - }, + } - jumpForward: function() { + jumpForward() { this._jumpN(1); - }, + } - jumpBack: function() { + jumpBack() { this._jumpN(-1); - }, + } - push: function(route) { + push(route) { invariant(!!route, 'Must supply route to push'); let activeLength = this.state.presentedIndex + 1; let activeStack = this.state.routeStack.slice(0, activeLength); @@ -920,9 +918,9 @@ let Navigator = React.createClass({ this._enableScene(destIndex); this._transitionTo(destIndex); }); - }, + } - _popN: function(n) { + _popN(n) { if (n === 0) { return; } @@ -942,9 +940,9 @@ let Navigator = React.createClass({ this._cleanScenesPastIndex(popIndex); } ); - }, + } - pop: function() { + pop() { if (this.state.transitionQueue.length) { // This is the workaround to prevent user from firing multiple `pop()` // calls that may pop the routes beyond the limit. @@ -958,7 +956,7 @@ let Navigator = React.createClass({ if (this.state.presentedIndex > 0) { this._popN(1); } - }, + } /** * Replace a route in the navigation stack. @@ -966,7 +964,7 @@ let Navigator = React.createClass({ * `index` specifies the route in the stack that should be replaced. * If it's negative, it counts from the back. */ - replaceAtIndex: function(route, index, cb) { + replaceAtIndex(route, index, cb) { invariant(!!route, 'Must supply route to replace'); if (index < 0) { index += this.state.routeStack.length; @@ -993,27 +991,27 @@ let Navigator = React.createClass({ } cb && cb(); }); - }, + } /** * Replaces the current scene in the stack. */ - replace: function(route) { + replace(route) { this.replaceAtIndex(route, this.state.presentedIndex); - }, + } /** * Replace the current route's parent. */ - replacePrevious: function(route) { + replacePrevious(route) { this.replaceAtIndex(route, this.state.presentedIndex - 1); - }, + } - popToTop: function() { + popToTop() { this.popToRoute(this.state.routeStack[0]); - }, + } - popToRoute: function(route) { + popToRoute(route) { let indexOfRoute = this.state.routeStack.indexOf(route); invariant( indexOfRoute !== -1, @@ -1021,17 +1019,17 @@ let Navigator = React.createClass({ ); let numToPop = this.state.presentedIndex - indexOfRoute; this._popN(numToPop); - }, + } - replacePreviousAndPop: function(route) { + replacePreviousAndPop(route) { if (this.state.routeStack.length < 2) { return; } this.replacePrevious(route); this.pop(); - }, + } - resetTo: function(route) { + resetTo(route) { invariant(!!route, 'Must supply route to push'); this.replaceAtIndex(route, 0, () => { // Do not use popToRoute here, because race conditions could prevent the @@ -1040,14 +1038,22 @@ let Navigator = React.createClass({ this._popN(this.state.presentedIndex); } }); - }, + } - getCurrentRoutes: function() { + getCurrentRoutes() { // Clone before returning to avoid caller mutating the stack return this.state.routeStack.slice(); - }, + } + + _sceneRefs = {} - _cleanScenesPastIndex: function(index) { + _captureSceneRef(index) { + return ref => { + this._sceneRefs[index] = ref; + } + } + + _cleanScenesPastIndex(index) { let newStackLength = index + 1; // Remove any unneeded rendered routes. if (newStackLength < this.state.routeStack.length) { @@ -1056,9 +1062,9 @@ let Navigator = React.createClass({ routeStack: this.state.routeStack.slice(0, newStackLength), }); } - }, + } - _renderScene: function(route, i) { + _renderScene(route, i) { let disabledSceneStyle = null; let disabledScenePointerEvents = 'auto'; if (i !== this.state.presentedIndex) { @@ -1069,7 +1075,7 @@ let Navigator = React.createClass({ return ( { return (this.state.transitionFromIndex != null) || (this.state.transitionFromIndex != null); }} @@ -1081,9 +1087,9 @@ let Navigator = React.createClass({ )} ); - }, + } - _renderNavigationBar: function() { + _renderNavigationBar() { if (!this.props.navigationBar) { return null; } @@ -1094,9 +1100,9 @@ let Navigator = React.createClass({ navigator: this, navState: this.state, }); - }, + } - render: function() { + render() { let newRenderedSceneMap = new Map(); let scenes = this.state.routeStack.map((route, index) => { let renderedScene; @@ -1124,15 +1130,20 @@ let Navigator = React.createClass({ {this._renderNavigationBar()} ); - }, + } - _getNavigationContext: function() { + _getNavigationContext() { if (!this._navigationContext) { this._navigationContext = new NavigationContext(); } return this._navigationContext; } -}); +} + +mixin.onClass(Navigator, TimerMixin); +mixin.onClass(Navigator, InteractionMixin); +mixin.onClass(Navigator, Subscribable.Mixin); +autobind(Navigator); Navigator.isReactNativeComponent = true; diff --git a/Libraries/Navigator/NavigatorBreadcrumbNavigationBar.js b/Libraries/Navigator/NavigatorBreadcrumbNavigationBar.js index a791d16..e2731e1 100644 --- a/Libraries/Navigator/NavigatorBreadcrumbNavigationBar.js +++ b/Libraries/Navigator/NavigatorBreadcrumbNavigationBar.js @@ -9,7 +9,8 @@ */ 'use strict'; -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; import NavigatorBreadcrumbNavigationBarStyles from 'ReactNavigatorBreadcrumbNavigationBarStyles'; import NavigatorNavigationBarStylesAndroid from 'ReactNavigatorNavigationBarStylesAndroid'; import NavigatorNavigationBarStylesIOS from 'ReactNavigatorNavigationBarStylesIOS'; @@ -70,9 +71,9 @@ class NavigatorBreadcrumbNavigationBar extends Component { titleContentForRoute: PropTypes.func, iconForRoute: PropTypes.func, }), - navState: React.PropTypes.shape({ - routeStack: React.PropTypes.arrayOf(React.PropTypes.object), - presentedIndex: React.PropTypes.number, + navState: PropTypes.shape({ + routeStack: PropTypes.arrayOf(PropTypes.object), + presentedIndex: PropTypes.number, }), style: View.propTypes.style, } @@ -103,18 +104,18 @@ class NavigatorBreadcrumbNavigationBar extends Component { } if (interpolate.Crumb(CRUMB_PROPS[index].style, amount)) { - this._setPropsIfExists('crumb_' + index, CRUMB_PROPS[index]); + this._setPropsIfExists(this._crumbRefs[index], CRUMB_PROPS[index]); } if (interpolate.Icon(ICON_PROPS[index].style, amount)) { - this._setPropsIfExists('icon_' + index, ICON_PROPS[index]); + this._setPropsIfExists(this._iconRefs[index], ICON_PROPS[index]); } if (interpolate.Separator(SEPARATOR_PROPS[index].style, amount)) { - this._setPropsIfExists('separator_' + index, SEPARATOR_PROPS[index]); + this._setPropsIfExists(this._separatorRefs[index], SEPARATOR_PROPS[index]); } if (interpolate.Title(TITLE_PROPS[index].style, amount)) { - this._setPropsIfExists('title_' + index, TITLE_PROPS[index]); + this._setPropsIfExists(this._titleRefs[index], TITLE_PROPS[index]); } - var right = this.refs['right_' + index]; + var right = this._rightRefs[index]; if (right && interpolate.RightItem(RIGHT_BUTTON_PROPS[index].style, amount)) { right.setNativeProps(RIGHT_BUTTON_PROPS[index]) @@ -149,10 +150,10 @@ class NavigatorBreadcrumbNavigationBar extends Component { renderToHardwareTextureAndroid: renderToHardwareTexture, }; - this._setPropsIfExists('icon_' + index, props); - this._setPropsIfExists('separator_' + index, props); - this._setPropsIfExists('title_' + index, props); - this._setPropsIfExists('right_' + index, props); + this._setPropsIfExists(this._iconRefs[index], props); + this._setPropsIfExists(this._separatorRefs[index], props); + this._setPropsIfExists(this._titleRefs[index], props); + this._setPropsIfExists(this._rightRefs[index], props); } componentWillMount() { @@ -177,6 +178,38 @@ class NavigatorBreadcrumbNavigationBar extends Component { ); } + _crumbRefs = {} + _iconRefs = {} + _separatorRefs = {} + _titleRefs = {} + _rightRefs = {} + + _captureCrumbRef(index) { + return ref => { + this._crumbRefs[index] = ref; + } + } + _captureIconRef(index) { + return ref => { + this._iconRefs[index] = ref; + } + } + _captureSeparatorRef(index) { + return ref => { + this._separatorRefs[index] = ref; + } + } + _captureTitleRef(index) { + return ref => { + this._titleRefs[index] = ref; + } + } + _captureRightRef(index) { + return ref => { + this._rightRefs[index] = ref; + } + } + _getBreadcrumb(route, index) { if (this._descriptors.crumb.has(route)) { return this._descriptors.crumb.get(route); @@ -186,11 +219,11 @@ class NavigatorBreadcrumbNavigationBar extends Component { var firstStyles = initStyle(index, navStatePresentedIndex(this.props.navState)); var breadcrumbDescriptor = ( - - + + {navBarRouteMapper.iconForRoute(route, this.props.navigator)} - + {navBarRouteMapper.separatorForRoute(route, this.props.navigator)} @@ -212,7 +245,7 @@ class NavigatorBreadcrumbNavigationBar extends Component { var firstStyles = initStyle(index, navStatePresentedIndex(this.props.navState)); var titleDescriptor = ( - + {titleContent} ); @@ -234,7 +267,7 @@ class NavigatorBreadcrumbNavigationBar extends Component { } var firstStyles = initStyle(index, navStatePresentedIndex(this.props.navState)); var rightButtonDescriptor = ( - + {rightContent} ); @@ -243,7 +276,6 @@ class NavigatorBreadcrumbNavigationBar extends Component { } _setPropsIfExists(ref, props) { - var ref = this.refs[ref]; ref && ref.setNativeProps(props); } diff --git a/Libraries/Navigator/NavigatorNavigationBar.js b/Libraries/Navigator/NavigatorNavigationBar.js index eb91d99..95bed11 100644 --- a/Libraries/Navigator/NavigatorNavigationBar.js +++ b/Libraries/Navigator/NavigatorNavigationBar.js @@ -9,7 +9,8 @@ */ 'use strict'; -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; import NavigatorNavigationBarStylesAndroid from 'ReactNavigatorNavigationBarStylesAndroid'; import NavigatorNavigationBarStylesIOS from 'ReactNavigatorNavigationBarStylesIOS'; import Platform from 'ReactStyleSheet'; diff --git a/Libraries/NetInfo/NetInfo.web.js b/Libraries/NetInfo/NetInfo.web.js new file mode 100644 index 0000000..cc8bbf3 --- /dev/null +++ b/Libraries/NetInfo/NetInfo.web.js @@ -0,0 +1,115 @@ +/** + * Copyright (c) 2015-present, Alibaba Group Holding Limited. + * Copyright (c) 2015-present, Nicolas Gallagher. + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * @providesModule ReactNetInfo + */ +'use strict'; + +import ExecutionEnvironment from 'fbjs/lib/ExecutionEnvironment'; +import invariant from 'fbjs/lib/invariant'; + +const connection = + ExecutionEnvironment.canUseDOM && + (window.navigator.connection || + window.navigator.mozConnection || + window.navigator.webkitConnection); + +const eventTypes = ['change']; + +const connectionListeners = []; + +/** + * Navigator online: https://developer.mozilla.org/en-US/docs/Web/API/NavigatorOnLine/onLine + * Network Connection API: https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation + */ +const NetInfo = { + addEventListener(type: string, handler: Function): { remove: () => void } { + invariant(eventTypes.indexOf(type) !== -1, 'Trying to subscribe to unknown event: "%s"', type); + if (!connection) { + console.error( + 'Network Connection API is not supported. Not listening for connection type changes.' + ); + return { + remove: () => {} + }; + } + + connection.addEventListener(type, handler); + return { + remove: () => NetInfo.removeEventListener(type, handler) + }; + }, + + removeEventListener(type: string, handler: Function): void { + invariant(eventTypes.indexOf(type) !== -1, 'Trying to subscribe to unknown event: "%s"', type); + if (!connection) { + return; + } + connection.removeEventListener(type, handler); + }, + + fetch(): Promise { + return new Promise((resolve, reject) => { + try { + resolve(connection.type); + } catch (err) { + resolve('unknown'); + } + }); + }, + + isConnected: { + addEventListener(type: string, handler: Function): { remove: () => void } { + invariant( + eventTypes.indexOf(type) !== -1, + 'Trying to subscribe to unknown event: "%s"', + type + ); + const onlineCallback = () => handler(true); + const offlineCallback = () => handler(false); + connectionListeners.push([handler, onlineCallback, offlineCallback]); + + window.addEventListener('online', onlineCallback, false); + window.addEventListener('offline', offlineCallback, false); + + return { + remove: () => NetInfo.isConnected.removeEventListener(type, handler) + }; + }, + + removeEventListener(type: string, handler: Function): void { + invariant( + eventTypes.indexOf(type) !== -1, + 'Trying to subscribe to unknown event: "%s"', + type + ); + + const listenerIndex = connectionListeners.findIndex(pair => pair[0] === handler); + invariant( + listenerIndex !== -1, + 'Trying to remove NetInfo connection listener for unregistered handler' + ); + const [, onlineCallback, offlineCallback] = connectionListeners[listenerIndex]; + + window.removeEventListener('online', onlineCallback, false); + window.removeEventListener('offline', offlineCallback, false); + + connectionListeners.splice(listenerIndex, 1); + }, + + fetch(): Promise { + return new Promise((resolve, reject) => { + try { + resolve(window.navigator.onLine); + } catch (err) { + resolve(true); + } + }); + } + } +}; + +export default NetInfo; diff --git a/Libraries/PanResponder/injectResponderEventPlugin.web.js b/Libraries/PanResponder/injectResponderEventPlugin.web.js index 62ae5eb..8b0143d 100644 --- a/Libraries/PanResponder/injectResponderEventPlugin.web.js +++ b/Libraries/PanResponder/injectResponderEventPlugin.web.js @@ -5,10 +5,13 @@ */ 'use strict'; -import EventPluginRegistry from 'react-dom/lib/EventPluginRegistry'; -import ResponderEventPlugin from 'react-dom/lib/ResponderEventPlugin'; -import ResponderTouchHistoryStore from 'react-dom/lib/ResponderTouchHistoryStore'; +import ReactDOMUnstableNativeDependencies from 'react-dom/unstable-native-dependencies'; +const { + injectEventPluginsByName, + ResponderEventPlugin, + ResponderTouchHistoryStore, +} = ReactDOMUnstableNativeDependencies; let eventTypes = ResponderEventPlugin.eventTypes; eventTypes.startShouldSetResponder.dependencies = [ @@ -93,6 +96,6 @@ ResponderTouchHistoryStore.recordTouchTrack = (topLevelType, nativeEvent) => { }); }; -EventPluginRegistry.injectEventPluginsByName({ +injectEventPluginsByName({ ResponderEventPlugin }); diff --git a/Libraries/Picker/Picker.web.js b/Libraries/Picker/Picker.web.js index e1d3d1b..6c550be 100644 --- a/Libraries/Picker/Picker.web.js +++ b/Libraries/Picker/Picker.web.js @@ -6,11 +6,10 @@ */ 'use strict'; -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; import autobind from 'autobind-decorator'; -const PICKER = 'picker'; - class Picker extends Component { static propTypes = { onValueChange: PropTypes.func, @@ -18,22 +17,23 @@ class Picker extends Component { } _onChange(event) { - // shim the native event - event.nativeEvent.newValue = this.refs[PICKER].value; - if (this.props.onChange) { this.props.onChange(event); } if (this.props.onValueChange) { - this.props.onValueChange(event.nativeEvent.newValue); + const { + selectedIndex, + value + } = event.target; + this.props.onValueChange(value, selectedIndex); } } render() { return (