diff --git a/.eslintignore b/.eslintignore index 66f736ad..81cc5d99 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,3 +2,4 @@ coverage dist node_modules packages/**/vendor/* +packages/examples diff --git a/.flowconfig b/.flowconfig index a135f243..78ed67be 100644 --- a/.flowconfig +++ b/.flowconfig @@ -4,6 +4,7 @@ [ignore] /.*/__tests__/.* /packages/.*/dist/.* +/packages/examples/.* /packages/website/.* .*/node_modules/babel-plugin-transform-react-remove-prop-types/* diff --git a/.prettierignore b/.prettierignore index 66f736ad..81cc5d99 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,3 +2,4 @@ coverage dist node_modules packages/**/vendor/* +packages/examples diff --git a/package.json b/package.json index 6ddd7c17..c674d04e 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,8 @@ "compile:es": "cd packages/react-native-web && babel src --out-dir dist --ignore \"**/__tests__\"", "benchmarks": "cd packages/benchmarks && yarn build", "benchmarks:release": "cd packages/benchmarks && yarn release", + "examples": "cd packages/examples && yarn build", + "examples:release": "cd packages/examples && yarn release", "website": "cd packages/website && yarn start", "website:release": "cd packages/website && yarn release", "flow": "flow", diff --git a/packages/babel-plugin-react-native-web/src/moduleMap.js b/packages/babel-plugin-react-native-web/src/moduleMap.js index f1752c0b..7db088bb 100644 --- a/packages/babel-plugin-react-native-web/src/moduleMap.js +++ b/packages/babel-plugin-react-native-web/src/moduleMap.js @@ -60,6 +60,7 @@ module.exports = { View: true, ViewPropTypes: true, VirtualizedList: true, + YellowBox: true, createElement: true, findNodeHandle: true, processColor: true, diff --git a/packages/examples/README.md b/packages/examples/README.md new file mode 100644 index 00000000..9c141846 --- /dev/null +++ b/packages/examples/README.md @@ -0,0 +1,34 @@ +# examples + +These examples are a clone of React Native's +[RNTester](https://github.com/facebook/react-native/tree/v0.55.4/RNTester). +RNTester showcases React Native views and modules. It's used to manually verify +the appearance and behavior of React Native for Web. + +Try the [examples app](https://necolas.github.io/react-native-web/examples) online. + +To run the examples locally: + +``` +yarn examples +open ./packages/examples/dist/index.html +``` + +Develop against these examples: + +``` +yarn compile --watch +NODE_ENV=development yarn examples --watch +``` + +## Notes + +The RNTester examples rely on features specific to [React Native's +packager](https://github.com/facebook/metro) such as loading resolution +variants of images, Babel transforms, compiling ES module dependencies, and +Haste – Facebook's module system. These features are *emulated* in this +package's `webpack.config.js`. + +The commonjs export in `react-native-web` is also used to ensure that webpack +can load the aliased modules correctly (as compiled ES modules are only +available from the `.default` property of a module.) diff --git a/packages/examples/SHA b/packages/examples/SHA new file mode 100644 index 00000000..15da5731 --- /dev/null +++ b/packages/examples/SHA @@ -0,0 +1,2 @@ +facebook/react-native@v0.55.4 +facebook/react-native@370bcffba748e895ad8afa825bfef40bff859c95 diff --git a/packages/examples/package.json b/packages/examples/package.json new file mode 100644 index 00000000..84477e63 --- /dev/null +++ b/packages/examples/package.json @@ -0,0 +1,23 @@ +{ + "private": true, + "name": "react-native-examples", + "version": "0.7.3", + "scripts": { + "build": "mkdir -p dist && cp -f src/index.html dist/index.html && ./node_modules/.bin/webpack-cli --config ./webpack.config.js", + "release": "yarn build && git checkout gh-pages && rm -rf ../../examples && mv dist ../../examples && git add -A && git commit -m \"Examples deploy\" && git push origin gh-pages && git checkout -" + }, + "dependencies": { + "babel-runtime": "^6.26.0", + "react": "^16.3.2", + "react-dom": "^16.3.2", + "react-native-web": "0.7.3" + }, + "devDependencies": { + "babel-plugin-react-native-web": "0.7.3", + "babel-plugin-transform-runtime": "^6.23.0", + "file-loader": "^1.1.11", + "webpack": "^4.8.1", + "webpack-bundle-analyzer": "^2.11.1", + "webpack-cli": "^2.1.3" + } +} diff --git a/packages/examples/src/RNTester/ARTExample.js b/packages/examples/src/RNTester/ARTExample.js new file mode 100644 index 00000000..704a9c06 --- /dev/null +++ b/packages/examples/src/RNTester/ARTExample.js @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @providesModule ARTExample + */ +'use strict'; + +var React = require('react'); +var ReactNative = require('react-native'); +var { + ART, + Platform, + View, +} = ReactNative; + +const { + Surface, + Path, + Group, + Transform, + Shape, +} = ART; + + +var scale = Platform.isTVOS ? 4 : 1; + +class ARTExample extends React.Component<{}> { + render(){ + const pathRect = new Path() + .moveTo(scale * 0,scale * 0) + .lineTo(scale * 0,scale * 110) + .lineTo(scale * 110,scale * 110) + .lineTo(scale * 110,scale * 0) + .close(); + + const pathCircle0 = new Path() + .moveTo(scale * 30,scale * 5) + .arc(scale * 0,scale * 50,scale * 25) + .arc(scale * 0,-scale * 50,scale * 25) + .close(); + + const pathCircle1 = new Path() + .moveTo(scale * 30,scale * 55) + .arc(scale * 0,scale * 50,scale * 25) + .arc(scale * 0,-scale * 50,scale * 25) + .close(); + + const pathCircle2 = new Path() + .moveTo(scale * 55,scale * 30) + .arc(scale * 50,scale * 0,scale * 25) + .arc(-scale * 50,scale * 0,scale * 25) + .close(); + + const pathCircle3 = new Path() + .moveTo(scale * 55,scale * 80) + .arc(scale * 50,scale * 0,scale * 25) + .arc(-scale * 50,scale * 0,scale * 25) + .close(); + + return ( + + + + + + + + + + + + ); + } +} + +exports.title = ''; +exports.displayName = 'ARTExample'; +exports.description = 'ART input for numeric values'; +exports.examples = [ + { + title: 'ART Example', + render(): React.Element { + return ; + } + }, +]; diff --git a/packages/examples/src/RNTester/AccessibilityAndroidExample.android.js b/packages/examples/src/RNTester/AccessibilityAndroidExample.android.js new file mode 100644 index 00000000..54b087fa --- /dev/null +++ b/packages/examples/src/RNTester/AccessibilityAndroidExample.android.js @@ -0,0 +1,236 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @providesModule AccessibilityAndroidExample + */ +'use strict'; + +var React = require('react'); +var ReactNative = require('react-native'); +var { + AccessibilityInfo, + StyleSheet, + Text, + View, + ToastAndroid, + TouchableWithoutFeedback, +} = ReactNative; + +var RNTesterBlock = require('./RNTesterBlock'); +var RNTesterPage = require('./RNTesterPage'); + +var importantForAccessibilityValues = ['auto', 'yes', 'no', 'no-hide-descendants']; + +class AccessibilityAndroidExample extends React.Component { + static title = 'Accessibility'; + static description = 'Examples of using Accessibility API.'; + + state = { + count: 0, + backgroundImportantForAcc: 0, + forgroundImportantForAcc: 0, + screenReaderEnabled: false, + }; + + componentDidMount() { + AccessibilityInfo.addEventListener( + 'change', + this._handleScreenReaderToggled + ); + AccessibilityInfo.fetch().done((isEnabled) => { + this.setState({ + screenReaderEnabled: isEnabled + }); + }); + } + + componentWillUnmount() { + AccessibilityInfo.removeEventListener( + 'change', + this._handleScreenReaderToggled + ); + } + + _handleScreenReaderToggled = (isEnabled) => { + this.setState({ + screenReaderEnabled: isEnabled, + }); + } + + _addOne = () => { + this.setState({ + count: ++this.state.count, + }); + }; + + _changeBackgroundImportantForAcc = () => { + this.setState({ + backgroundImportantForAcc: (this.state.backgroundImportantForAcc + 1) % 4, + }); + }; + + _changeForgroundImportantForAcc = () => { + this.setState({ + forgroundImportantForAcc: (this.state.forgroundImportantForAcc + 1) % 4, + }); + }; + + render() { + return ( + + + + + + This is + + + nontouchable normal view. + + + + + + + + This is + + + nontouchable accessible view without label. + + + + + + + + This is + + + nontouchable accessible view with label. + + + + + + ToastAndroid.show('Toasts work by default', ToastAndroid.SHORT)} + accessibilityComponentType="button"> + + Click me + Or not + + + + + + + + Click me + + + + Clicked {this.state.count} times + + + + + + The screen reader is {this.state.screenReaderEnabled ? 'enabled' : 'disabled'}. + + + + + + + + + Hello + + + + + + + world + + + + + + + + Change importantForAccessibility for background layout. + + + + + + Background layout importantForAccessibility + + + {importantForAccessibilityValues[this.state.backgroundImportantForAcc]} + + + + + + Change importantForAccessibility for forground layout. + + + + + + Forground layout importantForAccessibility + + + {importantForAccessibilityValues[this.state.forgroundImportantForAcc]} + + + + + + ); + } +} + +var styles = StyleSheet.create({ + embedded: { + backgroundColor: 'yellow', + padding:10, + }, + container: { + flex: 1, + backgroundColor: 'white', + padding: 10, + height:150, + }, +}); + +module.exports = AccessibilityAndroidExample; diff --git a/packages/examples/src/RNTester/AccessibilityIOSExample.js b/packages/examples/src/RNTester/AccessibilityIOSExample.js new file mode 100644 index 00000000..0deead75 --- /dev/null +++ b/packages/examples/src/RNTester/AccessibilityIOSExample.js @@ -0,0 +1,116 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @providesModule AccessibilityIOSExample + */ +'use strict'; + +var React = require('react'); +var ReactNative = require('react-native'); +var { + AccessibilityInfo, + Text, + View, +} = ReactNative; + +class AccessibilityIOSExample extends React.Component<{}> { + render() { + return ( + + alert('onAccessibilityTap success')} + accessible={true}> + + Accessibility normal tap example + + + alert('onMagicTap success')} + accessible={true}> + + Accessibility magic tap example + + + + + Accessibility label example + + + + + Accessibility traits example + + + + Text's accessibilityLabel is the raw text itself unless it is set explicitly. + + + This text component's accessibilityLabel is set explicitly. + + + This view's children are hidden from the accessibility tree + + + ); + } +} + +class ScreenReaderStatusExample extends React.Component<{}, $FlowFixMeState> { + state = { + screenReaderEnabled: false, + } + + componentDidMount() { + AccessibilityInfo.addEventListener( + 'change', + this._handleScreenReaderToggled + ); + AccessibilityInfo.fetch().done((isEnabled) => { + this.setState({ + screenReaderEnabled: isEnabled + }); + }); + } + + componentWillUnmount() { + AccessibilityInfo.removeEventListener( + 'change', + this._handleScreenReaderToggled + ); + } + + _handleScreenReaderToggled = (isEnabled) => { + this.setState({ + screenReaderEnabled: isEnabled, + }); + } + + render() { + return ( + + + The screen reader is {this.state.screenReaderEnabled ? 'enabled' : 'disabled'}. + + + ); + } +} + +exports.title = 'AccessibilityIOS'; +exports.description = 'Interface to show iOS\' accessibility samples'; +exports.examples = [ + { + title: 'Accessibility elements', + render(): React.Element { return ; } + }, + { + title: 'Check if the screen reader is enabled', + render(): React.Element { return ; } + }, +]; diff --git a/packages/examples/src/RNTester/ActionSheetIOSExample.js b/packages/examples/src/RNTester/ActionSheetIOSExample.js new file mode 100644 index 00000000..ac9d9737 --- /dev/null +++ b/packages/examples/src/RNTester/ActionSheetIOSExample.js @@ -0,0 +1,211 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @providesModule ActionSheetIOSExample + */ +'use strict'; + +var React = require('react'); +var ReactNative = require('react-native'); +var { + ActionSheetIOS, + StyleSheet, + takeSnapshot, + Text, + View, +} = ReactNative; + +var BUTTONS = [ + 'Option 0', + 'Option 1', + 'Option 2', + 'Delete', + 'Cancel', +]; +var DESTRUCTIVE_INDEX = 3; +var CANCEL_INDEX = 4; + +class ActionSheetExample extends React.Component<{}, $FlowFixMeState> { + state = { + clicked: 'none', + }; + + render() { + return ( + + + Click to show the ActionSheet + + + Clicked button: {this.state.clicked} + + + ); + } + + showActionSheet = () => { + ActionSheetIOS.showActionSheetWithOptions({ + options: BUTTONS, + cancelButtonIndex: CANCEL_INDEX, + destructiveButtonIndex: DESTRUCTIVE_INDEX, + }, + (buttonIndex) => { + this.setState({ clicked: BUTTONS[buttonIndex] }); + }); + }; +} + +class ActionSheetTintExample extends React.Component<{}, $FlowFixMeState> { + state = { + clicked: 'none', + }; + + render() { + return ( + + + Click to show the ActionSheet + + + Clicked button: {this.state.clicked} + + + ); + } + + showActionSheet = () => { + ActionSheetIOS.showActionSheetWithOptions({ + options: BUTTONS, + cancelButtonIndex: CANCEL_INDEX, + destructiveButtonIndex: DESTRUCTIVE_INDEX, + tintColor: 'green', + }, + (buttonIndex) => { + this.setState({ clicked: BUTTONS[buttonIndex] }); + }); + }; +} + +class ShareActionSheetExample extends React.Component<$FlowFixMeProps, $FlowFixMeState> { + state = { + text: '' + }; + + render() { + return ( + + + Click to show the Share ActionSheet + + + {this.state.text} + + + ); + } + + showShareActionSheet = () => { + ActionSheetIOS.showShareActionSheetWithOptions({ + url: this.props.url, + message: 'message to go with the shared url', + subject: 'a subject to go in the email heading', + excludedActivityTypes: [ + 'com.apple.UIKit.activity.PostToTwitter' + ] + }, + (error) => alert(error), + (completed, method) => { + var text; + if (completed) { + text = `Shared via ${method}`; + } else { + text = 'You didn\'t share'; + } + this.setState({text}); + }); + }; +} + +class ShareScreenshotExample extends React.Component<{}, $FlowFixMeState> { + state = { + text: '' + }; + + render() { + return ( + + + Click to show the Share ActionSheet + + + {this.state.text} + + + ); + } + + showShareActionSheet = () => { + // Take the snapshot (returns a temp file uri) + takeSnapshot('window').then((uri) => { + // Share image data + ActionSheetIOS.showShareActionSheetWithOptions({ + url: uri, + excludedActivityTypes: [ + 'com.apple.UIKit.activity.PostToTwitter' + ] + }, + (error) => alert(error), + (completed, method) => { + var text; + if (completed) { + text = `Shared via ${method}`; + } else { + text = 'You didn\'t share'; + } + this.setState({text}); + }); + }).catch((error) => alert(error)); + }; +} + +var style = StyleSheet.create({ + button: { + marginBottom: 10, + fontWeight: '500', + } +}); + +exports.title = 'ActionSheetIOS'; +exports.description = 'Interface to show iOS\' action sheets'; +exports.examples = [ + { + title: 'Show Action Sheet', + render(): React.Element { return ; } + }, + { + title: 'Show Action Sheet with tinted buttons', + render(): React.Element { return ; } + }, + { + title: 'Show Share Action Sheet', + render(): React.Element { + return ; + } + }, + { + title: 'Share Local Image', + render(): React.Element { + return ; + } + }, + { + title: 'Share Screenshot', + render(): React.Element { + return ; + } + } +]; diff --git a/packages/examples/src/RNTester/ActivityIndicatorExample.js b/packages/examples/src/RNTester/ActivityIndicatorExample.js new file mode 100644 index 00000000..78856a80 --- /dev/null +++ b/packages/examples/src/RNTester/ActivityIndicatorExample.js @@ -0,0 +1,193 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @providesModule ActivityIndicatorExample + */ +'use strict'; + +import React, { Component } from 'react'; +import { ActivityIndicator, StyleSheet, View } from 'react-native'; + +/** + * Optional Flowtype state and timer types definition + */ +type State = { animating: boolean; }; +type Timer = number; + +class ToggleAnimatingActivityIndicator extends Component<$FlowFixMeProps, State> { + _timer: Timer; + + constructor(props) { + super(props); + this.state = { + animating: true, + }; + } + + componentDidMount() { + this.setToggleTimeout(); + } + + componentWillUnmount() { + /* $FlowFixMe(>=0.63.0 site=react_native_fb) This comment suppresses an + * error found when Flow v0.63 was deployed. To see the error delete this + * comment and run Flow. */ + clearTimeout(this._timer); + } + + setToggleTimeout() { + /* $FlowFixMe(>=0.63.0 site=react_native_fb) This comment suppresses an + * error found when Flow v0.63 was deployed. To see the error delete this + * comment and run Flow. */ + this._timer = setTimeout(() => { + this.setState({animating: !this.state.animating}); + this.setToggleTimeout(); + }, 2000); + } + + render() { + return ( + + ); + } +} + + + +exports.displayName = (undefined: ?string); +exports.framework = 'React'; +exports.title = ''; +exports.description = 'Animated loading indicators.'; + +exports.examples = [ + { + title: 'Default (small, white)', + render() { + return ( + + ); + } + }, + { + title: 'Gray', + render() { + return ( + + + + + ); + } + }, + { + title: 'Custom colors', + render() { + return ( + + + + + + + ); + } + }, + { + title: 'Large', + render() { + return ( + + ); + } + }, + { + title: 'Large, custom colors', + render() { + return ( + + + + + + + ); + } + }, + { + title: 'Start/stop', + render() { + return ; + } + }, + { + title: 'Custom size', + render() { + return ( + + ); + } + }, + { + // platform: 'android', + title: 'Custom size (size: 75)', + render() { + return ( + + ); + } + }, +]; + + +const styles = StyleSheet.create({ + centering: { + alignItems: 'center', + justifyContent: 'center', + padding: 8, + }, + gray: { + backgroundColor: '#cccccc', + }, + horizontal: { + flexDirection: 'row', + justifyContent: 'space-around', + padding: 8, + }, +}); diff --git a/packages/examples/src/RNTester/AlertExample.js b/packages/examples/src/RNTester/AlertExample.js new file mode 100644 index 00000000..93c82e40 --- /dev/null +++ b/packages/examples/src/RNTester/AlertExample.js @@ -0,0 +1,157 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @providesModule AlertExample + */ + +'use strict'; + +var React = require('react'); +var ReactNative = require('react-native'); +var { + Alert, + StyleSheet, + Text, + TouchableHighlight, + View, +} = ReactNative; + +var RNTesterBlock = require('./RNTesterBlock'); + +// corporate ipsum > lorem ipsum +var alertMessage = 'Credibly reintermediate next-generation potentialities after goal-oriented ' + + 'catalysts for change. Dynamically revolutionize.'; + +/** + * Simple alert examples. + */ +class SimpleAlertExampleBlock extends React.Component { + render() { + return ( + + Alert.alert( + 'Alert Title', + alertMessage, + )}> + + Alert with message and default button + + + Alert.alert( + 'Alert Title', + alertMessage, + [ + {text: 'OK', onPress: () => console.log('OK Pressed!')}, + ] + )}> + + Alert with one button + + + Alert.alert( + 'Alert Title', + alertMessage, + [ + {text: 'Cancel', onPress: () => console.log('Cancel Pressed!')}, + {text: 'OK', onPress: () => console.log('OK Pressed!')}, + ] + )}> + + Alert with two buttons + + + Alert.alert( + 'Alert Title', + null, + [ + {text: 'Foo', onPress: () => console.log('Foo Pressed!')}, + {text: 'Bar', onPress: () => console.log('Bar Pressed!')}, + {text: 'Baz', onPress: () => console.log('Baz Pressed!')}, + ] + )}> + + Alert with three buttons + + + Alert.alert( + 'Foo Title', + alertMessage, + '..............'.split('').map((dot, index) => ({ + text: 'Button ' + index, + onPress: () => console.log('Pressed ' + index) + })) + )}> + + Alert with too many buttons + + + Alert.alert( + 'Alert Title', + null, + [ + {text: 'OK', onPress: () => console.log('OK Pressed!')}, + ], + { + cancelable: false + } + )}> + + Alert that cannot be dismissed + + + Alert.alert( + '', + alertMessage, + [ + {text: 'OK', onPress: () => console.log('OK Pressed!')}, + ] + )}> + + Alert without title + + + + ); + } +} + +class AlertExample extends React.Component { + static title = 'Alert'; + + static description = 'Alerts display a concise and informative message ' + + 'and prompt the user to make a decision.'; + + render() { + return ( + + + + ); + } +} + +var styles = StyleSheet.create({ + wrapper: { + borderRadius: 5, + marginBottom: 5, + }, + button: { + backgroundColor: '#eeeeee', + padding: 10, + }, +}); + +module.exports = { + AlertExample, + SimpleAlertExampleBlock, +}; diff --git a/packages/examples/src/RNTester/AlertIOSExample.js b/packages/examples/src/RNTester/AlertIOSExample.js new file mode 100644 index 00000000..b8b1cee4 --- /dev/null +++ b/packages/examples/src/RNTester/AlertIOSExample.js @@ -0,0 +1,183 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @providesModule AlertIOSExample + */ +'use strict'; + +var React = require('react'); +var ReactNative = require('react-native'); +var { + StyleSheet, + View, + Text, + TouchableHighlight, + AlertIOS, +} = ReactNative; + +var { SimpleAlertExampleBlock } = require('./AlertExample'); + +exports.framework = 'React'; +exports.title = 'AlertIOS'; +exports.description = 'iOS alerts and action sheets'; +exports.examples = [{ + title: 'Alerts', + render() { + return ; + } +}, +{ + title: 'Prompt Options', + render(): React.Element { + return ; + } +}, +{ + title: 'Prompt Types', + render() { + return ( + + AlertIOS.prompt('Plain Text Entry')}> + + + + plain-text + + + + + AlertIOS.prompt('Secure Text', null, null, 'secure-text')}> + + + + secure-text + + + + + AlertIOS.prompt('Login & Password', null, null, 'login-password')}> + + + + login-password + + + + + + ); + } +}]; + +class PromptOptions extends React.Component<$FlowFixMeProps, any> { + customButtons: Array; + + constructor(props) { + super(props); + + // $FlowFixMe this seems to be a Flow bug, `saveResponse` is defined below + this.saveResponse = this.saveResponse.bind(this); + + this.customButtons = [{ + text: 'Custom OK', + onPress: this.saveResponse + }, { + text: 'Custom Cancel', + style: 'cancel', + }]; + + this.state = { + promptValue: undefined, + }; + } + + render() { + return ( + + + Prompt value: {this.state.promptValue} + + + AlertIOS.prompt('Type a value', null, this.saveResponse)}> + + + + prompt with title & callback + + + + + AlertIOS.prompt('Type a value', null, this.customButtons)}> + + + + prompt with title & custom buttons + + + + + AlertIOS.prompt('Type a phone number', null, null, 'plain-text', undefined, 'phone-pad')}> + + + + prompt with title & custom keyboard + + + + + AlertIOS.prompt('Type a value', null, this.saveResponse, undefined, 'Default value')}> + + + + prompt with title, callback & default value + + + + + AlertIOS.prompt('Type a value', null, this.customButtons, 'login-password', 'admin@site.com')}> + + + + prompt with title, custom buttons, login/password & default value + + + + + ); + } + + saveResponse(promptValue) { + this.setState({ promptValue: JSON.stringify(promptValue) }); + } +} + +var styles = StyleSheet.create({ + wrapper: { + borderRadius: 5, + marginBottom: 5, + }, + button: { + backgroundColor: '#eeeeee', + padding: 10, + }, +}); diff --git a/packages/examples/src/RNTester/AnimatedExample.js b/packages/examples/src/RNTester/AnimatedExample.js new file mode 100644 index 00000000..8130a9d9 --- /dev/null +++ b/packages/examples/src/RNTester/AnimatedExample.js @@ -0,0 +1,226 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @providesModule AnimatedExample + */ +'use strict'; + +var React = require('react'); +var ReactNative = require('react-native'); +var { + Animated, + Easing, + StyleSheet, + Text, + View, +} = ReactNative; +var RNTesterButton = require('./RNTesterButton'); + +exports.framework = 'React'; +exports.title = 'Animated - Examples'; +exports.description = 'Animated provides a powerful ' + + 'and easy-to-use API for building modern, ' + + 'interactive user experiences.'; + +exports.examples = [ + { + title: 'FadeInView', + description: 'Uses a simple timing animation to ' + + 'bring opacity from 0 to 1 when the component ' + + 'mounts.', + render: function() { + class FadeInView extends React.Component<$FlowFixMeProps, any> { + constructor(props) { + super(props); + this.state = { + fadeAnim: new Animated.Value(0), // opacity 0 + }; + } + componentDidMount() { + Animated.timing( // Uses easing functions + this.state.fadeAnim, // The value to drive + { + toValue: 1, // Target + duration: 2000, // Configuration + }, + ).start(); // Don't forget start! + } + render() { + return ( + + {this.props.children} + + ); + } + } + class FadeInExample extends React.Component<$FlowFixMeProps, any> { + constructor(props) { + super(props); + this.state = { + show: true, + }; + } + render() { + return ( + + { + this.setState((state) => ( + {show: !state.show} + )); + }}> + Press to {this.state.show ? + 'Hide' : 'Show'} + + {this.state.show && + + FadeInView + + } + + ); + } + } + return ; + }, + }, + { + title: 'Transform Bounce', + description: 'One `Animated.Value` is driven by a ' + + 'spring with custom constants and mapped to an ' + + 'ordered set of transforms. Each transform has ' + + 'an interpolation to convert the value into the ' + + 'right range and units.', + render: function() { + this.anim = this.anim || new Animated.Value(0); + return ( + + { + Animated.spring(this.anim, { + toValue: 0, // Returns to the start + velocity: 3, // Velocity makes it move + tension: -10, // Slow + friction: 1, // Oscillate a lot + }).start(); }}> + Press to Fling it! + + + Transforms! + + + ); + }, + }, + { + title: 'Composite Animations with Easing', + description: 'Sequence, parallel, delay, and ' + + 'stagger with different easing functions.', + render: function() { + this.anims = this.anims || [1,2,3].map( + () => new Animated.Value(0) + ); + return ( + + { + var timing = Animated.timing; + Animated.sequence([ // One after the other + timing(this.anims[0], { + toValue: 200, + easing: Easing.linear, + }), + Animated.delay(400), // Use with sequence + timing(this.anims[0], { + toValue: 0, + easing: Easing.elastic(2), // Springy + }), + Animated.delay(400), + Animated.stagger(200, + this.anims.map((anim) => timing( + anim, {toValue: 200} + )).concat( + this.anims.map((anim) => timing( + anim, {toValue: 0} + ))), + ), + Animated.delay(400), + Animated.parallel([ + Easing.inOut(Easing.quad), // Symmetric + Easing.back(1.5), // Goes backwards first + Easing.ease // Default bezier + ].map((easing, ii) => ( + timing(this.anims[ii], { + toValue: 320, easing, duration: 3000, + }) + ))), + Animated.delay(400), + Animated.stagger(200, + this.anims.map((anim) => timing(anim, { + toValue: 0, + easing: Easing.bounce, // Like a ball + duration: 2000, + })), + ), + ]).start(); }}> + Press to Animate + + {['Composite', 'Easing', 'Animations!'].map( + (text, ii) => ( + + {text} + + ) + )} + + ); + }, + }, + { + title: 'Continuous Interactions', + description: 'Gesture events, chaining, 2D ' + + 'values, interrupting and transitioning ' + + 'animations, etc.', + render: () => ( + Checkout the Gratuitous Animation App! + ), + } +]; + +var styles = StyleSheet.create({ + content: { + backgroundColor: 'deepskyblue', + borderWidth: 1, + borderColor: 'dodgerblue', + padding: 20, + margin: 20, + borderRadius: 10, + alignItems: 'center', + }, +}); diff --git a/packages/examples/src/RNTester/AnimatedGratuitousApp/AnExApp.js b/packages/examples/src/RNTester/AnimatedGratuitousApp/AnExApp.js new file mode 100644 index 00000000..1116a84e --- /dev/null +++ b/packages/examples/src/RNTester/AnimatedGratuitousApp/AnExApp.js @@ -0,0 +1,323 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @providesModule AnExApp + * @flow + */ +'use strict'; + +var React = require('react'); +var ReactNative = require('react-native'); +var { + Animated, + LayoutAnimation, + PanResponder, + StyleSheet, + View, +} = ReactNative; + +var AnExSet = require('AnExSet'); + +var CIRCLE_SIZE = 80; +var CIRCLE_MARGIN = 18; +var NUM_CIRCLES = 30; + +class Circle extends React.Component { + longTimer: number; + + _onLongPress: () => void; + _toggleIsActive: () => void; + constructor(props: Object): void { + super(); + this._onLongPress = this._onLongPress.bind(this); + this._toggleIsActive = this._toggleIsActive.bind(this); + this.state = { + isActive: false, + pan: new Animated.ValueXY(), // Vectors reduce boilerplate. (step1: uncomment) + pop: new Animated.Value(0), // Initial value. (step2a: uncomment) + }; + } + + _onLongPress(): void { + var config = {tension: 40, friction: 3}; + this.state.pan.addListener((value) => { // Async listener for state changes (step1: uncomment) + this.props.onMove && this.props.onMove(value); + }); + Animated.spring(this.state.pop, { + toValue: 1, // Pop to larger size. (step2b: uncomment) + ...config, // Reuse config for convenient consistency (step2b: uncomment) + }).start(); + this.setState({panResponder: PanResponder.create({ + onPanResponderMove: Animated.event([ + null, // native event - ignore (step1: uncomment) + {dx: this.state.pan.x, dy: this.state.pan.y}, // links pan to gestureState (step1: uncomment) + ]), + onPanResponderRelease: (e, gestureState) => { + LayoutAnimation.easeInEaseOut(); // @flowfixme animates layout update as one batch (step3: uncomment) + Animated.spring(this.state.pop, { + toValue: 0, // Pop back to 0 (step2c: uncomment) + ...config, + }).start(); + this.setState({panResponder: undefined}); + this.props.onMove && this.props.onMove({ + x: gestureState.dx + this.props.restLayout.x, + y: gestureState.dy + this.props.restLayout.y, + }); + this.props.onDeactivate(); + this.state.pan.removeAllListeners(); + }, + })}, () => { + this.props.onActivate(); + }); + } + + render(): React.Node { + if (this.state.panResponder) { + var handlers = this.state.panResponder.panHandlers; + var dragStyle = { // Used to position while dragging + position: 'absolute', // Hoist out of layout (step1: uncomment) + ...this.state.pan.getLayout(), // Convenience converter (step1: uncomment) + }; + } else { + handlers = { + onStartShouldSetResponder: () => !this.state.isActive, + onResponderGrant: () => { + this.state.pan.setValue({x: 0, y: 0}); // reset (step1: uncomment) + this.state.pan.setOffset(this.props.restLayout); // offset from onLayout (step1: uncomment) + /* $FlowFixMe(>=0.63.0 site=react_native_fb) This comment suppresses + * an error found when Flow v0.63 was deployed. To see the error + * delete this comment and run Flow. */ + this.longTimer = setTimeout(this._onLongPress, 300); + }, + onResponderRelease: () => { + if (!this.state.panResponder) { + /* $FlowFixMe(>=0.63.0 site=react_native_fb) This comment + * suppresses an error found when Flow v0.63 was deployed. To see + * the error delete this comment and run Flow. */ + clearTimeout(this.longTimer); + this._toggleIsActive(); + } + } + }; + } + var animatedStyle: Object = { + shadowOpacity: this.state.pop, // no need for interpolation (step2d: uncomment) + transform: [ + {scale: this.state.pop.interpolate({ + inputRange: [0, 1], + outputRange: [1, 1.3] // scale up from 1 to 1.3 (step2d: uncomment) + })}, + ], + }; + var openVal = this.props.openVal; + if (this.props.dummy) { + animatedStyle.opacity = 0; + } else if (this.state.isActive) { + var innerOpenStyle = [styles.open, { // (step4: uncomment) + left: openVal.interpolate({inputRange: [0, 1], outputRange: [this.props.restLayout.x, 0]}), + top: openVal.interpolate({inputRange: [0, 1], outputRange: [this.props.restLayout.y, 0]}), + width: openVal.interpolate({inputRange: [0, 1], outputRange: [CIRCLE_SIZE, this.props.containerLayout.width]}), + height: openVal.interpolate({inputRange: [0, 1], outputRange: [CIRCLE_SIZE, this.props.containerLayout.height]}), + margin: openVal.interpolate({inputRange: [0, 1], outputRange: [CIRCLE_MARGIN, 0]}), + borderRadius: openVal.interpolate({inputRange: [-0.15, 0, 0.5, 1], outputRange: [0, CIRCLE_SIZE / 2, CIRCLE_SIZE * 1.3, 0]}), + }]; + } + return ( + + + + + + ); + } + _toggleIsActive(velocity) { + var config = {tension: 30, friction: 7}; + if (this.state.isActive) { + Animated.spring(this.props.openVal, {toValue: 0, ...config}).start(() => { // (step4: uncomment) + this.setState({isActive: false}, this.props.onDeactivate); + }); // (step4: uncomment) + } else { + this.props.onActivate(); + this.setState({isActive: true, panResponder: undefined}, () => { + // this.props.openVal.setValue(1); // (step4: comment) + Animated.spring(this.props.openVal, {toValue: 1, ...config}).start(); // (step4: uncomment) + }); + } + } +} + +class AnExApp extends React.Component { + static title = 'Animated - Gratuitous App'; + static description = 'Bunch of Animations - tap a circle to ' + + 'open a view with more animations, or longPress and drag to reorder circles.'; + + _onMove: (position: Point) => void; + constructor(props: any): void { + super(props); + var keys = []; + for (var idx = 0; idx < NUM_CIRCLES; idx++) { + keys.push('E' + idx); + } + this.state = { + keys, + restLayouts: [], + openVal: new Animated.Value(0), + }; + this._onMove = this._onMove.bind(this); + } + + render(): React.Node { + var circles = this.state.keys.map((key, idx) => { + if (key === this.state.activeKey) { + return ; + } else { + if (!this.state.restLayouts[idx]) { + var onLayout = function(index, e) { + var layout = e.nativeEvent.layout; + this.setState((state) => { + state.restLayouts[index] = layout; + return state; + }); + }.bind(this, idx); + } + return ( + + ); + } + }); + if (this.state.activeKey) { + circles.push( + + ); + circles.push( + { this.setState({activeKey: undefined}); }} + /> + ); + } + return ( + + this.setState({layout: e.nativeEvent.layout})}> + {circles} + + + ); + } + + _onMove(position: Point): void { + var newKeys = moveToClosest(this.state, position); + if (newKeys !== this.state.keys) { + LayoutAnimation.easeInEaseOut(); // animates layout update as one batch (step3: uncomment) + this.setState({keys: newKeys}); + } + } +} + +type Point = {x: number, y: number}; +function distance(p1: Point, p2: Point): number { + var dx = p1.x - p2.x; + var dy = p1.y - p2.y; + return dx * dx + dy * dy; +} + +function moveToClosest({activeKey, keys, restLayouts}, position) { + var activeIdx = -1; + var closestIdx = activeIdx; + var minDist = Infinity; + var newKeys = []; + keys.forEach((key, idx) => { + var dist = distance(position, restLayouts[idx]); + if (key === activeKey) { + idx = activeIdx; + } else { + newKeys.push(key); + } + if (dist < minDist) { + minDist = dist; + closestIdx = idx; + } + }); + if (closestIdx === activeIdx) { + return keys; // nothing changed + } else { + newKeys.splice(closestIdx, 0, activeKey); + return newKeys; + } +} + +var styles = StyleSheet.create({ + container: { + flex: 1, + }, + grid: { + flex: 1, + justifyContent: 'center', + flexDirection: 'row', + flexWrap: 'wrap', + backgroundColor: 'transparent', + }, + circle: { + width: CIRCLE_SIZE, + height: CIRCLE_SIZE, + borderRadius: CIRCLE_SIZE / 2, + borderWidth: 1, + borderColor: 'black', + margin: CIRCLE_MARGIN, + overflow: 'hidden', + }, + dragView: { + shadowRadius: 10, + shadowColor: 'rgba(0,0,0,0.7)', + shadowOffset: {height: 8}, + alignSelf: 'flex-start', + backgroundColor: 'transparent', + }, + open: { + position: 'absolute', + left: 0, + top: 0, + right: 0, + bottom: 0, + width: undefined, // unset value from styles.circle + height: undefined, // unset value from styles.circle + borderRadius: 0, // unset value from styles.circle + }, + darkening: { + backgroundColor: 'black', + position: 'absolute', + left: 0, + top: 0, + right: 0, + bottom: 0, + }, +}); + +module.exports = AnExApp; diff --git a/packages/examples/src/RNTester/AnimatedGratuitousApp/AnExBobble.js b/packages/examples/src/RNTester/AnimatedGratuitousApp/AnExBobble.js new file mode 100644 index 00000000..d3f88852 --- /dev/null +++ b/packages/examples/src/RNTester/AnimatedGratuitousApp/AnExBobble.js @@ -0,0 +1,156 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @providesModule AnExBobble + * @flow + */ +'use strict'; + +var React = require('react'); +var ReactNative = require('react-native'); +var { + Animated, + PanResponder, + StyleSheet, + View, +} = ReactNative; + +var NUM_BOBBLES = 5; +var RAD_EACH = Math.PI / 2 / (NUM_BOBBLES - 2); +var RADIUS = 160; +var BOBBLE_SPOTS = [...Array(NUM_BOBBLES)].map((_, i) => { // static positions + return i === 0 ? {x: 0, y: 0} : { // first bobble is the selector + x: -Math.cos(RAD_EACH * (i - 1)) * RADIUS, + y: -Math.sin(RAD_EACH * (i - 1)) * RADIUS, + }; +}); + +class AnExBobble extends React.Component { + constructor(props: Object) { + super(props); + this.state = {}; + this.state.bobbles = BOBBLE_SPOTS.map((_, i) => { + return new Animated.ValueXY(); + }); + this.state.selectedBobble = null; + var bobblePanListener = (e, gestureState) => { // async events => change selection + var newSelected = computeNewSelected(gestureState); + if (this.state.selectedBobble !== newSelected) { + if (this.state.selectedBobble !== null) { + var restSpot = BOBBLE_SPOTS[this.state.selectedBobble]; + Animated.spring(this.state.bobbles[this.state.selectedBobble], { + toValue: restSpot, // return previously selected bobble to rest position + }).start(); + } + if (newSelected !== null && newSelected !== 0) { + Animated.spring(this.state.bobbles[newSelected], { + toValue: this.state.bobbles[0], // newly selected should track the selector + }).start(); + } + this.state.selectedBobble = newSelected; + } + }; + var releaseBobble = () => { + this.state.bobbles.forEach((bobble, i) => { + Animated.spring(bobble, { + toValue: {x: 0, y: 0} // all bobbles return to zero + }).start(); + }); + }; + this.state.bobbleResponder = PanResponder.create({ + onStartShouldSetPanResponder: () => true, + onPanResponderGrant: () => { + BOBBLE_SPOTS.forEach((spot, idx) => { + Animated.spring(this.state.bobbles[idx], { + toValue: spot, // spring each bobble to its spot + friction: 3, // less friction => bouncier + }).start(); + }); + }, + onPanResponderMove: Animated.event( + [ null, {dx: this.state.bobbles[0].x, dy: this.state.bobbles[0].y} ], + {listener: bobblePanListener} // async state changes with arbitrary logic + ), + onPanResponderRelease: releaseBobble, + onPanResponderTerminate: releaseBobble, + }); + } + + render(): React.Node { + return ( + + {this.state.bobbles.map((_, i) => { + var j = this.state.bobbles.length - i - 1; // reverse so lead on top + var handlers = j > 0 ? {} : this.state.bobbleResponder.panHandlers; + return ( + + ); + })} + + ); + } +} + +var styles = StyleSheet.create({ + circle: { + position: 'absolute', + height: 60, + width: 60, + borderRadius: 30, + borderWidth: 0.5, + }, + bobbleContainer: { + top: -68, + paddingRight: 66, + flexDirection: 'row', + flex: 1, + justifyContent: 'flex-end', + backgroundColor: 'transparent', + }, +}); + +function computeNewSelected( + gestureState: Object, +): ?number { + var {dx, dy} = gestureState; + var minDist = Infinity; + var newSelected = null; + var pointRadius = Math.sqrt(dx * dx + dy * dy); + if (Math.abs(RADIUS - pointRadius) < 80) { + BOBBLE_SPOTS.forEach((spot, idx) => { + var delta = {x: spot.x - dx, y: spot.y - dy}; + var dist = delta.x * delta.x + delta.y * delta.y; + if (dist < minDist) { + minDist = dist; + newSelected = idx; + } + }); + } + return newSelected; +} + +function randColor(): string { + var colors = [0,1,2].map(() => Math.floor(Math.random() * 150 + 100)); + return 'rgb(' + colors.join(',') + ')'; +} + +var BOBBLE_IMGS = [ + 'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xpf1/t39.1997-6/10173489_272703316237267_1025826781_n.png', + 'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xaf1/l/t39.1997-6/p240x240/851578_631487400212668_2087073502_n.png', + 'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xaf1/t39.1997-6/p240x240/851583_654446917903722_178118452_n.png', + 'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xaf1/t39.1997-6/p240x240/851565_641023175913294_875343096_n.png', + 'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xaf1/t39.1997-6/851562_575284782557566_1188781517_n.png', +]; + +module.exports = AnExBobble; diff --git a/packages/examples/src/RNTester/AnimatedGratuitousApp/AnExChained.js b/packages/examples/src/RNTester/AnimatedGratuitousApp/AnExChained.js new file mode 100644 index 00000000..63a83804 --- /dev/null +++ b/packages/examples/src/RNTester/AnimatedGratuitousApp/AnExChained.js @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @providesModule AnExChained + * @flow + */ +'use strict'; + +var React = require('react'); +var ReactNative = require('react-native'); +var { + Animated, + PanResponder, + StyleSheet, + View, +} = ReactNative; + +class AnExChained extends React.Component { + constructor(props: Object) { + super(props); + this.state = { + stickers: [new Animated.ValueXY()], // 1 leader + }; + var stickerConfig = {tension: 2, friction: 3}; // soft spring + for (var i = 0; i < 4; i++) { // 4 followers + var sticker = new Animated.ValueXY(); + Animated.spring(sticker, { + ...stickerConfig, + toValue: this.state.stickers[i], // Animated toValue's are tracked + }).start(); + this.state.stickers.push(sticker); // push on the followers + } + var releaseChain = (e, gestureState) => { + this.state.stickers[0].flattenOffset(); // merges offset into value and resets + Animated.sequence([ // spring to start after decay finishes + Animated.decay(this.state.stickers[0], { // coast to a stop + velocity: {x: gestureState.vx, y: gestureState.vy}, + deceleration: 0.997, + }), + Animated.spring(this.state.stickers[0], { + toValue: {x: 0, y: 0} // return to start + }), + ]).start(); + }; + this.state.chainResponder = PanResponder.create({ + onStartShouldSetPanResponder: () => true, + onPanResponderGrant: () => { + this.state.stickers[0].stopAnimation((value) => { + this.state.stickers[0].setOffset(value); // start where sticker animated to + this.state.stickers[0].setValue({x: 0, y: 0}); // avoid flicker before next event + }); + }, + onPanResponderMove: Animated.event( + [null, {dx: this.state.stickers[0].x, dy: this.state.stickers[0].y}] // map gesture to leader + ), + onPanResponderRelease: releaseChain, + onPanResponderTerminate: releaseChain, + }); + } + + render() { + return ( + + {this.state.stickers.map((_, i) => { + var j = this.state.stickers.length - i - 1; // reverse so leader is on top + var handlers = (j === 0) ? this.state.chainResponder.panHandlers : {}; + return ( + + ); + })} + + ); + } +} + +var styles = StyleSheet.create({ + chained: { + alignSelf: 'flex-end', + top: -160, + right: 126 + }, + sticker: { + position: 'absolute', + height: 120, + width: 120, + backgroundColor: 'transparent', + }, +}); + +var CHAIN_IMGS = [ + require('../hawk.png'), + require('../bunny.png'), + require('../relay.png'), + require('../hawk.png'), + require('../bunny.png') +]; + +module.exports = AnExChained; diff --git a/packages/examples/src/RNTester/AnimatedGratuitousApp/AnExScroll.js b/packages/examples/src/RNTester/AnimatedGratuitousApp/AnExScroll.js new file mode 100644 index 00000000..b7f69425 --- /dev/null +++ b/packages/examples/src/RNTester/AnimatedGratuitousApp/AnExScroll.js @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @providesModule AnExScroll + * @flow + */ +'use strict'; + +var React = require('react'); +var ReactNative = require('react-native'); +var { + Animated, + Image, + ScrollView, + StyleSheet, + Text, + View, +} = ReactNative; + +class AnExScroll extends React.Component<$FlowFixMeProps, any> { + state: any = { scrollX: new Animated.Value(0) }; + + render() { + var width = this.props.panelWidth; + return ( + + + + + + {'I\'ll find something to put here.'} + + + + {'And here.'} + + + {'But not here.'} + + + + + ); + } +} + +var styles = StyleSheet.create({ + container: { + backgroundColor: 'transparent', + flex: 1, + }, + text: { + padding: 4, + paddingBottom: 10, + fontWeight: 'bold', + fontSize: 18, + backgroundColor: 'transparent', + }, + bunny: { + backgroundColor: 'transparent', + position: 'absolute', + height: 160, + width: 160, + }, + page: { + alignItems: 'center', + justifyContent: 'flex-end', + }, +}); + +var HAWK_PIC = {uri: 'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xfa1/t39.1997-6/10734304_1562225620659674_837511701_n.png'}; +var BUNNY_PIC = {uri: 'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xaf1/t39.1997-6/851564_531111380292237_1898871086_n.png'}; + +module.exports = AnExScroll; diff --git a/packages/examples/src/RNTester/AnimatedGratuitousApp/AnExSet.js b/packages/examples/src/RNTester/AnimatedGratuitousApp/AnExSet.js new file mode 100644 index 00000000..a89fd200 --- /dev/null +++ b/packages/examples/src/RNTester/AnimatedGratuitousApp/AnExSet.js @@ -0,0 +1,147 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @providesModule AnExSet + * @flow + */ +'use strict'; + +var React = require('react'); +var ReactNative = require('react-native'); +var { + Animated, + PanResponder, + StyleSheet, + Text, + View, +} = ReactNative; + +var AnExBobble = require('./AnExBobble'); +var AnExChained = require('./AnExChained'); +var AnExScroll = require('./AnExScroll'); +var AnExTilt = require('./AnExTilt'); + +class AnExSet extends React.Component { + constructor(props: Object) { + super(props); + function randColor() { + var colors = [0,1,2].map(() => Math.floor(Math.random() * 150 + 100)); + return 'rgb(' + colors.join(',') + ')'; + } + this.state = { + closeColor: randColor(), + openColor: randColor(), + }; + } + render(): React.Node { + var backgroundColor = this.props.openVal ? + this.props.openVal.interpolate({ + inputRange: [0, 1], + outputRange: [ + this.state.closeColor, // interpolates color strings + this.state.openColor + ], + }) : + this.state.closeColor; + var panelWidth = this.props.containerLayout && this.props.containerLayout.width || 320; + return ( + + + + {this.props.id} + + + {this.props.isActive && + + + + July 2nd + + + + + + + + } + + ); + } + + UNSAFE_componentWillMount() { + this.state.dismissY = new Animated.Value(0); + this.state.dismissResponder = PanResponder.create({ + onStartShouldSetPanResponder: () => this.props.isActive, + onPanResponderGrant: () => { + Animated.spring(this.props.openVal, { // Animated value passed in. + toValue: this.state.dismissY.interpolate({ // Track dismiss gesture + inputRange: [0, 300], // and interpolate pixel distance + outputRange: [1, 0], // to a fraction. + }), + useNativeDriver: true, + }).start(); + }, + onPanResponderMove: Animated.event( + [null, {dy: this.state.dismissY}], // track pan gesture + {useNativeDriver: true} + ), + onPanResponderRelease: (e, gestureState) => { + if (gestureState.dy > 100) { + this.props.onDismiss(gestureState.vy); // delegates dismiss action to parent + } else { + Animated.spring(this.props.openVal, { + toValue: 1, // animate back open if released early + useNativeDriver: true, + }).start(); + } + }, + }); + } +} + +var styles = StyleSheet.create({ + container: { + flex: 1, + }, + header: { + alignItems: 'center', + paddingTop: 18, + height: 90, + }, + stream: { + flex: 1, + backgroundColor: 'rgb(230, 230, 230)', + }, + card: { + margin: 8, + padding: 8, + borderRadius: 6, + backgroundColor: 'white', + shadowRadius: 2, + shadowColor: 'black', + shadowOpacity: 0.2, + shadowOffset: {height: 0.5}, + }, + text: { + padding: 4, + paddingBottom: 10, + fontWeight: 'bold', + fontSize: 18, + backgroundColor: 'transparent', + }, + headerText: { + fontSize: 25, + color: 'white', + shadowRadius: 3, + shadowColor: 'black', + shadowOpacity: 1, + shadowOffset: {height: 1}, + }, +}); + +module.exports = AnExSet; diff --git a/packages/examples/src/RNTester/AnimatedGratuitousApp/AnExSlides.md b/packages/examples/src/RNTester/AnimatedGratuitousApp/AnExSlides.md new file mode 100644 index 00000000..bdc0bf1e --- /dev/null +++ b/packages/examples/src/RNTester/AnimatedGratuitousApp/AnExSlides.md @@ -0,0 +1,107 @@ +

+# React Native: Animated + +ReactEurope 2015, Paris - Spencer Ahrens - Facebook + +

+ +## Fluid Interactions + +- People expect smooth, delightful experiences +- Complex interactions are hard +- Common patterns can be optimized + +

+ + +## Declarative Interactions + +- Wire up inputs (events) to outputs (props) + transforms (springs, easing, etc.) +- Arbitrary code can define/update this config +- Config can be serialized -> native/main thread +- No refs or lifecycle to worry about + +

+ + +## var { Animated } = require('react-native'); + +- New library soon to be released for React Native +- 100% JS implementation -> X-Platform +- Per-platform native optimizations planned +- This talk -> usage examples, not implementation + +

+ + +## Gratuitous Animation Demo App + +- Layout uses `flexWrap: 'wrap'` +- longPress -> drag to reorder +- Tap to open example sets + +

+ +## Gratuitous Animation Codez + +- Step 1: 2D tracking pan gesture +- Step 2: Simple pop-out spring on select +- Step 3: Animate grid reordering with `LayoutAnimation` +- Step 4: Opening animation + +

+ +## Animation Example Set + +- `Animated.Value` `this.props.open` passed in from parent +- `interpolate` works with string "shapes," e.g. `'rgb(0, 0, 255)'`, `'45deg'` +- Examples easily composed as separate components +- Dismissing tracks interpolated gesture +- Custom release logic + +

+ + +## Tilting Photo + +- Pan -> translateX * 2, rotate, opacity (via tracking) +- Gesture release triggers separate animations +- `addListener` for async, arbitrary logic on animation progress +- `interpolate` easily creates parallax and other effects + +

+ +## Bobbles + +- Static positions defined +- Listens to events to maybe change selection + - Springs previous selection back + - New selection tracks selector +- `getTranslateTransform` adds convenience + +

+ +## Chained + +- Classic "Chat Heads" animation +- Each sticker tracks the one before it with a soft spring +- `decay` maintains gesture velocity, followed by `spring` to home +- `stopAnimation` provides the last value for `setOffset` + +

+ +## Scrolling + +- `Animated.event` can track all sorts of stuff +- Multi-part ranges and extrapolation options +- Transforms decompose into ordered components + +

+ +# React Native: Animated + +- Landing soon in master (days) +- GitHub: @vjeux, @sahrens +- Questions? + +
diff --git a/packages/examples/src/RNTester/AnimatedGratuitousApp/AnExTilt.js b/packages/examples/src/RNTester/AnimatedGratuitousApp/AnExTilt.js new file mode 100644 index 00000000..a5678b68 --- /dev/null +++ b/packages/examples/src/RNTester/AnimatedGratuitousApp/AnExTilt.js @@ -0,0 +1,129 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @providesModule AnExTilt + * @flow + */ +'use strict'; + +var React = require('react'); +var ReactNative = require('react-native'); +var { + Animated, + PanResponder, + StyleSheet, +} = ReactNative; + +class AnExTilt extends React.Component { + constructor(props: Object) { + super(props); + this.state = { + panX: new Animated.Value(0), + opacity: new Animated.Value(1), + burns: new Animated.Value(1.15), + }; + this.state.tiltPanResponder = PanResponder.create({ + onStartShouldSetPanResponder: () => true, + onPanResponderGrant: () => { + Animated.timing(this.state.opacity, { + toValue: this.state.panX.interpolate({ + inputRange: [-300, 0, 300], // pan is in pixels + outputRange: [0, 1, 0], // goes to zero at both edges + }), + duration: 0, // direct tracking + }).start(); + }, + onPanResponderMove: Animated.event( + [null, {dx: this.state.panX}] // panX is linked to the gesture + ), + onPanResponderRelease: (e, gestureState) => { + var toValue = 0; + if (gestureState.dx > 100) { + toValue = 500; + } else if (gestureState.dx < -100) { + toValue = -500; + } + Animated.spring(this.state.panX, { + toValue, // animate back to center or off screen + velocity: gestureState.vx, // maintain gesture velocity + tension: 10, + friction: 3, + }).start(); + this.state.panX.removeAllListeners(); + var id = this.state.panX.addListener(({value}) => { // listen until offscreen + if (Math.abs(value) > 400) { + this.state.panX.removeListener(id); // offscreen, so stop listening + Animated.timing(this.state.opacity, { + toValue: 1, // Fade back in. This unlinks it from tracking this.state.panX + }).start(); + this.state.panX.setValue(0); // Note: stops the spring animation + toValue !== 0 && this._startBurnsZoom(); + } + }); + }, + }); + } + + _startBurnsZoom() { + this.state.burns.setValue(1); // reset to beginning + Animated.decay(this.state.burns, { + velocity: 1, // sublte zoom + deceleration: 0.9999, // slow decay + }).start(); + } + + UNSAFE_componentWillMount() { + this._startBurnsZoom(); + } + + render(): React.Node { + return ( + + + + ); + } +} + +var styles = StyleSheet.create({ + tilt: { + overflow: 'hidden', + height: 200, + marginBottom: 4, + backgroundColor: 'rgb(130, 130, 255)', + borderColor: 'rgba(0, 0, 0, 0.2)', + borderWidth: 1, + borderRadius: 20, + }, +}); + +module.exports = AnExTilt; diff --git a/packages/examples/src/RNTester/AnimatedGratuitousApp/trees.jpg b/packages/examples/src/RNTester/AnimatedGratuitousApp/trees.jpg new file mode 100644 index 00000000..3d628783 Binary files /dev/null and b/packages/examples/src/RNTester/AnimatedGratuitousApp/trees.jpg differ diff --git a/packages/examples/src/RNTester/AppStateExample.js b/packages/examples/src/RNTester/AppStateExample.js new file mode 100644 index 00000000..8088d77f --- /dev/null +++ b/packages/examples/src/RNTester/AppStateExample.js @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @providesModule AppStateExample + * @flow + */ +'use strict'; + +const React = require('react'); +const ReactNative = require('react-native'); +const { + AppState, + Text, + View +} = ReactNative; + +class AppStateSubscription extends React.Component<$FlowFixMeProps, $FlowFixMeState> { + state = { + appState: AppState.currentState, + previousAppStates: [], + memoryWarnings: 0, + }; + + componentDidMount() { + AppState.addEventListener('change', this._handleAppStateChange); + AppState.addEventListener('memoryWarning', this._handleMemoryWarning); + } + + componentWillUnmount() { + AppState.removeEventListener('change', this._handleAppStateChange); + AppState.removeEventListener('memoryWarning', this._handleMemoryWarning); + } + + _handleMemoryWarning = () => { + this.setState({memoryWarnings: this.state.memoryWarnings + 1}); + }; + + _handleAppStateChange = (appState) => { + var previousAppStates = this.state.previousAppStates.slice(); + previousAppStates.push(this.state.appState); + this.setState({ + appState, + previousAppStates, + }); + }; + + render() { + if (this.props.showMemoryWarnings) { + return ( + + {this.state.memoryWarnings} + + ); + } + if (this.props.showCurrentOnly) { + return ( + + {this.state.appState} + + ); + } + return ( + + {JSON.stringify(this.state.previousAppStates)} + + ); + } +} + +exports.title = 'AppState'; +exports.description = 'app background status'; +exports.examples = [ + { + title: 'AppState.currentState', + description: 'Can be null on app initialization', + render() { return {AppState.currentState}; } + }, + { + title: 'Subscribed AppState:', + description: 'This changes according to the current state, so you can only ever see it rendered as "active"', + render(): React.Element { return ; } + }, + { + title: 'Previous states:', + render(): React.Element { return ; } + }, + { + platform: 'ios', + title: 'Memory Warnings', + description: 'In the IOS simulator, hit Shift+Command+M to simulate a memory warning.', + render(): React.Element { return ; } + }, +]; diff --git a/packages/examples/src/RNTester/AssetScaledImageExample.js b/packages/examples/src/RNTester/AssetScaledImageExample.js new file mode 100644 index 00000000..b723d8f5 --- /dev/null +++ b/packages/examples/src/RNTester/AssetScaledImageExample.js @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @providesModule AssetScaledImageExample + */ +'use strict'; + +var React = require('react'); +var ReactNative = require('react-native'); +var { + Image, + StyleSheet, + View, + ScrollView +} = ReactNative; + +class AssetScaledImageExample extends React.Component<$FlowFixMeProps, $FlowFixMeState> { + state = { + asset: this.props.asset + }; + + render() { + var image = this.state.asset.node.image; + return ( + + + + + + + + + + + + + + + ); + } +} + +var styles = StyleSheet.create({ + row: { + padding: 5, + flex: 1, + flexDirection: 'row', + alignSelf: 'center', + }, + textColumn: { + flex: 1, + flexDirection: 'column', + }, + imageWide: { + borderWidth: 1, + borderColor: 'black', + width: 320, + height: 240, + margin: 5, + }, + imageThumb: { + borderWidth: 1, + borderColor: 'black', + width: 100, + height: 100, + margin: 5, + }, + imageT1: { + borderWidth: 1, + borderColor: 'black', + width: 212, + height: 320, + margin: 5, + }, + imageT2: { + borderWidth: 1, + borderColor: 'black', + width: 100, + height: 320, + margin: 5, + }, +}); + +exports.title = ''; +exports.description = 'Example component that displays the automatic scaling capabilities of the tag'; +module.exports = AssetScaledImageExample; diff --git a/packages/examples/src/RNTester/AsyncStorageExample.js b/packages/examples/src/RNTester/AsyncStorageExample.js new file mode 100644 index 00000000..5e16a6b8 --- /dev/null +++ b/packages/examples/src/RNTester/AsyncStorageExample.js @@ -0,0 +1,112 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @providesModule AsyncStorageExample + */ +'use strict'; + +var React = require('react'); +var ReactNative = require('react-native'); +var { + AsyncStorage, + PickerIOS, + Text, + View +} = ReactNative; +var PickerItemIOS = PickerIOS.Item; + +var STORAGE_KEY = '@AsyncStorageExample:key'; +var COLORS = ['red', 'orange', 'yellow', 'green', 'blue']; + +class BasicStorageExample extends React.Component<{}, $FlowFixMeState> { + state = { + selectedValue: COLORS[0], + messages: [], + }; + + componentDidMount() { + this._loadInitialState().done(); + } + + _loadInitialState = async () => { + try { + var value = await AsyncStorage.getItem(STORAGE_KEY); + if (value !== null){ + this.setState({selectedValue: value}); + this._appendMessage('Recovered selection from disk: ' + value); + } else { + this._appendMessage('Initialized with no selection on disk.'); + } + } catch (error) { + this._appendMessage('AsyncStorage error: ' + error.message); + } + }; + + render() { + var color = this.state.selectedValue; + return ( + + + {COLORS.map((value) => ( + + ))} + + + {'Selected: '} + + {this.state.selectedValue} + + + {' '} + + Press here to remove from storage. + + {' '} + Messages: + {this.state.messages.map((m) => {m})} + + ); + } + + _onValueChange = async (selectedValue) => { + this.setState({selectedValue}); + try { + await AsyncStorage.setItem(STORAGE_KEY, selectedValue); + this._appendMessage('Saved selection to disk: ' + selectedValue); + } catch (error) { + this._appendMessage('AsyncStorage error: ' + error.message); + } + }; + + _removeStorage = async () => { + try { + await AsyncStorage.removeItem(STORAGE_KEY); + this._appendMessage('Selection removed from disk.'); + } catch (error) { + this._appendMessage('AsyncStorage error: ' + error.message); + } + }; + + _appendMessage = (message) => { + this.setState({messages: this.state.messages.concat(message)}); + }; +} + +exports.title = 'AsyncStorage'; +exports.description = 'Asynchronous local disk storage.'; +exports.examples = [ + { + title: 'Basics - getItem, setItem, removeItem', + render(): React.Element { return ; } + }, +]; diff --git a/packages/examples/src/RNTester/BorderExample.js b/packages/examples/src/RNTester/BorderExample.js new file mode 100644 index 00000000..e663e24f --- /dev/null +++ b/packages/examples/src/RNTester/BorderExample.js @@ -0,0 +1,288 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @providesModule BorderExample + */ +'use strict'; + +var React = require('react'); +var ReactNative = require('react-native'); +var { + StyleSheet, + View +} = ReactNative; + +var styles = StyleSheet.create({ + box: { + width: 100, + height: 100, + }, + border1: { + borderWidth: 10, + borderColor: 'brown', + }, + borderRadius: { + borderWidth: 10, + borderRadius: 10, + borderColor: 'cyan', + }, + border2: { + borderWidth: 10, + borderTopColor: 'red', + borderRightColor: 'yellow', + borderBottomColor: 'green', + borderLeftColor: 'blue', + }, + border3: { + borderColor: 'purple', + borderTopWidth: 10, + borderRightWidth: 20, + borderBottomWidth: 30, + borderLeftWidth: 40, + }, + border4: { + borderTopWidth: 10, + borderTopColor: 'red', + borderRightWidth: 20, + borderRightColor: 'yellow', + borderBottomWidth: 30, + borderBottomColor: 'green', + borderLeftWidth: 40, + borderLeftColor: 'blue', + }, + border5: { + borderRadius: 50, + borderTopWidth: 10, + borderTopColor: 'red', + borderRightWidth: 20, + borderRightColor: 'yellow', + borderBottomWidth: 30, + borderBottomColor: 'green', + borderLeftWidth: 40, + borderLeftColor: 'blue', + }, + border6: { + borderTopWidth: 10, + borderTopColor: 'red', + borderRightWidth: 20, + borderRightColor: 'yellow', + borderBottomWidth: 30, + borderBottomColor: 'green', + borderLeftWidth: 40, + borderLeftColor: 'blue', + + borderTopLeftRadius: 100, + }, + border7: { + borderWidth: 10, + borderColor: '#f007', + borderRadius: 30, + overflow: 'hidden', + }, + border7_inner: { + backgroundColor: 'blue', + width: 100, + height: 100 + }, + border8: { + width: 60, + height: 60, + borderColor: 'black', + marginRight: 10, + backgroundColor: 'lightgrey', + }, + border9: { + borderWidth: 10, + borderTopLeftRadius: 10, + borderBottomRightRadius: 20, + borderColor: 'black', + }, + border10: { + borderWidth: 10, + backgroundColor: 'white', + borderTopLeftRadius: 10, + borderBottomRightRadius: 20, + borderColor: 'black', + elevation: 10, + }, + border11: { + width: 0, + height: 0, + borderStyle: 'solid', + overflow: 'hidden', + borderTopWidth: 50, + borderRightWidth: 0, + borderBottomWidth: 50, + borderLeftWidth: 100, + borderTopColor: 'transparent', + borderRightColor: 'transparent', + borderBottomColor: 'transparent', + borderLeftColor: 'red', + }, + border12: { + borderStyle: 'solid', + overflow: 'hidden', + borderTopWidth: 10, + borderRightWidth: 20, + borderBottomWidth: 30, + borderLeftWidth: 40, + borderRadius: 20, + }, + border13: { + borderStyle: 'solid', + overflow: 'hidden', + borderTopWidth: 10, + borderRightWidth: 20, + borderBottomWidth: 30, + borderLeftWidth: 40, + borderTopColor: 'red', + borderRightColor: 'green', + borderBottomColor: 'blue', + borderLeftColor: 'magenta', + borderRadius: 20, + }, + border14: { + borderStyle: 'solid', + overflow: 'hidden', + borderTopWidth: 10, + borderRightWidth: 20, + borderBottomWidth: 30, + borderLeftWidth: 40, + borderTopColor: 'red', + borderRightColor: 'green', + borderBottomColor: 'blue', + borderLeftColor: 'magenta', + borderTopLeftRadius: 10, + borderTopRightRadius: 40, + borderBottomRightRadius: 30, + borderBottomLeftRadius: 40, + } +}); + +exports.title = 'Border'; +exports.description = 'Demonstrates some of the border styles available to Views.'; +exports.examples = [ + { + title: 'Equal-Width / Same-Color', + description: 'borderWidth & borderColor', + render() { + return ; + } + }, + { + title: 'Equal-Width / Same-Color', + description: 'borderWidth & borderColor & borderRadius', + render() { + return ; + } + }, + { + title: 'Equal-Width Borders', + description: 'borderWidth & border*Color', + render() { + return ; + } + }, + { + title: 'Same-Color Borders', + description: 'border*Width & borderColor', + render() { + return ; + } + }, + { + title: 'Custom Borders', + description: 'border*Width & border*Color', + render() { + return ; + } + }, + { + title: 'Custom Borders', + description: 'border*Width & border*Color', + // platform: 'ios', + render() { + return ; + } + }, + { + title: 'Custom Borders', + description: 'border*Width & border*Color', + // platform: 'ios', + render() { + return ; + } + }, + { + title: 'Custom Borders', + description: 'borderRadius & clipping', + // platform: 'ios', + render() { + return ( + + + + ); + } + }, + { + title: 'Single Borders', + description: 'top, left, bottom right', + render() { + return ( + + + + + + + ); + } + }, + { + title: 'Corner Radii', + description: 'borderTopLeftRadius & borderBottomRightRadius', + render() { + return ; + } + }, + { + title: 'Corner Radii / Elevation', + description: 'borderTopLeftRadius & borderBottomRightRadius & elevation', + platform: 'android', + render() { + return ; + } + }, + { + title: 'CSS Trick - Triangle', + description: 'create a triangle by manipulating border colors and widths', + render() { + return ; + } + }, + { + title: 'Curved border(Left|Right|Bottom|Top)Width', + description: 'Make a non-uniform width curved border', + render() { + return ; + } + }, + { + title: 'Curved border(Left|Right|Bottom|Top)Color', + description: 'Make a non-uniform color curved border', + render() { + return ; + } + }, + { + title: 'Curved border(Top|Bottom)(Left|Right)Radius', + description: 'Make a non-uniform radius curved border', + render() { + return ; + } + } +]; diff --git a/packages/examples/src/RNTester/BoxShadowExample.js b/packages/examples/src/RNTester/BoxShadowExample.js new file mode 100644 index 00000000..bddce049 --- /dev/null +++ b/packages/examples/src/RNTester/BoxShadowExample.js @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @providesModule BoxShadowExample + */ +'use strict'; + +var React = require('react'); +var ReactNative = require('react-native'); +var { + Image, + StyleSheet, + View +} = ReactNative; + +var styles = StyleSheet.create({ + box: { + width: 100, + height: 100, + borderWidth: 2, + }, + shadow1: { + shadowOpacity: 0.5, + shadowRadius: 3, + shadowOffset: {width: 2, height: 2}, + }, + shadow2: { + shadowOpacity: 1.0, + shadowColor: 'red', + shadowRadius: 0, + shadowOffset: {width: 3, height: 3}, + }, +}); + +exports.title = 'Box Shadow'; +exports.description = 'Demonstrates some of the shadow styles available to Views.'; +exports.examples = [ + { + title: 'Basic shadow', + description: 'shadowOpacity: 0.5, shadowOffset: {2, 2}', + render() { + return ; + } + }, + { + title: 'Colored shadow', + description: 'shadowColor: \'red\', shadowRadius: 0', + render() { + return ; + } + }, + { + title: 'Shaped shadow', + description: 'borderRadius: 50', + render() { + return ; + } + }, + { + title: 'Image shadow', + description: 'Image shadows are derived exactly from the pixels.', + render() { + return ; + } + }, + { + title: 'Child shadow', + description: 'For views without an opaque background color, shadow will be derived from the subviews.', + render() { + return + + ; + } + }, +]; diff --git a/packages/examples/src/RNTester/ButtonExample.js b/packages/examples/src/RNTester/ButtonExample.js new file mode 100644 index 00000000..6c850a21 --- /dev/null +++ b/packages/examples/src/RNTester/ButtonExample.js @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @providesModule ButtonExample + */ +'use strict'; + +const React = require('react'); +const ReactNative = require('react-native'); +const { + Alert, + Button, + View, +} = ReactNative; + +const onButtonPress = () => { + Alert.alert('Button has been pressed!'); +}; + +exports.displayName = 'ButtonExample'; +exports.framework = 'React'; +exports.title = ' + + + + + Animation Type + + + + + + + Transparent + {this.renderSwitch()} + + {this.renderPickers()} + + + ); + } + renderPickers() { + if (Platform.isTVOS) { + return null; + } + return ( + + + Presentation style + this.setState({presentationStyle})} + itemStyle={styles.pickerItem} + > + + + + + + + + + + Supported orientations + this.setState({selectedSupportedOrientation: i})} + itemStyle={styles.pickerItem} + > + + + + + + + + + + ); + } +} + + +exports.examples = [ + { + title: 'Modal Presentation', + description: 'Modals can be presented with or without animation', + render: () => , + }, +]; + +var styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + padding: 20, + }, + innerContainer: { + borderRadius: 10, + alignItems: 'center', + }, + row: { + alignItems: 'center', + flex: 1, + flexDirection: 'row', + marginBottom: 20, + }, + rowTitle: { + flex: 1, + fontWeight: 'bold', + }, + button: { + borderRadius: 5, + flexGrow: 1, + height: 44, + alignSelf: 'stretch', + justifyContent: 'center', + overflow: 'hidden', + }, + buttonText: { + fontSize: 18, + margin: 5, + textAlign: 'center', + }, + modalButton: { + marginTop: 10, + }, + pickerItem: { + fontSize: 16, + }, +}); diff --git a/packages/examples/src/RNTester/MultiColumnExample.js b/packages/examples/src/RNTester/MultiColumnExample.js new file mode 100644 index 00000000..cc323cfc --- /dev/null +++ b/packages/examples/src/RNTester/MultiColumnExample.js @@ -0,0 +1,154 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @providesModule MultiColumnExample + */ +'use strict'; + +const React = require('react'); +const ReactNative = require('react-native'); +const { + FlatList, + StyleSheet, + Text, + View, +} = ReactNative; + +const RNTesterPage = require('./RNTesterPage'); + +const infoLog = require('infoLog'); + +const { + FooterComponent, + HeaderComponent, + ItemComponent, + PlainInput, + SeparatorComponent, + genItemData, + getItemLayout, + pressItem, + renderSmallSwitchOption, +} = require('./ListExampleShared'); + +class MultiColumnExample extends React.PureComponent<$FlowFixMeProps, $FlowFixMeState> { + static title = ' - MultiColumn'; + static description = 'Performant, scrollable grid of data.'; + + state = { + data: genItemData(1000), + filterText: '', + fixedHeight: true, + logViewable: false, + numColumns: 2, + virtualized: true, + }; + _onChangeFilterText = (filterText) => { + this.setState(() => ({filterText})); + }; + _onChangeNumColumns = (numColumns) => { + this.setState(() => ({numColumns: Number(numColumns)})); + }; + render() { + const filterRegex = new RegExp(String(this.state.filterText), 'i'); + const filter = (item) => (filterRegex.test(item.text) || filterRegex.test(item.title)); + const filteredData = this.state.data.filter(filter); + return ( + - MultiColumn'} + noSpacer={true} + noScroll={true}> + + + + numColumns: + + + + {renderSmallSwitchOption(this, 'virtualized')} + {renderSmallSwitchOption(this, 'fixedHeight')} + {renderSmallSwitchOption(this, 'logViewable')} + + + + alert('onRefresh: nothing to refresh :P')} + refreshing={false} + renderItem={this._renderItemComponent} + disableVirtualization={!this.state.virtualized} + onViewableItemsChanged={this._onViewableItemsChanged} + legacyImplementation={false} + /> + + ); + } + _getItemLayout(data: any, index: number): {length: number, offset: number, index: number} { + const length = getItemLayout(data, index).length + 2 * (CARD_MARGIN + BORDER_WIDTH); + return {length, offset: length * index, index}; + } + _renderItemComponent = ({item}) => { + return ( + + + + ); + }; + // This is called when items change viewability by scrolling into or out of the viewable area. + _onViewableItemsChanged = (info: { + changed: Array<{ + key: string, isViewable: boolean, item: {columns: Array<*>}, index: ?number, section?: any + }>}, + ) => { + // Impressions can be logged here + if (this.state.logViewable) { + infoLog('onViewableItemsChanged: ', info.changed.map((v) => ({...v, item: '...'}))); + } + }; + _pressItem = (key: string) => { + pressItem(this, key); + }; +} + +const CARD_MARGIN = 4; +const BORDER_WIDTH = 1; + +const styles = StyleSheet.create({ + card: { + margin: CARD_MARGIN, + borderRadius: 10, + flex: 1, + overflow: 'hidden', + borderColor: 'lightgray', + borderWidth: BORDER_WIDTH, + }, + row: { + flexDirection: 'row', + alignItems: 'center', + }, + searchRow: { + padding: 10, + }, +}); + +module.exports = MultiColumnExample; diff --git a/packages/examples/src/RNTester/NativeAnimationsExample.js b/packages/examples/src/RNTester/NativeAnimationsExample.js new file mode 100644 index 00000000..983b0c9d --- /dev/null +++ b/packages/examples/src/RNTester/NativeAnimationsExample.js @@ -0,0 +1,622 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @providesModule NativeAnimationsExample + */ +'use strict'; + +const React = require('react'); +const ReactNative = require('react-native'); +const { + View, + Text, + Animated, + StyleSheet, + TouchableWithoutFeedback, + Slider, +} = ReactNative; + +var AnimatedSlider = Animated.createAnimatedComponent(Slider); + +class Tester extends React.Component<$FlowFixMeProps, $FlowFixMeState> { + state = { + native: new Animated.Value(0), + js: new Animated.Value(0), + }; + + current = 0; + + onPress = () => { + const animConfig = this.current && this.props.reverseConfig + ? this.props.reverseConfig + : this.props.config; + this.current = this.current ? 0 : 1; + const config: Object = { + ...animConfig, + toValue: this.current, + }; + + Animated[this.props.type](this.state.native, { + ...config, + useNativeDriver: true, + }).start(); + Animated[this.props.type](this.state.js, { + ...config, + useNativeDriver: false, + }).start(); + }; + + render() { + return ( + + + + Native: + + + {this.props.children(this.state.native)} + + + JavaScript: + + + {this.props.children(this.state.js)} + + + + ); + } +} + +class ValueListenerExample extends React.Component<{}, $FlowFixMeState> { + state = { + anim: new Animated.Value(0), + progress: 0, + }; + _current = 0; + + componentDidMount() { + this.state.anim.addListener(e => this.setState({progress: e.value})); + } + + componentWillUnmount() { + this.state.anim.removeAllListeners(); + } + + _onPress = () => { + this._current = this._current ? 0 : 1; + const config = { + duration: 1000, + toValue: this._current, + }; + + Animated.timing(this.state.anim, { + ...config, + useNativeDriver: true, + }).start(); + }; + + render() { + return ( + + + + + + Value: {this.state.progress} + + + ); + } +} + +class LoopExample extends React.Component<{}, $FlowFixMeState> { + state = { + value: new Animated.Value(0), + }; + + componentDidMount() { + Animated.loop( + Animated.timing(this.state.value, { + toValue: 1, + duration: 5000, + useNativeDriver: true, + }), + ).start(); + } + + render() { + return ( + + + + ); + } +} + +const RNTesterSettingSwitchRow = require('RNTesterSettingSwitchRow'); +class InternalSettings extends React.Component<{}, {busyTime: number | string, filteredStall: number}> { + _stallInterval: ?number; + render() { + return ( + + { + /* $FlowFixMe(>=0.63.0 site=react_native_fb) This comment + * suppresses an error found when Flow v0.63 was deployed. To see + * the error delete this comment and run Flow. */ + this._stallInterval = setInterval(() => { + const start = Date.now(); + console.warn('burn CPU'); + while (Date.now() - start < 100) { + } + }, 300); + }} + onDisable={() => { + /* $FlowFixMe(>=0.63.0 site=react_native_fb) This comment + * suppresses an error found when Flow v0.63 was deployed. To see + * the error delete this comment and run Flow. */ + clearInterval(this._stallInterval || 0); + }} + /> + { + require('JSEventLoopWatchdog').install({thresholdMS: 25}); + this.setState({busyTime: ''}); + require('JSEventLoopWatchdog').addHandler({ + onStall: ({busyTime}) => + this.setState(state => ({ + busyTime, + filteredStall: (state.filteredStall || 0) * 0.97 + + busyTime * 0.03, + })), + }); + }} + onDisable={() => { + console.warn('Cannot disable yet....'); + }} + /> + {this.state && + + {`JS Stall filtered: ${Math.round(this.state.filteredStall)}, `} + {`last: ${this.state.busyTime}`} + } + + ); + } +} + +class EventExample extends React.Component<{}, $FlowFixMeState> { + state = { + scrollX: new Animated.Value(0), + }; + + render() { + const opacity = this.state.scrollX.interpolate({ + inputRange: [0, 200], + outputRange: [1, 0], + }); + return ( + + + + + Scroll me! + + + + ); + } +} + +class TrackingExample extends React.Component<$FlowFixMeProps, $FlowFixMeState> { + state = { + native: new Animated.Value(0), + toNative: new Animated.Value(0), + js: new Animated.Value(0), + toJS: new Animated.Value(0), + }; + + componentDidMount() { + // we configure spring to take a bit of time to settle so that the user + // have time to click many times and see "toValue" getting updated and + const longSettlingSpring = { + tension: 20, + friction: 0.5, + }; + Animated.spring(this.state.native, { + ...longSettlingSpring, + toValue: this.state.toNative, + useNativeDriver: true, + }).start(); + Animated.spring(this.state.js, { + ...longSettlingSpring, + toValue: this.state.toJS, + useNativeDriver: false, + }).start(); + } + + onPress = () => { + // select next value to be tracked by random + const nextValue = Math.random() * 200; + this.state.toNative.setValue(nextValue); + this.state.toJS.setValue(nextValue); + }; + + renderBlock = (anim, dest) => [ + , + , + ] + + render() { + return ( + + + + Native: + + + {this.renderBlock(this.state.native, this.state.toNative)} + + + JavaScript: + + + {this.renderBlock(this.state.js, this.state.toJS)} + + + + ); + } +} + +const styles = StyleSheet.create({ + row: { + padding: 10, + zIndex: 1, + }, + block: { + width: 50, + height: 50, + backgroundColor: 'blue', + }, + line: { + position: 'absolute', + left: 35, + top: 0, + bottom: 0, + width: 1, + backgroundColor: 'red', + }, +}); + +exports.framework = 'React'; +exports.title = 'Native Animated Example'; +exports.description = 'Test out Native Animations'; + +exports.examples = [ + { + title: 'Multistage With Multiply and rotation', + render: function() { + return ( + + {anim => ( + + )} + + ); + }, + }, + { + title: 'Multistage With Multiply', + render: function() { + return ( + + {anim => ( + + )} + + ); + }, + }, + { + title: 'Scale interpolation with clamping', + render: function() { + return ( + + {anim => ( + + )} + + ); + }, + }, + { + title: 'Opacity with delay', + render: function() { + return ( + + {anim => ( + + )} + + ); + }, + }, + { + title: 'Rotate interpolation', + render: function() { + return ( + + {anim => ( + + )} + + ); + }, + }, + { + title: 'translateX => Animated.spring (bounciness/speed)', + render: function() { + return ( + + {anim => ( + + )} + + ); + }, + }, + { + title: 'translateX => Animated.spring (stiffness/damping/mass)', + render: function() { + return ( + + {anim => ( + + )} + + ); + }, + }, + { + title: 'translateX => Animated.decay', + render: function() { + return ( + + {anim => ( + + )} + + ); + }, + }, + { + title: 'Drive custom property', + render: function() { + return ( + + {anim => } + + ); + }, + }, + { + title: 'Animated value listener', + render: function() { + return ; + }, + }, + { + title: 'Animated loop', + render: function() { + return ; + }, + }, + { + title: 'Animated events', + render: function() { + return ; + }, + }, + { + title: 'Animated Tracking - tap me many times', + render: function() { + return ; + }, + }, + { + title: 'Internal Settings', + render: function() { + return ; + }, + }, +]; diff --git a/packages/examples/src/RNTester/NavigatorIOSBarStyleExample.js b/packages/examples/src/RNTester/NavigatorIOSBarStyleExample.js new file mode 100644 index 00000000..d9d53edd --- /dev/null +++ b/packages/examples/src/RNTester/NavigatorIOSBarStyleExample.js @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @providesModule NavigatorIOSBarStyleExample + */ + +'use strict'; + +var React = require('react'); +var ReactNative = require('react-native'); +var { + NavigatorIOS, + StatusBar, + StyleSheet, + Text, + View +} = ReactNative; + +class EmptyPage extends React.Component<{ + text: string, +}> { + render() { + return ( + + + {this.props.text} + + + ); + } +} + +class NavigatorIOSColors extends React.Component<{}> { + static title = ' - Custom Bar Style'; + static description = 'iOS navigation with custom nav bar colors'; + + render() { + // Set StatusBar with light contents to get better contrast + StatusBar.setBarStyle('light-content'); + + return ( + ', + rightButtonTitle: 'Done', + onRightButtonPress: () => { + StatusBar.setBarStyle('default'); + this.props.onExampleExit(); + }, + passProps: { + text: 'The nav bar is black with barStyle prop.', + }, + }} + barStyle="black" + /> + ); + } +} + +var styles = StyleSheet.create({ + container: { + flex: 1, + }, + emptyPage: { + flex: 1, + paddingTop: 64, + }, + emptyPageText: { + margin: 10, + }, +}); + +NavigatorIOSColors.external = true; + +module.exports = NavigatorIOSColors; diff --git a/packages/examples/src/RNTester/NavigatorIOSColorsExample.js b/packages/examples/src/RNTester/NavigatorIOSColorsExample.js new file mode 100644 index 00000000..f34c9c9d --- /dev/null +++ b/packages/examples/src/RNTester/NavigatorIOSColorsExample.js @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @providesModule NavigatorIOSColorsExample + */ +'use strict'; + +var React = require('react'); +var ReactNative = require('react-native'); +var { + NavigatorIOS, + StatusBar, + StyleSheet, + Text, + View +} = ReactNative; + +class EmptyPage extends React.Component { + render() { + return ( + + + {this.props.text} + + + ); + } +} + +class NavigatorIOSColors extends React.Component { + static title = ' - Custom Colors'; + static description = 'iOS navigation with custom nav bar colors'; + + render() { + // Set StatusBar with light contents to get better contrast + StatusBar.setBarStyle('light-content'); + + return ( + ', + rightButtonTitle: 'Done', + onRightButtonPress: () => { + StatusBar.setBarStyle('default'); + this.props.onExampleExit(); + }, + passProps: { + text: 'The nav bar has custom colors with tintColor, ' + + 'barTintColor and titleTextColor props.', + }, + }} + tintColor="#FFFFFF" + barTintColor="#183E63" + titleTextColor="#FFFFFF" + translucent={true} + /> + ); + } +} + +var styles = StyleSheet.create({ + container: { + flex: 1, + }, + emptyPage: { + flex: 1, + paddingTop: 64, + }, + emptyPageText: { + margin: 10, + }, +}); + +NavigatorIOSColors.external = true; + +module.exports = NavigatorIOSColors; diff --git a/packages/examples/src/RNTester/NavigatorIOSExample.js b/packages/examples/src/RNTester/NavigatorIOSExample.js new file mode 100644 index 00000000..79ee6f0f --- /dev/null +++ b/packages/examples/src/RNTester/NavigatorIOSExample.js @@ -0,0 +1,300 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @providesModule NavigatorIOSExample + */ +'use strict'; + +const React = require('react'); +const ReactNative = require('react-native'); +const ViewExample = require('./ViewExample'); + +const createExamplePage = require('./createExamplePage'); +const nativeImageSource = require('nativeImageSource'); +const { + AlertIOS, + NavigatorIOS, + ScrollView, + StyleSheet, + Text, + TouchableHighlight, + View, +} = ReactNative; + +class EmptyPage extends React.Component<$FlowFixMeProps> { + render() { + return ( + + + {this.props.text} + + + ); + } +} + +class NavigatorIOSExamplePage extends React.Component<$FlowFixMeProps> { + render() { + var recurseTitle = 'Recurse Navigation'; + if (!this.props.depth || this.props.depth === 1) { + recurseTitle += ' - more examples here'; + } + return ( + + + + {this._renderRow(recurseTitle, () => { + this.props.navigator.push({ + title: NavigatorIOSExample.title, + component: NavigatorIOSExamplePage, + backButtonTitle: 'Custom Back', + passProps: {depth: this.props.depth ? this.props.depth + 1 : 1}, + }); + })} + {this._renderRow('Push View Example', () => { + this.props.navigator.push({ + title: 'Very Long Custom View Example Title', + component: createExamplePage(null, ViewExample), + }); + })} + {this._renderRow('Custom title image Example', () => { + this.props.navigator.push({ + title: 'Custom title image Example', + titleImage: require('./relay.png'), + component: createExamplePage(null, ViewExample), + }); + })} + {this._renderRow('Custom Right Button', () => { + this.props.navigator.push({ + title: NavigatorIOSExample.title, + component: EmptyPage, + rightButtonTitle: 'Cancel', + onRightButtonPress: () => this.props.navigator.pop(), + passProps: { + text: 'This page has a right button in the nav bar', + } + }); + })} + {this._renderRow('Custom Right System Button', () => { + this.props.navigator.push({ + title: NavigatorIOSExample.title, + component: EmptyPage, + rightButtonSystemIcon: 'bookmarks', + onRightButtonPress: () => this.props.navigator.pop(), + passProps: { + text: 'This page has a right system button in the nav bar', + } + }); + })} + {this._renderRow('Custom Left & Right Icons', () => { + this.props.navigator.push({ + title: NavigatorIOSExample.title, + component: EmptyPage, + leftButtonTitle: 'Custom Left', + onLeftButtonPress: () => this.props.navigator.pop(), + rightButtonIcon: nativeImageSource({ + ios: 'NavBarButtonPlus', + width: 17, + height: 17 + }), + onRightButtonPress: () => { + AlertIOS.alert( + 'Bar Button Action', + 'Recognized a tap on the bar button icon', + [ + { + text: 'OK', + onPress: () => console.log('Tapped OK'), + }, + ] + ); + }, + passProps: { + text: 'This page has an icon for the right button in the nav bar', + } + }); + })} + {this._renderRow('Custom Left & Right System Icons', () => { + this.props.navigator.push({ + title: NavigatorIOSExample.title, + component: EmptyPage, + leftButtonSystemIcon: 'cancel', + onLeftButtonPress: () => this.props.navigator.pop(), + rightButtonSystemIcon: 'search', + onRightButtonPress: () => { + AlertIOS.alert( + 'Bar Button Action', + 'Recognized a tap on the bar button icon', + [ + { + text: 'OK', + onPress: () => console.log('Tapped OK'), + }, + ] + ); + }, + passProps: { + text: 'This page has an icon for the right button in the nav bar', + } + }); + })} + {this._renderRow('Pop', () => { + this.props.navigator.pop(); + })} + {this._renderRow('Pop to top', () => { + this.props.navigator.popToTop(); + })} + {this._renderReplace()} + {this._renderReplacePrevious()} + {this._renderReplacePreviousAndPop()} + {this._renderRow('Exit NavigatorIOS Example', this.props.onExampleExit)} + + + + ); + } + + _renderReplace = () => { + if (!this.props.depth) { + // this is to avoid replacing the top of the stack + return null; + } + return this._renderRow('Replace here', () => { + var prevRoute = this.props.route; + this.props.navigator.replace({ + title: 'New Navigation', + component: EmptyPage, + rightButtonTitle: 'Undo', + onRightButtonPress: () => this.props.navigator.replace(prevRoute), + passProps: { + text: 'The component is replaced, but there is currently no ' + + 'way to change the right button or title of the current route', + } + }); + }); + }; + + _renderReplacePrevious = () => { + if (!this.props.depth || this.props.depth < 2) { + // this is to avoid replacing the top of the stack + return null; + } + return this._renderRow('Replace previous', () => { + this.props.navigator.replacePrevious({ + title: 'Replaced', + component: EmptyPage, + passProps: { + text: 'This is a replaced "previous" page', + }, + wrapperStyle: styles.customWrapperStyle, + }); + }); + }; + + _renderReplacePreviousAndPop = () => { + if (!this.props.depth || this.props.depth < 2) { + // this is to avoid replacing the top of the stack + return null; + } + return this._renderRow('Replace previous and pop', () => { + this.props.navigator.replacePreviousAndPop({ + title: 'Replaced and Popped', + component: EmptyPage, + passProps: { + text: 'This is a replaced "previous" page', + }, + wrapperStyle: styles.customWrapperStyle, + }); + }); + }; + + _renderRow = (title: string, onPress: Function) => { + return ( + + + + + {title} + + + + + + ); + }; +} + +class NavigatorIOSExample extends React.Component<$FlowFixMeProps> { + static title = ''; + static description = 'iOS navigation capabilities'; + static external = true; + + render() { + const {onExampleExit} = this.props; + return ( + + ); + } +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + customWrapperStyle: { + backgroundColor: '#bbdddd', + }, + emptyPage: { + flex: 1, + paddingTop: 64, + }, + emptyPageText: { + margin: 10, + }, + list: { + backgroundColor: '#eeeeee', + marginTop: 10, + }, + group: { + backgroundColor: 'white', + }, + groupSpace: { + height: 15, + }, + line: { + backgroundColor: '#bbbbbb', + height: StyleSheet.hairlineWidth, + }, + row: { + backgroundColor: 'white', + justifyContent: 'center', + paddingHorizontal: 15, + paddingVertical: 15, + }, + separator: { + height: StyleSheet.hairlineWidth, + backgroundColor: '#bbbbbb', + marginLeft: 15, + }, + rowNote: { + fontSize: 17, + }, + rowText: { + fontSize: 17, + fontWeight: '500', + }, +}); + +module.exports = NavigatorIOSExample; diff --git a/packages/examples/src/RNTester/NetInfoExample.js b/packages/examples/src/RNTester/NetInfoExample.js new file mode 100644 index 00000000..0ba8dd2f --- /dev/null +++ b/packages/examples/src/RNTester/NetInfoExample.js @@ -0,0 +1,183 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @providesModule NetInfoExample + */ +'use strict'; + +const React = require('react'); +const ReactNative = require('react-native'); +const { + NetInfo, + Text, + View, + TouchableWithoutFeedback, +} = ReactNative; + +class ConnectionInfoSubscription extends React.Component<{}, $FlowFixMeState> { + state = { + connectionInfoHistory: [], + }; + + componentDidMount() { + NetInfo.addEventListener( + 'change', + this._handleConnectionInfoChange + ); + } + + componentWillUnmount() { + NetInfo.removeEventListener( + 'change', + this._handleConnectionInfoChange + ); + } + + _handleConnectionInfoChange = (connectionInfo) => { + const connectionInfoHistory = this.state.connectionInfoHistory.slice(); + connectionInfoHistory.push(connectionInfo); + this.setState({ + connectionInfoHistory, + }); + }; + + render() { + return ( + + {JSON.stringify(this.state.connectionInfoHistory)} + + ); + } +} + +class ConnectionInfoCurrent extends React.Component<{}, $FlowFixMeState> { + state = { + connectionInfo: null, + }; + + componentDidMount() { + NetInfo.addEventListener( + 'change', + this._handleConnectionInfoChange + ); + NetInfo.fetch().done( + (connectionInfo) => { this.setState({connectionInfo}); } + ); + } + + componentWillUnmount() { + NetInfo.removeEventListener( + 'change', + this._handleConnectionInfoChange + ); + } + + _handleConnectionInfoChange = (connectionInfo) => { + this.setState({ + connectionInfo, + }); + }; + + render() { + return ( + + {this.state.connectionInfo} + + ); + } +} + +class IsConnected extends React.Component<{}, $FlowFixMeState> { + state = { + isConnected: null, + }; + + componentDidMount() { + NetInfo.isConnected.addEventListener( + 'change', + this._handleConnectivityChange + ); + NetInfo.isConnected.fetch().done( + (isConnected) => { this.setState({isConnected}); } + ); + } + + componentWillUnmount() { + NetInfo.isConnected.removeEventListener( + 'change', + this._handleConnectivityChange + ); + } + + _handleConnectivityChange = (isConnected) => { + this.setState({ + isConnected, + }); + }; + + render() { + return ( + + {this.state.isConnected ? 'Online' : 'Offline'} + + ); + } +} + +class IsConnectionExpensive extends React.Component<{}, $FlowFixMeState> { + state = { + isConnectionExpensive: (null : ?boolean), + }; + + _checkIfExpensive = () => { + NetInfo.isConnectionExpensive().then( + isConnectionExpensive => { this.setState({isConnectionExpensive}); } + ); + }; + + render() { + return ( + + + + Click to see if connection is expensive: + {this.state.isConnectionExpensive === true ? 'Expensive' : + this.state.isConnectionExpensive === false ? 'Not expensive' + : 'Unknown'} + + + + + ); + } +} + +exports.title = 'NetInfo'; +exports.description = 'Monitor network status'; +exports.examples = [ + { + title: 'NetInfo.isConnected', + description: 'Asynchronously load and observe connectivity', + render(): React.Element { return ; } + }, + { + title: 'NetInfo.update', + description: 'Asynchronously load and observe connectionInfo', + render(): React.Element { return ; } + }, + { + title: 'NetInfo.updateHistory', + description: 'Observed updates to connectionInfo', + render(): React.Element { return ; } + }, + { + platform: 'android', + title: 'NetInfo.isConnectionExpensive (Android)', + description: 'Asynchronously check isConnectionExpensive', + render(): React.Element { return ; } + }, +]; diff --git a/packages/examples/src/RNTester/OrientationChangeExample.js b/packages/examples/src/RNTester/OrientationChangeExample.js new file mode 100644 index 00000000..af6f9b1f --- /dev/null +++ b/packages/examples/src/RNTester/OrientationChangeExample.js @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @providesModule OrientationChangeExample + * @flow + */ +'use strict'; + +const React = require('react'); +const ReactNative = require('react-native'); +const { + DeviceEventEmitter, + Text, + View, +} = ReactNative; + +import type EmitterSubscription from 'EmitterSubscription'; + +class OrientationChangeExample extends React.Component<{}, $FlowFixMeState> { + _orientationSubscription: EmitterSubscription; + + state = { + currentOrientation: '', + orientationDegrees: 0, + isLandscape: false, + }; + + componentDidMount() { + this._orientationSubscription = DeviceEventEmitter.addListener( + 'namedOrientationDidChange', this._onOrientationChange, + ); + } + + componentWillUnmount() { + this._orientationSubscription.remove(); + } + + _onOrientationChange = (orientation: Object) => { + this.setState({ + currentOrientation: orientation.name, + orientationDegrees: orientation.rotationDegrees, + isLandscape: orientation.isLandscape, + }); + } + + render() { + return ( + + {JSON.stringify(this.state)} + + ); + } +} + +exports.title = 'OrientationChangeExample'; +exports.description = 'listening to orientation changes'; +exports.examples = [ + { + title: 'OrientationChangeExample', + description: 'listening to device orientation changes', + render() { return ; }, + }, +]; diff --git a/packages/examples/src/RNTester/PanResponderExample.js b/packages/examples/src/RNTester/PanResponderExample.js new file mode 100644 index 00000000..27d9a801 --- /dev/null +++ b/packages/examples/src/RNTester/PanResponderExample.js @@ -0,0 +1,130 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow weak + * @providesModule PanResponderExample + */ +'use strict'; + +var React = require('react'); +var createReactClass = require('create-react-class'); +var ReactNative = require('react-native'); +var { + PanResponder, + StyleSheet, + View, +} = ReactNative; + +var CIRCLE_SIZE = 80; + +var PanResponderExample = createReactClass({ + displayName: 'PanResponderExample', + + statics: { + title: 'PanResponder Sample', + description: 'Shows the use of PanResponder to provide basic gesture handling.', + }, + + _panResponder: {}, + _previousLeft: 0, + _previousTop: 0, + _circleStyles: {}, + circle: (null : ?{ setNativeProps(props: Object): void }), + + UNSAFE_componentWillMount: function() { + this._panResponder = PanResponder.create({ + onStartShouldSetPanResponder: this._handleStartShouldSetPanResponder, + onMoveShouldSetPanResponder: this._handleMoveShouldSetPanResponder, + onPanResponderGrant: this._handlePanResponderGrant, + onPanResponderMove: this._handlePanResponderMove, + onPanResponderRelease: this._handlePanResponderEnd, + onPanResponderTerminate: this._handlePanResponderEnd, + }); + this._previousLeft = 20; + this._previousTop = 84; + this._circleStyles = { + style: { + left: this._previousLeft, + top: this._previousTop, + backgroundColor: 'green', + } + }; + }, + + componentDidMount: function() { + this._updateNativeStyles(); + }, + + render: function() { + return ( + + { + this.circle = circle; + }} + style={styles.circle} + {...this._panResponder.panHandlers} + /> + + ); + }, + + _highlight: function() { + this._circleStyles.style.backgroundColor = 'blue'; + this._updateNativeStyles(); + }, + + _unHighlight: function() { + this._circleStyles.style.backgroundColor = 'green'; + this._updateNativeStyles(); + }, + + _updateNativeStyles: function() { + this.circle && this.circle.setNativeProps(this._circleStyles); + }, + + _handleStartShouldSetPanResponder: function(e: Object, gestureState: Object): boolean { + // Should we become active when the user presses down on the circle? + return true; + }, + + _handleMoveShouldSetPanResponder: function(e: Object, gestureState: Object): boolean { + // Should we become active when the user moves a touch over the circle? + return true; + }, + + _handlePanResponderGrant: function(e: Object, gestureState: Object) { + this._highlight(); + }, + _handlePanResponderMove: function(e: Object, gestureState: Object) { + this._circleStyles.style.left = this._previousLeft + gestureState.dx; + this._circleStyles.style.top = this._previousTop + gestureState.dy; + this._updateNativeStyles(); + }, + _handlePanResponderEnd: function(e: Object, gestureState: Object) { + this._unHighlight(); + this._previousLeft += gestureState.dx; + this._previousTop += gestureState.dy; + }, +}); + +var styles = StyleSheet.create({ + circle: { + width: CIRCLE_SIZE, + height: CIRCLE_SIZE, + borderRadius: CIRCLE_SIZE / 2, + position: 'absolute', + left: 0, + top: 0, + }, + container: { + flex: 1, + paddingTop: 64, + }, +}); + +module.exports = PanResponderExample; diff --git a/packages/examples/src/RNTester/PermissionsExampleAndroid.android.js b/packages/examples/src/RNTester/PermissionsExampleAndroid.android.js new file mode 100644 index 00000000..cb837183 --- /dev/null +++ b/packages/examples/src/RNTester/PermissionsExampleAndroid.android.js @@ -0,0 +1,121 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @providesModule PermissionsExampleAndroid + * @flow + */ +'use strict'; + +const React = require('react'); +const ReactNative = require('react-native'); +const { + PermissionsAndroid, + Picker, + StyleSheet, + Text, + TouchableWithoutFeedback, + View, +} = ReactNative; + +const Item = Picker.Item; + +exports.displayName = (undefined: ?string); +exports.framework = 'React'; +exports.title = 'PermissionsAndroid'; +exports.description = 'Permissions example for API 23+.'; + +class PermissionsExample extends React.Component<{}, $FlowFixMeState> { + state = { + permission: PermissionsAndroid.PERMISSIONS.CAMERA, + hasPermission: 'Not Checked', + }; + + render() { + return ( + + Permission Name: + + + + + + + + Check Permission + + + Permission Status: {this.state.hasPermission} + + + Request Permission + + + + ); + } + + _onSelectPermission = (permission: string) => { + this.setState({ + permission: permission, + }); + }; + + _checkPermission = async () => { + let result = await PermissionsAndroid.check(this.state.permission); + this.setState({ + hasPermission: (result ? 'Granted' : 'Revoked') + ' for ' + + this.state.permission, + }); + }; + + _requestPermission = async () => { + let result = await PermissionsAndroid.request( + this.state.permission, + { + title: 'Permission Explanation', + message: + 'The app needs the following permission ' + this.state.permission + + ' because of reasons. Please approve.' + }, + ); + + this.setState({ + hasPermission: result + ' for ' + + this.state.permission, + }); + }; +} + +exports.examples = [ + { + title: 'Permissions Example', + description: 'Short example of how to use the runtime permissions API introduced in Android M.', + render: () => , + }, +]; + +var styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: 'white', + }, + singleLine: { + fontSize: 16, + padding: 4, + }, + text: { + margin: 10, + }, + touchable: { + color: '#007AFF', + }, + picker: { + flex: 1, + } +}); diff --git a/packages/examples/src/RNTester/PickerExample.js b/packages/examples/src/RNTester/PickerExample.js new file mode 100644 index 00000000..1a1005cd --- /dev/null +++ b/packages/examples/src/RNTester/PickerExample.js @@ -0,0 +1,128 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @providesModule PickerExample + */ +'use strict'; + +const React = require('react'); +const ReactNative = require('react-native'); +const StyleSheet = require('StyleSheet'); +const RNTesterBlock = require('RNTesterBlock'); +const RNTesterPage = require('RNTesterPage'); + +const { + Picker, + Text, +} = ReactNative; + +const Item = Picker.Item; + +class PickerExample extends React.Component<{}, $FlowFixMeState> { + static title = ''; + static description = 'Provides multiple options to choose from, using either a dropdown menu or a dialog.'; + + state = { + selected1: 'key1', + selected2: 'key1', + selected3: 'key1', + color: 'red', + mode: Picker.MODE_DIALOG, + }; + + render() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Cannot change the value of this picker because it doesn't update selectedValue. + + + + + + + + + + + + + + + + ); + } + + changeMode = () => { + const newMode = this.state.mode === Picker.MODE_DIALOG + ? Picker.MODE_DROPDOWN + : Picker.MODE_DIALOG; + this.setState({mode: newMode}); + }; + + onValueChange = (key: string, value: string) => { + const newState = {}; + newState[key] = value; + this.setState(newState); + }; +} + +var styles = StyleSheet.create({ + picker: { + width: 100, + }, +}); + +module.exports = PickerExample; diff --git a/packages/examples/src/RNTester/PickerIOSExample.js b/packages/examples/src/RNTester/PickerIOSExample.js new file mode 100644 index 00000000..a4d5de63 --- /dev/null +++ b/packages/examples/src/RNTester/PickerIOSExample.js @@ -0,0 +1,144 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @providesModule PickerIOSExample + */ +'use strict'; + +var React = require('react'); +var ReactNative = require('react-native'); +var { + PickerIOS, + Text, + View, +} = ReactNative; + +var PickerItemIOS = PickerIOS.Item; + +var CAR_MAKES_AND_MODELS = { + amc: { + name: 'AMC', + models: ['AMX', 'Concord', 'Eagle', 'Gremlin', 'Matador', 'Pacer'], + }, + alfa: { + name: 'Alfa-Romeo', + models: ['159', '4C', 'Alfasud', 'Brera', 'GTV6', 'Giulia', 'MiTo', 'Spider'], + }, + aston: { + name: 'Aston Martin', + models: ['DB5', 'DB9', 'DBS', 'Rapide', 'Vanquish', 'Vantage'], + }, + audi: { + name: 'Audi', + models: ['90', '4000', '5000', 'A3', 'A4', 'A5', 'A6', 'A7', 'A8', 'Q5', 'Q7'], + }, + austin: { + name: 'Austin', + models: ['America', 'Maestro', 'Maxi', 'Mini', 'Montego', 'Princess'], + }, + borgward: { + name: 'Borgward', + models: ['Hansa', 'Isabella', 'P100'], + }, + buick: { + name: 'Buick', + models: ['Electra', 'LaCrosse', 'LeSabre', 'Park Avenue', 'Regal', + 'Roadmaster', 'Skylark'], + }, + cadillac: { + name: 'Cadillac', + models: ['Catera', 'Cimarron', 'Eldorado', 'Fleetwood', 'Sedan de Ville'], + }, + chevrolet: { + name: 'Chevrolet', + models: ['Astro', 'Aveo', 'Bel Air', 'Captiva', 'Cavalier', 'Chevelle', + 'Corvair', 'Corvette', 'Cruze', 'Nova', 'SS', 'Vega', 'Volt'], + }, +}; + +class PickerExample extends React.Component<{}, $FlowFixMeState> { + state = { + carMake: 'cadillac', + modelIndex: 3, + }; + + render() { + var make = CAR_MAKES_AND_MODELS[this.state.carMake]; + var selectionString = make.name + ' ' + make.models[this.state.modelIndex]; + return ( + + Please choose a make for your car: + this.setState({carMake, modelIndex: 0})}> + {Object.keys(CAR_MAKES_AND_MODELS).map((carMake) => ( + + ))} + + Please choose a model of {make.name}: + this.setState({modelIndex})}> + {CAR_MAKES_AND_MODELS[this.state.carMake].models.map((modelName, modelIndex) => ( + + ))} + + You selected: {selectionString} + + ); + } +} + +class PickerStyleExample extends React.Component<{}, $FlowFixMeState> { + state = { + carMake: 'cadillac', + modelIndex: 0, + }; + + render() { + return ( + this.setState({carMake, modelIndex: 0})}> + {Object.keys(CAR_MAKES_AND_MODELS).map((carMake) => ( + + ))} + + ); + } +} + +exports.displayName = (undefined: ?string); +exports.title = ''; +exports.description = 'Render lists of selectable options with UIPickerView.'; +exports.examples = [ +{ + title: '', + render: function(): React.Element { + return ; + }, +}, +{ + title: ' with custom styling', + render: function(): React.Element { + return ; + }, +}]; diff --git a/packages/examples/src/RNTester/PointerEventsExample.js b/packages/examples/src/RNTester/PointerEventsExample.js new file mode 100644 index 00000000..ac838c4e --- /dev/null +++ b/packages/examples/src/RNTester/PointerEventsExample.js @@ -0,0 +1,254 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @providesModule PointerEventsExample + */ +'use strict'; + +var React = require('react'); +var ReactNative = require('react-native'); +var { + StyleSheet, + Text, + View, +} = ReactNative; + +class ExampleBox extends React.Component<$FlowFixMeProps, $FlowFixMeState> { + state = { + log: [], + }; + + handleLog = (msg) => { + this.state.log = this.state.log.concat([msg]); + }; + + flushReactChanges = () => { + this.forceUpdate(); + }; + + /** + * Capture phase of bubbling to append separator before any of the bubbling + * happens. + */ + handleTouchCapture = () => { + this.state.log = this.state.log.concat(['---']); + }; + + 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. */} + + + + + {this.state.log.join('\n')} + + + + ); + } +} + +class NoneExample extends React.Component<$FlowFixMeProps> { + render() { + return ( + this.props.onLog('A unspecified touched')} + style={styles.box}> + + A: unspecified + + this.props.onLog('B none touched')} + style={[styles.box, styles.boxPassedThrough]}> + + B: none + + this.props.onLog('C unspecified touched')} + style={[styles.box, styles.boxPassedThrough]}> + + C: unspecified + + + + + ); + } +} + +/** + * Special demo text that makes itself untouchable so that it doesn't destroy + * the experiment and confuse the output. + */ +class DemoText extends React.Component<$FlowFixMeProps> { + render() { + return ( + + + {this.props.children} + + + ); + } +} + +class BoxNoneExample extends React.Component<$FlowFixMeProps> { + render() { + return ( + this.props.onLog('A unspecified touched')} + style={styles.box}> + + A: unspecified + + this.props.onLog('B box-none touched')} + style={[styles.box, styles.boxPassedThrough]}> + + B: box-none + + this.props.onLog('C unspecified touched')} + style={styles.box}> + + C: unspecified + + + this.props.onLog('C explicitly unspecified touched')} + style={[styles.box]}> + + C: explicitly unspecified + + + + + ); + } +} + +class BoxOnlyExample extends React.Component<$FlowFixMeProps> { + render() { + return ( + this.props.onLog('A unspecified touched')} + style={styles.box}> + + A: unspecified + + this.props.onLog('B box-only touched')} + style={styles.box}> + + B: box-only + + this.props.onLog('C unspecified touched')} + style={[styles.box, styles.boxPassedThrough]}> + + C: unspecified + + + this.props.onLog('C explicitly unspecified touched')} + style={[styles.box, styles.boxPassedThrough]}> + + C: explicitly unspecified + + + + + ); + } +} + +type ExampleClass = { + Component: React.ComponentType, + title: string, + description: string, +}; + +var exampleClasses: Array = [ + { + Component: NoneExample, + title: '`none`', + description: '`none` causes touch events on the container and its child components to pass through to the parent container.', + }, + { + Component: BoxNoneExample, + title: '`box-none`', + description: '`box-none` causes touch events on the container to pass through and will only detect touch events on its child components.', + }, + { + Component: BoxOnlyExample, + title: '`box-only`', + description: '`box-only` causes touch events on the container\'s child components to pass through and will only detect touch events on the container itself.', + } +]; + +var infoToExample = (info) => { + return { + title: info.title, + description: info.description, + render: function() { + return ; + }, + }; +}; + +var styles = StyleSheet.create({ + text: { + fontSize: 10, + color: '#5577cc', + }, + textPassedThrough: { + color: '#88aadd', + }, + box: { + backgroundColor: '#aaccff', + borderWidth: 1, + borderColor: '#7799cc', + padding: 10, + margin: 5, + }, + boxPassedThrough: { + borderColor: '#99bbee', + }, + logText: { + fontSize: 9, + }, + logBox: { + padding: 20, + margin: 10, + borderWidth: 0.5, + borderColor: '#f0f0f0', + backgroundColor: '#f9f9f9', + }, + bottomSpacer: { + marginBottom: 100, + }, +}); + +exports.framework = 'React'; +exports.title = 'Pointer Events'; +exports.description = 'Demonstrates the use of the pointerEvents prop of a ' + + 'View to control how touches should be handled.'; +exports.examples = exampleClasses.map(infoToExample); diff --git a/packages/examples/src/RNTester/ProgressBarAndroidExample.android.js b/packages/examples/src/RNTester/ProgressBarAndroidExample.android.js new file mode 100644 index 00000000..baa75b9f --- /dev/null +++ b/packages/examples/src/RNTester/ProgressBarAndroidExample.android.js @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @providesModule ProgressBarAndroidExample + */ +'use strict'; + +var ProgressBar = require('ProgressBarAndroid'); +var React = require('React'); +var createReactClass = require('create-react-class'); +var RNTesterBlock = require('RNTesterBlock'); +var RNTesterPage = require('RNTesterPage'); + +var TimerMixin = require('react-timer-mixin'); + +var MovingBar = createReactClass({ + displayName: 'MovingBar', + mixins: [TimerMixin], + + getInitialState: function() { + return { + progress: 0 + }; + }, + + componentDidMount: function() { + this.setInterval( + () => { + var progress = (this.state.progress + 0.02) % 1; + this.setState({progress: progress}); + }, 50 + ); + }, + + render: function() { + return ; + }, +}); + +class ProgressBarAndroidExample extends React.Component<{}> { + static title = ''; + static description = 'Horizontal bar to show the progress of some operation.'; + + render() { + return ( + + + + + + + + + + + + + + + + + + ); + } +} + +module.exports = ProgressBarAndroidExample; diff --git a/packages/examples/src/RNTester/ProgressViewIOSExample.js b/packages/examples/src/RNTester/ProgressViewIOSExample.js new file mode 100644 index 00000000..d0641760 --- /dev/null +++ b/packages/examples/src/RNTester/ProgressViewIOSExample.js @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @providesModule ProgressViewIOSExample + */ +'use strict'; + +var React = require('react'); +var createReactClass = require('create-react-class'); +var ReactNative = require('react-native'); +var { + ProgressViewIOS, + StyleSheet, + View, +} = ReactNative; +/* $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 TimerMixin = require('react-timer-mixin'); + +var ProgressViewExample = createReactClass({ + displayName: 'ProgressViewExample', + mixins: [TimerMixin], + + getInitialState() { + return { + progress: 0, + }; + }, + + componentDidMount() { + this.updateProgress(); + }, + + updateProgress() { + var progress = this.state.progress + 0.01; + this.setState({ progress }); + this.requestAnimationFrame(() => this.updateProgress()); + }, + + getProgress(offset) { + var progress = this.state.progress + offset; + return Math.sin(progress % Math.PI) % 1; + }, + + render() { + return ( + + + + + + + + ); + }, +}); + +exports.displayName = (undefined: ?string); +exports.framework = 'React'; +exports.title = 'ProgressViewIOS'; +exports.description = 'ProgressViewIOS'; +exports.examples = [{ + title: 'ProgressViewIOS', + render() { + return ( + + ); + } +}]; + +var styles = StyleSheet.create({ + container: { + marginTop: -20, + backgroundColor: 'transparent', + }, + progressView: { + marginTop: 20, + } +}); diff --git a/packages/examples/src/RNTester/PushNotificationIOSExample.js b/packages/examples/src/RNTester/PushNotificationIOSExample.js new file mode 100644 index 00000000..c2368e9a --- /dev/null +++ b/packages/examples/src/RNTester/PushNotificationIOSExample.js @@ -0,0 +1,215 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @providesModule PushNotificationIOSExample + */ +'use strict'; + +var React = require('react'); +var ReactNative = require('react-native'); +var { + AlertIOS, + PushNotificationIOS, + StyleSheet, + Text, + TouchableHighlight, + View, +} = ReactNative; + +class Button extends React.Component<$FlowFixMeProps> { + render() { + return ( + + + {this.props.label} + + + ); + } +} + +class NotificationExample extends React.Component<{}> { + UNSAFE_componentWillMount() { + PushNotificationIOS.addEventListener('register', this._onRegistered); + PushNotificationIOS.addEventListener('registrationError', this._onRegistrationError); + PushNotificationIOS.addEventListener('notification', this._onRemoteNotification); + PushNotificationIOS.addEventListener('localNotification', this._onLocalNotification); + + PushNotificationIOS.requestPermissions(); + } + + componentWillUnmount() { + PushNotificationIOS.removeEventListener('register', this._onRegistered); + PushNotificationIOS.removeEventListener('registrationError', this._onRegistrationError); + PushNotificationIOS.removeEventListener('notification', this._onRemoteNotification); + PushNotificationIOS.removeEventListener('localNotification', this._onLocalNotification); + } + + render() { + return ( + + + + + diff --git a/packages/examples/src/RNTester/relay@3x.png b/packages/examples/src/RNTester/relay@3x.png new file mode 100644 index 00000000..59a4386a Binary files /dev/null and b/packages/examples/src/RNTester/relay@3x.png differ diff --git a/packages/examples/src/RNTester/slider-left.png b/packages/examples/src/RNTester/slider-left.png new file mode 100644 index 00000000..dff4e11e Binary files /dev/null and b/packages/examples/src/RNTester/slider-left.png differ diff --git a/packages/examples/src/RNTester/slider-left@2x.png b/packages/examples/src/RNTester/slider-left@2x.png new file mode 100644 index 00000000..83c6dd82 Binary files /dev/null and b/packages/examples/src/RNTester/slider-left@2x.png differ diff --git a/packages/examples/src/RNTester/slider-right.png b/packages/examples/src/RNTester/slider-right.png new file mode 100644 index 00000000..57e8c7d3 Binary files /dev/null and b/packages/examples/src/RNTester/slider-right.png differ diff --git a/packages/examples/src/RNTester/slider-right@2x.png b/packages/examples/src/RNTester/slider-right@2x.png new file mode 100644 index 00000000..8546ba8c Binary files /dev/null and b/packages/examples/src/RNTester/slider-right@2x.png differ diff --git a/packages/examples/src/RNTester/slider.png b/packages/examples/src/RNTester/slider.png new file mode 100644 index 00000000..91419910 Binary files /dev/null and b/packages/examples/src/RNTester/slider.png differ diff --git a/packages/examples/src/RNTester/slider@2x.png b/packages/examples/src/RNTester/slider@2x.png new file mode 100644 index 00000000..396614fa Binary files /dev/null and b/packages/examples/src/RNTester/slider@2x.png differ diff --git a/packages/examples/src/RNTester/story-background.png b/packages/examples/src/RNTester/story-background.png new file mode 100644 index 00000000..fc082986 Binary files /dev/null and b/packages/examples/src/RNTester/story-background.png differ diff --git a/packages/examples/src/RNTester/uie_comment_highlighted@2x.png b/packages/examples/src/RNTester/uie_comment_highlighted@2x.png new file mode 100644 index 00000000..b3372675 Binary files /dev/null and b/packages/examples/src/RNTester/uie_comment_highlighted@2x.png differ diff --git a/packages/examples/src/RNTester/uie_comment_normal@2x.png b/packages/examples/src/RNTester/uie_comment_normal@2x.png new file mode 100644 index 00000000..6491689f Binary files /dev/null and b/packages/examples/src/RNTester/uie_comment_normal@2x.png differ diff --git a/packages/examples/src/RNTester/uie_thumb_big.png b/packages/examples/src/RNTester/uie_thumb_big.png new file mode 100644 index 00000000..dbfdb1b9 Binary files /dev/null and b/packages/examples/src/RNTester/uie_thumb_big.png differ diff --git a/packages/examples/src/RNTester/uie_thumb_normal@2x.png b/packages/examples/src/RNTester/uie_thumb_normal@2x.png new file mode 100644 index 00000000..72683dfa Binary files /dev/null and b/packages/examples/src/RNTester/uie_thumb_normal@2x.png differ diff --git a/packages/examples/src/RNTester/uie_thumb_selected@2x.png b/packages/examples/src/RNTester/uie_thumb_selected@2x.png new file mode 100644 index 00000000..79eb69cf Binary files /dev/null and b/packages/examples/src/RNTester/uie_thumb_selected@2x.png differ diff --git a/packages/examples/src/RNTester/websocket_test_server.js b/packages/examples/src/RNTester/websocket_test_server.js new file mode 100755 index 00000000..83a48ed2 --- /dev/null +++ b/packages/examples/src/RNTester/websocket_test_server.js @@ -0,0 +1,49 @@ +#!/usr/bin/env node +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @providesModule websocket_test_server + */ +'use strict'; + +/* eslint-env node */ + +/* $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 WebSocket = require('ws'); + +const fs = require('fs'); +const path = require('path'); + +console.log(`\ +Test server for WebSocketExample + +This will send each incoming message right back to the other side. +Restart with the '--binary' command line flag to have it respond with an +ArrayBuffer instead of a string. +`); + +const respondWithBinary = process.argv.indexOf('--binary') !== -1; +const server = new WebSocket.Server({port: 5555}); +server.on('connection', (ws) => { + ws.on('message', (message) => { + console.log('Received message:', message); + console.log('Cookie:', ws.upgradeReq.headers.cookie); + if (respondWithBinary) { + message = new Buffer(message); + } + if (message === 'getImage') { + message = fs.readFileSync(path.resolve(__dirname, 'flux@3x.png')); + } + console.log('Replying with:', message); + ws.send(message); + }); + + console.log('Incoming connection!'); + ws.send('Why hello there!'); +}); diff --git a/packages/examples/src/index.html b/packages/examples/src/index.html new file mode 100644 index 00000000..5e88572c --- /dev/null +++ b/packages/examples/src/index.html @@ -0,0 +1,16 @@ + + + + + React Native examples + + + + +
+ + + diff --git a/packages/examples/src/index.js b/packages/examples/src/index.js new file mode 100644 index 00000000..1a13c4e1 --- /dev/null +++ b/packages/examples/src/index.js @@ -0,0 +1,4 @@ +import { AppRegistry } from 'react-native'; +import RNTesterApp from './RNTester/RNTesterApp'; + +AppRegistry.runApplication('RNTesterApp', { rootTag: document.getElementById('root') }); diff --git a/packages/examples/src/nativeImageSource.js b/packages/examples/src/nativeImageSource.js new file mode 100644 index 00000000..49bd99ba --- /dev/null +++ b/packages/examples/src/nativeImageSource.js @@ -0,0 +1,7 @@ +const nativeImageSource = (obj) => { + const uri = obj.android || obj.ios; + obj.uri = `./${uri}.png`; + return obj; +} + +module.exports = nativeImageSource; diff --git a/packages/examples/src/polyfills.js b/packages/examples/src/polyfills.js new file mode 100644 index 00000000..db42ca65 --- /dev/null +++ b/packages/examples/src/polyfills.js @@ -0,0 +1,3 @@ +global.Promise.prototype.done = global.Promise.prototype.then; +global.clearImmediate = id => clearTimeout(id); +global.setImmediate = fn => setTimeout(fn, 0); diff --git a/packages/examples/webpack.config.js b/packages/examples/webpack.config.js new file mode 100644 index 00000000..7f2b7b7d --- /dev/null +++ b/packages/examples/webpack.config.js @@ -0,0 +1,113 @@ +const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; +const path = require('path'); + +const appDirectory = path.resolve(__dirname); + +module.exports = { + mode: process.env.NODE_ENV || 'production', + context: __dirname, + entry: [ + // polyfill non-standard APIs + './src/polyfills', + // app entry file + './src' + ], + output: { + path: path.resolve(appDirectory, 'dist'), + filename: 'bundle.js' + }, + module: { + rules: [ + { + test: /\.(gif|jpe?g|png|svg)$/, + use: { + loader: 'file-loader' + } + }, + { + test: /\.js$/, + include: [ + // anything that needs to be compiled to ES5 + path.resolve(appDirectory, 'src') + ], + use: { + loader: 'babel-loader', + options: { + cacheDirectory: false, + presets: ['react-native'], + plugins: [ + // needed to support async/await + 'transform-runtime' + ] + } + } + } + ] + }, + plugins: [ + new BundleAnalyzerPlugin({ + analyzerMode: 'static', + openAnalyzer: false + }) + ], + resolve: { + alias: Object.assign( + { + // use commonjs modules due to mock haste resolver aliases + 'react-native$': 'react-native-web/dist/cjs' + }, + + { + // temporary hack to work around image loading + './flux.png': './flux@3x.png', + '../relay.png': '../relay@3x.png', + './uie_comment_highlighted.png': './uie_comment_highlighted@2x.png', + './uie_comment_normal.png': './uie_comment_normal@2x.png', + './uie_thumb_normal.png': './uie_thumb_normal@2x.png', + './uie_thumb_selected.png': './uie_thumb_selected@2x.png' + }, + + // mock haste resolver + [ + 'ActivityIndicator', + 'Alert', + 'AsyncStorage', + 'Button', + 'DeviceInfo', + 'Modal', + 'NativeModules', + 'Platform', + 'SafeAreaView', + 'SectionList', + 'StyleSheet', + 'Switch', + 'Text', + 'TextInput', + 'TouchableHighlight', + 'TouchableWithoutFeedback', + 'View', + 'ViewPropTypes' + ].reduce( + (acc, curr) => { + acc[curr] = `react-native-web/dist/cjs/exports/${curr}`; + return acc; + }, + { + JSEventLoopWatchdog: 'react-native-web/dist/cjs/vendor/react-native/JSEventLoopWatchdog', + React$: 'react', + ReactNative$: 'react-native-web/dist/cjs', + AnExSet: path.resolve(__dirname, './src/RNTester/AnimatedGratuitousApp/AnExSet'), + RNTesterBlock: path.resolve(__dirname, './src/RNTester/RNTesterBlock'), + RNTesterPage: path.resolve(__dirname, './src/RNTester/RNTesterPage'), + RNTesterSettingSwitchRow: path.resolve( + __dirname, + './src/RNTester/RNTesterSettingSwitchRow' + ), + infoLog$: 'react-native-web/dist/cjs/vendor/react-native/infoLog', + nativeImageSource$: path.resolve(__dirname, './src/nativeImageSource') + } + ) + ), + extensions: ['.web.js', '.js', '.json'] + } +}; diff --git a/packages/react-native-web/src/exports/YellowBox/index.js b/packages/react-native-web/src/exports/YellowBox/index.js new file mode 100644 index 00000000..541951fe --- /dev/null +++ b/packages/react-native-web/src/exports/YellowBox/index.js @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2016-present, Nicolas Gallagher. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import React from 'react'; +import UnimplementedView from '../../modules/UnimplementedView'; + +class YellowBox extends React.Component<*> { + static ignoreWarnings() {} + render() { + return ; + } +} + +export default YellowBox; diff --git a/packages/react-native-web/src/index.js b/packages/react-native-web/src/index.js index a62aeb99..4ee97a32 100644 --- a/packages/react-native-web/src/index.js +++ b/packages/react-native-web/src/index.js @@ -65,6 +65,7 @@ import TouchableOpacity from './exports/TouchableOpacity'; import TouchableWithoutFeedback from './exports/TouchableWithoutFeedback'; import View from './exports/View'; import VirtualizedList from './exports/VirtualizedList'; +import YellowBox from './exports/YellowBox'; // propTypes import ColorPropType from './exports/ColorPropType'; @@ -92,7 +93,6 @@ const ToastAndroid = UnimplementedView; const ToolbarAndroid = UnimplementedView; const ViewPagerAndroid = UnimplementedView; const WebView = UnimplementedView; -const YellowBox = UnimplementedView; // compat (apis) const ActionSheetIOS = emptyObject; const AlertIOS = emptyObject; @@ -174,6 +174,7 @@ export { TouchableWithoutFeedback, View, VirtualizedList, + YellowBox, // propTypes ColorPropType, EdgeInsetsPropType, @@ -198,7 +199,6 @@ export { ToolbarAndroid, ViewPagerAndroid, WebView, - YellowBox, // compat (apis) ActionSheetIOS, AlertIOS, @@ -281,11 +281,11 @@ const ReactNative = { TouchableWithoutFeedback, View, VirtualizedList, + YellowBox, // propTypes ColorPropType, EdgeInsetsPropType, PointPropType, - // compat (components) DatePickerIOS, DrawerLayoutAndroid, @@ -306,7 +306,6 @@ const ReactNative = { ToolbarAndroid, ViewPagerAndroid, WebView, - YellowBox, // compat (apis) ActionSheetIOS, AlertIOS, diff --git a/packages/react-native-web/src/vendor/react-native/JSEventLoopWatchdog/index.js b/packages/react-native-web/src/vendor/react-native/JSEventLoopWatchdog/index.js new file mode 100644 index 00000000..1fc16b43 --- /dev/null +++ b/packages/react-native-web/src/vendor/react-native/JSEventLoopWatchdog/index.js @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ + +'use strict'; + +import infoLog from '../infoLog'; +/* $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 performanceNow from 'fbjs/lib/performanceNow'; + +type Handler = { + onIterate?: () => void, + onStall: (params: { lastInterval: number, busyTime: number }) => ?string +}; + +/** + * A utility for tracking stalls in the JS event loop that prevent timers and + * other events from being processed in a timely manner. + * + * The "stall" time is defined as the amount of time in access of the acceptable + * threshold, which is typically around 100-200ms. So if the treshold is set to + * 100 and a timer fires 150 ms later than it was scheduled because the event + * loop was tied up, that would be considered a 50ms stall. + * + * By default, logs stall events to the console when installed. Can also be + * queried with `getStats`. + */ +const JSEventLoopWatchdog = { + getStats: function(): Object { + return { stallCount, totalStallTime, longestStall, acceptableBusyTime }; + }, + reset: function() { + infoLog('JSEventLoopWatchdog: reset'); + totalStallTime = 0; + stallCount = 0; + longestStall = 0; + lastInterval = performanceNow(); + }, + addHandler: function(handler: Handler) { + handlers.push(handler); + }, + install: function({ thresholdMS }: { thresholdMS: number }) { + acceptableBusyTime = thresholdMS; + if (installed) { + return; + } + installed = true; + lastInterval = performanceNow(); + function iteration() { + const now = performanceNow(); + const busyTime = now - lastInterval; + if (busyTime >= thresholdMS) { + const stallTime = busyTime - thresholdMS; + stallCount++; + totalStallTime += stallTime; + longestStall = Math.max(longestStall, stallTime); + let msg = + `JSEventLoopWatchdog: JS thread busy for ${busyTime}ms. ` + + `${totalStallTime}ms in ${stallCount} stalls so far. `; + handlers.forEach(handler => { + msg += handler.onStall({ lastInterval, busyTime }) || ''; + }); + infoLog(msg); + } + handlers.forEach(handler => { + handler.onIterate && handler.onIterate(); + }); + lastInterval = now; + setTimeout(iteration, thresholdMS / 5); + } + iteration(); + } +}; + +let acceptableBusyTime = 0; +let installed = false; +let totalStallTime = 0; +let stallCount = 0; +let longestStall = 0; +let lastInterval = 0; +const handlers: Array = []; + +export default JSEventLoopWatchdog;