Skip to content

Commit

Permalink
Introducing <SafeAreaView>
Browse files Browse the repository at this point in the history
Summary:
<SafeAreaView> renders nested content and automatically applies paddings reflect the portion of the view
that is not covered by navigation bars, tab bars, toolbars, and other ancestor views.
Moreover, and most importantly, Safe Area's paddings feflect physical limitation of the screen,
such as rounded corners or camera notches (aka sensor housing area on iPhone X).

Reviewed By: mmmulani

Differential Revision: D5886411

fbshipit-source-id: 7ecc7aa34de8f5527c4e59b0fb4efba3aaea28c8
  • Loading branch information
shergin authored and facebook-github-bot committed Sep 25, 2017
1 parent 8b4ed94 commit 983b054
Show file tree
Hide file tree
Showing 11 changed files with 307 additions and 0 deletions.
14 changes: 14 additions & 0 deletions Libraries/Components/SafeAreaView/SafeAreaView.android.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* 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 SafeAreaView
* @flow
*/
'use strict';

module.exports = require('View');
48 changes: 48 additions & 0 deletions Libraries/Components/SafeAreaView/SafeAreaView.ios.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* 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 SafeAreaView
* @flow
* @format
*/

const React = require('React');
const ViewPropTypes = require('ViewPropTypes');
const requireNativeComponent = require('requireNativeComponent');

import type {ViewProps} from 'ViewPropTypes';

type Props = ViewProps & {
children: any,
};

/**
* Renders nested content and automatically applies paddings reflect the portion of the view
* that is not covered by navigation bars, tab bars, toolbars, and other ancestor views.
* Moreover, and most importantly, Safe Area's paddings feflect physical limitation of the screen,
* such as rounded corners or camera notches (aka sensor housing area on iPhone X).
*/
class SafeAreaView extends React.Component<Props> {
static propTypes = {
...ViewPropTypes,
};

render() {
return <RCTSafeAreaView {...this.props} />;
}
}

const RCTSafeAreaView = requireNativeComponent('RCTSafeAreaView', {
name: 'RCTSafeAreaView',
displayName: 'RCTSafeAreaView',
propTypes: {
...ViewPropTypes,
},
});

