Skip to content

Commit

Permalink
Native SafeAreaView (AppAndFlow#78)
Browse files Browse the repository at this point in the history
* Native SafeAreaView (iOS only)

* Android, tests

* revert unrelated change

* revert unrelated change

* Update examples

* Fix wording

* Add android implementation, change edges prop format

* Update README.md

Co-authored-by: Janic Duplessis <[email protected]>

* Update android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaView.java

Co-authored-by: Jacob Parker <[email protected]>
Co-authored-by: Janic Duplessis <[email protected]>
  • Loading branch information
3 people authored May 21, 2020
1 parent f126189 commit fc9a4ec
Show file tree
Hide file tree
Showing 36 changed files with 1,023 additions and 277 deletions.
66 changes: 46 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,45 @@ protected List<ReactPackage> getPackages() {

## Usage

Add `SafeAreaProvider` in your app root component:
`SafeAreaView` is a regular `View` component with the safe area edges applied as padding.

If you set your own padding on the view, it will be added to the padding from the safe area.

**If you are targeting web, you must set up `SafeAreaProvider` in as described in the hooks section**. You do not need to for native platforms.

```js
import { SafeAreaView } from 'react-native-safe-area-context';

function SomeComponent() {
return (
<SafeAreaView>
<View />
</SafeAreaView>
);
}
```

### Props

All props are optional.

#### `emulateUnlessSupported`

`true` (default) or `false`

On iOS 10, emulate the safe area using the status bar height and home indicator sizes.

#### `edges`

Array of `top`, `right`, `bottom`, and `left`. Defaults to all.

Sets the edges to apply the safe area insets to.

## Hooks

Hooks give you direct access to the safe area insets. This is a more advanced use-case, and might perform worse than `SafeAreaView` when rotating the device.

First, add `SafeAreaProvider` in your app root component. You may need to add it in other places too, including at the root of any modals any any routes when using `react-native-screen`.

```js
import { SafeAreaProvider } from 'react-native-safe-area-context';
Expand All @@ -93,7 +131,7 @@ function App() {
}
```

Usage with hooks api:
You use the `useSafeAreaInsets` hook to get the insets in the form of `{ top: number, right: number, bottom: number: number, left: number }`.

```js
import { useSafeAreaInsets } from 'react-native-safe-area-context';
Expand All @@ -114,39 +152,27 @@ class ClassComponent extends React.Component {
render() {
return (
<SafeAreaInsetsContext.Consumer>
{insets => <View style={{ paddingTop: insets.top }} />}
{(insets) => <View style={{ paddingTop: insets.top }} />}
</SafeAreaInsetsContext.Consumer>
);
}
}
```

Usage with `SafeAreaView`:

```js
import { SafeAreaView } from 'react-native-safe-area-context';

function SomeComponent() {
return (
<SafeAreaView>
<View />
</SafeAreaView>
);
}
```

### Web SSR
## Web SSR

If you are doing server side rendering on the web you can use `initialMetrics` to inject insets and frame value based on the device the user has, or simply pass zero values. Since insets measurement is async it will break rendering your page content otherwise.

### Optimization
## Optimization

If you can, use `SafeAreaView`. It's implemented natively so when rotating the device, there is no delay from the asynchronous bridge.

To speed up the initial render, you can import `initialWindowMetrics` from this package and set as the `initialMetrics` prop on the provider as described in Web SSR. You cannot do this if your provider remounts, or you are using `react-native-navigation`.

```js
import {
SafeAreaProvider,
initialWindowMetrics
initialWindowMetrics,
} from 'react-native-safe-area-context';

function App() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

Expand All @@ -21,7 +22,10 @@ public List<NativeModule> createNativeModules(@Nonnull ReactApplicationContext r
@Nonnull
@Override
public List<ViewManager> createViewManagers(@Nonnull ReactApplicationContext reactContext) {
return Collections.<ViewManager>singletonList(new SafeAreaViewManager(reactContext));
return Arrays.<ViewManager>asList(
new SafeAreaProviderManager(reactContext),
new SafeAreaViewManager()
);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.th3rdwave.safeareacontext;

import android.annotation.SuppressLint;
import android.content.Context;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;

import com.facebook.infer.annotation.Assertions;
import com.facebook.react.views.view.ReactViewGroup;

import androidx.annotation.Nullable;

@SuppressLint("ViewConstructor")
public class SafeAreaProvider extends ReactViewGroup implements ViewTreeObserver.OnGlobalLayoutListener {
public interface OnInsetsChangeListener {
void onInsetsChange(SafeAreaProvider view, EdgeInsets insets, Rect frame);
}

private @Nullable OnInsetsChangeListener mInsetsChangeListener;
private @Nullable EdgeInsets mLastInsets;
private @Nullable Rect mLastFrame;

public SafeAreaProvider(Context context) {
super(context);
}

private void maybeUpdateInsets() {
EdgeInsets edgeInsets = SafeAreaUtils.getSafeAreaInsets(getRootView(), this);
Rect frame = SafeAreaUtils.getFrame((ViewGroup) getRootView(), this);
if (edgeInsets != null && frame != null &&
(mLastInsets == null ||
mLastFrame == null ||
!mLastInsets.equalsToEdgeInsets(edgeInsets) ||
!mLastFrame.equalsToRect(frame))) {
Assertions.assertNotNull(mInsetsChangeListener).onInsetsChange(this, edgeInsets, frame);
mLastInsets = edgeInsets;
mLastFrame = frame;
}
}

@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();

getViewTreeObserver().addOnGlobalLayoutListener(this);
maybeUpdateInsets();
}

@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();

getViewTreeObserver().removeOnGlobalLayoutListener(this);
}

@Override
public void onGlobalLayout() {
maybeUpdateInsets();
}

public void setOnInsetsChangeListener(OnInsetsChangeListener listener) {
mInsetsChangeListener = listener;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.th3rdwave.safeareacontext;

import android.app.Activity;
import android.view.View;
import android.view.ViewGroup;

import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.events.EventDispatcher;

import java.util.Map;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

public class SafeAreaProviderManager extends ViewGroupManager<SafeAreaProvider> {
private final ReactApplicationContext mContext;

public SafeAreaProviderManager(ReactApplicationContext context) {
super();

mContext = context;
}

@Override
@NonNull
public String getName() {
return "RNCSafeAreaProvider";
}

@Override
@NonNull
public SafeAreaProvider createViewInstance(@NonNull ThemedReactContext context) {
return new SafeAreaProvider(context);
}

@Override
protected void addEventEmitters(@NonNull ThemedReactContext reactContext, @NonNull final SafeAreaProvider view) {
final EventDispatcher dispatcher =
reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher();
view.setOnInsetsChangeListener(new SafeAreaProvider.OnInsetsChangeListener() {
@Override
public void onInsetsChange(SafeAreaProvider view, EdgeInsets insets, Rect frame) {
dispatcher.dispatchEvent(new InsetsChangeEvent(view.getId(), insets, frame));
}
});
}

@Override
public Map<String, Object> getExportedCustomDirectEventTypeConstants() {
return MapBuilder.<String, Object>builder()
.put(InsetsChangeEvent.EVENT_NAME, MapBuilder.of("registrationName", "onInsetsChange"))
.build();
}

@Nullable
@Override
public Map<String, Object> getExportedViewConstants() {
Activity activity = mContext.getCurrentActivity();
if (activity == null) {
return null;
}

ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
if (decorView == null) {
return null;
}

View contentView = decorView.findViewById(android.R.id.content);
EdgeInsets insets = SafeAreaUtils.getSafeAreaInsets(decorView, contentView);
Rect frame = SafeAreaUtils.getFrame(decorView, contentView);
if (insets == null || frame == null) {
return null;
}
return MapBuilder.<String, Object>of(
"initialWindowMetrics",
MapBuilder.<String, Object>of(
"insets",
SerializationUtils.edgeInsetsToJavaMap(insets),
"frame",
SerializationUtils.rectToJavaMap(frame)));

}
}
Original file line number Diff line number Diff line change
@@ -1,40 +1,56 @@
package com.th3rdwave.safeareacontext;

import static com.facebook.react.uimanager.UIManagerHelper.getReactContext;

import android.annotation.SuppressLint;
import android.content.Context;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;

import androidx.annotation.Nullable;

import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.views.view.ReactViewGroup;
import com.facebook.react.uimanager.UIManagerModule;

import androidx.annotation.Nullable;
import java.util.EnumSet;

@SuppressLint("ViewConstructor")
public class SafeAreaView extends ReactViewGroup implements ViewTreeObserver.OnGlobalLayoutListener {
public interface OnInsetsChangeListener {
void onInsetsChange(SafeAreaView view, EdgeInsets insets, Rect frame);
}

private @Nullable OnInsetsChangeListener mInsetsChangeListener;
private @Nullable EdgeInsets mLastInsets;
private @Nullable Rect mLastFrame;
private @Nullable EdgeInsets mInsets;
private @Nullable EnumSet<SafeAreaViewEdges> mEdges;

public SafeAreaView(Context context) {
super(context);
}

private void updateInsets() {
if (mInsets != null) {
EnumSet<SafeAreaViewEdges> edges = mEdges != null
? mEdges
: EnumSet.allOf(SafeAreaViewEdges.class);

SafeAreaViewLocalData localData = new SafeAreaViewLocalData(mInsets, edges);

ReactContext reactContext = getReactContext(this);
UIManagerModule uiManager = reactContext.getNativeModule(UIManagerModule.class);
if (uiManager != null) {
uiManager.setViewLocalData(getId(), localData);
}
}
}

public void setEdges(EnumSet<SafeAreaViewEdges> edges) {
mEdges = edges;
updateInsets();
}

private void maybeUpdateInsets() {
EdgeInsets edgeInsets = SafeAreaUtils.getSafeAreaInsets(getRootView(), this);
Rect frame = SafeAreaUtils.getFrame((ViewGroup) getRootView(), this);
if (edgeInsets != null && frame != null &&
(mLastInsets == null ||
mLastFrame == null ||
!mLastInsets.equalsToEdgeInsets(edgeInsets) ||
!mLastFrame.equalsToRect(frame))) {
Assertions.assertNotNull(mInsetsChangeListener).onInsetsChange(this, edgeInsets, frame);
mLastInsets = edgeInsets;
mLastFrame = frame;
if (edgeInsets != null && (mInsets == null || !mInsets.equalsToEdgeInsets(edgeInsets))) {
mInsets = edgeInsets;
updateInsets();
}
}

Expand All @@ -58,7 +74,9 @@ public void onGlobalLayout() {
maybeUpdateInsets();
}

public void setOnInsetsChangeListener(OnInsetsChangeListener listener) {
mInsetsChangeListener = listener;
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
maybeUpdateInsets();
super.onLayout(changed, left, top, right, bottom);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.th3rdwave.safeareacontext;

public enum SafeAreaViewEdges {
TOP,
RIGHT,
BOTTOM,
LEFT
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.th3rdwave.safeareacontext;

import java.util.EnumSet;

public class SafeAreaViewLocalData {
private EdgeInsets mInsets;
private EnumSet<SafeAreaViewEdges> mEdges;

public SafeAreaViewLocalData(EdgeInsets insets, EnumSet<SafeAreaViewEdges> edges) {
mInsets = insets;
mEdges = edges;
}

public EdgeInsets getInsets() {
return mInsets;
}

public EnumSet<SafeAreaViewEdges> getEdges() {
return mEdges;
}
}
Loading

0 comments on commit fc9a4ec

Please sign in to comment.