Skip to content

Commit

Permalink
Add resizeToAvoidBottomInset parameter in FScaffold (#304)
Browse files Browse the repository at this point in the history
* Initial fix

* Add _wrapper object

* Fix render object for scaffold

* Fix flashing navbar

* Complete implementation w tests

* Commit from GitHub Actions (Forui Presubmit)

* Fix failing build

* Commit from GitHub Actions (Forui Presubmit)

* Apply suggestions from code review

Co-authored-by: Matthias Ngeo <[email protected]>

* Fix pr issues

---------

Co-authored-by: Matthias Ngeo <[email protected]>
  • Loading branch information
kawaijoe and Pante authored Dec 13, 2024
1 parent 34dc133 commit 12d8d75
Show file tree
Hide file tree
Showing 10 changed files with 220 additions and 33 deletions.
2 changes: 1 addition & 1 deletion .idea/runConfigurations/Run_Samples_build_runner.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion docs/pages/docs/layout/scaffold.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,6 @@ FScaffold(
),
],
),
contentPad: false,
content: Column(
children: [
FCard(
Expand All @@ -141,5 +140,7 @@ FScaffold(
],
),
footer: FBottomNavigationBar(items: const []),
contentPad: true,
resizeToAvoidBottomInset: true,
);
```
2 changes: 2 additions & 0 deletions forui/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ Bump minimum Flutter version to 3.27.0.

* Add `FSelectMenuTile.builder`.

* Add `resizeToAvoidBottomInset` to `FScaffold(...)`.

### Changes

* Change `FCalendarController.date(...)` to automatically strip and truncate all DateTimes to dates in UTC timezone.
Expand Down
2 changes: 2 additions & 0 deletions forui/example/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/

# IntelliJ related
Expand Down
28 changes: 18 additions & 10 deletions forui/example/lib/sandbox.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,35 @@ class Sandbox extends StatefulWidget {
}

class _SandboxState extends State<Sandbox> with SingleTickerProviderStateMixin {
final FCalendarController<DateTime?> controller = FCalendarController.date();
late FPopoverController popoverController;
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
late FCalendarController<DateTime?> calendarController = FCalendarController.date();

@override
void initState() {
super.initState();
}

@override
Widget build(BuildContext context) => Column(
mainAxisSize: MainAxisSize.min,
children: [
FLineCalendar(
controller: controller,
),
],
Widget build(BuildContext context) => Form(
key: _formKey,
child: ListView(
padding: EdgeInsets.zero,
children: [
const FTextField(
label: Text('Username'),
hint: 'JaneDoe',
description: Text('Please enter your username.'),
maxLines: 1,
),
FCalendar(controller: calendarController),
FCalendar(controller: calendarController),
],
),
);

@override
void dispose() {
controller.dispose();
calendarController.dispose();
super.dispose();
}
}
Expand Down
185 changes: 164 additions & 21 deletions forui/lib/src/widgets/scaffold.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import 'dart:math';

import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';

import 'package:forui/forui.dart';
import 'package:forui/src/foundation/rendering.dart';
import 'package:meta/meta.dart';

/// A scaffold.
Expand All @@ -12,7 +16,7 @@ import 'package:meta/meta.dart';
/// See:
/// * https://forui.dev/docs/layout/scaffold for working examples.
/// * [FScaffoldStyle] for customizing a scaffold's appearance.
class FScaffold extends StatefulWidget {
class FScaffold extends StatelessWidget {
/// The content.
final Widget content;

Expand All @@ -25,6 +29,15 @@ class FScaffold extends StatefulWidget {
/// True if [FScaffoldStyle.contentPadding] should be applied to the [content]. Defaults to `true`.
final bool contentPad;

/// If true the [content] and the scaffold's floating widgets should size themselves to avoid the onscreen keyboard
/// whose height is defined by the ambient [MediaQuery]'s [MediaQueryData.viewInsets] `bottom` property.
///
/// For example, if there is an onscreen keyboard displayed above the scaffold, the body can be resized to avoid
/// overlapping the keyboard, which prevents widgets inside the body from being obscured by the keyboard.
///
/// Defaults to `true`.
final bool resizeToAvoidBottomInset;

/// The style. Defaults to [FThemeData.scaffoldStyle].
final FScaffoldStyle? style;

Expand All @@ -34,45 +47,60 @@ class FScaffold extends StatefulWidget {
this.header,
this.footer,
this.contentPad = true,
this.resizeToAvoidBottomInset = true,
this.style,
super.key,
});

@override
State<FScaffold> createState() => _State();

@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty('style', style))
..add(FlagProperty('contentPad', value: contentPad, defaultValue: true, ifTrue: 'pad'));
}
}

class _State extends State<FScaffold> {
@override
Widget build(BuildContext context) {
final style = widget.style ?? context.theme.scaffoldStyle;
Widget content = widget.content;
final style = this.style ?? context.theme.scaffoldStyle;
Widget content = this.content;
final Widget footer = this.footer != null
? DecoratedBox(
decoration: style.footerDecoration,
child: this.footer!,
)
: const SizedBox();

if (widget.contentPad) {
if (contentPad) {
content = Padding(padding: style.contentPadding, child: content);
}

return FSheets(
child: ColoredBox(
color: style.backgroundColor,
child: Column(
child: _RenderScaffoldWidget(
resizeToAvoidBottomInset: resizeToAvoidBottomInset,
children: [
if (widget.header != null) DecoratedBox(decoration: style.headerDecoration, child: widget.header!),
Expanded(child: content),
if (widget.footer != null) DecoratedBox(decoration: style.footerDecoration, child: widget.footer!),
Column(
children: [
if (header != null) DecoratedBox(decoration: style.headerDecoration, child: header!),
Expanded(child: content),
],
),
footer,
],
),
),
);
}

@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty('style', style))
..add(FlagProperty('contentPad', value: contentPad, ifTrue: 'contentPad', defaultValue: true))
..add(
FlagProperty(
'resizeToAvoidBottomInset',
value: resizeToAvoidBottomInset,
ifTrue: 'resizeToAvoidBottomInset',
defaultValue: true,
),
);
}
}

/// [FScaffold]'s style.
Expand Down Expand Up @@ -151,3 +179,118 @@ final class FScaffoldStyle with Diagnosticable {
int get hashCode =>
backgroundColor.hashCode ^ contentPadding.hashCode ^ headerDecoration.hashCode ^ footerDecoration.hashCode;
}

class _RenderScaffoldWidget extends MultiChildRenderObjectWidget {
final bool resizeToAvoidBottomInset;

const _RenderScaffoldWidget({
required this.resizeToAvoidBottomInset,
required super.children,
});

@override
RenderObject createRenderObject(BuildContext context) {
final viewInsets = MediaQuery.viewInsetsOf(context);

return _RenderScaffold(
resizeToAvoidBottomInset: resizeToAvoidBottomInset,
insets: viewInsets,
);
}

@override
void updateRenderObject(BuildContext context, _RenderScaffold renderObject) {
final viewInsets = MediaQuery.viewInsetsOf(context);
renderObject
..insets = viewInsets
..resizeToAvoidBottomInset = resizeToAvoidBottomInset;
}

@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(
FlagProperty(
'resizeToAvoidBottomInset',
value: resizeToAvoidBottomInset,
ifTrue: 'resizeToAvoidBottomInset',
),
);
}
}

class _Data extends ContainerBoxParentData<RenderBox> with ContainerParentDataMixin<RenderBox> {}

class _RenderScaffold extends RenderBox
with ContainerRenderObjectMixin<RenderBox, _Data>, RenderBoxContainerDefaultsMixin<RenderBox, _Data> {
bool _resizeToAvoidBottomInset;
EdgeInsets _insets;

_RenderScaffold({
required bool resizeToAvoidBottomInset,
required EdgeInsets insets,
}) : _resizeToAvoidBottomInset = resizeToAvoidBottomInset,
_insets = insets;

@override
void setupParentData(covariant RenderObject child) => child.parentData = _Data();

@override
void performLayout() {
size = constraints.biggest;
final others = firstChild!;

final footerConstraints = constraints.loosen();
final footer = lastChild!..layout(footerConstraints, parentUsesSize: true);
final footerHeight = _resizeToAvoidBottomInset ? max(insets.bottom, footer.size.height) : footer.size.height;

final othersHeight = constraints.maxHeight - footerHeight;
others.layout(constraints.copyWith(minHeight: 0, maxHeight: othersHeight));

others.data.offset = Offset.zero;
footer.data.offset = Offset(0, size.height - footer.size.height);
}

@override
void paint(PaintingContext context, Offset offset) => defaultPaint(context, offset);

@override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) =>
defaultHitTestChildren(result, position: position);

EdgeInsets get insets => _insets;

set insets(EdgeInsets value) {
if (_insets == value) {
return;
}

_insets = value;
markNeedsLayout();
}

bool get resizeToAvoidBottomInset => _resizeToAvoidBottomInset;

set resizeToAvoidBottomInset(bool value) {
if (_resizeToAvoidBottomInset == value) {
return;
}

_resizeToAvoidBottomInset = value;
markNeedsLayout();
}

@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(
FlagProperty(
'resizeToAvoidBottomInset',
value: resizeToAvoidBottomInset,
ifTrue: 'resizeToAvoidBottomInset',
),
)
..add(DiagnosticsProperty('insets', insets));
}
}
Binary file modified forui/test/golden/resizable/expanded-Axis.horizontal.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 31 additions & 0 deletions forui/test/src/widgets/scaffold_golden_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -97,5 +97,36 @@ void main() {
await expectLater(find.byType(TestScaffold), matchesGoldenFile('scaffold/${theme.name}-sheets.png'));
});
}

for (final resizeToAvoidBottomInset in [true, false]) {
testWidgets('resizeToAvoidBottomInset - $resizeToAvoidBottomInset', (tester) async {
await tester.pumpWidget(
TestScaffold(
theme: TestScaffold.themes[0].data,
child: FScaffold(
resizeToAvoidBottomInset: resizeToAvoidBottomInset,
header: Container(
decoration: const BoxDecoration(color: Colors.red),
height: 100,
),
content: const Placeholder(),
footer: Container(
decoration: const BoxDecoration(color: Colors.green),
height: 100,
),
),
),
);

// Simulate keyboard.
tester.view.viewInsets = const FakeViewPadding(bottom: 800);
await tester.pump();

await expectLater(
find.byType(TestScaffold),
matchesGoldenFile('scaffold/resizeToAvoidBottomInset-$resizeToAvoidBottomInset.png'),
);
});
}
});
}

0 comments on commit 12d8d75

Please sign in to comment.