Skip to content

Commit

Permalink
Update react-native-safe-area-context (#8459)
Browse files Browse the repository at this point in the history
* Update react-native-safe-area-context

* Update NCL and docs

* Include all necessary files and add UIKit header import to get it to compile

* Update home and expo pkg to use SafeAreaView from react-native-safe-area-context

* Update dev home
  • Loading branch information
brentvatne committed May 23, 2020
1 parent 23bf7c3 commit deff557
Show file tree
Hide file tree
Showing 49 changed files with 1,229 additions and 369 deletions.
@@ -1,19 +1,19 @@
package versioned.host.exp.exponent.modules.api.safeareacontext;

/* package */ class EdgeInsets {
public float top;
public float right;
public float bottom;
public float left;
float top;
float right;
float bottom;
float left;

public EdgeInsets(float top, float right, float bottom, float left) {
EdgeInsets(float top, float right, float bottom, float left) {
this.top = top;
this.right = right;
this.bottom = bottom;
this.left = left;
}

public boolean equalsToEdgeInsets(EdgeInsets other) {
boolean equalsToEdgeInsets(EdgeInsets other) {
if (this == other) {
return true;
}
Expand Down
Expand Up @@ -2,19 +2,20 @@

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.events.Event;
import com.facebook.react.uimanager.events.RCTEventEmitter;

/* package */ class InsetsChangeEvent extends Event<InsetsChangeEvent> {
public static final String EVENT_NAME = "topInsetsChange";
static final String EVENT_NAME = "topInsetsChange";

private EdgeInsets mInsets;
private Rect mFrame;

protected InsetsChangeEvent(int viewTag, EdgeInsets insets) {
InsetsChangeEvent(int viewTag, EdgeInsets insets, Rect frame) {
super(viewTag);

mInsets = insets;
mFrame = frame;
}

@Override
Expand All @@ -25,7 +26,8 @@ public String getEventName() {
@Override
public void dispatch(RCTEventEmitter rctEventEmitter) {
WritableMap event = Arguments.createMap();
event.putMap("insets", SafeAreaUtils.edgeInsetsToJsMap(mInsets));
event.putMap("insets", SerializationUtils.edgeInsetsToJsMap(mInsets));
event.putMap("frame", SerializationUtils.rectToJsMap(mFrame));
rctEventEmitter.receiveEvent(getViewTag(), getEventName(), event);
}
}
@@ -0,0 +1,22 @@
package versioned.host.exp.exponent.modules.api.safeareacontext;

/* package */ class Rect {
float x;
float y;
float width;
float height;

Rect(float x, float y, float width, float height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}

boolean equalsToRect(Rect other) {
if (this == other) {
return true;
}
return this.x == other.x && this.y == other.y && this.width == other.width && this.height == other.height;
}
}
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()
);
}

}
@@ -0,0 +1,64 @@
package versioned.host.exp.exponent.modules.api.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;
}
}
@@ -0,0 +1,87 @@
package versioned.host.exp.exponent.modules.api.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)));

}
}
Expand Up @@ -2,88 +2,75 @@

import android.graphics.Rect;
import android.os.Build;
import android.view.Surface;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.WindowManager;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.uimanager.PixelUtil;

import java.util.Map;

import androidx.annotation.Nullable;

