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+ * Observes padding from a view's layout bounds to the window safe area , and emits
2425 * [SafeAreaInsetsChangeEvent] whenever 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,14 @@ 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 toLocal = Matrix ()
117+ val safeRect = RectF ()
118+ val matrixValues = FloatArray (9 )
119+ }
120+
110121 companion object {
111122 private const val TOP = 0
112123 private const val RIGHT = 1
@@ -131,46 +142,101 @@ internal class SafeAreaInsetsObserver private constructor(private val view: View
131142 }
132143
133144 /* *
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.
145+ * Padding from [view]'s full layout bounds to the window safe area, in local physical pixels,
146+ * rounded to integers. Ancestor clipping is only a visibility check, not a change to those
147+ * bounds. Returns null for empty/fully clipped views, missing window insets, or unsupported
148+ * transforms. The event preserves fractional pixels and converts them to dp.
136149 */
137150 @JvmStatic
138151 fun getSafeAreaInsets (view : View ): Insets ? {
139- val insets = IntArray (4 )
140- if (! computeSafeAreaInsets(view, Rect (), insets)) {
152+ val insets = FloatArray (4 )
153+ if (! computeSafeAreaInsets(view, Geometry (), insets)) {
141154 return null
142155 }
143- return Insets .of(insets[LEFT ], insets[TOP ], insets[RIGHT ], insets[BOTTOM ])
156+ return Insets .of(
157+ insets[LEFT ].roundToInt(),
158+ insets[TOP ].roundToInt(),
159+ insets[RIGHT ].roundToInt(),
160+ insets[BOTTOM ].roundToInt(),
161+ )
144162 }
145163
146164 /* *
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.
165+ * Writes local physical-pixel insets into [out], ordered [TOP], [RIGHT], [BOTTOM], [LEFT].
166+ * Returns false when they cannot be computed, leaving [out] untouched.
150167 */
151- private fun computeSafeAreaInsets (view : View , visibleRect : Rect , out : IntArray ): Boolean {
152- if (view.width == 0 || view.height = = 0 ) {
168+ private fun computeSafeAreaInsets (view : View , geometry : Geometry , out : FloatArray ): Boolean {
169+ if (view.width <= 0 || view.height < = 0 ) {
153170 return false
154171 }
155172 val rootView = view.rootView
173+ if (rootView.width <= 0 || rootView.height <= 0 ) {
174+ return false
175+ }
156176 val windowInsets =
157177 ViewCompat .getRootWindowInsets(rootView)
158178 ?.getInsets(
159179 WindowInsetsCompat .Type .systemBars() or WindowInsetsCompat .Type .displayCutout(),
160180 ) ? : return false
161181
162- if (! view.getGlobalVisibleRect(visibleRect)) {
182+ if (! view.getGlobalVisibleRect(geometry. visibleRect)) {
163183 // The view is fully clipped by an ancestor (e.g. scrolled out of a
164184 // scroll view); the rect is undefined in that case, and a view that is
165185 // not visible has no meaningful insets.
166186 return false
167187 }
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 )
188+
189+ val toRoot = geometry.toRoot
190+ toRoot.reset()
191+ var current = view
192+ while (current != = rootView) {
193+ val parent = current.parent as ? View ? : return false
194+ toRoot.postConcat(current.matrix)
195+ toRoot.postTranslate(
196+ (current.left - parent.scrollX).toFloat(),
197+ (current.top - parent.scrollY).toFloat(),
198+ )
199+ current = parent
200+ }
201+ toRoot.getValues(geometry.matrixValues)
202+ if (
203+ geometry.matrixValues.any { ! it.isFinite() } ||
204+ geometry.matrixValues[Matrix .MSKEW_X ] != 0f ||
205+ geometry.matrixValues[Matrix .MSKEW_Y ] != 0f ||
206+ geometry.matrixValues[Matrix .MPERSP_0 ] != 0f ||
207+ geometry.matrixValues[Matrix .MPERSP_1 ] != 0f ||
208+ geometry.matrixValues[Matrix .MPERSP_2 ] != 1f ||
209+ ! toRoot.invert(geometry.toLocal)
210+ ) {
211+ return false
212+ }
213+
214+ val safeRect = geometry.safeRect
215+ safeRect.set(
216+ windowInsets.left.toFloat(),
217+ windowInsets.top.toFloat(),
218+ (rootView.width - windowInsets.right).toFloat(),
219+ (rootView.height - windowInsets.bottom).toFloat(),
220+ )
221+ if (safeRect.isEmpty) {
222+ return false
223+ }
224+ // mapRect returns ordered bounding edges, including under reflection.
225+ geometry.toLocal.mapRect(safeRect)
226+ if (
227+ ! safeRect.left.isFinite() ||
228+ ! safeRect.top.isFinite() ||
229+ ! safeRect.right.isFinite() ||
230+ ! safeRect.bottom.isFinite()
231+ ) {
232+ return false
233+ }
234+ val width = view.width.toFloat()
235+ val height = view.height.toFloat()
236+ out [TOP ] = safeRect.top.coerceIn(0f , height)
237+ out [RIGHT ] = (width - safeRect.right).coerceIn(0f , width)
238+ out [BOTTOM ] = (height - safeRect.bottom).coerceIn(0f , height)
239+ out [LEFT ] = safeRect.left.coerceIn(0f , width)
174240 return true
175241 }
176242 }
0 commit comments