fix(ui): floating buttons stay put while the keyboard is up

A screen making way for the keyboard had its floating buttons lifted
above it, where they stood over what was typed, such as the Library's
search results. They now stay where they stand, behind the keyboard.

Whats-New: The buttons at the foot of a screen no longer rise with the keyboard.
Whats-New-De: Die Knöpfe am Fuß eines Bildschirms steigen nicht mehr mit der Tastatur.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-10 22:39:10 +01:00
commit 29f900992b
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
2 changed files with 44 additions and 9 deletions

View file

@ -1,3 +1,5 @@
import 'dart:math' as math;
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:halkyon_ui/l10n/halkyon_ui_localizations.dart';
@ -660,28 +662,43 @@ class const FrameFade({super.key}) extends StatefulWidget {
FloatingActionButtonLocation base = FloatingActionButtonLocation.endFloat,
]) {
final foot = context.dependOnInheritedWidgetOfExactType<_FrameFoot>();
return foot != null && foot.showsBar ? _OverTheFade(base) : base;
return foot != null && foot.showsBar
? _OverTheFade(base, foot.claims)
: base;
}
@override
State<FrameFade> createState() => _FrameFadeState();
}
/// [base] lowered by the fade's height.
final class const _OverTheFade(final FloatingActionButtonLocation base)
extends FloatingActionButtonLocation {
/// [base] lowered by the fade's height, and kept where it stands while the
/// keyboard is up (the developer's review, 10 Nov 2026): the Scaffold lifts
/// a floating button above the keyboard, where it stood in the way of
/// what is typed, as a search's results; lowered by as much as the keyboard
/// rises above the foot ([_FadeClaims.footHeight]), it stays behind it.
final class const _OverTheFade(
final FloatingActionButtonLocation base,
final _FadeClaims foot,
) extends FloatingActionButtonLocation {
@override
Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) => base
.getOffset(scaffoldGeometry)
.translate(0, FlushNavigationBar.fadeHeight);
Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) {
final raised = math.max(
0,
scaffoldGeometry.minInsets.bottom - foot.footHeight,
);
return base
.getOffset(scaffoldGeometry)
.translate(0, FlushNavigationBar.fadeHeight + raised);
}
// Equal for the same base, so that a screen built again doesn't move its
// button as if to another place.
@override
bool operator ==(Object other) => other is _OverTheFade && other.base == base;
bool operator ==(Object other) =>
other is _OverTheFade && other.base == base && other.foot == foot;
@override
int get hashCode => Object.hash(_OverTheFade, base);
int get hashCode => Object.hash(_OverTheFade, base, foot);
}
class _FrameFadeState() extends State<FrameFade> {
@ -759,6 +776,7 @@ class _FrameFadeState() extends State<FrameFade> {
final height = keyboard
? _height ?? MediaQuery.paddingOf(context).bottom
: _height = MediaQuery.paddingOf(context).bottom;
_foot!.claims.footHeight = height;
return SizedBox(
height: height,
child: Align(
@ -773,6 +791,10 @@ class _FrameFadeState() extends State<FrameFade> {
/// ([FrameFade]); the frame paints its own while none does.
class _FadeClaims() extends ChangeNotifier {
final _claims = <Object>{};
/// The height of the foot a [FrameFade] keeps beneath its screen, as last
/// built: what its button stands above.
double footHeight = 0;
bool _disposed = false;
/// Whether a screen in view paints the fade.

View file

@ -299,6 +299,19 @@ void main() {
expect(tester.getRect(find.byType(FloatingActionButton)), before);
});
testWidgets('the button stays where it stands, behind the keyboard, on a '
'screen that makes way for it (a search in the Library)', (tester) async {
await showPhone(tester, fadingScreen());
await tester.pumpAndSettle();
final before = tester.getRect(find.byType(FloatingActionButton));
tester.view.viewInsets = const FakeViewPadding(bottom: 300);
addTearDown(tester.view.resetViewInsets);
await tester.pumpAndSettle();
expect(tester.getRect(find.byType(FloatingActionButton)), before);
});
testWidgets("a screen without its own fade keeps the frame's", (
tester,
) async {