diff --git a/Examples/UIExplorer/StatusBarExample.js b/Examples/UIExplorer/StatusBarExample.js index e33dbd71a..27acf7e26 100644 --- a/Examples/UIExplorer/StatusBarExample.js +++ b/Examples/UIExplorer/StatusBarExample.js @@ -17,26 +17,13 @@ const React = require('react-native'); const { + StatusBar, StyleSheet, - View, Text, TouchableHighlight, - StatusBar, + View, } = React; -type BarStyle = 'default' | 'light-content'; -type ShowHideTransition = 'fade' | 'slide'; - -type State = { - animated: boolean, - backgroundColor: string, - hidden?: boolean, - showHideTransition: ShowHideTransition, - translucent?: boolean, - barStyle?: BarStyle, - networkActivityIndicatorVisible?: boolean -}; - exports.framework = 'React'; exports.title = ''; exports.description = 'Component for controlling the status bar'; @@ -57,242 +44,412 @@ const showHideTransitions = [ 'slide', ]; -function getValue(values: Array, index: number): any { +function getValue(values: Array, index: number): T { return values[index % values.length]; } -const StatusBarExample = React.createClass({ - getInitialState(): State { +const StatusBarHiddenExample = React.createClass({ + getInitialState() { return { animated: true, - backgroundColor: getValue(colors, 0), + hidden: false, showHideTransition: getValue(showHideTransitions, 0), }; }, - _colorIndex: 0, - _barStyleIndex: 0, _showHideTransitionIndex: 0, + _onChangeAnimated() { + this.setState({animated: !this.state.animated}); + }, + + _onChangeHidden() { + this.setState({hidden: !this.state.hidden}); + }, + + _onChangeTransition() { + this._showHideTransitionIndex++; + this.setState({ + showHideTransition: getValue(showHideTransitions, this._showHideTransitionIndex), + }); + }, + + render() { + return ( + + + ); + }, +}); + +const StatusBarStyleExample = React.createClass({ + getInitialState() { + return { + animated: true, + barStyle: getValue(barStyles, this._barStyleIndex), + }; + }, + + _barStyleIndex: 0, + + _onChangeBarStyle() { + this._barStyleIndex++; + this.setState({barStyle: getValue(barStyles, this._barStyleIndex)}); + }, + + _onChangeAnimated() { + this.setState({animated: !this.state.animated}); + }, + + render() { + return ( + + + + + style: '{getValue(barStyles, this._barStyleIndex)}' + + + + + animated: {this.state.animated ? 'true' : 'false'} + + + + ); + }, +}); + +const StatusBarNetworkActivityExample = React.createClass({ + getInitialState() { + return { + networkActivityIndicatorVisible: false, + }; + }, + + _onChangeNetworkIndicatorVisible() { + this.setState({ + networkActivityIndicatorVisible: !this.state.networkActivityIndicatorVisible, + }); + }, + + render() { + return ( + + + + + + networkActivityIndicatorVisible: + {this.state.networkActivityIndicatorVisible ? 'true' : 'false'} + + + + + ); + }, +}); + +const StatusBarBackgroundColorExample = React.createClass({ + getInitialState() { + return { + animated: true, + backgroundColor: getValue(colors, 0), + }; + }, + + _colorIndex: 0, + + _onChangeBackgroundColor() { + this._colorIndex++; + this.setState({backgroundColor: getValue(colors, this._colorIndex)}); + }, + + _onChangeAnimated() { + this.setState({animated: !this.state.animated}); + }, + render() { return ( ); }, }); -const StatusBarStaticExample = React.createClass({ - _colorIndex: 0, - _barStyleIndex: 0, - _showHideTransitionIndex: 0, +const StatusBarTranslucentExample = React.createClass({ getInitialState() { return { - backgroundColor: getValue(colors, 0), - barStyle: getValue(barStyles, 0), - hidden: false, - networkActivityIndicatorVisible: false, translucent: false, }; }, + _onChangeTranslucent() { + this.setState({ + translucent: !this.state.translucent, + }); + }, + render() { return ( - - { - const hidden = !this.state.hidden; - StatusBar.setHidden(hidden, 'slide'); - this.setState({hidden}); - }}> - - hidden: {this.state.hidden ? 'true' : 'false'} - - - - iOS - - { - this._barStyleIndex++; - const barStyle = getValue(barStyles, this._barStyleIndex); - StatusBar.setBarStyle(barStyle, true); - this.setState({barStyle}); - }}> - - style: '{getValue(barStyles, this._barStyleIndex)}' - - - - - { - const networkActivityIndicatorVisible = !this.state.networkActivityIndicatorVisible; - StatusBar.setNetworkActivityIndicatorVisible(networkActivityIndicatorVisible); - this.setState({networkActivityIndicatorVisible}); - }}> - - - networkActivityIndicatorVisible: - {this.state.networkActivityIndicatorVisible ? 'true' : 'false'} - - - - - Android - - { - this._colorIndex++; - const backgroundColor = getValue(colors, this._colorIndex); - StatusBar.setBackgroundColor(backgroundColor, true); - this.setState({backgroundColor}); - }}> - - backgroundColor: '{getValue(colors, this._colorIndex)}' - - - - - { - const translucent = !this.state.translucent; - const backgroundColor = !this.state.translucent ? 'rgba(0, 0, 0, 0.4)' : 'black'; - StatusBar.setTranslucent(translucent); - StatusBar.setBackgroundColor(backgroundColor, true); - this.setState({ - translucent, - backgroundColor, - }); - }}> - - translucent: {this.state.translucent ? 'true' : 'false'} - - - + + + + translucent: {this.state.translucent ? 'true' : 'false'} + + ); }, }); -exports.examples = [{ - title: 'StatusBar', +const StatusBarStaticIOSExample = React.createClass({ render() { - return ; + return ( + + { + StatusBar.setHidden(true, 'slide'); + }}> + + setHidden(true, 'slide') + + + { + StatusBar.setHidden(false, 'fade'); + }}> + + setHidden(false, 'fade') + + + { + StatusBar.setBarStyle('default', true); + }}> + + setBarStyle('default', true) + + + { + StatusBar.setBarStyle('light-content', true); + }}> + + setBarStyle('light-content', true) + + + { + StatusBar.setNetworkActivityIndicatorVisible(true); + }}> + + setNetworkActivityIndicatorVisible(true) + + + { + StatusBar.setNetworkActivityIndicatorVisible(false); + }}> + + setNetworkActivityIndicatorVisible(false) + + + + ); }, +}); + +const StatusBarStaticAndroidExample = React.createClass({ + render() { + return ( + + { + StatusBar.setHidden(true); + }}> + + setHidden(true) + + + { + StatusBar.setHidden(false); + }}> + + setHidden(false) + + + { + StatusBar.setBackgroundColor('#ff00ff', true); + }}> + + setBackgroundColor('#ff00ff', true) + + + { + StatusBar.setBackgroundColor('#00ff00', true); + }}> + + setBackgroundColor('#00ff00', true) + + + { + StatusBar.setTranslucent(true); + StatusBar.setBackgroundColor('rgba(0, 0, 0, 0.4)', true); + }}> + + setTranslucent(true) and setBackgroundColor('rgba(0, 0, 0, 0.4)', true) + + + { + StatusBar.setTranslucent(false); + StatusBar.setBackgroundColor('black', true); + }}> + + setTranslucent(false) and setBackgroundColor('black', true) + + + + ); + }, +}); + +const examples = [{ + title: 'StatusBar hidden', + render() { + return ; + }, +}, { + title: 'StatusBar style', + render() { + return ; + }, + platform: 'ios', +}, { + title: 'StatusBar network activity indicator', + render() { + return ; + }, + platform: 'ios', +}, { + title: 'StatusBar background color', + render() { + return ; + }, + platform: 'android', +}, { + title: 'StatusBar background color', + render() { + return ; + }, + platform: 'android', }, { title: 'StatusBar static API', render() { - return ; + return ; }, + platform: 'ios', +}, { + title: 'StatusBar static API', + render() { + return ; + }, + platform: 'android', +}, { + title: 'StatusBar dimensions', + render() { + return ( + + Height: {StatusBar.currentHeight} pts + + ); + }, + platform: 'android', }]; +exports.examples = examples; + var styles = StyleSheet.create({ wrapper: { borderRadius: 5, diff --git a/Libraries/Components/StatusBar/StatusBar.js b/Libraries/Components/StatusBar/StatusBar.js index 159577b58..b7872ef56 100644 --- a/Libraries/Components/StatusBar/StatusBar.js +++ b/Libraries/Components/StatusBar/StatusBar.js @@ -39,8 +39,37 @@ type DefaultProps = { */ function mergePropsStack(propsStack: Array, defaultValues: Object): Object { return propsStack.reduce((prev, cur) => { - return Object.assign(prev, cur); - }, defaultValues); + for (let prop in cur) { + if (cur[prop] != null) { + prev[prop] = cur[prop]; + } + } + return prev; + }, Object.assign({}, defaultValues)); +} + +/** + * Returns an object to insert in the props stack from the props + * and the transition/animation info. + */ +function createStackEntry(props: any): any { + return { + backgroundColor: props.backgroundColor != null ? { + value: props.backgroundColor, + animated: props.animated, + } : null, + barStyle: props.barStyle != null ? { + value: props.barStyle, + animated: props.animated, + } : null, + translucent: props.translucent, + hidden: props.hidden != null ? { + value: props.hidden, + animated: props.animated, + transition: props.showHideTransition, + } : null, + networkActivityIndicatorVisible: props.networkActivityIndicatorVisible, + }; } /** @@ -81,19 +110,34 @@ function mergePropsStack(propsStack: Array, defaultValues: Object): Obje const StatusBar = React.createClass({ statics: { _propsStack: [], - _defaultProps: { + _defaultProps: createStackEntry({ + animated: false, + showHideTransition: 'fade', backgroundColor: 'black', barStyle: 'default', translucent: false, hidden: false, networkActivityIndicatorVisible: false, - }, + }), + // Timer for updating the native module values at the end of the frame. + _updateImmediate: null, + // The current merged values from the props stack. + _currentValues: null, + + // TODO(janic): Provide a real API to deal with status bar height. See the + // discussion in #6195. + /** + * The current height of the status bar on the device. + * + * @platform android + */ + currentHeight: StatusBarManager.HEIGHT, // Provide an imperative API as static functions of the component. // See the corresponding prop for more detail. setHidden(hidden: boolean, animation?: StatusBarAnimation) { animation = animation || 'none'; - StatusBar._defaultProps.hidden = hidden; + StatusBar._defaultProps.hidden.value = hidden; if (Platform.OS === 'ios') { StatusBarManager.setHidden(hidden, animation); } else if (Platform.OS === 'android') { @@ -107,7 +151,7 @@ const StatusBar = React.createClass({ return; } animated = animated || false; - StatusBar._defaultProps.barStyle = style; + StatusBar._defaultProps.barStyle.value = style; StatusBarManager.setStyle(style, animated); }, @@ -126,7 +170,7 @@ const StatusBar = React.createClass({ return; } animated = animated || false; - StatusBar._defaultProps.backgroundColor = color; + StatusBar._defaultProps.backgroundColor.value = color; StatusBarManager.setColor(processColor(color), animated); }, @@ -197,27 +241,31 @@ const StatusBar = React.createClass({ }; }, + _stackEntry: null, + componentDidMount() { // Every time a StatusBar component is mounted, we push it's prop to a stack // and always update the native status bar with the props from the top of then // stack. This allows having multiple StatusBar components and the one that is // added last or is deeper in the view hierachy will have priority. - StatusBar._propsStack.push(this.props); + this._stackEntry = createStackEntry(this.props); + StatusBar._propsStack.push(this._stackEntry); this._updatePropsStack(); }, componentWillUnmount() { // When a StatusBar is unmounted, remove itself from the stack and update // the native bar with the next props. - const index = StatusBar._propsStack.indexOf(this.props); + const index = StatusBar._propsStack.indexOf(this._stackEntry); StatusBar._propsStack.splice(index, 1); this._updatePropsStack(); }, - componentDidUpdate(oldProps: Object) { - const index = StatusBar._propsStack.indexOf(oldProps); - StatusBar._propsStack[index] = this.props; + componentDidUpdate() { + const index = StatusBar._propsStack.indexOf(this._stackEntry); + this._stackEntry = createStackEntry(this.props); + StatusBar._propsStack[index] = this._stackEntry; this._updatePropsStack(); }, @@ -226,34 +274,51 @@ const StatusBar = React.createClass({ * Updates the native status bar with the props from the stack. */ _updatePropsStack() { - const mergedProps = mergePropsStack(StatusBar._propsStack, StatusBar._defaultProps); + // Send the update to the native module only once at the end of the frame. + clearImmediate(StatusBar._updateImmediate); + StatusBar._updateImmediate = setImmediate(() => { + const oldProps = StatusBar._currentValues; + const mergedProps = mergePropsStack(StatusBar._propsStack, StatusBar._defaultProps); - if (Platform.OS === 'ios') { - if (mergedProps.barStyle !== undefined) { - StatusBarManager.setStyle(mergedProps.barStyle, this.props.animated); + // Update the props that have changed using the merged values from the props stack. + if (Platform.OS === 'ios') { + if (!oldProps || oldProps.barStyle.value !== mergedProps.barStyle.value) { + StatusBarManager.setStyle( + mergedProps.barStyle.value, + mergedProps.barStyle.animated, + ); + } + if (!oldProps || oldProps.hidden.value !== mergedProps.hidden.value) { + StatusBarManager.setHidden( + mergedProps.hidden.value, + mergedProps.hidden.animated ? + mergedProps.hidden.transition : + 'none', + ); + } + + if (!oldProps || oldProps.networkActivityIndicatorVisible !== mergedProps.networkActivityIndicatorVisible) { + StatusBarManager.setNetworkActivityIndicatorVisible( + mergedProps.networkActivityIndicatorVisible + ); + } + } else if (Platform.OS === 'android') { + if (!oldProps || oldProps.backgroundColor.value !== mergedProps.backgroundColor.value) { + StatusBarManager.setColor( + processColor(mergedProps.backgroundColor.value), + mergedProps.backgroundColor.animated, + ); + } + if (!oldProps || oldProps.hidden.value !== mergedProps.hidden.value) { + StatusBarManager.setHidden(mergedProps.hidden.value); + } + if (!oldProps || oldProps.translucent !== mergedProps.translucent) { + StatusBarManager.setTranslucent(mergedProps.translucent); + } } - if (mergedProps.hidden !== undefined) { - StatusBarManager.setHidden( - mergedProps.hidden, - this.props.animated ? this.props.showHideTransition : 'none' - ); - } - if (mergedProps.networkActivityIndicatorVisible !== undefined) { - StatusBarManager.setNetworkActivityIndicatorVisible( - mergedProps.networkActivityIndicatorVisible - ); - } - } else if (Platform.OS === 'android') { - if (mergedProps.backgroundColor !== undefined) { - StatusBarManager.setColor(processColor(mergedProps.backgroundColor), this.props.animated); - } - if (mergedProps.hidden !== undefined) { - StatusBarManager.setHidden(mergedProps.hidden); - } - if (mergedProps.translucent !== undefined) { - StatusBarManager.setTranslucent(mergedProps.translucent); - } - } + // Update the current prop values. + StatusBar._currentValues = mergedProps; + }); }, render(): ?ReactElement { diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/statusbar/BUCK b/ReactAndroid/src/main/java/com/facebook/react/modules/statusbar/BUCK index 3cedd2172..ba21450a2 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/statusbar/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/statusbar/BUCK @@ -6,6 +6,7 @@ android_library( deps = [ react_native_target('java/com/facebook/react/bridge:bridge'), react_native_target('java/com/facebook/react/common:common'), + react_native_target('java/com/facebook/react/uimanager:uimanager'), react_native_dep('third-party/android/support/v4:lib-support-v4'), react_native_dep('third-party/java/infer-annotations:infer-annotations'), react_native_dep('third-party/java/jsr-305:jsr-305'), diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/statusbar/StatusBarModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/statusbar/StatusBarModule.java index 29d08a833..efe9fc920 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/statusbar/StatusBarModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/statusbar/StatusBarModule.java @@ -7,30 +7,41 @@ * of patent rights can be found in the PATENTS file in the same directory. */ - package com.facebook.react.modules.statusbar; import android.animation.ArgbEvaluator; import android.animation.ValueAnimator; import android.annotation.TargetApi; import android.app.Activity; +import android.content.Context; import android.os.Build; import android.support.v4.view.ViewCompat; import android.view.View; import android.view.WindowManager; +import java.util.Map; + +import javax.annotation.Nullable; + import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.UiThreadUtil; +import com.facebook.react.common.MapBuilder; +import com.facebook.react.uimanager.PixelUtil; +/** + * {@link NativeModule} that allows changing the appearance of the status bar. + */ public class StatusBarModule extends ReactContextBaseJavaModule { private static final String ERROR_NO_ACTIVITY = "E_NO_ACTIVITY"; private static final String ERROR_NO_ACTIVITY_MESSAGE = "Tried to change the status bar while not attached to an Activity"; + private static final String HEIGHT_KEY = "HEIGHT"; + public StatusBarModule(ReactApplicationContext reactContext) { super(reactContext); } @@ -40,6 +51,19 @@ public class StatusBarModule extends ReactContextBaseJavaModule { return "StatusBarManager"; } + @Override + public @Nullable Map getConstants() { + final Context context = getReactApplicationContext(); + final int heightResId = context.getResources().getIdentifier("status_bar_height", "dimen", "android"); + final float height = heightResId > 0 ? + PixelUtil.toDIPFromPixel(context.getResources().getDimensionPixelSize(heightResId)) : + 0; + + return MapBuilder.of( + HEIGHT_KEY, height + ); + } + @ReactMethod public void setColor(final int color, final boolean animated, final Promise res) { final Activity activity = getCurrentActivity();