Permalink
Browse files

Introducing <SafeAreaView>

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 983b05441d3d23809b924de2e4bf96a55c47428d
@@ -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');
@@ -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;
@@ -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'); },
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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

This comment has been minimized.

Copy link
Contributor

skv-headless replied Nov 13, 2017

@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

This comment has been minimized.

Copy link
Contributor Author

shergin replied Nov 14, 2017

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

@Nodonisko

This comment has been minimized.

Copy link

Nodonisko replied Dec 7, 2017

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

@shergin

This comment has been minimized.

Copy link
Contributor Author

shergin replied Dec 8, 2017

@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

This comment has been minimized.

Copy link

wd replied Jan 25, 2018

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

This comment has been minimized.

Copy link

Nodonisko replied Jan 25, 2018

@shergin @wd Thanks!

@B3rry

This comment has been minimized.

Copy link

B3rry replied Jan 30, 2018

@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.