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
This commit is contained in:
Janic Duplessis 2016-05-26 13:46:58 -07:00 committed by Facebook Github Bot 1
parent 98dd91825f
commit 26e8426248
14 changed files with 229 additions and 133 deletions

View File

@ -15,41 +15,38 @@
*/ */
'use strict'; 'use strict';
var React = require('react'); const React = require('react');
var ReactNative = require('react-native'); const ReactNative = require('react-native');
var { const {
ActivityIndicatorIOS, ActivityIndicator,
StyleSheet, StyleSheet,
View, View,
} = ReactNative; } = ReactNative;
var TimerMixin = require('react-timer-mixin'); const TimerMixin = require('react-timer-mixin');
var ToggleAnimatingActivityIndicator = React.createClass({ const ToggleAnimatingActivityIndicator = React.createClass({
mixins: [TimerMixin], mixins: [TimerMixin],
getInitialState: function() { getInitialState() {
return { return {
animating: true, animating: true,
}; };
}, },
setToggleTimeout: function() { setToggleTimeout() {
this.setTimeout( this.setTimeout(() => {
() => { this.setState({animating: !this.state.animating});
this.setState({animating: !this.state.animating}); this.setToggleTimeout();
this.setToggleTimeout(); }, 2000);
},
1200
);
}, },
componentDidMount: function() { componentDidMount() {
this.setToggleTimeout(); this.setToggleTimeout();
}, },
render: function() { render() {
return ( return (
<ActivityIndicatorIOS <ActivityIndicator
animating={this.state.animating} animating={this.state.animating}
style={[styles.centering, {height: 80}]} style={[styles.centering, {height: 80}]}
size="large" size="large"
@ -60,31 +57,31 @@ var ToggleAnimatingActivityIndicator = React.createClass({
exports.displayName = (undefined: ?string); exports.displayName = (undefined: ?string);
exports.framework = 'React'; exports.framework = 'React';
exports.title = '<ActivityIndicatorIOS>'; exports.title = '<ActivityIndicator>';
exports.description = 'Animated loading indicators.'; exports.description = 'Animated loading indicators.';
exports.examples = [ exports.examples = [
{ {
title: 'Default (small, white)', title: 'Default (small, white)',
render: function() { render() {
return ( return (
<ActivityIndicatorIOS <ActivityIndicator
style={[styles.centering, styles.gray, {height: 40}]} style={[styles.centering, styles.gray]}
color="white" color="white"
/> />
); );
} }
}, },
{ {
title: 'Gray', title: 'Gray',
render: function() { render() {
return ( return (
<View> <View>
<ActivityIndicatorIOS <ActivityIndicator
style={[styles.centering, {height: 40}]} style={[styles.centering]}
/> />
<ActivityIndicatorIOS <ActivityIndicator
style={[styles.centering, {backgroundColor: '#eeeeee', height: 40}]} style={[styles.centering, {backgroundColor: '#eeeeee'}]}
/> />
</View> </View>
); );
@ -92,23 +89,23 @@ exports.examples = [
}, },
{ {
title: 'Custom colors', title: 'Custom colors',
render: function() { render() {
return ( return (
<View style={styles.horizontal}> <View style={styles.horizontal}>
<ActivityIndicatorIOS color="#0000ff" /> <ActivityIndicator color="#0000ff" />
<ActivityIndicatorIOS color="#aa00aa" /> <ActivityIndicator color="#aa00aa" />
<ActivityIndicatorIOS color="#aa3300" /> <ActivityIndicator color="#aa3300" />
<ActivityIndicatorIOS color="#00aa00" /> <ActivityIndicator color="#00aa00" />
</View> </View>
); );
} }
}, },
{ {
title: 'Large', title: 'Large',
render: function() { render() {
return ( return (
<ActivityIndicatorIOS <ActivityIndicator
style={[styles.centering, styles.gray, {height: 80}]} style={[styles.centering, styles.gray]}
color="white" color="white"
size="large" size="large"
/> />
@ -117,22 +114,22 @@ exports.examples = [
}, },
{ {
title: 'Large, custom colors', title: 'Large, custom colors',
render: function() { render() {
return ( return (
<View style={styles.horizontal}> <View style={styles.horizontal}>
<ActivityIndicatorIOS <ActivityIndicator
size="large" size="large"
color="#0000ff" color="#0000ff"
/> />
<ActivityIndicatorIOS <ActivityIndicator
size="large" size="large"
color="#aa00aa" color="#aa00aa"
/> />
<ActivityIndicatorIOS <ActivityIndicator
size="large" size="large"
color="#aa3300" color="#aa3300"
/> />
<ActivityIndicatorIOS <ActivityIndicator
size="large" size="large"
color="#00aa00" color="#00aa00"
/> />
@ -142,16 +139,28 @@ exports.examples = [
}, },
{ {
title: 'Start/stop', title: 'Start/stop',
render: function(): ReactElement<any> { render() {
return <ToggleAnimatingActivityIndicator />; return <ToggleAnimatingActivityIndicator />;
} }
}, },
{
title: 'Custom size',
render() {
return (
<ActivityIndicator
style={[styles.centering, {transform: [{scale: 1.5}]}]}
size="large"
/>
);
}
},
]; ];
var styles = StyleSheet.create({ const styles = StyleSheet.create({
centering: { centering: {
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
padding: 8,
}, },
gray: { gray: {
backgroundColor: '#cccccc', backgroundColor: '#cccccc',
@ -159,5 +168,6 @@ var styles = StyleSheet.create({
horizontal: { horizontal: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-around', justifyContent: 'space-around',
padding: 8,
}, },
}); });

View File

@ -49,41 +49,12 @@ var ProgressBarAndroidExample = React.createClass({
statics: { statics: {
title: '<ProgressBarAndroid>', title: '<ProgressBarAndroid>',
description: 'Visual indicator of progress of some operation. ' + description: 'Horizontal bar to show the progress of some operation.',
'Shows either a cyclic animation or a horizontal bar.',
}, },
render: function() { render: function() {
return ( return (
<UIExplorerPage title="ProgressBar Examples"> <UIExplorerPage title="ProgressBar Examples">
<UIExplorerBlock title="Default ProgressBar">
<ProgressBar />
</UIExplorerBlock>
<UIExplorerBlock title="Normal ProgressBar">
<ProgressBar styleAttr="Normal" />
</UIExplorerBlock>
<UIExplorerBlock title="Small ProgressBar">
<ProgressBar styleAttr="Small" />
</UIExplorerBlock>
<UIExplorerBlock title="Large ProgressBar">
<ProgressBar styleAttr="Large" />
</UIExplorerBlock>
<UIExplorerBlock title="Inverse ProgressBar">
<ProgressBar styleAttr="Inverse" />
</UIExplorerBlock>
<UIExplorerBlock title="Small Inverse ProgressBar">
<ProgressBar styleAttr="SmallInverse" />
</UIExplorerBlock>
<UIExplorerBlock title="Large Inverse ProgressBar">
<ProgressBar styleAttr="LargeInverse" />
</UIExplorerBlock>
<UIExplorerBlock title="Horizontal Indeterminate ProgressBar"> <UIExplorerBlock title="Horizontal Indeterminate ProgressBar">
<ProgressBar styleAttr="Horizontal" /> <ProgressBar styleAttr="Horizontal" />
</UIExplorerBlock> </UIExplorerBlock>
@ -92,10 +63,6 @@ var ProgressBarAndroidExample = React.createClass({
<MovingBar styleAttr="Horizontal" indeterminate={false} /> <MovingBar styleAttr="Horizontal" indeterminate={false} />
</UIExplorerBlock> </UIExplorerBlock>
<UIExplorerBlock title="Large Red ProgressBar">
<ProgressBar styleAttr="Large" color="red" />
</UIExplorerBlock>
<UIExplorerBlock title="Horizontal Black Indeterminate ProgressBar"> <UIExplorerBlock title="Horizontal Black Indeterminate ProgressBar">
<ProgressBar styleAttr="Horizontal" color="black" /> <ProgressBar styleAttr="Horizontal" color="black" />
</UIExplorerBlock> </UIExplorerBlock>

View File

@ -23,6 +23,10 @@ export type UIExplorerExample = {
}; };
var ComponentExamples: Array<UIExplorerExample> = [ var ComponentExamples: Array<UIExplorerExample> = [
{
key: 'ActivityIndicatorExample',
module: require('./ActivityIndicatorExample'),
},
{ {
key: 'SliderExample', key: 'SliderExample',
module: require('./SliderExample'), module: require('./SliderExample'),

View File

@ -29,8 +29,8 @@ export type UIExplorerExample = {
const ComponentExamples: Array<UIExplorerExample> = [ const ComponentExamples: Array<UIExplorerExample> = [
{ {
key: 'ActivityIndicatorIOSExample', key: 'ActivityIndicatorExample',
module: require('./ActivityIndicatorIOSExample'), module: require('./ActivityIndicatorExample'),
}, },
{ {
key: 'DatePickerIOSExample', key: 'DatePickerIOSExample',

View File

@ -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 (
<View
onLayout={onLayout}
style={[styles.container, style]}>
<RCTActivityIndicator
{...props}
style={sizeStyle}
styleAttr="Normal"
indeterminate
/>
</View>
);
}
});
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;

View File

@ -7,27 +7,18 @@
* of patent rights can be found in the PATENTS file in the same directory. * of patent rights can be found in the PATENTS file in the same directory.
* *
* @providesModule ActivityIndicatorIOS * @providesModule ActivityIndicatorIOS
* @flow
*/ */
'use strict'; 'use strict';
var ActivityIndicator = require('ActivityIndicator');
var NativeMethodsMixin = require('NativeMethodsMixin'); var NativeMethodsMixin = require('NativeMethodsMixin');
var PropTypes = require('ReactPropTypes'); var PropTypes = require('ReactPropTypes');
var React = require('React'); var React = require('React');
var StyleSheet = require('StyleSheet');
var View = require('View'); var View = require('View');
var requireNativeComponent = require('requireNativeComponent'); /**
* Deprecated, use ActivityIndicator instead.
var GRAY = '#999999'; */
type DefaultProps = {
animating: boolean;
color: string;
hidesWhenStopped: boolean;
size: 'small' | 'large';
};
var ActivityIndicatorIOS = React.createClass({ var ActivityIndicatorIOS = React.createClass({
mixins: [NativeMethodsMixin], mixins: [NativeMethodsMixin],
@ -60,47 +51,13 @@ var ActivityIndicatorIOS = React.createClass({
onLayout: PropTypes.func, onLayout: PropTypes.func,
}, },
getDefaultProps: function(): DefaultProps { componentDidMount: function() {
return { console.warn('ActivityIndicatorIOS is deprecated. Use ActivityIndicator instead.');
animating: true,
color: GRAY,
hidesWhenStopped: true,
size: 'small',
};
}, },
render: function() { render: function() {
var {onLayout, style, ...props} = this.props; return <ActivityIndicator {...this.props} />;
var sizeStyle = (this.props.size === 'large') ? styles.sizeLarge : styles.sizeSmall;
return (
<View
onLayout={onLayout}
style={[styles.container, style]}>
<RCTActivityIndicatorView {...props} style={sizeStyle} />
</View>
);
} }
}); });
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; module.exports = ActivityIndicatorIOS;

View File

@ -13,7 +13,6 @@
var NativeMethodsMixin = require('NativeMethodsMixin'); var NativeMethodsMixin = require('NativeMethodsMixin');
var React = require('React'); var React = require('React');
var ReactPropTypes = require('ReactPropTypes'); var ReactPropTypes = require('ReactPropTypes');
var ReactNativeViewAttributes = require('ReactNativeViewAttributes');
var View = require('View'); var View = require('View');
var ColorPropType = require('ColorPropType'); var ColorPropType = require('ColorPropType');
@ -107,11 +106,24 @@ var ProgressBarAndroid = React.createClass({
mixins: [NativeMethodsMixin], mixins: [NativeMethodsMixin],
componentDidMount: function() {
if (this.props.indeterminate && this.props.styleAttr !== 'Horizontal') {
console.warn(
'Circular indeterminate `ProgressBarAndroid`' +
'is deprecated. Use `ActivityIndicator` instead.'
);
}
},
render: function() { render: function() {
return <AndroidProgressBar {...this.props} />; return <AndroidProgressBar {...this.props} />;
}, },
}); });
var AndroidProgressBar = requireNativeComponent('AndroidProgressBar', ProgressBarAndroid); var AndroidProgressBar = requireNativeComponent(
'AndroidProgressBar',
ProgressBarAndroid,
{nativeOnly: {animating: true}},
);
module.exports = ProgressBarAndroid; module.exports = ProgressBarAndroid;

View File

@ -27,6 +27,7 @@ if (__DEV__) {
// Export React, plus some native additions. // Export React, plus some native additions.
const ReactNative = { const ReactNative = {
// Components // Components
get ActivityIndicator() { return require('ActivityIndicator'); },
get ActivityIndicatorIOS() { return require('ActivityIndicatorIOS'); }, get ActivityIndicatorIOS() { return require('ActivityIndicatorIOS'); },
get ART() { return require('ReactNativeART'); }, get ART() { return require('ReactNativeART'); },
get DatePickerIOS() { return require('DatePickerIOS'); }, get DatePickerIOS() { return require('DatePickerIOS'); },

View File

@ -25,6 +25,7 @@
// //
var ReactNative = Object.assign(Object.create(require('ReactNative')), { var ReactNative = Object.assign(Object.create(require('ReactNative')), {
// Components // Components
ActivityIndicator: require('ActivityIndicator'),
ActivityIndicatorIOS: require('ActivityIndicatorIOS'), ActivityIndicatorIOS: require('ActivityIndicatorIOS'),
ART: require('ReactNativeART'), ART: require('ReactNativeART'),
DatePickerIOS: require('DatePickerIOS'), DatePickerIOS: require('DatePickerIOS'),

View File

@ -7,6 +7,7 @@ import javax.annotation.Nullable;
import android.content.Context; import android.content.Context;
import android.graphics.PorterDuff; import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.ProgressBar; import android.widget.ProgressBar;
@ -22,6 +23,7 @@ import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
private @Nullable Integer mColor; private @Nullable Integer mColor;
private boolean mIndeterminate = true; private boolean mIndeterminate = true;
private boolean mAnimating = true;
private double mProgress; private double mProgress;
private @Nullable ProgressBar mProgressBar; private @Nullable ProgressBar mProgressBar;
@ -53,6 +55,10 @@ import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
mProgress = progress; mProgress = progress;
} }
public void setAnimating(boolean animating) {
mAnimating = animating;
}
public void apply() { public void apply() {
if (mProgressBar == null) { if (mProgressBar == null) {
throw new JSApplicationIllegalArgumentException("setStyle() not called"); throw new JSApplicationIllegalArgumentException("setStyle() not called");
@ -61,6 +67,11 @@ import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
mProgressBar.setIndeterminate(mIndeterminate); mProgressBar.setIndeterminate(mIndeterminate);
setColor(mProgressBar); setColor(mProgressBar);
mProgressBar.setProgress((int) (mProgress * MAX_PROGRESS)); mProgressBar.setProgress((int) (mProgress * MAX_PROGRESS));
if (mAnimating) {
mProgressBar.setVisibility(View.VISIBLE);
} else {
mProgressBar.setVisibility(View.GONE);
}
} }
private void setColor(ProgressBar progressBar) { private void setColor(ProgressBar progressBar) {

View File

@ -32,6 +32,7 @@ public class ReactProgressBarViewManager extends
/* package */ static final String PROP_STYLE = "styleAttr"; /* package */ static final String PROP_STYLE = "styleAttr";
/* package */ static final String PROP_INDETERMINATE = "indeterminate"; /* package */ static final String PROP_INDETERMINATE = "indeterminate";
/* package */ static final String PROP_PROGRESS = "progress"; /* 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 REACT_CLASS = "AndroidProgressBar";
/* package */ static final String DEFAULT_STYLE = "Normal"; /* package */ static final String DEFAULT_STYLE = "Normal";
@ -79,6 +80,11 @@ public class ReactProgressBarViewManager extends
view.setProgress(progress); view.setProgress(progress);
} }
@ReactProp(name = PROP_ANIMATING)
public void setAnimating(ProgressBarContainerView view, boolean animating) {
view.setAnimating(animating);
}
@Override @Override
public ProgressBarShadowNode createShadowNodeInstance() { public ProgressBarShadowNode createShadowNodeInstance() {
return new ProgressBarShadowNode(); return new ProgressBarShadowNode();

View File

@ -4,7 +4,7 @@ title: Building React Native from source
layout: docs layout: docs
category: Guides (Android) category: Guides (Android)
permalink: docs/android-building-from-source.html 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. 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.

View File

@ -250,7 +250,7 @@ function renderStyle(filepath) {
} }
const components = [ const components = [
'../Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js', '../Libraries/Components/ActivityIndicator/ActivityIndicator.js',
'../Libraries/Components/DatePicker/DatePickerIOS.ios.js', '../Libraries/Components/DatePicker/DatePickerIOS.ios.js',
'../Libraries/Components/DrawerAndroid/DrawerLayoutAndroid.android.js', '../Libraries/Components/DrawerAndroid/DrawerLayoutAndroid.android.js',
'../Libraries/Image/Image.ios.js', '../Libraries/Image/Image.ios.js',