appandflow / appandflow/react-native-safe-area-context

Fix proposal for the inset flicker (#364, #485): SafeAreaProvider emits a transient inset → 0 → inset across separate frames

Đang mở
#738 1 bình luận 2 reaction 0 người được giao Xem trên GitHub
Ngôn ngữ chính
TypeScript
Star
2.8k
Fork
258
Merge trung bình
3 ngày 1 giờ
Pull request đã merge (30 ngày)
2

Mô tả

## Summary

`SafeAreaProvider` can emit a transient `inset → 0 → inset` whose three values land in **separate frames**. Because they aren't in one batch, React commits the intermediate `0` as its own render, producing a one-frame layout flicker/jump. This affects both `` and `useSafeAreaInsets()` (both read from the provider). This is a root-cause write-up + proposed fix for the long-standing #364 (Android flicker) and #485 (iOS `insets.top` → 0 on orientation change), which appear to be the same underlying behavior.

## Environment
- `react-native-safe-area-context` 5.8.0 (also present in earlier 4.x/5.x per the linked issues)
- Android edge-to-edge, transparent status bar, `windowSoftInputMode="adjustResize"` (Pixel 8a, New Architecture / Fabric)
- Per #485, also reproduces on iOS during orientation changes

## Root cause (native)

Both `SafeAreaProvider.kt` and `SafeAreaView.kt` register an `OnPreDrawListener` and recompute the inset **on every draw** via `SafeAreaUtils.getSafeAreaInsets()`:

```kotlin
val windowInsets = getRootWindowInsetsCompat(rootView) ?: return null // rootView.rootWindowInsets
view.getGlobalVisibleRect(visibleRect)
top = max(windowInsets.top - visibleRect.top, 0f)
```

The inset is **not stored**, it's re-derived every draw from `rootView.rootWindowInsets`. On Android edge-to-edge the OS **re-dispatches `WindowInsets`** on focus/config/redraw events and momentarily reports `top = 0` before the real value re-applies. `SafeAreaProvider.maybeUpdateInsets()` only guards `mLastInsets != edgeInsets`, so `46 → 0` passes through, then `0 → 46` passes through again, the transient reaches JS.

## Why it becomes a visible flicker (the key subtlety)

In `SafeAreaContext.tsx`, `onInsetsChange` calls `setInsets`. React 18 auto-batches, so if `46 → 0 → 46` arrived in **one** event batch it would net to `46` and never render `0`. But the emissions land **one frame apart**, measured **~19 ms** on a 60 Hz device, so React commits the 0 as its own render as its own render. This means **same-frame event coalescing** (RNscroll-event `canCoalesce`, floated in #485) does **not** fix it: the transient is **cross-frame**.

## Evidence

Instrumenting `onInsetsChange` (raw event) and `setInsets` (committed):

```
[RAW] top = 46.095 initial
[RAW] top = 0 transient emitted by the library
[RAW] top = 46.095 reverts 19 ms later
→ React committed 46 → 0 → 46 (a one-frame layout jump)
```

## Proposed fix (JS provider, cross-platform)

Coalesce in `onInsetsChange`. A spurious value is always momentarily smaller than the real one, so we defer only decreases by a short window and drop them if the inset **reverts** to the committed value:

```
committed == null → commit immediately (initial mount)
insets === committed → drop pending (reverted → it was the transient)
increase (all edges ≥) → commit immediately (cold-start restore, keyboard close)
decrease → defer; a revert cancels it, a persistent value commits after the window
```

- **Never blanket-suppresses `0`**, a *persistent* `0` still commits (respects the note in #485 that insets can legitimately be 0).
- **No added latency** on increases/initial, only a real *shrink* (rotation, hidden bars) waits one window.
- **One place fixes both platforms** (Android #364 + iOS #485), since both go through the shared JS provider.

This is a mitigation of the *propagation*; the deeper native fix would stop all (validate/debounce before dispatch, or move Android off per-draw`OnPreDrawListener` polling onto a `WindowInsets` listener). Happy to go whichever direction the maintainers prefer, I have a working implementation with on-device validation and can open a PR.

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Đánh giá

Issue này chưa được đánh giá.

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.