/* package */ class SafeAreaUtils {
static WritableMap edgeInsetsToJsMap(EdgeInsets insets) {
WritableMap insetsMap = Arguments.createMap();
insetsMap.putDouble("top", PixelUtil.toDIPFromPixel(insets.top));
insetsMap.putDouble("right", PixelUtil.toDIPFromPixel(insets.right));
insetsMap.putDouble("bottom", PixelUtil.toDIPFromPixel(insets.bottom));
insetsMap.putDouble("left", PixelUtil.toDIPFromPixel(insets.left));
return insetsMap;
}

static Map<String, Float> edgeInsetsToJavaMap(EdgeInsets insets) {
return MapBuilder.of(
"top",
PixelUtil.toDIPFromPixel(insets.top),
"right",
PixelUtil.toDIPFromPixel(insets.right),
"bottom",
PixelUtil.toDIPFromPixel(insets.bottom),
"left",
PixelUtil.toDIPFromPixel(insets.left));
}

static @Nullable EdgeInsets getSafeAreaInsets(WindowManager windowManager, View rootView) {
// Window insets are parts of the window that are covered by system views (status bar,
// navigation bar, notches). There are no apis the get these values for android < M so we
// do a best effort polyfill.
EdgeInsets windowInsets;
private static @Nullable EdgeInsets getRootWindowInsetsCompat(View rootView) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
WindowInsets insets = rootView.getRootWindowInsets();
if (insets == null) {
return null;
}
windowInsets = new EdgeInsets(
return new EdgeInsets(
insets.getSystemWindowInsetTop(),
insets.getSystemWindowInsetRight(),
insets.getSystemWindowInsetBottom(),
// System insets are more reliable to account for notches but the
// system inset for bottom includes the soft keyboard which we don't
// want to be consistent with iOS. In practice it should always be
// correct since there cannot be a notch on this edge.
insets.getStableInsetBottom(),
insets.getSystemWindowInsetLeft());
} else {
int rotation = windowManager.getDefaultDisplay().getRotation();
int statusBarHeight = 0;
int resourceId = rootView.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
statusBarHeight = rootView.getResources().getDimensionPixelSize(resourceId);
}
int navbarHeight = 0;
resourceId = rootView.getResources().getIdentifier("navigation_bar_height", "dimen", "android");
if (resourceId > 0) {
navbarHeight = rootView.getResources().getDimensionPixelSize(resourceId);
}
Rect visibleRect = new Rect();
rootView.getWindowVisibleDisplayFrame(visibleRect);
return new EdgeInsets(
visibleRect.top,
rootView.getWidth() - visibleRect.right,
rootView.getHeight() - visibleRect.bottom,
visibleRect.left);
}
}

windowInsets = new EdgeInsets(
statusBarHeight,
rotation == Surface.ROTATION_90 ? navbarHeight : 0,
rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180 ? navbarHeight : 0,
rotation == Surface.ROTATION_270 ? navbarHeight : 0);
static @Nullable EdgeInsets getSafeAreaInsets(View rootView, View view) {
EdgeInsets windowInsets = getRootWindowInsetsCompat(rootView);
if (windowInsets == null) {
return null;
}

// Calculate the part of the root view that overlaps with window insets.
View contentView = rootView.findViewById(android.R.id.content);
// Calculate the part of the view that overlaps with window insets.
float windowWidth = rootView.getWidth();
float windowHeight = rootView.getHeight();
Rect visibleRect = new Rect();
contentView.getGlobalVisibleRect(visibleRect);
view.getGlobalVisibleRect(visibleRect);

windowInsets.top = Math.max(windowInsets.top - visibleRect.top, 0);
windowInsets.left = Math.max(windowInsets.left - visibleRect.left, 0);
windowInsets.bottom = Math.max(visibleRect.top + contentView.getHeight() + windowInsets.bottom - windowHeight, 0);
windowInsets.right = Math.max(visibleRect.left + contentView.getWidth() + windowInsets.right - windowWidth, 0);
windowInsets.bottom = Math.max(visibleRect.top + view.getHeight() + windowInsets.bottom - windowHeight, 0);
windowInsets.right = Math.max(visibleRect.left + view.getWidth() + windowInsets.right - windowWidth, 0);
return windowInsets;
}

static @Nullable versioned.host.exp.exponent.modules.api.safeareacontext.Rect getFrame(ViewGroup rootView, View view) {
// This can happen while the view gets unmounted.
if (view.getParent() == null) {
return null;
}
Rect offset = new Rect();
view.getDrawingRect(offset);
try {
rootView.offsetDescendantRectToMyCoords(view, offset);
} catch (IllegalArgumentException ex) {
// This can throw if the view is not a descendant of rootView. This should not
// happen but avoid potential crashes.
ex.printStackTrace();
return null;
}

return new versioned.host.exp.exponent.modules.api.safeareacontext.Rect(offset.left, offset.top, view.getWidth(), view.getHeight());
}
}

0 comments on commit deff557

Please sign in to comment.