module.exports = SafeAreaView;
1 change: 1 addition & 0 deletions Libraries/react-native/react-native-implementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const ReactNative = {
get PickerIOS() { return require('PickerIOS'); },
get ProgressBarAndroid() { return require('ProgressBarAndroid'); },
get ProgressViewIOS() { return require('ProgressViewIOS'); },
get SafeAreaView() { return require('SafeAreaView'); },
get ScrollView() { return require('ScrollView'); },
get SectionList() { return require('SectionList'); },
get SegmentedControlIOS() { return require('SegmentedControlIOS'); },
Expand Down
18 changes: 18 additions & 0 deletions React/Views/SafeAreaView/RCTSafeAreaShadowView.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* 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.
*/

#import <React/RCTShadowView.h>

NS_ASSUME_NONNULL_BEGIN

@interface RCTSafeAreaShadowView : RCTShadowView

@end

NS_ASSUME_NONNULL_END
44 changes: 44 additions & 0 deletions React/Views/SafeAreaView/RCTSafeAreaShadowView.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* 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.
*/

#import "RCTSafeAreaShadowView.h"

#import <React/RCTAssert.h>
#import <yoga/Yoga.h>

#import "RCTSafeAreaViewLocalData.h"

@implementation RCTSafeAreaShadowView

- (void)setLocalData:(RCTSafeAreaViewLocalData *)localData
{
RCTAssert([localData isKindOfClass:[RCTSafeAreaViewLocalData class]],
@"Local data object for `RCTSafeAreaShadowView` must be `RCTSafeAreaViewLocalData` instance.");

UIEdgeInsets insets = localData.insets;

super.paddingLeft = (YGValue){insets.left, YGUnitPoint};
super.paddingRight = (YGValue){insets.right, YGUnitPoint};
super.paddingTop = (YGValue){insets.top, YGUnitPoint};
super.paddingBottom = (YGValue){insets.bottom, YGUnitPoint};

[self didSetProps:@[@"paddingLeft", @"paddingRight", @"paddingTop", @"paddingBottom"]];
}

/**
* Removing support for setting padding from any outside code
* to prevent interferring this with local data.
*/
- (void)setPadding:(YGValue)value {}
- (void)setPaddingLeft:(YGValue)value {}
- (void)setPaddingRight:(YGValue)value {}
- (void)setPaddingTop:(YGValue)value {}
- (void)setPaddingBottom:(YGValue)value {}

@end
24 changes: 24 additions & 0 deletions React/Views/SafeAreaView/RCTSafeAreaView.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* 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.
*/

#import <UIKit/UIKit.h>

#import <React/RCTView.h>

NS_ASSUME_NONNULL_BEGIN

@class RCTBridge;

@interface RCTSafeAreaView : RCTView

- (instancetype)initWithBridge:(RCTBridge *)bridge;

@end

NS_ASSUME_NONNULL_END
65 changes: 65 additions & 0 deletions React/Views/SafeAreaView/RCTSafeAreaView.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* 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.
*/

#import "RCTSafeAreaView.h"

#import <React/RCTBridge.h>
#import <React/RCTUIManager.h>

#import "RCTSafeAreaViewLocalData.h"

@implementation RCTSafeAreaView {
RCTBridge *_bridge;
UIEdgeInsets _currentSafeAreaInsets;
}

- (instancetype)initWithBridge:(RCTBridge *)bridge
{
if (self = [super initWithFrame:CGRectZero]) {
_bridge = bridge;
}

return self;
}

RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)decoder)
RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame)

#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */

static BOOL UIEdgeInsetsEqualToEdgeInsetsWithThreshold(UIEdgeInsets insets1, UIEdgeInsets insets2, CGFloat threshold) {
return
ABS(insets1.left - insets2.left) <= threshold &&
ABS(insets1.right - insets2.right) <= threshold &&
ABS(insets1.top - insets2.top) <= threshold &&
ABS(insets1.bottom - insets2.bottom) <= threshold;
}

- (void)safeAreaInsetsDidChange
{
if (![self respondsToSelector:@selector(safeAreaInsets)]) {
return;
}

UIEdgeInsets safeAreaInsets = self.safeAreaInsets;

if (UIEdgeInsetsEqualToEdgeInsetsWithThreshold(safeAreaInsets, _currentSafeAreaInsets, 1.0 / RCTScreenScale())) {
return;
}

_currentSafeAreaInsets = safeAreaInsets;

RCTSafeAreaViewLocalData *localData =
[[RCTSafeAreaViewLocalData alloc] initWithInsets:safeAreaInsets];
[_bridge.uiManager setLocalData:localData forView:self];
}

#endif

@end
22 changes: 22 additions & 0 deletions React/Views/SafeAreaView/RCTSafeAreaViewLocalData.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* 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.
*/

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface RCTSafeAreaViewLocalData : NSObject

- (instancetype)initWithInsets:(UIEdgeInsets)insets;

@property (atomic, readonly) UIEdgeInsets insets;

@end

NS_ASSUME_NONNULL_END
23 changes: 23 additions & 0 deletions React/Views/SafeAreaView/RCTSafeAreaViewLocalData.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* 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.
*/

#import "RCTSafeAreaViewLocalData.h"

@implementation RCTSafeAreaViewLocalData

- (instancetype)initWithInsets:(UIEdgeInsets)insets
{
if (self = [super init]) {
_insets = insets;
}

return self;
}

@end
18 changes: 18 additions & 0 deletions React/Views/SafeAreaView/RCTSafeAreaViewManager.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* 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.
*/

#import <React/RCTViewManager.h>

NS_ASSUME_NONNULL_BEGIN

