diff --git a/Examples/UIExplorer/StatusBarExample.js b/Examples/UIExplorer/StatusBarExample.js new file mode 100644 index 000000000..094148c4d --- /dev/null +++ b/Examples/UIExplorer/StatusBarExample.js @@ -0,0 +1,206 @@ +/** + * 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. + * + * @flow + */ +'use strict'; + +const React = require('react-native'); +const { + StyleSheet, + View, + Text, + TouchableHighlight, + StatusBar, +} = 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'; + +const colors = [ + '#ff0000', + '#00ff00', + '#0000ff', +]; + +const barStyles = [ + 'default', + 'light-content', +]; + +const showHideTransitions = [ + 'fade', + 'slide', +]; + +const StatusBarExample = React.createClass({ + getInitialState(): State { + return { + animated: true, + backgroundColor: this._getValue(colors, 0), + showHideTransition: this._getValue(showHideTransitions, 0), + }; + }, + + _colorIndex: 0, + _barStyleIndex: 0, + _showHideTransitionIndex: 0, + + _getValue(values: Array, index: number): any { + return values[index % values.length]; + }, + + render() { + return ( + + + ); + }, +}); + +exports.examples = [{ + title: 'Status Bar', + render() { + return ; + }, +}]; + +var styles = StyleSheet.create({ + wrapper: { + borderRadius: 5, + marginBottom: 5, + }, + button: { + borderRadius: 5, + backgroundColor: '#eeeeee', + padding: 10, + }, + title: { + marginTop: 16, + marginBottom: 8, + fontWeight: 'bold', + } +}); diff --git a/Examples/UIExplorer/UIExplorerApp.android.js b/Examples/UIExplorer/UIExplorerApp.android.js index c9bd2418f..25a4745bf 100644 --- a/Examples/UIExplorer/UIExplorerApp.android.js +++ b/Examples/UIExplorer/UIExplorerApp.android.js @@ -25,6 +25,7 @@ var { StyleSheet, ToolbarAndroid, View, + StatusBar, } = React; var UIExplorerList = require('./UIExplorerList.android'); @@ -98,6 +99,9 @@ var UIExplorerApp = React.createClass({ var Component = this.state.example.component; return ( + { - this.setState({ openExternalExample: example, }); - }, - } - }} - itemWrapperStyle={styles.itemWrapper} - tintColor="#008888" - /> + + + { + this.setState({ openExternalExample: example, }); + }, + } + }} + itemWrapperStyle={styles.itemWrapper} + tintColor="#008888" + /> + ); } }); diff --git a/Examples/UIExplorer/UIExplorerList.android.js b/Examples/UIExplorer/UIExplorerList.android.js index 367b256de..ed84e62aa 100644 --- a/Examples/UIExplorer/UIExplorerList.android.js +++ b/Examples/UIExplorer/UIExplorerList.android.js @@ -30,6 +30,7 @@ var COMPONENTS = [ require('./PullToRefreshViewAndroidExample.android'), require('./RefreshControlExample'), require('./ScrollViewSimpleExample'), + require('./StatusBarExample'), require('./SwitchExample'), require('./TextExample.android'), require('./TextInputExample.android'), diff --git a/Examples/UIExplorer/UIExplorerList.ios.js b/Examples/UIExplorer/UIExplorerList.ios.js index 3fa57d818..1cd0eaaaf 100644 --- a/Examples/UIExplorer/UIExplorerList.ios.js +++ b/Examples/UIExplorer/UIExplorerList.ios.js @@ -46,6 +46,7 @@ var COMPONENTS = [ require('./ScrollViewExample'), require('./SegmentedControlIOSExample'), require('./SliderIOSExample'), + require('./StatusBarExample'), require('./SwitchExample'), require('./TabBarIOSExample'), require('./TextExample.ios'), diff --git a/Libraries/Components/StatusBar/StatusBar.js b/Libraries/Components/StatusBar/StatusBar.js new file mode 100644 index 000000000..b6571146a --- /dev/null +++ b/Libraries/Components/StatusBar/StatusBar.js @@ -0,0 +1,194 @@ +/** + * 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 StatusBar + * @flow + */ +'use strict'; + +const React = require('React'); +const ColorPropType = require('ColorPropType'); +const Platform = require('Platform'); + +const processColor = require('processColor'); + +const StatusBarManager = require('NativeModules').StatusBarManager; + +type DefaultProps = { + animated: boolean; +}; + +/** + * Merges the prop stack with the default values. + */ +function mergePropsStack(propsStack: Array): Object { + return propsStack.reduce((prev, cur) => { + return Object.assign(prev, cur); + }, { + backgroundColor: 'black', + barStyle: 'default', + translucent: false, + hidden: false, + networkActivityIndicatorVisible: false, + }); +} + +/** + * Component to control the app status bar. + * + * ### Usage with Navigator + * + * It is possible to have multiple `StatusBar` components mounted at the same + * time. The props will be merged in the order the `StatusBar` components were + * mounted. One use case is to specify status bar styles per route using `Navigator`. + * + * ``` + * + * + * + * + * + * } + * /> + * + * ``` + */ +const StatusBar = React.createClass({ + statics: { + _propsStack: [], + }, + + propTypes: { + /** + * If the status bar is hidden. + */ + hidden: React.PropTypes.bool, + /** + * If the transition between status bar property changes should be animated. + * Supported for backgroundColor, barStyle and hidden. + */ + animated: React.PropTypes.bool, + /** + * The background color of the status bar. + * @platform android + */ + backgroundColor: ColorPropType, + /** + * If the status bar is translucent. + * When translucent is set to true, the app will draw under the status bar. + * This is useful when using a semi transparent status bar color. + * + * @platform android + */ + translucent: React.PropTypes.bool, + /** + * Sets the color of the status bar text. + * + * @platform ios + */ + barStyle: React.PropTypes.oneOf([ + 'default', + 'light-content', + ]), + /** + * If the network activity indicator should be visible. + * + * @platform ios + */ + networkActivityIndicatorVisible: React.PropTypes.bool, + /** + * The transition effect when showing and hiding the status bar using the `hidden` + * prop. Defaults to 'fade'. + * + * @platform ios + */ + showHideTransition: React.PropTypes.oneOf([ + 'fade', + 'slide', + ]), + }, + + getDefaultProps(): DefaultProps { + return { + animated: false, + showHideTransition: 'fade', + }; + }, + + 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._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); + StatusBar._propsStack.splice(index, 1); + + this._updatePropsStack(); + }, + + componentDidUpdate(oldProps: Object) { + const index = StatusBar._propsStack.indexOf(oldProps); + StatusBar._propsStack[index] = this.props; + + this._updatePropsStack(); + }, + + /** + * Updates the native status bar with the props from the stack. + */ + _updatePropsStack() { + const mergedProps = mergePropsStack(StatusBar._propsStack); + + if (Platform.OS === 'ios') { + if (mergedProps.barStyle !== undefined) { + StatusBarManager.setStyle(mergedProps.barStyle, this.props.animated); + } + 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); + } + } + }, + + render(): ?ReactElement { + return null; + }, +}); + +module.exports = StatusBar; diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index 6efc9af3c..79a4ec0d5 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -37,6 +37,7 @@ var ReactNative = { get PullToRefreshViewAndroid() { return require('PullToRefreshViewAndroid'); }, get RecyclerViewBackedScrollView() { return require('RecyclerViewBackedScrollView'); }, get RefreshControl() { return require('RefreshControl'); }, + get StatusBar() { return require('StatusBar'); }, get SwitchAndroid() { return require('SwitchAndroid'); }, get SwitchIOS() { return require('SwitchIOS'); }, get TabBarIOS() { return require('TabBarIOS'); }, diff --git a/Libraries/react-native/react-native.js.flow b/Libraries/react-native/react-native.js.flow index a0c20f5e0..6b4f333c0 100644 --- a/Libraries/react-native/react-native.js.flow +++ b/Libraries/react-native/react-native.js.flow @@ -45,6 +45,7 @@ var ReactNative = Object.assign(Object.create(require('React')), { SegmentedControlIOS: require('SegmentedControlIOS'), SliderIOS: require('SliderIOS'), SnapshotViewIOS: require('SnapshotViewIOS'), + StatusBar: require('StatusBar'), Switch: require('Switch'), PullToRefreshViewAndroid: require('PullToRefreshViewAndroid'), RecyclerViewBackedScrollView: require('RecyclerViewBackedScrollView'), diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/Promise.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/Promise.java index 87a9b4b53..db192da98 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/Promise.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/Promise.java @@ -9,6 +9,8 @@ package com.facebook.react.bridge; +import javax.annotation.Nullable; + /** * Interface that represents a JavaScript Promise which can be passed to the native module as a * method parameter. @@ -21,7 +23,7 @@ public interface Promise { /** * Successfully resolve the Promise. */ - void resolve(Object value); + void resolve(@Nullable Object value); /** * Report an error which wasn't caused by an exception. diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/statusbar/BUCK b/ReactAndroid/src/main/java/com/facebook/react/modules/statusbar/BUCK new file mode 100644 index 000000000..3cedd2172 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/statusbar/BUCK @@ -0,0 +1,20 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'statusbar', + srcs = glob(['**/*.java']), + deps = [ + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/common:common'), + 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'), + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':statusbar', +) 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 new file mode 100644 index 000000000..ed905797a --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/statusbar/StatusBarModule.java @@ -0,0 +1,132 @@ +/** + * 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. + */ + + +package com.facebook.react.modules.statusbar; + +import android.animation.ArgbEvaluator; +import android.animation.ValueAnimator; +import android.annotation.TargetApi; +import android.app.Activity; +import android.os.Build; +import android.support.v4.view.ViewCompat; +import android.view.View; + +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; + +public class StatusBarModule extends ReactContextBaseJavaModule { + + private static final String ERROR_NO_ACTIVITY = + "Tried to change the status bar while not attached to an Activity"; + + private int mWindowFlags = 0; + + public StatusBarModule(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public String getName() { + return "StatusBarManager"; + } + + @ReactMethod + public void setColor(final int color, final boolean animated, final Promise res) { + final Activity activity = getCurrentActivity(); + if (activity == null) { + res.reject(ERROR_NO_ACTIVITY); + return; + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + UiThreadUtil.runOnUiThread( + new Runnable() { + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public void run() { + if (animated) { + int curColor = activity.getWindow().getStatusBarColor(); + ValueAnimator colorAnimation = ValueAnimator.ofObject( + new ArgbEvaluator(), curColor, color); + + colorAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animator) { + activity.getWindow().setStatusBarColor((Integer) animator.getAnimatedValue()); + } + }); + colorAnimation + .setDuration(300) + .setStartDelay(0); + colorAnimation.start(); + } else { + activity.getWindow().setStatusBarColor(color); + } + res.resolve(null); + } + } + ); + } else { + res.resolve(null); + } + } + + @ReactMethod + public void setTranslucent(final boolean translucent, final Promise res) { + final Activity activity = getCurrentActivity(); + if (activity == null) { + res.reject(ERROR_NO_ACTIVITY); + return; + } + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + if (translucent) { + mWindowFlags |= + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; + } else { + mWindowFlags &= + ~(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + } + activity.getWindow().getDecorView().setSystemUiVisibility(mWindowFlags); + ViewCompat.requestApplyInsets(activity.getWindow().getDecorView()); + res.resolve(null); + } + } + ); + } + + @ReactMethod + public void setHidden(final boolean hidden, final Promise res) { + final Activity activity = getCurrentActivity(); + if (activity == null) { + res.reject(ERROR_NO_ACTIVITY); + return; + } + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + if (hidden) { + mWindowFlags |= View.SYSTEM_UI_FLAG_FULLSCREEN; + } else { + mWindowFlags &= ~View.SYSTEM_UI_FLAG_FULLSCREEN; + } + activity.getWindow().getDecorView().setSystemUiVisibility(mWindowFlags); + res.resolve(null); + } + } + ); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK b/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK index 8af3f1421..88b94ebda 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK @@ -36,6 +36,7 @@ android_library( react_native_target('java/com/facebook/react/modules/location:location'), react_native_target('java/com/facebook/react/modules/netinfo:netinfo'), react_native_target('java/com/facebook/react/modules/network:network'), + react_native_target('java/com/facebook/react/modules/statusbar:statusbar'), react_native_target('java/com/facebook/react/modules/storage:storage'), react_native_target('java/com/facebook/react/modules/timepicker:timepicker'), react_native_target('java/com/facebook/react/modules/toast:toast'), diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java index 9933b41d5..d3ec2880f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java @@ -29,6 +29,7 @@ import com.facebook.react.modules.intent.IntentModule; import com.facebook.react.modules.location.LocationModule; import com.facebook.react.modules.netinfo.NetInfoModule; import com.facebook.react.modules.network.NetworkingModule; +import com.facebook.react.modules.statusbar.StatusBarModule; import com.facebook.react.modules.storage.AsyncStorageModule; import com.facebook.react.modules.timepicker.TimePickerDialogModule; import com.facebook.react.modules.toast.ToastModule; @@ -77,9 +78,11 @@ public class MainReactPackage implements ReactPackage { new LocationModule(reactContext), new NetworkingModule(reactContext), new NetInfoModule(reactContext), + new StatusBarModule(reactContext), new TimePickerDialogModule(reactContext), new ToastModule(reactContext), - new WebSocketModule(reactContext)); + new WebSocketModule(reactContext) + ); } @Override diff --git a/website/server/extractDocs.js b/website/server/extractDocs.js index 29d6a9878..8d8abba0b 100644 --- a/website/server/extractDocs.js +++ b/website/server/extractDocs.js @@ -208,6 +208,7 @@ var components = [ '../Libraries/Components/ScrollView/ScrollView.js', '../Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.ios.js', '../Libraries/Components/SliderIOS/SliderIOS.ios.js', + '../Libraries/Components/StatusBar/StatusBar.js', '../Libraries/Components/Switch/Switch.js', '../Libraries/Components/TabBarIOS/TabBarIOS.ios.js', '../Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js',