Browse Source

Change api, fix android

Janic Duplessis 5 years ago
parent
commit
eb9147ac73

+ 1
- 0
.eslintrc.js View File

@@ -23,6 +23,7 @@ module.exports = {
23 23
           '@typescript-eslint/explicit-member-accessibility': 'off',
24 24
           '@typescript-eslint/explicit-function-return-type': 'off',
25 25
           '@typescript-eslint/no-use-before-define': 'off',
26
+          'react-native/no-inline-styles': 'off',
26 27
         },
27 28
       ),
28 29
     },

+ 7
- 0
android/src/main/java/com/th3rdwave/safeareaview/EdgeInsets.java View File

@@ -12,4 +12,11 @@ package com.th3rdwave.safeareaview;
12 12
     this.bottom = bottom;
13 13
     this.left = left;
14 14
   }
15
+
16
+  public boolean equalsToEdgeInsets(EdgeInsets other) {
17
+    if (this == other) {
18
+      return true;
19
+    }
20
+    return this.top == other.top && this.right == other.right && this.bottom == other.bottom && this.left == other.left;
21
+  }
15 22
 }

+ 45
- 11
android/src/main/java/com/th3rdwave/safeareaview/SafeAreaView.java View File

@@ -1,25 +1,29 @@
1 1
 package com.th3rdwave.safeareaview;
2 2
 
3
+import android.app.Activity;
3 4
 import android.content.Context;
5
+import android.content.ContextWrapper;
4 6
 import android.os.Build;
5
-import android.util.DisplayMetrics;
6 7
 import android.view.Surface;
8
+import android.view.View;
9
+import android.view.ViewTreeObserver;
10
+import android.view.Window;
7 11
 import android.view.WindowInsets;
8 12
 import android.view.WindowManager;
9 13
 
10 14
 import com.facebook.infer.annotation.Assertions;
11
-import com.facebook.react.uimanager.DisplayMetricsHolder;
12 15
 import com.facebook.react.views.view.ReactViewGroup;
13 16
 
14 17
 import androidx.annotation.Nullable;
15 18
 