@interface RCTSafeAreaViewManager : RCTViewManager

@end

NS_ASSUME_NONNULL_END
30 changes: 30 additions & 0 deletions React/Views/SafeAreaView/RCTSafeAreaViewManager.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* 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.
*/

#import "RCTSafeAreaViewManager.h"

#import "RCTSafeAreaShadowView.h"
#import "RCTSafeAreaView.h"
#import "RCTUIManager.h"

@implementation RCTSafeAreaViewManager

RCT_EXPORT_MODULE()

- (UIView *)view
{
return [[RCTSafeAreaView alloc] initWithBridge:self.bridge];
}

- (RCTSafeAreaShadowView *)shadowView
{
return [RCTSafeAreaShadowView new];
}

@end

7 comments on commit 983b054

@skv-headless
Copy link
Contributor

@skv-headless skv-headless commented on 983b054 Nov 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shergin Do you know how to make SafeArea different colours?
For example orange on the top and grey on the bottom

screen shot 2017-11-13 at 14 55 03

I believe facebook app is also solving it somehow.
Maybe if replace here https://github.com/facebook/react-native/blob/master/React/Views/SafeAreaView/RCTSafeAreaShadowView.m#L26
paddings with borders then would be possible to set colour for each side.

@shergin
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@skv-headless Just wrap with another with background color.

@Nodonisko
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shergin What is I need two different colors on same page? One for top and one for bottom.

@shergin
Copy link
Contributor Author

@shergin shergin commented on 983b054 Dec 8, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Nodonisko Well, SafeAreaView is about insets, not about colors. You can solve your problem customizing the background view's background (something like applying 50/50 top/bottom splitted color).

But.. yeah, I have to admit, there is no way to dedicated special view to top or bottom inset area, but it is very interesting idea, I will think about that.

@wd
Copy link

@wd wd commented on 983b054 Jan 25, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have wrote a demo as @shergin said, post here if anyone needed.

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 * @flow
 */

import React, { Component } from 'react';
import {
  Platform,
  StyleSheet,
  Text,
    View,
    SafeAreaView
} from 'react-native';

const instructions = Platform.select({
  ios: 'Press Cmd+R to reload,\n' +
    'Cmd+D or shake for dev menu',
  android: 'Double tap R on your keyboard to reload,\n' +
    'Shake or press menu button for dev menu',
});

export default class App extends Component {
    render() {
        return (
            <View style={{flex: 1}}>
            <View style={{position: 'absolute', top: 0, width: '100%', height: 100, backgroundColor: 'red'}} />
            <View style={{position: 'absolute', bottom: 0, width: '100%', height: 100, backgroundColor: 'yellow'}} />
            <SafeAreaView style={{flex: 1}}>
            <View style={styles.container}>
            <Text style={styles.welcome}>
            Welcome to React Native!
            </Text>
            <Text style={styles.instructions}>
            To get started, edit App.js
            </Text>
            <Text style={styles.instructions}>
            {instructions}
            </Text>
            <View style={styles.bottom}>
            <Text>Bottom</Text>
            </View>
            </View>
            </SafeAreaView>
            </View>
        );
    }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    //justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
      flexDirection: 'column',
        justifyContent: 'space-between',
  },
  welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
  instructions: {
    textAlign: 'center',
    color: '#333333',
    marginBottom: 5,
  },
    bottom: {
        //justifyContent: 'flex-end',
        alignItems: 'flex-end'
    }
});

image

@Nodonisko
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shergin @wd Thanks!

@B3rry
Copy link

@B3rry B3rry commented on 983b054 Jan 30, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shergin:

Could you consider props for splitting these between two different locales? Something like forceInset in react-community/react-native-safe-area-view?

e.g. SafeAreaView with support for defining which edges to make "safe"? This way SafeAreaView could have independent use on root/container/component levels depending on need, while maintaining the ability to support different backgrounds on each.

Please sign in to comment.