Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add resizeToAvoidBottomInset parameter in FScaffold #304

Merged
merged 12 commits into from
Dec 13, 2024
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
170 changes: 149 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,53 @@ 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: _Wrapper(
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(DiagnosticsProperty<bool>('contentPad', contentPad))
..add(DiagnosticsProperty<bool>('resizeToAvoidBottomInset', resizeToAvoidBottomInset));
kawaijoe marked this conversation as resolved.
Show resolved Hide resolved
}
}

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

class _Wrapper extends MultiChildRenderObjectWidget {
kawaijoe marked this conversation as resolved.
Show resolved Hide resolved
final bool resizeToAvoidBottomInset;

const _Wrapper({
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(DiagnosticsProperty('resizeToAvoidBottomInset', resizeToAvoidBottomInset));
kawaijoe marked this conversation as resolved.
Show resolved Hide resolved
}
}

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);
double footerHeight = footer.size.height;
if (_resizeToAvoidBottomInset) {
footerHeight = max(insets.bottom, footer.size.height);
}
kawaijoe marked this conversation as resolved.
Show resolved Hide resolved

final othersHeight = constraints.maxHeight - footerHeight;
final othersConstraints = constraints.copyWith(minHeight: 0, maxHeight: othersHeight);
others.layout(othersConstraints);
kawaijoe marked this conversation as resolved.
Show resolved Hide resolved

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(DiagnosticsProperty('resizeToAvoidBottomInset', resizeToAvoidBottomInset))
..add(DiagnosticsProperty('insets', insets));
kawaijoe marked this conversation as resolved.
Show resolved Hide resolved
}
}
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'),
);
});
}
});
}
Loading