16
-public class SafeAreaView extends ReactViewGroup {
19
+public class SafeAreaView extends ReactViewGroup implements ViewTreeObserver.OnGlobalLayoutListener {
17 20
   public interface OnInsetsChangeListener {
18 21
     void onInsetsChange(SafeAreaView view, EdgeInsets insets);
19 22
   }
20 23
 
21 24
   private @Nullable OnInsetsChangeListener mInsetsChangeListener;
22
-  WindowManager mWindowManager;
25
+  private WindowManager mWindowManager;
26
+  private @Nullable EdgeInsets mLastInsets;
23 27
 
24 28
   public SafeAreaView(Context context) {
25 29
     super(context);
@@ -27,6 +31,17 @@ public class SafeAreaView extends ReactViewGroup {
27 31
     mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
28 32
   }
29 33
 
34
+  private Activity getActivity() {
35
+    Context context = getContext();
36
+    while (context instanceof ContextWrapper) {
37
+      if (context instanceof Activity) {
38
+        return (Activity)context;
39
+      }
40
+      context = ((ContextWrapper)context).getBaseContext();
41
+    }
42
+    return null;
43
+  }
44
+
30 45
   private EdgeInsets getSafeAreaInsets() {
31 46
     // Window insets are parts of the window that are covered by system views (status bar,
32 47
     // navigation bar, notches). There are no apis the get these values for android < M so we
@@ -60,21 +75,40 @@ public class SafeAreaView extends ReactViewGroup {
60 75
     }
61 76
 
62 77
     // Calculate the part of the root view that overlaps with window insets.
78
+    View rootView = getRootView();
79
+    View contentView = rootView.findViewById(android.R.id.content);
80
+    float windowWidth = rootView.getWidth();
81
+    float windowHeight = rootView.getHeight();
63 82
     int[] windowLocation = new int[2];
64
-    getLocationInWindow(windowLocation);
65
-    DisplayMetrics screenMetrics = DisplayMetricsHolder.getScreenDisplayMetrics();
83
+    contentView.getLocationInWindow(windowLocation);
66 84
     windowInsets.top = Math.max(windowInsets.top - windowLocation[1], 0);
67 85
     windowInsets.left = Math.max(windowInsets.left - windowLocation[0], 0);
68
-    windowInsets.bottom = Math.max(windowLocation[1] + getHeight() + windowInsets.bottom - screenMetrics.heightPixels, 0);
69
-    windowInsets.right = Math.max(windowLocation[0] + getWidth() + windowInsets.right - screenMetrics.widthPixels, 0);
86
+    windowInsets.bottom = Math.max(windowLocation[1] + contentView.getHeight() + windowInsets.bottom - windowHeight, 0);
87
+    windowInsets.right = Math.max(windowLocation[0] + contentView.getWidth() + windowInsets.right - windowWidth, 0);
70 88
     return windowInsets;
71 89
   }
72 90
 
73 91
   @Override
74
-  protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
75
-    super.onLayout(changed, left, top, right, bottom);
92
+  protected void onAttachedToWindow() {
93
+    super.onAttachedToWindow();
94
+
95
+    getRootView().getViewTreeObserver().addOnGlobalLayoutListener(this);
96
+  }
97
+
98
+  @Override
99
+  protected void onDetachedFromWindow() {
100
+    super.onDetachedFromWindow();
101
+
102
+    getRootView().getViewTreeObserver().removeOnGlobalLayoutListener(this);
103
+  }
76 104
 
77
-    Assertions.assertNotNull(mInsetsChangeListener).onInsetsChange(this, getSafeAreaInsets());
105
+  @Override
106
+  public void onGlobalLayout() {
107
+    EdgeInsets edgeInsets = getSafeAreaInsets();
108
+    if (mLastInsets == null || !mLastInsets.equalsToEdgeInsets(edgeInsets)) {
109
+      Assertions.assertNotNull(mInsetsChangeListener).onInsetsChange(this, edgeInsets);
110
+      mLastInsets = edgeInsets;
111
+    }
78 112
   }
79 113
 
80 114
   public void setOnInsetsChangeListener(OnInsetsChangeListener listener) {

+ 87
- 45
example/App.tsx View File

@@ -8,7 +8,6 @@
8 8
 
9 9
 import * as React from 'react';
10 10
 import { StyleSheet, ScrollView, View, Text, StatusBar } from 'react-native';
11
-import SafeAreaView from '..';
12 11
 
13 12
 import {
14 13
   Header,
@@ -17,57 +16,96 @@ import {
17 16
   DebugInstructions,
18 17
   ReloadInstructions,
19 18
 } from 'react-native/Libraries/NewAppScreen';
19
+import { SafeAreaProvider, useSafeArea } from '..';
20
+
21
+type ForceInsets = 'always' | 'never';
22
+
23
+const CompatSafeAreaView = ({
24
+  forceInsets = {},
25
+  children,
26
+}: {
27
+  forceInsets?: {
28
+    top?: ForceInsets;
29
+    right?: ForceInsets;
30
+    bottom?: ForceInsets;
31
+    left?: ForceInsets;
32
+  };
33
+  children?: React.ReactNode;
34
+}) => {
35
+  const insets = useSafeArea();
36
+  const top = forceInsets.top === 'never' ? 0 : insets.top;
37
+  const right = forceInsets.right === 'never' ? 0 : insets.right;
38
+  const bottom = forceInsets.bottom === 'never' ? 0 : insets.bottom;
39
+  const left = forceInsets.left === 'never' ? 0 : insets.left;
40
+  return (
41
+    <View
42
+      style={{
43
+        paddingTop: top,
44
+        paddingRight: right,
45
+        paddingBottom: bottom,
46
+        paddingLeft: left,
47
+      }}
48
+    >
49
+      {children}
50
+    </View>
51
+  );
52
+};
20 53
 
21 54
 const App = () => {
55
+  const insets = useSafeArea();
22 56
   return (
23 57
     <>
24 58
       <StatusBar
25 59
         barStyle="dark-content"
26
-        translucent
27
-        backgroundColor="rgba(0, 0, 0, 0.3)"
60
+        translucent={false}
61
+        backgroundColor="transparent"
28 62
       />
29
-      <SafeAreaView>
30
-        {insets => (
31
-          <ScrollView
32
-            contentInsetAdjustmentBehavior="automatic"
33
-            contentContainerStyle={{
34
-              paddingTop: insets.top,
35
-              paddingBottom: insets.bottom,
36
-            }}
37
-            style={styles.scrollView}
38
-          >
39
-            <Header />
40
-            <View style={styles.body}>
41
-              <View style={styles.sectionContainer}>
42
-                <Text style={styles.sectionTitle}>Step One</Text>
43
-                <Text style={styles.sectionDescription}>
44
-                  Edit <Text style={styles.highlight}>App.js</Text> to change
45
-                  this screen and then come back to see your edits.
46
-                </Text>
47
-              </View>
48
-              <View style={styles.sectionContainer}>
49
-                <Text style={styles.sectionTitle}>See Your Changes</Text>
50
-                <Text style={styles.sectionDescription}>
51
-                  <ReloadInstructions />
52
-                </Text>
53
-              </View>
54
-              <View style={styles.sectionContainer}>
55
-                <Text style={styles.sectionTitle}>Debug</Text>
56
-                <Text style={styles.sectionDescription}>
57
-                  <DebugInstructions />
58
-                </Text>
59
-              </View>
60
-              <View style={styles.sectionContainer}>
61
-                <Text style={styles.sectionTitle}>Learn More</Text>
62
-                <Text style={styles.sectionDescription}>
63
-                  Read the docs to discover what to do next:
64
-                </Text>
65
-              </View>
66
-              <LearnMoreLinks />
63
+      <ScrollView
64
+        contentInsetAdjustmentBehavior="never"
65
+        style={styles.scrollView}
66
+      >
67
+        <CompatSafeAreaView>
68
+          <Header />
69
+          <View style={styles.body}>
70
+            <View style={styles.sectionContainer}>
71
+              <Text style={styles.sectionTitle}>Step One</Text>
72
+              <Text style={styles.sectionDescription}>
73
+                Edit <Text style={styles.highlight}>App.js</Text> to change this
74
+                screen and then come back to see your edits.
75
+              </Text>
67 76
             </View>
68
-          </ScrollView>
69
-        )}
70
-      </SafeAreaView>
77
+            <View style={styles.sectionContainer}>
78
+              <Text style={styles.sectionTitle}>See Your Changes</Text>
79
+              <Text style={styles.sectionDescription}>
80
+                <ReloadInstructions />
81
+              </Text>
82
+            </View>
83
+            <View style={styles.sectionContainer}>
84
+              <Text style={styles.sectionTitle}>Debug</Text>
85
+              <Text style={styles.sectionDescription}>
86
+                <DebugInstructions />
87
+              </Text>
88
+            </View>
89
+            <View style={styles.sectionContainer}>
90
+              <Text style={styles.sectionTitle}>Learn More</Text>
91
+              <Text style={styles.sectionDescription}>
92
+                Read the docs to discover what to do next:
93
+              </Text>
94
+            </View>
95
+            <LearnMoreLinks />
96
+          </View>
97
+        </CompatSafeAreaView>
98
+      </ScrollView>
99
+      <View
100
+        style={{
101
+          position: 'absolute',
102
+          top: 0,
103
+          right: 0,
104
+          left: 0,
105
+          height: insets.top,
106
+          backgroundColor: 'rgba(0, 0, 0, 0.3)',
107
+        }}
108
+      />
71 109
     </>
72 110
   );
73 111
 };
@@ -111,4 +149,8 @@ const styles = StyleSheet.create({
111 149
   },
112 150
 });
113 151
 
114
-export default App;
152
+export default () => (
153
+  <SafeAreaProvider>
154
+    <App />
155
+  </SafeAreaProvider>
156
+);

+ 18
- 0
ios/SafeAreaView/RNCSafeAreaView.m View File

@@ -5,7 +5,18 @@
5 5
 #import <React/RCTBridge.h>
6 6
 #import <React/RCTUIManager.h>
7 7
 
8
+static BOOL UIEdgeInsetsEqualToEdgeInsetsWithThreshold(UIEdgeInsets insets1, UIEdgeInsets insets2, CGFloat threshold) {
9
+  return
10
+  ABS(insets1.left - insets2.left) <= threshold &&
11
+  ABS(insets1.right - insets2.right) <= threshold &&
12
+  ABS(insets1.top - insets2.top) <= threshold &&
13
+  ABS(insets1.bottom - insets2.bottom) <= threshold;
14
+}
15
+
8 16
 @implementation RNCSafeAreaView
17
+{
18
+  UIEdgeInsets _currentSafeAreaInsets;
19
+}
9 20
 
10 21
 - (BOOL)isSupportedByOS
11 22
 {
@@ -57,6 +68,13 @@
57 68
 - (void)invalidateSafeAreaInsets
58 69
 {
59 70
   UIEdgeInsets safeAreaInsets = [self realOrEmulateSafeAreaInsets];
71
+
72
+  if (UIEdgeInsetsEqualToEdgeInsetsWithThreshold(safeAreaInsets, _currentSafeAreaInsets, 1.0 / RCTScreenScale())) {
73
+    return;
74
+  }
75
+
76
+  _currentSafeAreaInsets = safeAreaInsets;
77
+
60 78
   self.onInsetsChange(@{
61 79
     @"insets": @{
62 80
       @"top": @(safeAreaInsets.top),

+ 20
- 11
src/index.tsx View File

@@ -2,7 +2,6 @@ import * as React from 'react';
2 2
 import {
3 3
   requireNativeComponent,
4 4
   NativeSyntheticEvent,
5
-  ViewStyle,
6 5
   StyleSheet,
7 6
 } from 'react-native';
8 7
 
@@ -15,12 +14,13 @@ export interface EdgeInsets {
15 14
   left: number;
16 15
 }
17 16
 
17
+const SafeAreaContext = React.createContext<EdgeInsets | null>(null);
18
+
18 19
 export interface SafeAreaViewProps {
19
-  children: (insets: EdgeInsets) => React.ReactNode;
20
-  style?: ViewStyle;
20
+  children?: React.ReactNode;
21 21
 }
22 22
 
23
-export default function SafeAreaView({ children, style }: SafeAreaViewProps) {
23
+export function SafeAreaProvider({ children }: SafeAreaViewProps) {
24 24
   const [insets, setInsets] = React.useState<EdgeInsets | null>(null);
25 25
   const onInsetsChange = React.useCallback(
26 26
     (event: NativeSyntheticEvent<{ insets: EdgeInsets }>) => {
@@ -29,14 +29,13 @@ export default function SafeAreaView({ children, style }: SafeAreaViewProps) {
29 29
     [],
30 30
   );
31 31
 
32
-  console.warn(insets);
33
-
34 32
   return (
35
-    <NativeSafeAreaView
36
-      style={[styles.fill, style]}
37
-      onInsetsChange={onInsetsChange}
38
-    >
39
-      {insets !== null ? children(insets) : null}
33
+    <NativeSafeAreaView style={styles.fill} onInsetsChange={onInsetsChange}>
34
+      {insets !== null ? (
35
+        <SafeAreaContext.Provider value={insets}>
36
+          {children}
37
+        </SafeAreaContext.Provider>
38
+      ) : null}
40 39
     </NativeSafeAreaView>
41 40
   );
42 41
 }
@@ -44,3 +43,13 @@ export default function SafeAreaView({ children, style }: SafeAreaViewProps) {
44 43
 const styles = StyleSheet.create({
45 44
   fill: { flex: 1 },
46 45
 });
46
+
47
+export function useSafeArea(): EdgeInsets {
48
+  const safeArea = React.useContext(SafeAreaContext);
49
+  if (safeArea == null) {
50
+    throw new Error(
51
+      'No safe area value available. Make sure you are rendering `<SafeAreaProvider>` at the top of your app.',
52
+    );
53
+  }
54
+  return safeArea;
55
+}