From 26e84262484767dd9abdfd78ff16fa37fcb8e334 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Thu, 26 May 2016 13:46:58 -0700 Subject: [PATCH] Cross platform ActivityIndicator Summary: The API for `ActivityIndiatorIOS` and `ProgressBarAndroid` is very similar and can be merged in a cross platform component that displays a circular indeterminate loading indicator. This deprecates `ActivityIndiatorIOS` and non-horizontal `ProgressBarAndroid` in favor of this new component. **Test plan (required)** Tested with the ActivityIndicator example in UIExplorer on android and ios. Also made sure that `ActivityIndicatorIOS` still works and displays a deprecation warning. Also tested that `ProgressBarAndroid` with `indeterminate == true` and `styleAttr != 'Horizontal'` displays a deprecation warning. Closes https://github.com/facebook/react-native/pull/6897 Differential Revision: D3351607 Pulled By: dmmiller fbshipit-source-id: b107ce99d966359003e8b3118cd97b90fa1d3d7d --- ...Example.js => ActivityIndicatorExample.js} | 96 +++++++------ .../ProgressBarAndroidExample.android.js | 35 +---- Examples/UIExplorer/UIExplorerList.android.js | 4 + Examples/UIExplorer/UIExplorerList.ios.js | 4 +- .../ActivityIndicator/ActivityIndicator.js | 127 ++++++++++++++++++ .../ActivityIndicatorIOS.android.js | 0 .../ActivityIndicatorIOS.ios.js | 57 +------- .../ProgressBarAndroid.android.js | 16 ++- Libraries/react-native/react-native.js | 1 + Libraries/react-native/react-native.js.flow | 1 + .../progressbar/ProgressBarContainerView.java | 11 ++ .../ReactProgressBarViewManager.java | 6 + docs/AndroidBuildingFromSource.md | 2 +- website/server/extractDocs.js | 2 +- 14 files changed, 229 insertions(+), 133 deletions(-) rename Examples/UIExplorer/{ActivityIndicatorIOSExample.js => ActivityIndicatorExample.js} (61%) create mode 100644 Libraries/Components/ActivityIndicator/ActivityIndicator.js rename Libraries/Components/{ActivityIndicatorIOS => ActivityIndicator}/ActivityIndicatorIOS.android.js (100%) rename Libraries/Components/{ActivityIndicatorIOS => ActivityIndicator}/ActivityIndicatorIOS.ios.js (55%) diff --git a/Examples/UIExplorer/ActivityIndicatorIOSExample.js b/Examples/UIExplorer/ActivityIndicatorExample.js similarity index 61% rename from Examples/UIExplorer/ActivityIndicatorIOSExample.js rename to Examples/UIExplorer/ActivityIndicatorExample.js index 967f04275..3ebf8c109 100644 --- a/Examples/UIExplorer/ActivityIndicatorIOSExample.js +++ b/Examples/UIExplorer/ActivityIndicatorExample.js @@ -15,41 +15,38 @@ */ 'use strict'; -var React = require('react'); -var ReactNative = require('react-native'); -var { - ActivityIndicatorIOS, +const React = require('react'); +const ReactNative = require('react-native'); +const { + ActivityIndicator, StyleSheet, View, } = ReactNative; -var TimerMixin = require('react-timer-mixin'); +const TimerMixin = require('react-timer-mixin'); -var ToggleAnimatingActivityIndicator = React.createClass({ +const ToggleAnimatingActivityIndicator = React.createClass({ mixins: [TimerMixin], - getInitialState: function() { + getInitialState() { return { animating: true, }; }, - setToggleTimeout: function() { - this.setTimeout( - () => { - this.setState({animating: !this.state.animating}); - this.setToggleTimeout(); - }, - 1200 - ); + setToggleTimeout() { + this.setTimeout(() => { + this.setState({animating: !this.state.animating}); + this.setToggleTimeout(); + }, 2000); }, - componentDidMount: function() { + componentDidMount() { this.setToggleTimeout(); }, - render: function() { + render() { return ( - + /> ); } }, { title: 'Gray', - render: function() { + render() { return ( - - ); @@ -92,23 +89,23 @@ exports.examples = [ }, { title: 'Custom colors', - render: function() { + render() { return ( - - - - + + + + ); } }, { title: 'Large', - render: function() { + render() { return ( - @@ -117,22 +114,22 @@ exports.examples = [ }, { title: 'Large, custom colors', - render: function() { + render() { return ( - - - - @@ -142,16 +139,28 @@ exports.examples = [ }, { title: 'Start/stop', - render: function(): ReactElement { + render() { return ; } }, + { + title: 'Custom size', + render() { + return ( + + ); + } + }, ]; -var styles = StyleSheet.create({ +const styles = StyleSheet.create({ centering: { alignItems: 'center', justifyContent: 'center', + padding: 8, }, gray: { backgroundColor: '#cccccc', @@ -159,5 +168,6 @@ var styles = StyleSheet.create({ horizontal: { flexDirection: 'row', justifyContent: 'space-around', + padding: 8, }, }); diff --git a/Examples/UIExplorer/ProgressBarAndroidExample.android.js b/Examples/UIExplorer/ProgressBarAndroidExample.android.js index 19b182bc7..2e08ab738 100644 --- a/Examples/UIExplorer/ProgressBarAndroidExample.android.js +++ b/Examples/UIExplorer/ProgressBarAndroidExample.android.js @@ -49,41 +49,12 @@ var ProgressBarAndroidExample = React.createClass({ statics: { title: '', - description: 'Visual indicator of progress of some operation. ' + - 'Shows either a cyclic animation or a horizontal bar.', + description: 'Horizontal bar to show the progress of some operation.', }, render: function() { return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -92,10 +63,6 @@ var ProgressBarAndroidExample = React.createClass({ - - - - diff --git a/Examples/UIExplorer/UIExplorerList.android.js b/Examples/UIExplorer/UIExplorerList.android.js index ceb3ec844..25279bdf6 100644 --- a/Examples/UIExplorer/UIExplorerList.android.js +++ b/Examples/UIExplorer/UIExplorerList.android.js @@ -23,6 +23,10 @@ export type UIExplorerExample = { }; var ComponentExamples: Array = [ + { + key: 'ActivityIndicatorExample', + module: require('./ActivityIndicatorExample'), + }, { key: 'SliderExample', module: require('./SliderExample'), diff --git a/Examples/UIExplorer/UIExplorerList.ios.js b/Examples/UIExplorer/UIExplorerList.ios.js index e9ddd367b..b93518faa 100644 --- a/Examples/UIExplorer/UIExplorerList.ios.js +++ b/Examples/UIExplorer/UIExplorerList.ios.js @@ -29,8 +29,8 @@ export type UIExplorerExample = { const ComponentExamples: Array = [ { - key: 'ActivityIndicatorIOSExample', - module: require('./ActivityIndicatorIOSExample'), + key: 'ActivityIndicatorExample', + module: require('./ActivityIndicatorExample'), }, { key: 'DatePickerIOSExample', diff --git a/Libraries/Components/ActivityIndicator/ActivityIndicator.js b/Libraries/Components/ActivityIndicator/ActivityIndicator.js new file mode 100644 index 000000000..636f68327 --- /dev/null +++ b/Libraries/Components/ActivityIndicator/ActivityIndicator.js @@ -0,0 +1,127 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ActivityIndicator + * @flow + */ +'use strict'; + +const ColorPropType = require('ColorPropType'); +const NativeMethodsMixin = require('NativeMethodsMixin'); +const Platform = require('Platform'); +const PropTypes = require('ReactPropTypes'); +const React = require('React'); +const StyleSheet = require('StyleSheet'); +const View = require('View'); + +const requireNativeComponent = require('requireNativeComponent'); + +const GRAY = '#999999'; + +/** + * Displays a circular loading indicator. + */ +const ActivityIndicator = React.createClass({ + mixins: [NativeMethodsMixin], + + propTypes: { + ...View.propTypes, + /** + * Whether to show the indicator (true, the default) or hide it (false). + */ + animating: PropTypes.bool, + /** + * The foreground color of the spinner (default is gray). + */ + color: ColorPropType, + /** + * Size of the indicator. Small has a height of 20, large has a height of 36. + * Other sizes can be obtained using a scale transform. + */ + size: PropTypes.oneOf([ + 'small', + 'large', + ]), + /** + * Whether the indicator should hide when not animating (true by default). + * + * @platform ios + */ + hidesWhenStopped: PropTypes.bool, + }, + + getDefaultProps() { + return { + animating: true, + color: GRAY, + hidesWhenStopped: true, + size: 'small', + }; + }, + + render() { + const {onLayout, style, ...props} = this.props; + let sizeStyle; + switch (props.size) { + case 'small': + sizeStyle = styles.sizeSmall; + break; + case 'large': + sizeStyle = styles.sizeLarge; + break; + } + return ( + + + + ); + } +}); + +const styles = StyleSheet.create({ + container: { + alignItems: 'center', + justifyContent: 'center', + }, + sizeSmall: { + width: 20, + height: 20, + }, + sizeLarge: { + width: 36, + height: 36, + }, +}); + +if (Platform.OS === 'ios') { + var RCTActivityIndicator = requireNativeComponent( + 'RCTActivityIndicatorView', + ActivityIndicator, + {nativeOnly: {activityIndicatorViewStyle: true}}, + ); +} else if (Platform.OS === 'android') { + var RCTActivityIndicator = requireNativeComponent( + 'AndroidProgressBar', + ActivityIndicator, + // Ignore props that are specific to non inderterminate ProgressBar. + {nativeOnly: { + indeterminate: true, + progress: true, + styleAttr: true, + }}, + ); +} + +module.exports = ActivityIndicator; diff --git a/Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.android.js b/Libraries/Components/ActivityIndicator/ActivityIndicatorIOS.android.js similarity index 100% rename from Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.android.js rename to Libraries/Components/ActivityIndicator/ActivityIndicatorIOS.android.js diff --git a/Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js b/Libraries/Components/ActivityIndicator/ActivityIndicatorIOS.ios.js similarity index 55% rename from Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js rename to Libraries/Components/ActivityIndicator/ActivityIndicatorIOS.ios.js index fa97eb83a..869a62e5a 100644 --- a/Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js +++ b/Libraries/Components/ActivityIndicator/ActivityIndicatorIOS.ios.js @@ -7,27 +7,18 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule ActivityIndicatorIOS - * @flow */ 'use strict'; +var ActivityIndicator = require('ActivityIndicator'); var NativeMethodsMixin = require('NativeMethodsMixin'); var PropTypes = require('ReactPropTypes'); var React = require('React'); -var StyleSheet = require('StyleSheet'); var View = require('View'); -var requireNativeComponent = require('requireNativeComponent'); - -var GRAY = '#999999'; - -type DefaultProps = { - animating: boolean; - color: string; - hidesWhenStopped: boolean; - size: 'small' | 'large'; -}; - +/** + * Deprecated, use ActivityIndicator instead. + */ var ActivityIndicatorIOS = React.createClass({ mixins: [NativeMethodsMixin], @@ -60,47 +51,13 @@ var ActivityIndicatorIOS = React.createClass({ onLayout: PropTypes.func, }, - getDefaultProps: function(): DefaultProps { - return { - animating: true, - color: GRAY, - hidesWhenStopped: true, - size: 'small', - }; + componentDidMount: function() { + console.warn('ActivityIndicatorIOS is deprecated. Use ActivityIndicator instead.'); }, render: function() { - var {onLayout, style, ...props} = this.props; - var sizeStyle = (this.props.size === 'large') ? styles.sizeLarge : styles.sizeSmall; - return ( - - - - ); + return ; } }); -var styles = StyleSheet.create({ - container: { - alignItems: 'center', - justifyContent: 'center', - }, - sizeSmall: { - width: 20, - height: 20, - }, - sizeLarge: { - width: 36, - height: 36, - } -}); - -var RCTActivityIndicatorView = requireNativeComponent( - 'RCTActivityIndicatorView', - ActivityIndicatorIOS, - {nativeOnly: {activityIndicatorViewStyle: true}}, -); - module.exports = ActivityIndicatorIOS; diff --git a/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js b/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js index 95e5504a4..fb6cb8255 100644 --- a/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js +++ b/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js @@ -13,7 +13,6 @@ var NativeMethodsMixin = require('NativeMethodsMixin'); var React = require('React'); var ReactPropTypes = require('ReactPropTypes'); -var ReactNativeViewAttributes = require('ReactNativeViewAttributes'); var View = require('View'); var ColorPropType = require('ColorPropType'); @@ -107,11 +106,24 @@ var ProgressBarAndroid = React.createClass({ mixins: [NativeMethodsMixin], + componentDidMount: function() { + if (this.props.indeterminate && this.props.styleAttr !== 'Horizontal') { + console.warn( + 'Circular indeterminate `ProgressBarAndroid`' + + 'is deprecated. Use `ActivityIndicator` instead.' + ); + } + }, + render: function() { return ; }, }); -var AndroidProgressBar = requireNativeComponent('AndroidProgressBar', ProgressBarAndroid); +var AndroidProgressBar = requireNativeComponent( + 'AndroidProgressBar', + ProgressBarAndroid, + {nativeOnly: {animating: true}}, +); module.exports = ProgressBarAndroid; diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index bd5e082a0..713becd5d 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -27,6 +27,7 @@ if (__DEV__) { // Export React, plus some native additions. const ReactNative = { // Components + get ActivityIndicator() { return require('ActivityIndicator'); }, get ActivityIndicatorIOS() { return require('ActivityIndicatorIOS'); }, get ART() { return require('ReactNativeART'); }, get DatePickerIOS() { return require('DatePickerIOS'); }, diff --git a/Libraries/react-native/react-native.js.flow b/Libraries/react-native/react-native.js.flow index 74c306f2c..191d3e222 100644 --- a/Libraries/react-native/react-native.js.flow +++ b/Libraries/react-native/react-native.js.flow @@ -25,6 +25,7 @@ // var ReactNative = Object.assign(Object.create(require('ReactNative')), { // Components + ActivityIndicator: require('ActivityIndicator'), ActivityIndicatorIOS: require('ActivityIndicatorIOS'), ART: require('ReactNativeART'), DatePickerIOS: require('DatePickerIOS'), diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ProgressBarContainerView.java b/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ProgressBarContainerView.java index d71b66922..9e29777d2 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ProgressBarContainerView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ProgressBarContainerView.java @@ -7,6 +7,7 @@ import javax.annotation.Nullable; import android.content.Context; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; +import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ProgressBar; @@ -22,6 +23,7 @@ import com.facebook.react.bridge.JSApplicationIllegalArgumentException; private @Nullable Integer mColor; private boolean mIndeterminate = true; + private boolean mAnimating = true; private double mProgress; private @Nullable ProgressBar mProgressBar; @@ -53,6 +55,10 @@ import com.facebook.react.bridge.JSApplicationIllegalArgumentException; mProgress = progress; } + public void setAnimating(boolean animating) { + mAnimating = animating; + } + public void apply() { if (mProgressBar == null) { throw new JSApplicationIllegalArgumentException("setStyle() not called"); @@ -61,6 +67,11 @@ import com.facebook.react.bridge.JSApplicationIllegalArgumentException; mProgressBar.setIndeterminate(mIndeterminate); setColor(mProgressBar); mProgressBar.setProgress((int) (mProgress * MAX_PROGRESS)); + if (mAnimating) { + mProgressBar.setVisibility(View.VISIBLE); + } else { + mProgressBar.setVisibility(View.GONE); + } } private void setColor(ProgressBar progressBar) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ReactProgressBarViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ReactProgressBarViewManager.java index 38454282e..969626489 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ReactProgressBarViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ReactProgressBarViewManager.java @@ -32,6 +32,7 @@ public class ReactProgressBarViewManager extends /* package */ static final String PROP_STYLE = "styleAttr"; /* package */ static final String PROP_INDETERMINATE = "indeterminate"; /* package */ static final String PROP_PROGRESS = "progress"; + /* package */ static final String PROP_ANIMATING = "animating"; /* package */ static final String REACT_CLASS = "AndroidProgressBar"; /* package */ static final String DEFAULT_STYLE = "Normal"; @@ -79,6 +80,11 @@ public class ReactProgressBarViewManager extends view.setProgress(progress); } + @ReactProp(name = PROP_ANIMATING) + public void setAnimating(ProgressBarContainerView view, boolean animating) { + view.setAnimating(animating); + } + @Override public ProgressBarShadowNode createShadowNodeInstance() { return new ProgressBarShadowNode(); diff --git a/docs/AndroidBuildingFromSource.md b/docs/AndroidBuildingFromSource.md index 5213a161d..5336f668e 100644 --- a/docs/AndroidBuildingFromSource.md +++ b/docs/AndroidBuildingFromSource.md @@ -4,7 +4,7 @@ title: Building React Native from source layout: docs category: Guides (Android) permalink: docs/android-building-from-source.html -next: activityindicatorios +next: activityindicator --- You will need to build React Native from source if you want to work on a new feature/bug fix, try out the latest features which are not released yet, or maintain your own fork with patches that cannot be merged to the core. diff --git a/website/server/extractDocs.js b/website/server/extractDocs.js index 77d153861..892927118 100644 --- a/website/server/extractDocs.js +++ b/website/server/extractDocs.js @@ -250,7 +250,7 @@ function renderStyle(filepath) { } const components = [ - '../Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js', + '../Libraries/Components/ActivityIndicator/ActivityIndicator.js', '../Libraries/Components/DatePicker/DatePickerIOS.ios.js', '../Libraries/Components/DrawerAndroid/DrawerLayoutAndroid.android.js', '../Libraries/Image/Image.ios.js',