Skip to content

Draw Android toasts above sheets and dialogs - #5

Open
simonhamp wants to merge 1 commit into
victorycodedev:mainfrom
simonhamp:fix/android-toasts-above-sheets
Open

simonhamp wants to merge 1 commit into
victorycodedev:mainfrom
simonhamp:fix/android-toasts-above-sheets

Conversation

@simonhamp

Copy link
Copy Markdown

What changed?

On Android each toast position now gets its own window, added through the activity's WindowManager while that position has toasts and removed once it's empty. Before, the hosts were views inside the activity's decor view.

An activity's windows stack in the order they were added. While a toast is showing, the overlay checks for a newer window belonging to the same activity, like a sheet or dialog opened over the toast, and re-adds itself so it stays on top. It finds those windows with WindowInspector.getGlobalWindowViews() and window tokens, which are public from API 29, the plugin's minimum. Windows of other activities are left alone.

Details:

  • The windows are not focusable, so a text field in a sheet keeps focus and the keyboard stays up when a toast appears.
  • On API 34+ only the toast cards take touches, via AttachedSurfaceControl.setTouchableRegion. On 29 to 33 there's no public way to do that, so taps within about 16dp of a showing toast land on the toast's window.
  • Each overlay keeps its own recomposer for the life of the activity, so a re-add doesn't restart a toast's animation or swipe state.
  • The windows and compositions are removed when the activity is destroyed.
  • Because the overlay now sits above the keyboard, it reads the keyboard's height from the activity's windows and keeps bottom toasts just above it. Before, a bottom toast hid behind the keyboard.

The public API, positions, queue and stack behaviour, swipe, actions and events are unchanged. iOS is unchanged. Its host already lives in its own window at .alert - 1.

Why?

A toast shown while a Material3 ModalBottomSheet or any dialog is open was invisible on Android. The sheet is a dialog window above the activity's, so it covered the toast host. ToastShown still fired, and the toast only appeared once the sheet closed. We hit this in an app that reopens its composer sheet and shows an error toast when a post fails.

Before After
before after
before after

Over an AlertDialog:

after dialog

How was it tested?

  • vendor/bin/pest: 190 passed
  • composer validate --strict
  • A NativePHP app on an API 35 emulator with a bottom sheet holding a text field. I checked:
    • a toast fired from inside the sheet;
    • a toast fired while the sheet was opening;
    • a sheet opened over a live toast;
    • a toast over an AlertDialog;
    • a toast with no sheet, which looks the same as before;
    • taps passing through around the toast;
    • action buttons firing ToastActionPressed;
    • swipe to dismiss;
    • the keyboard staying up and typing carrying on;
    • rotation;
    • relaunching the activity with a toast up, with no leaked windows.

Not tested: API 29 to 34 on a device, the dynamic presentation and the centre position. The window layout for those matches the old one.

If a sheet or dialog opens over a toast that's already showing, the toast can blink for a frame or two while it's re-added.

Platforms tested

  • PHP
  • JavaScript
  • Android
  • iOS
  • Not applicable / documentation only

Breaking changes?

None. The one visible difference is that bottom toasts now sit above the keyboard instead of behind it.

Checklist

  • Tests were added or updated where appropriate. The change is in the native Android host, which the PHP and JS suites don't cover.
  • Existing relevant tests pass.
  • Documentation was updated where necessary (CHANGELOG).
  • Public API and event/bridge compatibility were considered.
  • Android/iOS behavior parity was considered where relevant.
  • No unnecessary native dependencies were introduced.
  • No secrets, tokens, temporary logging, or debugging code were committed.
  • Native changes were tested in a generated NativePHP application where practical.

🤖 Generated with Claude Code

Android hosted the toasts in ComposeViews inside the activity's decor view.
Material3's ModalBottomSheet and every Dialog are separate windows stacked
above the activity window, so a toast shown while one was open was drawn
underneath it.

Each position now gets its own TYPE_APPLICATION window, added through the
activity's WindowManager while that position has toasts and removed when it
is empty. Windows that share the activity's token are ordered by when they
were added, so while toasts are on screen the overlay checks each frame
whether another window of the activity was added after it, and re-adds
itself if so. The hosts compose against a recomposer the overlay owns, so a
toast keeps its animation and swipe state across a re-add.

The windows are FLAG_NOT_FOCUSABLE, so a text field in a sheet keeps focus
and the keyboard stays up. On API 34+ the touchable region is set to the
toast cards, so taps around them reach the screen. Before 34 there is no
public way to do that and the band's padding takes taps while a toast shows.

The overlay sits above the keyboard and is not given IME insets, so the
bottom band reads the keyboard height from the activity's other windows and
pads for it. Before, a bottom toast was hidden behind the keyboard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant