77
88package com.facebook.react.uimanager.internal
99
10+ import android.graphics.Matrix
1011import android.graphics.Rect
12+ import android.graphics.RectF
1113import android.view.View
1214import android.view.ViewTreeObserver
1315import androidx.core.graphics.Insets
@@ -16,19 +18,18 @@ import androidx.core.view.WindowInsetsCompat
1618import com.facebook.react.R
1719import com.facebook.react.uimanager.UIManagerHelper
1820import com.facebook.react.uimanager.events.SafeAreaInsetsChangeEvent
19- import kotlin.math.max
20- import kotlin.math.min
21+ import kotlin.math.roundToInt
2122
2223/* *
23- * Observes the part of a view that is covered by the system UI, and emits
24- * [SafeAreaInsetsChangeEvent] whenever it changes.
24+ * Observes the part of a view covered by system UI, and emits [SafeAreaInsetsChangeEvent] whenever
25+ * it changes.
2526 */
2627internal class SafeAreaInsetsObserver private constructor(private val view : View ) :
2728 ViewTreeObserver .OnPreDrawListener , View .OnAttachStateChangeListener {
2829
29- private val visibleRect = Rect ()
30- private val insets = IntArray (4 )
31- private val lastInsets = IntArray (4 )
30+ private val geometry = Geometry ()
31+ private val insets = FloatArray (4 )
32+ private val lastInsets = FloatArray (4 )
3233
3334 private var hasLastInsets = false
3435 private var isListening = false
@@ -72,6 +73,7 @@ internal class SafeAreaInsetsObserver private constructor(private val view: View
7273
7374 override fun onViewDetachedFromWindow (v : View ) {
7475 stopListening()
76+ hasLastInsets = false
7577 }
7678
7779 override fun onPreDraw (): Boolean {
@@ -83,7 +85,8 @@ internal class SafeAreaInsetsObserver private constructor(private val view: View
8385 // Emitting on anything but an inset change would loop: the synchronous
8486 // render an event causes produces a new frame, which runs this listener
8587 // again.
86- if (! computeSafeAreaInsets(view, visibleRect, insets)) {
88+ if (! computeSafeAreaInsets(view, geometry, insets)) {
89+ hasLastInsets = false
8790 return
8891 }
8992 if (hasLastInsets && insets.contentEquals(lastInsets)) {
@@ -107,6 +110,13 @@ internal class SafeAreaInsetsObserver private constructor(private val view: View
107110 )
108111 }
109112
113+ private class Geometry {
114+ val visibleRect = Rect ()
115+ val toRoot = Matrix ()
116+ val boundsRect = RectF ()
117+ val matrixValues = FloatArray (9 )
118+ }
119+
110120 companion object {
111121 private const val TOP = 0
112122 private const val RIGHT = 1
@@ -131,46 +141,105 @@ internal class SafeAreaInsetsObserver private constructor(private val view: View
131141 }
132142
133143 /* *
134- * The insets of the window that overlap [view], in the view's own coordinate space. A view that
135- * does not reach under the system UI has no insets.
144+ * Insets from [view]'s full transformed bounds to the window safe area, in window physical
145+ * pixels, rounded to integers. Ancestor clipping is only a visibility check, not a change to
146+ * those bounds. Returns null for empty/fully clipped views, missing window insets, or
147+ * unsupported transforms. The event preserves fractional pixels and converts them to dp.
136148 */
137149 @JvmStatic
138150 fun getSafeAreaInsets (view : View ): Insets ? {
139- val insets = IntArray (4 )
140- if (! computeSafeAreaInsets(view, Rect (), insets)) {
151+ val insets = FloatArray (4 )
152+ if (! computeSafeAreaInsets(view, Geometry (), insets)) {
141153 return null
142154 }
143- return Insets .of(insets[LEFT ], insets[TOP ], insets[RIGHT ], insets[BOTTOM ])
155+ return Insets .of(
156+ insets[LEFT ].roundToInt(),
157+ insets[TOP ].roundToInt(),
158+ insets[RIGHT ].roundToInt(),
159+ insets[BOTTOM ].roundToInt(),
160+ )
144161 }
145162
146163 /* *
147- * Writes the insets of [view] into [out], ordered [TOP], [RIGHT], [BOTTOM], [LEFT], using
148- * [visibleRect] as scratch space. Returns false when they cannot be computed, leaving [out]
149- * untouched.
164+ * Writes window physical-pixel insets into [out], ordered [TOP], [RIGHT], [BOTTOM], [LEFT].
165+ * Returns false when they cannot be computed, leaving [out] untouched.
150166 */
151- private fun computeSafeAreaInsets (view : View , visibleRect : Rect , out : IntArray ): Boolean {
152- if (view.width == 0 || view.height = = 0 ) {
167+ private fun computeSafeAreaInsets (view : View , geometry : Geometry , out : FloatArray ): Boolean {
168+ if (view.width <= 0 || view.height < = 0 ) {
153169 return false
154170 }
155171 val rootView = view.rootView
172+ if (rootView.width <= 0 || rootView.height <= 0 ) {
173+ return false
174+ }
156175 val windowInsets =
157176 ViewCompat .getRootWindowInsets(rootView)
158177 ?.getInsets(
159178 WindowInsetsCompat .Type .systemBars() or WindowInsetsCompat .Type .displayCutout(),
160179 ) ? : return false
161180
162- if (! view.getGlobalVisibleRect(visibleRect)) {
181+ if (! view.getGlobalVisibleRect(geometry. visibleRect)) {
163182 // The view is fully clipped by an ancestor (e.g. scrolled out of a
164183 // scroll view); the rect is undefined in that case, and a view that is
165184 // not visible has no meaningful insets.
166185 return false
167186 }
168- out [TOP ] = max(windowInsets.top - visibleRect.top, 0 )
169- out [RIGHT ] =
170- max(min(visibleRect.left + view.width - rootView.width, 0 ) + windowInsets.right, 0 )
171- out [BOTTOM ] =
172- max(min(visibleRect.top + view.height - rootView.height, 0 ) + windowInsets.bottom, 0 )
173- out [LEFT ] = max(windowInsets.left - visibleRect.left, 0 )
187+
188+ val toRoot = geometry.toRoot
189+ toRoot.reset()
190+ var current = view
191+ while (current != = rootView) {
192+ val parent = current.parent as ? View ? : return false
193+ toRoot.postConcat(current.matrix)
194+ toRoot.postTranslate(
195+ (current.left - parent.scrollX).toFloat(),
196+ (current.top - parent.scrollY).toFloat(),
197+ )
198+ current = parent
199+ }
200+ toRoot.getValues(geometry.matrixValues)
201+ if (
202+ geometry.matrixValues.any { ! it.isFinite() } ||
203+ geometry.matrixValues[Matrix .MSKEW_X ] != 0f ||
204+ geometry.matrixValues[Matrix .MSKEW_Y ] != 0f ||
205+ geometry.matrixValues[Matrix .MPERSP_0 ] != 0f ||
206+ geometry.matrixValues[Matrix .MPERSP_1 ] != 0f ||
207+ geometry.matrixValues[Matrix .MPERSP_2 ] != 1f ||
208+ geometry.matrixValues[Matrix .MSCALE_X ] == 0f ||
209+ geometry.matrixValues[Matrix .MSCALE_Y ] == 0f
210+ ) {
211+ return false
212+ }
213+
214+ val safeLeft = windowInsets.left.toFloat()
215+ val safeTop = windowInsets.top.toFloat()
216+ val safeRight = (rootView.width - windowInsets.right).toFloat()
217+ val safeBottom = (rootView.height - windowInsets.bottom).toFloat()
218+ if (safeRight <= safeLeft || safeBottom <= safeTop) {
219+ return false
220+ }
221+ val bounds = geometry.boundsRect
222+ bounds.set(0f , 0f , view.width.toFloat(), view.height.toFloat())
223+ // mapRect returns ordered bounding edges, including under reflection.
224+ toRoot.mapRect(bounds)
225+ val width = bounds.width()
226+ val height = bounds.height()
227+ if (
228+ ! bounds.left.isFinite() ||
229+ ! bounds.top.isFinite() ||
230+ ! bounds.right.isFinite() ||
231+ ! bounds.bottom.isFinite() ||
232+ ! width.isFinite() ||
233+ ! height.isFinite() ||
234+ width <= 0f ||
235+ height <= 0f
236+ ) {
237+ return false
238+ }
239+ out [TOP ] = (safeTop - bounds.top).coerceIn(0f , height)
240+ out [RIGHT ] = (bounds.right - safeRight).coerceIn(0f , width)
241+ out [BOTTOM ] = (bounds.bottom - safeBottom).coerceIn(0f , height)
242+ out [LEFT ] = (safeLeft - bounds.left).coerceIn(0f , width)
174243 return true
175244 }
176245 }
0 commit comments