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:
@@ -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
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
@*
|
||||
@using SteUp.Shared.Core.Messages.System
|
||||
@inject BackButtonService BackButton
|
||||
@implements IDisposable
|
||||
|
||||
@*
|
||||
Bottom sheet (STILE_UI §6.14): sale dal basso, maniglia di trascinamento,
|
||||
testata con titolo e chiusura, un'unica linea di struttura, corpo scorrevole.
|
||||
Sostituisce i menu flottanti: stessa funzione, bersagli molto piu' grandi.
|
||||
@@ -43,9 +47,35 @@
|
||||
|
||||
[Parameter] public RenderFragment? ChildContent { get; set; }
|
||||
|
||||
// Il tasto indietro del dispositivo chiude lo sheet invece di uscire dalla
|
||||
// schermata sotto: e' il gesto che l'utente si aspetta, ed e' anche la
|
||||
// ragione per cui i gestori sono impilati (vedi BackButtonService).
|
||||
private IDisposable? _backRegistration;
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
if (Open && _backRegistration is null)
|
||||
{
|
||||
_backRegistration = BackButton.Register(() => InvokeAsync(Close));
|
||||
}
|
||||
else if (!Open)
|
||||
{
|
||||
Unregister();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Close()
|
||||
{
|
||||
Unregister();
|
||||
Open = false;
|
||||
await OpenChanged.InvokeAsync(false);
|
||||
}
|
||||
|
||||
private void Unregister()
|
||||
{
|
||||
_backRegistration?.Dispose();
|
||||
_backRegistration = null;
|
||||
}
|
||||
|
||||
public void Dispose() => Unregister();
|
||||
}
|
||||
|
||||
@@ -5,15 +5,32 @@ namespace SteUp.Shared.Core.Messages.System;
|
||||
/// Il form scheda si registra mentre è aperto così il back non manda l'app in
|
||||
/// background buttando via il lavoro non salvato: passa invece dallo stesso
|
||||
/// percorso del pulsante "Annulla", che propone il salvataggio come bozza.
|
||||
///
|
||||
/// I gestori sono impilati: sopra il form può aprirsi un bottom sheet, e il
|
||||
/// tasto indietro deve chiudere prima quello. Chiudendolo si torna al gestore
|
||||
/// sottostante, che resta valido — senza la pila il sheet, uscendo di scena,
|
||||
/// porterebbe via con sé anche la registrazione del form.
|
||||
/// </summary>
|
||||
public class BackButtonService
|
||||
{
|
||||
private Func<Task>? _handler;
|
||||
private readonly List<Func<Task>> _handlers = [];
|
||||
|
||||
/// Registra il gestore corrente. Ritorna l'azione di deregistrazione.
|
||||
/// <summary>
|
||||
/// Segnala all'host se in questo momento c'è qualcuno da avvisare. L'host
|
||||
/// se ne serve per accendere e spegnere la propria intercettazione, così
|
||||
/// quando la UI non ha nulla da dire il tasto indietro torna a comportarsi
|
||||
/// esattamente come sempre.
|
||||
/// </summary>
|
||||
public event Action<bool>? HandlersChanged;
|
||||
|
||||
public bool HasHandlers => _handlers.Count > 0;
|
||||
|
||||
/// Registra il gestore corrente in cima alla pila. Ritorna la deregistrazione.
|
||||
public IDisposable Register(Func<Task> handler)
|
||||
{
|
||||
_handler = handler;
|
||||
_handlers.Add(handler);
|
||||
HandlersChanged?.Invoke(true);
|
||||
|
||||
return new Registration(this, handler);
|
||||
}
|
||||
|
||||
@@ -23,11 +40,10 @@ public class BackButtonService
|
||||
/// </summary>
|
||||
public bool TryHandleBackPressed()
|
||||
{
|
||||
var handler = _handler;
|
||||
if (handler is null) return false;
|
||||
if (_handlers.Count == 0) return false;
|
||||
|
||||
// Fire-and-forget: il gestore rientra nel dispatcher Blazor da solo.
|
||||
_ = handler();
|
||||
_ = _handlers[^1]();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -35,8 +51,8 @@ public class BackButtonService
|
||||
{
|
||||
public void Dispose()
|
||||
{
|
||||
// Non azzerare se nel frattempo si è registrato qualcun altro.
|
||||
if (ReferenceEquals(owner._handler, handler)) owner._handler = null;
|
||||
if (!owner._handlers.Remove(handler)) return;
|
||||
owner.HandlersChanged?.Invoke(owner.HasHandlers);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user