Intercetta il tasto indietro dove passa davvero

Il salvataggio come bozza non e' mai scattato dal tasto indietro: la UI Blazor
non riceveva l'evento, e uscire da un form con modifiche in corso buttava via
il lavoro esattamente come prima che la bozza esistesse.

Il back non arrivava a OnBackPressed per due motivi diversi:
- da AndroidX Activity 1.6 il gesto passa dall'OnBackPressedDispatcher e il
  metodo dell'Activity non viene piu' invocato;
- con la navigazione a tre tasti il back arriva come evento di tastiera e la
  WebView lo intercetta per conto suo, tornando indietro nella propria
  cronologia, prima che Activity o dispatcher lo vedano.

Ora la chiave si ferma in DispatchKeyEvent, che passa prima della gerarchia di
view, e resta un callback sul dispatcher per la navigazione a gesti, dove
evento di tastiera non ce n'e'. Entrambi restano accesi solo finche' la UI ha
davvero qualcosa da dire: altrimenti il back si comporta come sempre.

I gestori diventano una pila, cosi' sopra il form puo' aprirsi un bottom sheet
e il back chiude prima quello senza portarsi via la registrazione sottostante.
ActionSheet si registra da solo mentre e' aperto.

Verificato sull'EDA52: back sul form con modifiche apre "Lavoro non salvato" e
salva la bozza; back sullo sheet chiude solo lo sheet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-17 12:45:23 +02:00
parent 811d8a86d8
commit 52b2994d3b
3 changed files with 129 additions and 15 deletions
+74 -6
View File
@@ -16,11 +16,15 @@ namespace SteUp.Maui
ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
public class MainActivity : MauiAppCompatActivity
{
private BackButtonService? _backButton;
private BlazorBackCallback? _backCallback;
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
ApplySystemBarsAppearance();
HookBackButton();
// Da Android 15 (API 35) il edge-to-edge e' forzato e adjustResize non ridimensiona
// piu' la finestra: la tastiera si limita a coprire la WebView. Riduciamo noi il
@@ -44,14 +48,78 @@ namespace SteUp.Maui
controller.AppearanceLightNavigationBars = true;
}
public override void OnBackPressed()
/// <summary>
/// Il tasto indietro va intercettato sul dispatcher di AndroidX, non
/// sovrascrivendo OnBackPressed: da AndroidX Activity 1.6 il gesto passa
/// tutto di li' e il metodo dell'Activity non viene piu' invocato — la
/// UI Blazor non riceveva mai il back e il lavoro non salvato del form
/// se ne andava con la navigazione.
///
/// Il callback resta acceso solo finche' c'e' davvero qualcuno in
/// ascolto: quando la UI non ha nulla da dire, il back torna a
/// comportarsi come sempre invece di essere inghiottito.
/// </summary>
private void HookBackButton()
{
// Se la UI Blazor ha un form aperto se ne occupa lei (propone il salvataggio
// come bozza); altrimenti comportamento standard.
var backButton = IPlatformApplication.Current?.Services.GetService<BackButtonService>();
if (backButton?.TryHandleBackPressed() == true) return;
_backButton = IPlatformApplication.Current?.Services.GetService<BackButtonService>();
if (_backButton is null) return;
base.OnBackPressed();
_backCallback = new BlazorBackCallback(_backButton);
OnBackPressedDispatcher.AddCallback(this, _backCallback);
_backCallback.Enabled = _backButton.HasHandlers;
_backButton.HandlersChanged += OnBackHandlersChanged;
}
private void OnBackHandlersChanged(bool hasHandlers)
{
if (_backCallback is null) return;
_backCallback.Enabled = hasHandlers;
if (!hasHandlers) return;
// Il dispatcher consulta i callback dall'ultimo registrato al primo,
// e BlazorWebView registra il suo (che fa tornare indietro la
// cronologia della WebView) quando l'handler viene creato, cioe' dopo
// OnCreate. Ri-registrandoci nel momento in cui la UI ha davvero
// qualcosa da dire torniamo in cima e il back arriva prima a noi.
_backCallback.Remove();
OnBackPressedDispatcher.AddCallback(this, _backCallback);
}
/// <summary>
/// Con la navigazione a tre tasti il back arriva come evento di tastiera
/// e la WebView lo intercetta per conto suo, tornando indietro nella
/// propria cronologia: ne' l'Activity ne' il dispatcher lo vedono mai.
/// Qui la chiave passa prima della gerarchia di view, quindi e' l'unico
/// punto in cui possiamo fermarla mentre un form ha lavoro non salvato.
/// Il callback sul dispatcher resta per la navigazione a gesti, dove
/// evento di tastiera non ce n'e'.
/// </summary>
public override bool DispatchKeyEvent(KeyEvent? e)
{
if (e?.KeyCode == Keycode.Back && _backButton?.HasHandlers == true)
{
// Si consuma anche il DOWN: lasciandolo passare la WebView
// reagirebbe lo stesso.
if (e.Action == KeyEventActions.Up) _backButton.TryHandleBackPressed();
return true;
}
return base.DispatchKeyEvent(e);
}
protected override void OnDestroy()
{
if (_backButton is not null) _backButton.HandlersChanged -= OnBackHandlersChanged;
base.OnDestroy();
}
private sealed class BlazorBackCallback(BackButtonService service)
: AndroidX.Activity.OnBackPressedCallback(false)
{
public override void HandleOnBackPressed() => service.TryHandleBackPressed();
}
private sealed class ImeInsetsListener : Java.Lang.Object, IOnApplyWindowInsetsListener