From 90e00f943467755f570d9e42ebeb5d01e2faff2b Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Sun, 29 Sep 2024 22:29:54 +0900 Subject: [PATCH 01/14] Rename "snappingBehavior" argument to "behavior" --- lib/src/foundation/sheet_physics.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/foundation/sheet_physics.dart b/lib/src/foundation/sheet_physics.dart index 0b04a51..a720c60 100644 --- a/lib/src/foundation/sheet_physics.dart +++ b/lib/src/foundation/sheet_physics.dart @@ -407,12 +407,12 @@ class SnappingSheetPhysics extends SheetPhysics with SheetPhysicsMixin { SheetPhysics copyWith({ SheetPhysics? parent, SpringDescription? spring, - SnappingSheetBehavior? snappingBehavior, + SnappingSheetBehavior? behavior, }) { return SnappingSheetPhysics( parent: parent ?? this.parent, spring: spring ?? this.spring, - behavior: snappingBehavior ?? this.behavior, + behavior: behavior ?? this.behavior, ); } From 48ea4962bf54264c4f99306f7b755ed20a661908 Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Sun, 29 Sep 2024 22:35:40 +0900 Subject: [PATCH 02/14] Deprecate SheetContentScaffold --- lib/src/foundation/sheet_content_scaffold.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/foundation/sheet_content_scaffold.dart b/lib/src/foundation/sheet_content_scaffold.dart index ce567ea..fad241c 100644 --- a/lib/src/foundation/sheet_content_scaffold.dart +++ b/lib/src/foundation/sheet_content_scaffold.dart @@ -8,7 +8,9 @@ import 'sheet_position.dart'; import 'sheet_position_scope.dart'; import 'sheet_viewport.dart'; +@Deprecated('Use SheetContent instead.') class SheetContentScaffold extends StatelessWidget { + @Deprecated('Use SheetContent instead.') const SheetContentScaffold({ super.key, this.primary = false, From 57b2d94b0972638f6c3cdfdb5039e64c9a1034ce Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Mon, 30 Sep 2024 22:00:31 +0900 Subject: [PATCH 03/14] Add SheetContent --- lib/src/foundation/foundation.dart | 1 + lib/src/foundation/sheet_content.dart | 264 ++++++++++++++++++++++++++ 2 files changed, 265 insertions(+) create mode 100644 lib/src/foundation/sheet_content.dart diff --git a/lib/src/foundation/foundation.dart b/lib/src/foundation/foundation.dart index f59bb80..286a5c6 100644 --- a/lib/src/foundation/foundation.dart +++ b/lib/src/foundation/foundation.dart @@ -5,6 +5,7 @@ export 'keyboard_dismissible.dart' DragUpSheetKeyboardDismissBehavior, SheetKeyboardDismissBehavior, SheetKeyboardDismissible; +export 'sheet_content.dart' show SheetContent; export 'sheet_content_scaffold.dart' show AnimatedBottomBarVisibility, diff --git a/lib/src/foundation/sheet_content.dart b/lib/src/foundation/sheet_content.dart new file mode 100644 index 0000000..e94ba4d --- /dev/null +++ b/lib/src/foundation/sheet_content.dart @@ -0,0 +1,264 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +class SheetContent extends StatelessWidget { + const SheetContent({ + super.key, + this.header, + required this.body, + this.footer, + this.extendBodyBehindHeader = false, + this.extendBodyBehindFooter = false, + this.resizeToAvoidBottomInset = true, + this.backgroundColor, + }); + + final Widget? header; + final Widget body; + final Widget? footer; + final bool extendBodyBehindHeader; + final bool extendBodyBehindFooter; + final bool resizeToAvoidBottomInset; + final Color? backgroundColor; + + @override + Widget build(BuildContext context) { + return Material( + color: backgroundColor ?? Theme.of(context).scaffoldBackgroundColor, + child: _SheetContentLayout( + header: header, + body: body, + footer: footer, + bottomMargin: resizeToAvoidBottomInset + ? MediaQuery.of(context).viewInsets.bottom + : 0, + extendBodyBehindHeader: extendBodyBehindHeader, + extendBodyBehindFooter: extendBodyBehindFooter, + resizeToAvoidBottomInset: resizeToAvoidBottomInset, + ), + ); + } +} + +enum _SheetContentSlot { header, body, footer } + +class _SheetContentLayout + extends SlottedMultiChildRenderObjectWidget<_SheetContentSlot, RenderBox> { + const _SheetContentLayout({ + required this.header, + required this.body, + required this.footer, + required this.bottomMargin, + required this.extendBodyBehindHeader, + required this.extendBodyBehindFooter, + required this.resizeToAvoidBottomInset, + }); + + final Widget? header; + final Widget body; + final Widget? footer; + final double bottomMargin; + final bool extendBodyBehindHeader; + final bool extendBodyBehindFooter; + final bool resizeToAvoidBottomInset; + + @override + Iterable<_SheetContentSlot> get slots => _SheetContentSlot.values; + + @override + Widget? childForSlot(_SheetContentSlot slot) { + return switch (slot) { + _SheetContentSlot.header => header, + _SheetContentSlot.body => body, + _SheetContentSlot.footer => footer, + }; + } + + @override + SlottedContainerRenderObjectMixin<_SheetContentSlot, RenderBox> + createRenderObject(BuildContext context) { + return _RenderSheetContentLayout( + extendBodyBehindHeader: extendBodyBehindHeader, + extendBodyBehindFooter: extendBodyBehindFooter, + bottomMargin: bottomMargin, + ); + } + + @override + void updateRenderObject( + BuildContext context, + _RenderSheetContentLayout renderObject, + ) { + renderObject + ..extendBodyBehindHeader = extendBodyBehindHeader + ..extendBodyBehindFooter = extendBodyBehindFooter + ..bottomMargin = bottomMargin; + } +} + +class _RenderSheetContentLayout extends RenderBox + with + SlottedContainerRenderObjectMixin<_SheetContentSlot, RenderBox>, + DebugOverflowIndicatorMixin { + _RenderSheetContentLayout({ + required bool extendBodyBehindHeader, + required bool extendBodyBehindFooter, + required double bottomMargin, + }) : _extendBodyBehindHeader = extendBodyBehindHeader, + _extendBodyBehindFooter = extendBodyBehindFooter, + _bottomMargin = bottomMargin; + + bool get extendBodyBehindHeader => _extendBodyBehindHeader; + bool _extendBodyBehindHeader; + + set extendBodyBehindHeader(bool value) { + if (_extendBodyBehindHeader != value) { + _extendBodyBehindHeader = value; + markNeedsLayout(); + } + } + + bool get extendBodyBehindFooter => _extendBodyBehindFooter; + bool _extendBodyBehindFooter; + + set extendBodyBehindFooter(bool value) { + if (_extendBodyBehindFooter != value) { + _extendBodyBehindFooter = value; + markNeedsLayout(); + } + } + + double get bottomMargin => _bottomMargin; + double _bottomMargin; + + set bottomMargin(double value) { + if (_bottomMargin != value) { + _bottomMargin = value; + markNeedsLayout(); + } + } + + @override + List get children { + final header = childForSlot(_SheetContentSlot.header); + final body = childForSlot(_SheetContentSlot.body); + final footer = childForSlot(_SheetContentSlot.footer); + return [ + // Sorted in hit-test order + if (header != null) header, + if (footer != null) footer, + if (body != null) body, + ]; + } + + late Size _lastMeasuredIntrinsicSize; + + @override + void performLayout() { + assert(constraints.hasBoundedWidth && constraints.hasBoundedHeight); + + final header = childForSlot(_SheetContentSlot.header); + final footer = childForSlot(_SheetContentSlot.footer); + final body = childForSlot(_SheetContentSlot.body)!; + + header?.layout(constraints, parentUsesSize: true); + footer?.layout(constraints, parentUsesSize: true); + + final headerHeight = header?.size.height ?? 0; + final footerHeight = footer?.size.height ?? 0; + final bodyTopPadding = extendBodyBehindHeader ? 0 : headerHeight; + final bodyBottomPadding = extendBodyBehindFooter ? 0 : footerHeight; + final maxBodyHeight = constraints.maxHeight - + bodyTopPadding - + bodyBottomPadding - + bottomMargin; + + body.layout( + constraints.copyWith(maxHeight: max(0.0, maxBodyHeight)), + parentUsesSize: true, + ); + + final bodyHeight = body.size.height + bodyTopPadding + bodyBottomPadding; + final intrinsicHeight = max(bodyHeight, max(headerHeight, footerHeight)); + _lastMeasuredIntrinsicSize = Size(constraints.maxWidth, intrinsicHeight); + size = constraints.constrain(_lastMeasuredIntrinsicSize); + + if (header != null) { + (header.parentData! as BoxParentData).offset = Offset.zero; + } + if (footer != null) { + (footer.parentData! as BoxParentData).offset = + Offset(0, size.height - footerHeight); + } + (body.parentData! as BoxParentData).offset = + extendBodyBehindHeader ? Offset.zero : Offset(0, headerHeight); + } + + @override + Size computeDryLayout(covariant BoxConstraints constraints) { + // TODO: DRY layout logic. + final header = childForSlot(_SheetContentSlot.header); + final footer = childForSlot(_SheetContentSlot.footer); + final body = childForSlot(_SheetContentSlot.body)!; + + final headerHeight = header?.getDryLayout(constraints).height ?? 0; + final footerHeight = footer?.getDryLayout(constraints).height ?? 0; + + final maxBodyHeight = constraints.maxHeight - + (extendBodyBehindHeader ? 0 : headerHeight) - + (extendBodyBehindFooter ? 0 : footerHeight) - + bottomMargin; + final bodyConstraints = + constraints.copyWith(maxHeight: max(0, maxBodyHeight)); + final bodyHeight = body.getDryLayout(bodyConstraints).height; + + return constraints.constrain( + Size( + constraints.maxWidth, + bodyHeight + + (extendBodyBehindHeader ? 0 : headerHeight) + + (extendBodyBehindFooter ? 0 : footerHeight), + ), + ); + } + + @override + bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { + for (final child in children) { + final childParentData = child.parentData! as BoxParentData; + final isHit = result.addWithPaintOffset( + offset: childParentData.offset, + position: position, + hitTest: (BoxHitTestResult result, Offset transformed) { + // The pointer position is transformed to the child's coordinate space + assert(transformed == position - childParentData.offset); + return child.hitTest(result, position: transformed); + }, + ); + if (isHit) { + return true; + } + } + return false; + } + + @override + void paint(PaintingContext context, Offset offset) { + for (final child in children.reversed) { + final childParentData = child.parentData! as BoxParentData; + context.paintChild(child, childParentData.offset + offset); + } + + assert(() { + paintOverflowIndicator( + context, + offset, + Offset.zero & size, + Offset.zero & _lastMeasuredIntrinsicSize, + ); + return true; + }()); + } +} From 146d403e0587d9b6088fd4808aac998a302372da Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Tue, 1 Oct 2024 02:01:58 +0900 Subject: [PATCH 04/14] wip --- .../lib/tutorial/sheet_content_example.dart | 136 ++++++++++++++++ lib/src/foundation/sheet_content.dart | 153 ++++++++++++++++-- 2 files changed, 275 insertions(+), 14 deletions(-) create mode 100644 example/lib/tutorial/sheet_content_example.dart diff --git a/example/lib/tutorial/sheet_content_example.dart b/example/lib/tutorial/sheet_content_example.dart new file mode 100644 index 0000000..5fa5be0 --- /dev/null +++ b/example/lib/tutorial/sheet_content_example.dart @@ -0,0 +1,136 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:smooth_sheets/smooth_sheets.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + // make navigation bar transparent + SystemChrome.setSystemUIOverlayStyle( + const SystemUiOverlayStyle( + systemNavigationBarColor: Colors.transparent, + ), + ); + // make flutter draw behind navigation bar + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + runApp(const _SheetContentExample()); +} + +class _SheetContentExample extends StatelessWidget { + const _SheetContentExample(); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + home: Stack( + children: [ + Scaffold(), + _MySheet(), + ], + ), + ); + } +} + +class _MySheet extends StatelessWidget { + const _MySheet(); + + @override + Widget build(BuildContext context) { + return DraggableSheet( + child: SheetContent( + header: GestureDetector( + onTap: () => debugPrint('Tap on header'), + child: Container( + color: Colors.blue.withOpacity(0.7), + width: double.infinity, + height: 80, + alignment: Alignment.bottomCenter, + child: const Row( + children: [ + _ViewportGeometryInfo(), + Spacer(), + _CloseKeyboardButton(), + ], + ), + ), + ), + body: GestureDetector( + onTap: () => debugPrint('Tap on body'), + child: ColoredBox( + color: Colors.red, + child: ConstrainedBox( + constraints: const BoxConstraints(minHeight: 200), + child: const SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _ViewportGeometryInfo(), + TextField( + scribbleEnabled: true, + maxLines: null, + ), + ], + ), + ), + ), + ), + ), + footer: GestureDetector( + onTap: () => debugPrint('Tap on footer'), + child: Container( + color: Colors.green.withOpacity(0.7), + width: double.infinity, + height: 80, + alignment: Alignment.topCenter, + child: const Row( + children: [ + _ViewportGeometryInfo(), + Spacer(), + _CloseKeyboardButton(), + ], + ), + ), + ), + ), + ); + } +} + +class _ViewportGeometryInfo extends StatelessWidget { + const _ViewportGeometryInfo(); + + @override + Widget build(BuildContext context) { + final viewInsets = MediaQuery.viewInsetsOf(context); + final padding = MediaQuery.paddingOf(context); + final viewPadding = MediaQuery.viewPaddingOf(context); + String displayText(String name, EdgeInsets value) { + return '$name: top= ${value.top.toStringAsFixed(1)}, bottom= ${value.bottom.toStringAsFixed(1)}'; + } + + return DefaultTextStyle( + style: const TextStyle(fontSize: 10, color: Colors.black), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text(displayText('viewInsets', viewInsets)), + Text(displayText('padding', padding)), + Text(displayText('viewPadding', viewPadding)), + ], + ), + ); + } +} + +class _CloseKeyboardButton extends StatelessWidget { + const _CloseKeyboardButton(); + + @override + Widget build(BuildContext context) { + return IconButton( + onPressed: () => primaryFocus?.unfocus(), + icon: const Icon(Icons.keyboard_hide), + ); + } +} diff --git a/lib/src/foundation/sheet_content.dart b/lib/src/foundation/sheet_content.dart index e94ba4d..b2a564b 100644 --- a/lib/src/foundation/sheet_content.dart +++ b/lib/src/foundation/sheet_content.dart @@ -3,6 +3,30 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +// Widget? _overridePadding({ +// required Widget? child, +// required MediaQueryData inheritedMediaQuery, +// EdgeInsets? viewInsets, +// EdgeInsets? padding, +// EdgeInsets? viewPadding, +// }) { +// if (child == null) { +// return null; +// } +// if (viewInsets == null && padding == null && viewPadding == null) { +// return child; +// } +// return MediaQuery( +// data: inheritedMediaQuery.copyWith( +// // viewInsets: viewInsets ?? inheritedMediaQuery.viewInsets, +// padding: padding ?? inheritedMediaQuery.padding, +// viewInsets: EdgeInsets.zero, +// viewPadding: viewPadding ?? inheritedMediaQuery.viewPadding, +// ), +// child: child, +// ); +// } + class SheetContent extends StatelessWidget { const SheetContent({ super.key, @@ -25,20 +49,115 @@ class SheetContent extends StatelessWidget { @override Widget build(BuildContext context) { - return Material( - color: backgroundColor ?? Theme.of(context).scaffoldBackgroundColor, + final mediaQuery = MediaQuery.of(context); + final bottomMargin = + resizeToAvoidBottomInset ? mediaQuery.viewInsets.bottom : 0.0; + final backgroundColor = + this.backgroundColor ?? Theme.of(context).scaffoldBackgroundColor; + + final body = extendBodyBehindHeader || extendBodyBehindFooter + ? _BodyContainer( + body: this.body, + extendBodyBehindHeader: extendBodyBehindHeader, + extendBodyBehindFooter: extendBodyBehindFooter, + ) + : this.body; + + Widget result = Material( + color: backgroundColor, child: _SheetContentLayout( header: header, body: body, footer: footer, - bottomMargin: resizeToAvoidBottomInset - ? MediaQuery.of(context).viewInsets.bottom - : 0, + bottomMargin: bottomMargin, extendBodyBehindHeader: extendBodyBehindHeader, extendBodyBehindFooter: extendBodyBehindFooter, - resizeToAvoidBottomInset: resizeToAvoidBottomInset, ), ); + + // if (mediaQuery.viewInsets.bottom > 0 && resizeToAvoidBottomInset) { + // result = MediaQuery( + // data: MediaQuery.of(context).copyWith( + // viewInsets: mediaQuery.viewInsets.copyWith(bottom: 0), + // ), + // child: result, + // ); + // } + + return result; + } +} + +class _BodyBoxConstraints extends BoxConstraints { + const _BodyBoxConstraints({ + required double width, + required super.maxHeight, + required this.footerHeight, + required this.headerHeight, + }) : assert(footerHeight >= 0), + assert(headerHeight >= 0), + super(minWidth: width, maxWidth: width); + + final double footerHeight; + final double headerHeight; + + // RenderObject.layout() will only short-circuit its call to its performLayout + // method if the new layout constraints are not == to the current constraints. + // If the height of the bottom widgets has changed, even though the constraints' + // min and max values have not, we still want performLayout to happen. + @override + bool operator ==(Object other) { + return super == other && + other is _BodyBoxConstraints && + other.footerHeight == footerHeight && + other.headerHeight == headerHeight; + } + + @override + int get hashCode => Object.hash( + super.hashCode, + footerHeight, + headerHeight, + ); +} + +class _BodyContainer extends StatelessWidget { + const _BodyContainer({ + required this.body, + required this.extendBodyBehindHeader, + required this.extendBodyBehindFooter, + }) : assert(extendBodyBehindFooter || extendBodyBehindHeader); + + final Widget body; + final bool extendBodyBehindHeader; + final bool extendBodyBehindFooter; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final bodyConstraints = constraints as _BodyBoxConstraints; + final metrics = MediaQuery.of(context); + + final bottom = extendBodyBehindFooter + ? max(metrics.padding.bottom, bodyConstraints.footerHeight) + : metrics.padding.bottom; + + final top = extendBodyBehindHeader + ? max(metrics.padding.top, bodyConstraints.headerHeight) + : metrics.padding.top; + + return MediaQuery( + data: metrics.copyWith( + padding: metrics.padding.copyWith( + top: top, + bottom: bottom, + ), + ), + child: body, + ); + }, + ); } } @@ -53,7 +172,6 @@ class _SheetContentLayout required this.bottomMargin, required this.extendBodyBehindHeader, required this.extendBodyBehindFooter, - required this.resizeToAvoidBottomInset, }); final Widget? header; @@ -62,7 +180,6 @@ class _SheetContentLayout final double bottomMargin; final bool extendBodyBehindHeader; final bool extendBodyBehindFooter; - final bool resizeToAvoidBottomInset; @override Iterable<_SheetContentSlot> get slots => _SheetContentSlot.values; @@ -158,32 +275,40 @@ class _RenderSheetContentLayout extends RenderBox @override void performLayout() { assert(constraints.hasBoundedWidth && constraints.hasBoundedHeight); + final fullWidthConstraints = + constraints.tighten(width: constraints.maxWidth); final header = childForSlot(_SheetContentSlot.header); final footer = childForSlot(_SheetContentSlot.footer); final body = childForSlot(_SheetContentSlot.body)!; - header?.layout(constraints, parentUsesSize: true); - footer?.layout(constraints, parentUsesSize: true); + header?.layout(fullWidthConstraints, parentUsesSize: true); + footer?.layout(fullWidthConstraints, parentUsesSize: true); final headerHeight = header?.size.height ?? 0; final footerHeight = footer?.size.height ?? 0; final bodyTopPadding = extendBodyBehindHeader ? 0 : headerHeight; final bodyBottomPadding = extendBodyBehindFooter ? 0 : footerHeight; - final maxBodyHeight = constraints.maxHeight - + final maxBodyHeight = fullWidthConstraints.maxHeight - bodyTopPadding - bodyBottomPadding - bottomMargin; body.layout( - constraints.copyWith(maxHeight: max(0.0, maxBodyHeight)), + _BodyBoxConstraints( + width: fullWidthConstraints.maxWidth, + maxHeight: max(0.0, maxBodyHeight), + footerHeight: footerHeight, + headerHeight: headerHeight, + ), parentUsesSize: true, ); final bodyHeight = body.size.height + bodyTopPadding + bodyBottomPadding; final intrinsicHeight = max(bodyHeight, max(headerHeight, footerHeight)); - _lastMeasuredIntrinsicSize = Size(constraints.maxWidth, intrinsicHeight); - size = constraints.constrain(_lastMeasuredIntrinsicSize); + _lastMeasuredIntrinsicSize = + Size(fullWidthConstraints.maxWidth, intrinsicHeight); + size = fullWidthConstraints.constrain(_lastMeasuredIntrinsicSize); if (header != null) { (header.parentData! as BoxParentData).offset = Offset.zero; From 54122f9662f488ed40944cc8ee12e9b1d39be3a9 Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Wed, 2 Oct 2024 01:39:40 +0900 Subject: [PATCH 05/14] Handle paddings --- lib/src/foundation/sheet_content.dart | 156 ++++++++++++++++---------- 1 file changed, 97 insertions(+), 59 deletions(-) diff --git a/lib/src/foundation/sheet_content.dart b/lib/src/foundation/sheet_content.dart index b2a564b..9bca503 100644 --- a/lib/src/foundation/sheet_content.dart +++ b/lib/src/foundation/sheet_content.dart @@ -3,35 +3,50 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -// Widget? _overridePadding({ -// required Widget? child, -// required MediaQueryData inheritedMediaQuery, -// EdgeInsets? viewInsets, -// EdgeInsets? padding, -// EdgeInsets? viewPadding, -// }) { -// if (child == null) { -// return null; -// } -// if (viewInsets == null && padding == null && viewPadding == null) { -// return child; -// } -// return MediaQuery( -// data: inheritedMediaQuery.copyWith( -// // viewInsets: viewInsets ?? inheritedMediaQuery.viewInsets, -// padding: padding ?? inheritedMediaQuery.padding, -// viewInsets: EdgeInsets.zero, -// viewPadding: viewPadding ?? inheritedMediaQuery.viewPadding, -// ), -// child: child, -// ); -// } +Widget? _removePadding({ + required Widget? child, + required BuildContext context, + bool removeBottomViewInset = false, + bool removeTopPadding = false, + bool removeBottomPadding = false, + bool removeTopViewPadding = false, + bool removeBottomViewPadding = false, +}) { + if (child == null) { + return null; + } + if (!removeBottomViewInset && + !removeTopPadding && + !removeBottomPadding && + !removeTopViewPadding && + !removeBottomViewPadding) { + return child; + } + + final mediaQuery = MediaQuery.of(context); + return MediaQuery( + data: mediaQuery.copyWith( + viewInsets: removeBottomViewInset + ? mediaQuery.viewInsets.copyWith(bottom: 0) + : mediaQuery.viewInsets, + padding: mediaQuery.padding.copyWith( + top: removeTopPadding ? 0 : mediaQuery.padding.top, + bottom: removeBottomPadding ? 0 : mediaQuery.padding.bottom, + ), + viewPadding: mediaQuery.viewPadding.copyWith( + top: removeTopViewPadding ? 0 : mediaQuery.viewPadding.top, + bottom: removeBottomViewPadding ? 0 : mediaQuery.viewPadding.bottom, + ), + ), + child: child, + ); +} class SheetContent extends StatelessWidget { const SheetContent({ super.key, - this.header, required this.body, + this.header, this.footer, this.extendBodyBehindHeader = false, this.extendBodyBehindFooter = false, @@ -39,8 +54,8 @@ class SheetContent extends StatelessWidget { this.backgroundColor, }); - final Widget? header; final Widget body; + final Widget? header; final Widget? footer; final bool extendBodyBehindHeader; final bool extendBodyBehindFooter; @@ -49,21 +64,58 @@ class SheetContent extends StatelessWidget { @override Widget build(BuildContext context) { + final header = _removePadding( + context: context, + // Always remove the bottom view inset regardless of + // `resizeToAvoidBottomInset` flag as the SheetViewport will + // push the sheet up when the keyboard is shown. + removeBottomViewInset: true, + removeBottomViewPadding: true, + removeBottomPadding: true, + child: this.header, + ); + + final footer = _removePadding( + context: context, + removeBottomViewInset: true, + removeTopViewPadding: true, + removeTopPadding: true, + child: this.footer, + ); + + final Widget body; + if ((header != null && extendBodyBehindHeader) || + (footer != null && extendBodyBehindFooter)) { + body = _removePadding( + context: context, + removeBottomViewInset: true, + // We don't remove the vertical paddings for the `body` here + // as the _BodyContainer will take care of it. + child: _BodyContainer( + body: this.body, + extendBodyBehindHeader: extendBodyBehindHeader, + extendBodyBehindFooter: extendBodyBehindFooter, + ), + )!; + } else { + body = _removePadding( + context: context, + removeBottomViewInset: true, + removeTopPadding: header != null, + removeTopViewPadding: header != null, + removeBottomPadding: footer != null, + removeBottomViewPadding: footer != null, + child: this.body, + )!; + } + final mediaQuery = MediaQuery.of(context); final bottomMargin = resizeToAvoidBottomInset ? mediaQuery.viewInsets.bottom : 0.0; final backgroundColor = this.backgroundColor ?? Theme.of(context).scaffoldBackgroundColor; - final body = extendBodyBehindHeader || extendBodyBehindFooter - ? _BodyContainer( - body: this.body, - extendBodyBehindHeader: extendBodyBehindHeader, - extendBodyBehindFooter: extendBodyBehindFooter, - ) - : this.body; - - Widget result = Material( + return Material( color: backgroundColor, child: _SheetContentLayout( header: header, @@ -74,17 +126,6 @@ class SheetContent extends StatelessWidget { extendBodyBehindFooter: extendBodyBehindFooter, ), ); - - // if (mediaQuery.viewInsets.bottom > 0 && resizeToAvoidBottomInset) { - // result = MediaQuery( - // data: MediaQuery.of(context).copyWith( - // viewInsets: mediaQuery.viewInsets.copyWith(bottom: 0), - // ), - // child: result, - // ); - // } - - return result; } } @@ -92,32 +133,28 @@ class _BodyBoxConstraints extends BoxConstraints { const _BodyBoxConstraints({ required double width, required super.maxHeight, - required this.footerHeight, required this.headerHeight, + required this.footerHeight, }) : assert(footerHeight >= 0), assert(headerHeight >= 0), super(minWidth: width, maxWidth: width); - final double footerHeight; final double headerHeight; + final double footerHeight; - // RenderObject.layout() will only short-circuit its call to its performLayout - // method if the new layout constraints are not == to the current constraints. - // If the height of the bottom widgets has changed, even though the constraints' - // min and max values have not, we still want performLayout to happen. @override bool operator ==(Object other) { return super == other && other is _BodyBoxConstraints && - other.footerHeight == footerHeight && - other.headerHeight == headerHeight; + other.headerHeight == headerHeight && + other.footerHeight == footerHeight; } @override int get hashCode => Object.hash( super.hashCode, - footerHeight, headerHeight, + footerHeight, ); } @@ -149,10 +186,8 @@ class _BodyContainer extends StatelessWidget { return MediaQuery( data: metrics.copyWith( - padding: metrics.padding.copyWith( - top: top, - bottom: bottom, - ), + padding: metrics.padding.copyWith(top: top, bottom: bottom), + viewPadding: metrics.viewPadding.copyWith(top: top, bottom: bottom), ), child: body, ); @@ -257,19 +292,22 @@ class _RenderSheetContentLayout extends RenderBox } } + /// Returns the children in hit test order. @override List get children { final header = childForSlot(_SheetContentSlot.header); final body = childForSlot(_SheetContentSlot.body); final footer = childForSlot(_SheetContentSlot.footer); return [ - // Sorted in hit-test order if (header != null) header, if (footer != null) footer, if (body != null) body, ]; } + /// Preferred size of this render box measured during the last layout. + /// + /// Used to paint the overflow indicator in debug mode. late Size _lastMeasuredIntrinsicSize; @override From cc75a1610381260925c896855682130bcf435b6a Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Wed, 2 Oct 2024 01:40:09 +0900 Subject: [PATCH 06/14] Ignore deprecated_member_use --- .../lib/tutorial/sheet_content_example.dart | 81 ++++++++++++------- lib/src/foundation/foundation.dart | 1 + .../foundation/sheet_content_scaffold.dart | 2 + 3 files changed, 57 insertions(+), 27 deletions(-) diff --git a/example/lib/tutorial/sheet_content_example.dart b/example/lib/tutorial/sheet_content_example.dart index 5fa5be0..b9169eb 100644 --- a/example/lib/tutorial/sheet_content_example.dart +++ b/example/lib/tutorial/sheet_content_example.dart @@ -38,6 +38,8 @@ class _MySheet extends StatelessWidget { Widget build(BuildContext context) { return DraggableSheet( child: SheetContent( + // extendBodyBehindHeader: true, + // extendBodyBehindFooter: true, header: GestureDetector( onTap: () => debugPrint('Tap on header'), child: Container( @@ -47,44 +49,69 @@ class _MySheet extends StatelessWidget { alignment: Alignment.bottomCenter, child: const Row( children: [ - _ViewportGeometryInfo(), + _DebugViewportGeometry(), Spacer(), _CloseKeyboardButton(), ], ), ), ), - body: GestureDetector( - onTap: () => debugPrint('Tap on body'), - child: ColoredBox( - color: Colors.red, - child: ConstrainedBox( - constraints: const BoxConstraints(minHeight: 200), - child: const SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _ViewportGeometryInfo(), - TextField( - scribbleEnabled: true, - maxLines: null, + body: Builder( + builder: (context) { + return Padding( + padding: EdgeInsets.only( + top: MediaQuery.paddingOf(context).top, + bottom: MediaQuery.paddingOf(context).bottom, + ), + child: GestureDetector( + onTap: () => debugPrint('Tap on body'), + child: ColoredBox( + color: Colors.red, + child: ConstrainedBox( + constraints: const BoxConstraints(minHeight: 200), + child: const SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _DebugViewportGeometry(), + TextField( + scribbleEnabled: true, + maxLines: null, + ), + ], + ), ), - ], + ), ), ), - ), - ), + ); + }, ), - footer: GestureDetector( - onTap: () => debugPrint('Tap on footer'), - child: Container( - color: Colors.green.withOpacity(0.7), + footer: const _SheetFooter(), + ), + ); + } +} + +class _SheetFooter extends StatelessWidget { + const _SheetFooter(); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => debugPrint('Tap on footer'), + child: ColoredBox( + color: Colors.green, + child: Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.paddingOf(context).bottom, + ), + child: const SizedBox( width: double.infinity, height: 80, - alignment: Alignment.topCenter, - child: const Row( + child: Row( children: [ - _ViewportGeometryInfo(), + _DebugViewportGeometry(), Spacer(), _CloseKeyboardButton(), ], @@ -96,8 +123,8 @@ class _MySheet extends StatelessWidget { } } -class _ViewportGeometryInfo extends StatelessWidget { - const _ViewportGeometryInfo(); +class _DebugViewportGeometry extends StatelessWidget { + const _DebugViewportGeometry(); @override Widget build(BuildContext context) { diff --git a/lib/src/foundation/foundation.dart b/lib/src/foundation/foundation.dart index 286a5c6..e6c2185 100644 --- a/lib/src/foundation/foundation.dart +++ b/lib/src/foundation/foundation.dart @@ -13,6 +13,7 @@ export 'sheet_content_scaffold.dart' ConditionalStickyBottomBarVisibility, FixedBottomBarVisibility, ResizeScaffoldBehavior, + // ignore: deprecated_member_use_from_same_package SheetContentScaffold, StickyBottomBarVisibility; export 'sheet_controller.dart' show DefaultSheetController, SheetController; diff --git a/lib/src/foundation/sheet_content_scaffold.dart b/lib/src/foundation/sheet_content_scaffold.dart index fad241c..4373527 100644 --- a/lib/src/foundation/sheet_content_scaffold.dart +++ b/lib/src/foundation/sheet_content_scaffold.dart @@ -1,3 +1,5 @@ +// ignore_for_file: deprecated_member_use_from_same_package + import 'dart:math'; import 'package:flutter/material.dart'; From ca1e7b0ce5d8a7d25b6bab050e1013003c3c0bdb Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Wed, 2 Oct 2024 21:42:42 +0900 Subject: [PATCH 07/14] Rename SheetContent to SheetLayout --- .../lib/tutorial/sheet_content_example.dart | 8 +-- lib/src/foundation/foundation.dart | 2 +- .../{sheet_content.dart => sheet_layout.dart} | 54 +++++++++---------- 3 files changed, 32 insertions(+), 32 deletions(-) rename lib/src/foundation/{sheet_content.dart => sheet_layout.dart} (89%) diff --git a/example/lib/tutorial/sheet_content_example.dart b/example/lib/tutorial/sheet_content_example.dart index b9169eb..7ce8eaa 100644 --- a/example/lib/tutorial/sheet_content_example.dart +++ b/example/lib/tutorial/sheet_content_example.dart @@ -37,9 +37,9 @@ class _MySheet extends StatelessWidget { @override Widget build(BuildContext context) { return DraggableSheet( - child: SheetContent( - // extendBodyBehindHeader: true, - // extendBodyBehindFooter: true, + child: SheetLayout( + extendBodyBehindHeader: true, + extendBodyBehindFooter: true, header: GestureDetector( onTap: () => debugPrint('Tap on header'), child: Container( @@ -87,7 +87,7 @@ class _MySheet extends StatelessWidget { ); }, ), - footer: const _SheetFooter(), + // footer: const _SheetFooter(), ), ); } diff --git a/lib/src/foundation/foundation.dart b/lib/src/foundation/foundation.dart index e6c2185..95f463c 100644 --- a/lib/src/foundation/foundation.dart +++ b/lib/src/foundation/foundation.dart @@ -5,7 +5,6 @@ export 'keyboard_dismissible.dart' DragUpSheetKeyboardDismissBehavior, SheetKeyboardDismissBehavior, SheetKeyboardDismissible; -export 'sheet_content.dart' show SheetContent; export 'sheet_content_scaffold.dart' show AnimatedBottomBarVisibility, @@ -24,6 +23,7 @@ export 'sheet_drag.dart' SheetDragEndDetails, SheetDragStartDetails, SheetDragUpdateDetails; +export 'sheet_layout.dart' show SheetLayout; export 'sheet_notification.dart' show SheetDragCancelNotification, diff --git a/lib/src/foundation/sheet_content.dart b/lib/src/foundation/sheet_layout.dart similarity index 89% rename from lib/src/foundation/sheet_content.dart rename to lib/src/foundation/sheet_layout.dart index 9bca503..247caf0 100644 --- a/lib/src/foundation/sheet_content.dart +++ b/lib/src/foundation/sheet_layout.dart @@ -42,8 +42,8 @@ Widget? _removePadding({ ); } -class SheetContent extends StatelessWidget { - const SheetContent({ +class SheetLayout extends StatelessWidget { + const SheetLayout({ super.key, required this.body, this.header, @@ -117,7 +117,7 @@ class SheetContent extends StatelessWidget { return Material( color: backgroundColor, - child: _SheetContentLayout( + child: _RenderSheetLayoutWidget( header: header, body: body, footer: footer, @@ -196,11 +196,11 @@ class _BodyContainer extends StatelessWidget { } } -enum _SheetContentSlot { header, body, footer } +enum _SheetLayoutSlot { header, body, footer } -class _SheetContentLayout - extends SlottedMultiChildRenderObjectWidget<_SheetContentSlot, RenderBox> { - const _SheetContentLayout({ +class _RenderSheetLayoutWidget + extends SlottedMultiChildRenderObjectWidget<_SheetLayoutSlot, RenderBox> { + const _RenderSheetLayoutWidget({ required this.header, required this.body, required this.footer, @@ -217,21 +217,21 @@ class _SheetContentLayout final bool extendBodyBehindFooter; @override - Iterable<_SheetContentSlot> get slots => _SheetContentSlot.values; + Iterable<_SheetLayoutSlot> get slots => _SheetLayoutSlot.values; @override - Widget? childForSlot(_SheetContentSlot slot) { + Widget? childForSlot(_SheetLayoutSlot slot) { return switch (slot) { - _SheetContentSlot.header => header, - _SheetContentSlot.body => body, - _SheetContentSlot.footer => footer, + _SheetLayoutSlot.header => header, + _SheetLayoutSlot.body => body, + _SheetLayoutSlot.footer => footer, }; } @override - SlottedContainerRenderObjectMixin<_SheetContentSlot, RenderBox> + SlottedContainerRenderObjectMixin<_SheetLayoutSlot, RenderBox> createRenderObject(BuildContext context) { - return _RenderSheetContentLayout( + return _RenderSheetLayout( extendBodyBehindHeader: extendBodyBehindHeader, extendBodyBehindFooter: extendBodyBehindFooter, bottomMargin: bottomMargin, @@ -241,7 +241,7 @@ class _SheetContentLayout @override void updateRenderObject( BuildContext context, - _RenderSheetContentLayout renderObject, + _RenderSheetLayout renderObject, ) { renderObject ..extendBodyBehindHeader = extendBodyBehindHeader @@ -250,11 +250,11 @@ class _SheetContentLayout } } -class _RenderSheetContentLayout extends RenderBox +class _RenderSheetLayout extends RenderBox with - SlottedContainerRenderObjectMixin<_SheetContentSlot, RenderBox>, + SlottedContainerRenderObjectMixin<_SheetLayoutSlot, RenderBox>, DebugOverflowIndicatorMixin { - _RenderSheetContentLayout({ + _RenderSheetLayout({ required bool extendBodyBehindHeader, required bool extendBodyBehindFooter, required double bottomMargin, @@ -295,9 +295,9 @@ class _RenderSheetContentLayout extends RenderBox /// Returns the children in hit test order. @override List get children { - final header = childForSlot(_SheetContentSlot.header); - final body = childForSlot(_SheetContentSlot.body); - final footer = childForSlot(_SheetContentSlot.footer); + final header = childForSlot(_SheetLayoutSlot.header); + final body = childForSlot(_SheetLayoutSlot.body); + final footer = childForSlot(_SheetLayoutSlot.footer); return [ if (header != null) header, if (footer != null) footer, @@ -316,9 +316,9 @@ class _RenderSheetContentLayout extends RenderBox final fullWidthConstraints = constraints.tighten(width: constraints.maxWidth); - final header = childForSlot(_SheetContentSlot.header); - final footer = childForSlot(_SheetContentSlot.footer); - final body = childForSlot(_SheetContentSlot.body)!; + final header = childForSlot(_SheetLayoutSlot.header); + final footer = childForSlot(_SheetLayoutSlot.footer); + final body = childForSlot(_SheetLayoutSlot.body)!; header?.layout(fullWidthConstraints, parentUsesSize: true); footer?.layout(fullWidthConstraints, parentUsesSize: true); @@ -362,9 +362,9 @@ class _RenderSheetContentLayout extends RenderBox @override Size computeDryLayout(covariant BoxConstraints constraints) { // TODO: DRY layout logic. - final header = childForSlot(_SheetContentSlot.header); - final footer = childForSlot(_SheetContentSlot.footer); - final body = childForSlot(_SheetContentSlot.body)!; + final header = childForSlot(_SheetLayoutSlot.header); + final footer = childForSlot(_SheetLayoutSlot.footer); + final body = childForSlot(_SheetLayoutSlot.body)!; final headerHeight = header?.getDryLayout(constraints).height ?? 0; final footerHeight = footer?.getDryLayout(constraints).height ?? 0; From 82978922ab581d3de6e5e070d1744130291ba368 Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Wed, 2 Oct 2024 21:44:38 +0900 Subject: [PATCH 08/14] Ignore .idea/caches --- .gitignore | 4 +- .idea/caches/deviceStreaming.xml | 318 ------------------------------- 2 files changed, 2 insertions(+), 320 deletions(-) delete mode 100644 .idea/caches/deviceStreaming.xml diff --git a/.gitignore b/.gitignore index bcae468..0ed744a 100644 --- a/.gitignore +++ b/.gitignore @@ -83,5 +83,5 @@ fabric.properties # Editor-based Rest Client .idea/httpRequests -# Android studio 3.1+ serialized cache file -.idea/caches/build_file_checksums.ser +# Caches +.idea/caches/ diff --git a/.idea/caches/deviceStreaming.xml b/.idea/caches/deviceStreaming.xml deleted file mode 100644 index af74dbf..0000000 --- a/.idea/caches/deviceStreaming.xml +++ /dev/null @@ -1,318 +0,0 @@ - - - - - - \ No newline at end of file From 7256f0a193a6dff72238f19b07a2f7d7d0c79ffa Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Wed, 2 Oct 2024 22:24:12 +0900 Subject: [PATCH 09/14] DRY layout logic --- lib/src/foundation/sheet_layout.dart | 118 +++++++++++++++------------ 1 file changed, 64 insertions(+), 54 deletions(-) diff --git a/lib/src/foundation/sheet_layout.dart b/lib/src/foundation/sheet_layout.dart index 247caf0..7c63b59 100644 --- a/lib/src/foundation/sheet_layout.dart +++ b/lib/src/foundation/sheet_layout.dart @@ -312,79 +312,89 @@ class _RenderSheetLayout extends RenderBox @override void performLayout() { - assert(constraints.hasBoundedWidth && constraints.hasBoundedHeight); - final fullWidthConstraints = - constraints.tighten(width: constraints.maxWidth); + final (constrainedSize, intrinsicSize) = _computeLayout( + constraints: constraints, + computeChildLayout: (child, constraints) { + child.layout(constraints, parentUsesSize: true); + return child.size; + }, + ); + size = constrainedSize; + _lastMeasuredIntrinsicSize = intrinsicSize; final header = childForSlot(_SheetLayoutSlot.header); final footer = childForSlot(_SheetLayoutSlot.footer); final body = childForSlot(_SheetLayoutSlot.body)!; - - header?.layout(fullWidthConstraints, parentUsesSize: true); - footer?.layout(fullWidthConstraints, parentUsesSize: true); - - final headerHeight = header?.size.height ?? 0; - final footerHeight = footer?.size.height ?? 0; - final bodyTopPadding = extendBodyBehindHeader ? 0 : headerHeight; - final bodyBottomPadding = extendBodyBehindFooter ? 0 : footerHeight; - final maxBodyHeight = fullWidthConstraints.maxHeight - - bodyTopPadding - - bodyBottomPadding - - bottomMargin; - - body.layout( - _BodyBoxConstraints( - width: fullWidthConstraints.maxWidth, - maxHeight: max(0.0, maxBodyHeight), - footerHeight: footerHeight, - headerHeight: headerHeight, - ), - parentUsesSize: true, - ); - - final bodyHeight = body.size.height + bodyTopPadding + bodyBottomPadding; - final intrinsicHeight = max(bodyHeight, max(headerHeight, footerHeight)); - _lastMeasuredIntrinsicSize = - Size(fullWidthConstraints.maxWidth, intrinsicHeight); - size = fullWidthConstraints.constrain(_lastMeasuredIntrinsicSize); - if (header != null) { (header.parentData! as BoxParentData).offset = Offset.zero; + (body.parentData! as BoxParentData).offset = + extendBodyBehindHeader ? Offset.zero : Offset(0, header.size.height); } if (footer != null) { (footer.parentData! as BoxParentData).offset = - Offset(0, size.height - footerHeight); + Offset(0, constrainedSize.height - footer.size.height); } - (body.parentData! as BoxParentData).offset = - extendBodyBehindHeader ? Offset.zero : Offset(0, headerHeight); } @override Size computeDryLayout(covariant BoxConstraints constraints) { - // TODO: DRY layout logic. + final (size, _) = _computeLayout( + constraints: constraints, + computeChildLayout: (child, constraints) => + child.getDryLayout(constraints), + ); + return size; + } + + (Size, Size) _computeLayout({ + required BoxConstraints constraints, + required Size Function( + RenderBox child, + BoxConstraints constraints, + ) computeChildLayout, + }) { + assert(constraints.hasBoundedWidth && constraints.hasBoundedHeight); + + final fullWidthConstraints = + constraints.tighten(width: constraints.maxWidth); + final header = childForSlot(_SheetLayoutSlot.header); - final footer = childForSlot(_SheetLayoutSlot.footer); - final body = childForSlot(_SheetLayoutSlot.body)!; + final headerSize = header != null + ? computeChildLayout(header, fullWidthConstraints) + : Size.zero; - final headerHeight = header?.getDryLayout(constraints).height ?? 0; - final footerHeight = footer?.getDryLayout(constraints).height ?? 0; + final footer = childForSlot(_SheetLayoutSlot.footer); + final footerSize = footer != null + ? computeChildLayout(footer, fullWidthConstraints) + : Size.zero; - final maxBodyHeight = constraints.maxHeight - - (extendBodyBehindHeader ? 0 : headerHeight) - - (extendBodyBehindFooter ? 0 : footerHeight) - + final body = childForSlot(_SheetLayoutSlot.body)!; + final bodyTopPadding = extendBodyBehindHeader ? 0.0 : headerSize.height; + final bodyBottomPadding = extendBodyBehindFooter ? 0.0 : footerSize.height; + final maxBodyHeight = fullWidthConstraints.maxHeight - + bodyTopPadding - + bodyBottomPadding - bottomMargin; - final bodyConstraints = - constraints.copyWith(maxHeight: max(0, maxBodyHeight)); - final bodyHeight = body.getDryLayout(bodyConstraints).height; - - return constraints.constrain( - Size( - constraints.maxWidth, - bodyHeight + - (extendBodyBehindHeader ? 0 : headerHeight) + - (extendBodyBehindFooter ? 0 : footerHeight), - ), + // We use a special BoxConstraints subclass to pass the header and footer + // heights to the descendant LayoutBuilder (see _BodyContainer). + final bodyConstraints = _BodyBoxConstraints( + width: fullWidthConstraints.maxWidth, + maxHeight: max(0.0, maxBodyHeight), + footerHeight: footerSize.height, + headerHeight: headerSize.height, ); + final bodySize = computeChildLayout(body, bodyConstraints); + + var height = bodySize.height; + if (header != null && !extendBodyBehindHeader) { + height += headerSize.height; + } + if (footer != null && !extendBodyBehindFooter) { + height += footerSize.height; + } + + final intrinsicSize = Size(fullWidthConstraints.maxWidth, height); + return (fullWidthConstraints.constrain(intrinsicSize), intrinsicSize); } @override From 1bba947d2cfbde4c5df2ffb63ceb45c3b396add3 Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Wed, 2 Oct 2024 22:52:07 +0900 Subject: [PATCH 10/14] Expose Material's props --- lib/src/foundation/sheet_layout.dart | 55 +++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/lib/src/foundation/sheet_layout.dart b/lib/src/foundation/sheet_layout.dart index 7c63b59..6a119bd 100644 --- a/lib/src/foundation/sheet_layout.dart +++ b/lib/src/foundation/sheet_layout.dart @@ -51,7 +51,15 @@ class SheetLayout extends StatelessWidget { this.extendBodyBehindHeader = false, this.extendBodyBehindFooter = false, this.resizeToAvoidBottomInset = true, - this.backgroundColor, + this.type = MaterialType.canvas, + this.elevation = 0.0, + this.color, + this.shadowColor, + this.surfaceTintColor, + this.borderRadius, + this.shape, + this.borderOnForeground = true, + this.clipBehavior = Clip.none, }); final Widget body; @@ -60,7 +68,38 @@ class SheetLayout extends StatelessWidget { final bool extendBodyBehindHeader; final bool extendBodyBehindFooter; final bool resizeToAvoidBottomInset; - final Color? backgroundColor; + + /// Forwarded to the [Material.type] property of the internal [Material]. + final MaterialType type; + + /// Forwarded to the [Material.elevation] property of the internal [Material]. + final double elevation; + + /// Forwarded to the [Material.color] property of the internal [Material]. + final Color? color; + + /// Forwarded to the [Material.shadowColor] property + /// of the internal [Material]. + final Color? shadowColor; + + /// Forwarded to the [Material.surfaceTintColor] property + /// of the internal [Material]. + final Color? surfaceTintColor; + + /// Forwarded to the [Material.borderRadius] property + /// of the internal [Material]. + final BorderRadiusGeometry? borderRadius; + + /// Forwarded to the [Material.shape] property of the internal [Material]. + final ShapeBorder? shape; + + /// Forwarded to the [Material.borderOnForeground] property + /// of the internal [Material]. + final bool borderOnForeground; + + /// Forwarded to the [Material.clipBehavior] property + /// of the internal [Material]. + final Clip clipBehavior; @override Widget build(BuildContext context) { @@ -112,11 +151,17 @@ class SheetLayout extends StatelessWidget { final mediaQuery = MediaQuery.of(context); final bottomMargin = resizeToAvoidBottomInset ? mediaQuery.viewInsets.bottom : 0.0; - final backgroundColor = - this.backgroundColor ?? Theme.of(context).scaffoldBackgroundColor; return Material( - color: backgroundColor, + type: type, + elevation: elevation, + color: color, + shadowColor: shadowColor, + surfaceTintColor: surfaceTintColor, + borderRadius: borderRadius, + shape: shape, + borderOnForeground: borderOnForeground, + clipBehavior: clipBehavior, child: _RenderSheetLayoutWidget( header: header, body: body, From c6269c8336a9ef14df6927a6d1391bda3f45c440 Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Wed, 2 Oct 2024 23:26:05 +0900 Subject: [PATCH 11/14] Support intrinsic dimensions --- lib/src/foundation/sheet_layout.dart | 50 ++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/lib/src/foundation/sheet_layout.dart b/lib/src/foundation/sheet_layout.dart index 6a119bd..a6f891f 100644 --- a/lib/src/foundation/sheet_layout.dart +++ b/lib/src/foundation/sheet_layout.dart @@ -442,6 +442,56 @@ class _RenderSheetLayout extends RenderBox return (fullWidthConstraints.constrain(intrinsicSize), intrinsicSize); } + @override + double computeMinIntrinsicWidth(double height) { + assert(() { + if (!RenderObject.debugCheckingIntrinsics) { + throw FlutterError( + 'SheetLayout does not support returning ' + 'intrinsic width dimensions.', + ); + } + return true; + }()); + + return 0.0; + } + + @override + double computeMaxIntrinsicWidth(double height) { + assert(() { + if (!RenderObject.debugCheckingIntrinsics) { + throw FlutterError( + 'SheetLayout does not support returning ' + 'intrinsic width dimensions.', + ); + } + return true; + }()); + + return 0.0; + } + + @override + double computeMinIntrinsicHeight(double width) { + final (_, intrinsicSize) = _computeLayout( + constraints: BoxConstraints.tightForFinite(width: width), + computeChildLayout: (child, constraints) => + Size(width, child.getMinIntrinsicHeight(constraints.maxWidth)), + ); + return intrinsicSize.height; + } + + @override + double computeMaxIntrinsicHeight(double width) { + final (_, intrinsicSize) = _computeLayout( + constraints: BoxConstraints.tightForFinite(width: width), + computeChildLayout: (child, constraints) => + Size(width, child.getMaxIntrinsicHeight(constraints.maxWidth)), + ); + return intrinsicSize.height; + } + @override bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { for (final child in children) { From 9369d6140f5763ec743879f0bf8effc3bed617ab Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Thu, 3 Oct 2024 23:10:36 +0900 Subject: [PATCH 12/14] Allow unconstrained height --- lib/src/foundation/sheet_layout.dart | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/src/foundation/sheet_layout.dart b/lib/src/foundation/sheet_layout.dart index a6f891f..aba62e9 100644 --- a/lib/src/foundation/sheet_layout.dart +++ b/lib/src/foundation/sheet_layout.dart @@ -398,8 +398,7 @@ class _RenderSheetLayout extends RenderBox BoxConstraints constraints, ) computeChildLayout, }) { - assert(constraints.hasBoundedWidth && constraints.hasBoundedHeight); - + assert(constraints.hasBoundedWidth); final fullWidthConstraints = constraints.tighten(width: constraints.maxWidth); @@ -416,10 +415,12 @@ class _RenderSheetLayout extends RenderBox final body = childForSlot(_SheetLayoutSlot.body)!; final bodyTopPadding = extendBodyBehindHeader ? 0.0 : headerSize.height; final bodyBottomPadding = extendBodyBehindFooter ? 0.0 : footerSize.height; - final maxBodyHeight = fullWidthConstraints.maxHeight - - bodyTopPadding - - bodyBottomPadding - - bottomMargin; + final maxBodyHeight = fullWidthConstraints.hasInfiniteHeight + ? double.infinity + : fullWidthConstraints.maxHeight - + bodyTopPadding - + bodyBottomPadding - + bottomMargin; // We use a special BoxConstraints subclass to pass the header and footer // heights to the descendant LayoutBuilder (see _BodyContainer). final bodyConstraints = _BodyBoxConstraints( From b9c2e2fd783a7bff5db588f9a573fae1be4e4e38 Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Sat, 5 Oct 2024 10:06:11 +0900 Subject: [PATCH 13/14] Add custom Overlay class --- lib/src/internal/shrink_wrap_overlay.dart | 145 ++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 lib/src/internal/shrink_wrap_overlay.dart diff --git a/lib/src/internal/shrink_wrap_overlay.dart b/lib/src/internal/shrink_wrap_overlay.dart new file mode 100644 index 0000000..4458c2c --- /dev/null +++ b/lib/src/internal/shrink_wrap_overlay.dart @@ -0,0 +1,145 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:meta/meta.dart'; + +/// Special version of [Overlay] that sizes itself to fit its [child]. +/// +/// This is used to prevent overlay components, such as the popup menu +/// opened by a [DropdownButton], from being overflown by the sheet's bounds +/// (see https://github.com/fujidaiti/smooth_sheets/issues/167). +/// +/// Here are the key points of [Overlay]'s internal layout mechanism +/// to understand the counterintuitive implementation of this widget: +/// +/// 1. The [Overlay] sizes itself to fit the first non-positioned [OverlayEntry] +/// with `canSizeOverlay: true` in its entries, **only** if the given +/// constraints are infinite (see _RenderTheater.performLayout in widgets/overlay.dart). +/// 2. The [Overlay] passes the given constraints to the first entry as-is, +/// if the entry is not positioned (meaning that the entry is not wrapped in +/// a [Positioned] widget). +/// +/// With these points in mind, the following is the layout mechanism of this +/// widget to make the [Overlay] size itself to fit its child: +/// +/// 1. The [_RenderShrinkWrapOverlay] wraps the given constraints with +/// [_OverlayConstraints] and passes it to the underlying [Overlay]. +/// 2. Before the [Overlay] performs layout, the constraints (type of +/// [_OverlayConstraints]) are infinite, so the [Overlay] lays out +/// the first (and only) entry with the inherited constraints +/// ([_OverlayConstraints.entryConstraints]). +/// 3. After the first entry is laid out, it updates the [Overlay]'s constraints +/// to be tight to the entry's size by setting [_OverlayConstraints.entrySize] +/// (this is done by [_RenderOverlayEntryLayout.performLayout]). +/// Updating the constraints is necessary as some [Overlay] related components +/// such as [DropdownMenu] assume that the [Overlay] has a finite constraints. +/// 4. The [Overlay] then sizes itself to fit the entry's size. +@internal +class ShrinkWrapOverlay extends SingleChildRenderObjectWidget { + factory ShrinkWrapOverlay({ + required Widget child, + }) { + return ShrinkWrapOverlay._( + child: Overlay( + initialEntries: [ + OverlayEntry( + opaque: true, + maintainState: true, + canSizeOverlay: true, + builder: (_) { + return _OverlayEntryLayout(child: child); + }, + ), + ], + ), + ); + } + + const ShrinkWrapOverlay._({required super.child}); + + @override + RenderObject createRenderObject(BuildContext context) { + return _RenderShrinkWrapOverlay(); + } +} + +class _RenderShrinkWrapOverlay extends RenderProxyBox { + @override + void performLayout() { + assert(child != null); + // This constraints object is passed to and held by + // the child render object associated with the underlying Overlay. + // Later, the child render object will pass this constraints to + // the first OverlayEntry in its layout method without modification. + final overlayConstraints = + _OverlayConstraints(entryConstraints: constraints); + child!.layout(overlayConstraints, parentUsesSize: true); + size = child!.size; + } +} + +class _OverlayEntryLayout extends SingleChildRenderObjectWidget { + const _OverlayEntryLayout({required super.child}); + + @override + RenderObject createRenderObject(BuildContext context) { + return _RenderOverlayEntryLayout(); + } +} + +class _RenderOverlayEntryLayout extends RenderProxyBox { + @override + void performLayout() { + assert(constraints is _OverlayConstraints); + assert(child != null); + final overlayConstraints = constraints as _OverlayConstraints; + child!.layout(overlayConstraints.entryConstraints, parentUsesSize: true); + overlayConstraints.entrySize = child!.size; + size = child!.size; + } +} + +/// Constraints of the inner [Overlay] widget in a [ShrinkWrapOverlay]. +/// +/// This is an infinite box constraints until the [entrySize] is set. +/// Once the [entrySize] is set, the constraints become a finite box constraints +/// that tightly fits the [entrySize]. See [ShrinkWrapOverlay] for more details. +/// +/// The [entryConstraints] is passed to the [Overlay]'s first entry. +class _OverlayConstraints extends BoxConstraints { + const _OverlayConstraints({ + required this.entryConstraints, + }) : super( + minWidth: 0, + minHeight: 0, + maxWidth: double.infinity, + maxHeight: double.infinity, + ); + + /// Constraints that will be passed to + /// the first [OverlayEntry] in the [Overlay]. + /// + /// See [_RenderOverlayEntryLayout.performLayout]. + final BoxConstraints entryConstraints; + + // Use Expando instead of simply storing the value as a member variable, + // because BoxConstraints is marked as @immutable, so we can't define mutable + // members in this class. + static final _entrySizeRegistry = Expando(); + + /// The finalized size of the [Overlay]'s first entry. + Size? get entrySize => _entrySizeRegistry[this]; + + set entrySize(Size? value) => _entrySizeRegistry[this] = value; + + @override + double get minWidth => entrySize?.width ?? super.minWidth; + + @override + double get maxWidth => entrySize?.width ?? super.maxWidth; + + @override + double get minHeight => entrySize?.height ?? super.minHeight; + + @override + double get maxHeight => entrySize?.height ?? super.maxHeight; +} From ccec0ff1ca758f495fbcbb81974d197e6066f9b5 Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Sat, 5 Oct 2024 10:12:11 +0900 Subject: [PATCH 14/14] Remove unused file --- lib/src/internal/monodrag.dart | 669 --------------------------------- 1 file changed, 669 deletions(-) delete mode 100644 lib/src/internal/monodrag.dart diff --git a/lib/src/internal/monodrag.dart b/lib/src/internal/monodrag.dart deleted file mode 100644 index 51679f9..0000000 --- a/lib/src/internal/monodrag.dart +++ /dev/null @@ -1,669 +0,0 @@ -// ignore_for_file: omit_local_variable_types, prefer_int_literals, comment_references, lines_longer_than_80_chars, unused_element - -/* - - Copied and modified from: - - https://github.com/flutter/flutter/blob/9e1c857886/packages/flutter/lib/src/gestures/monodrag.dart#L23 - - https://github.com/flutter/flutter/blob/9e1c857886/packages/flutter/lib/src/gestures/monodrag.dart#L70 - - https://github.com/flutter/flutter/blob/9e1c857886/packages/flutter/lib/src/gestures/monodrag.dart#L603 - - Changes done: - - Rename `DragGestureRecognizer` to `_DragGestureRecognizer`. - - Add `globalDistanceMoved` getter to `_DragGestureRecognizer`. - - Make `_DragGestureRecognizer.hasSufficientGlobalDistanceToAccept` public. - - ==================================================================================== - - Copyright 2014 The Flutter Authors. All rights reserved. - - Redistribution and use in source and binary forms, with or without modification, - are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of Google Inc. nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - ==================================================================================== - - */ - -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; - -enum _DragState { - ready, - possible, - accepted, -} - -abstract class _DragGestureRecognizer extends OneSequenceGestureRecognizer { - /// Initialize the object. - /// - /// {@macro flutter.gestures.GestureRecognizer.supportedDevices} - _DragGestureRecognizer({ - super.debugOwner, - this.dragStartBehavior = DragStartBehavior.start, - this.velocityTrackerBuilder = _defaultBuilder, - this.onlyAcceptDragOnThreshold = false, - super.supportedDevices, - AllowedButtonsFilter? allowedButtonsFilter, - }) : super( - allowedButtonsFilter: - allowedButtonsFilter ?? _defaultButtonAcceptBehavior); - - static VelocityTracker _defaultBuilder(PointerEvent event) => - VelocityTracker.withKind(event.kind); - - // Accept the input if, and only if, [kPrimaryButton] is pressed. - static bool _defaultButtonAcceptBehavior(int buttons) => - buttons == kPrimaryButton; - - /// Configure the behavior of offsets passed to [onStart]. - /// - /// If set to [DragStartBehavior.start], the [onStart] callback will be called - /// with the position of the pointer at the time this gesture recognizer won - /// the arena. If [DragStartBehavior.down], [onStart] will be called with - /// the position of the first detected down event for the pointer. When there - /// are no other gestures competing with this gesture in the arena, there's - /// no difference in behavior between the two settings. - /// - /// For more information about the gesture arena: - /// https://flutter.dev/docs/development/ui/advanced/gestures#gesture-disambiguation - /// - /// By default, the drag start behavior is [DragStartBehavior.start]. - /// - /// ## Example: - /// - /// A [HorizontalDragGestureRecognizer] and a [VerticalDragGestureRecognizer] - /// compete with each other. A finger presses down on the screen with - /// offset (500.0, 500.0), and then moves to position (510.0, 500.0) before - /// the [HorizontalDragGestureRecognizer] wins the arena. With - /// [dragStartBehavior] set to [DragStartBehavior.down], the [onStart] - /// callback will be called with position (500.0, 500.0). If it is - /// instead set to [DragStartBehavior.start], [onStart] will be called with - /// position (510.0, 500.0). - DragStartBehavior dragStartBehavior; - - /// A pointer has contacted the screen with a primary button and might begin - /// to move. - /// - /// The position of the pointer is provided in the callback's `details` - /// argument, which is a [DragDownDetails] object. - /// - /// See also: - /// - /// * [allowedButtonsFilter], which decides which button will be allowed. - /// * [DragDownDetails], which is passed as an argument to this callback. - GestureDragDownCallback? onDown; - - /// {@template flutter.gestures.monodrag.DragGestureRecognizer.onStart} - /// A pointer has contacted the screen with a primary button and has begun to - /// move. - /// {@endtemplate} - /// - /// The position of the pointer is provided in the callback's `details` - /// argument, which is a [DragStartDetails] object. The [dragStartBehavior] - /// determines this position. - /// - /// See also: - /// - /// * [allowedButtonsFilter], which decides which button will be allowed. - /// * [DragStartDetails], which is passed as an argument to this callback. - GestureDragStartCallback? onStart; - - /// {@template flutter.gestures.monodrag.DragGestureRecognizer.onUpdate} - /// A pointer that is in contact with the screen with a primary button and - /// moving has moved again. - /// {@endtemplate} - /// - /// The distance traveled by the pointer since the last update is provided in - /// the callback's `details` argument, which is a [DragUpdateDetails] object. - /// - /// If this gesture recognizer recognizes movement on a single axis (a - /// [VerticalDragGestureRecognizer] or [HorizontalDragGestureRecognizer]), - /// then `details` will reflect movement only on that axis and its - /// [DragUpdateDetails.primaryDelta] will be non-null. - /// If this gesture recognizer recognizes movement in all directions - /// (a [PanGestureRecognizer]), then `details` will reflect movement on - /// both axes and its [DragUpdateDetails.primaryDelta] will be null. - /// - /// See also: - /// - /// * [allowedButtonsFilter], which decides which button will be allowed. - /// * [DragUpdateDetails], which is passed as an argument to this callback. - GestureDragUpdateCallback? onUpdate; - - /// {@template flutter.gestures.monodrag.DragGestureRecognizer.onEnd} - /// A pointer that was previously in contact with the screen with a primary - /// button and moving is no longer in contact with the screen and was moving - /// at a specific velocity when it stopped contacting the screen. - /// {@endtemplate} - /// - /// The velocity is provided in the callback's `details` argument, which is a - /// [DragEndDetails] object. - /// - /// If this gesture recognizer recognizes movement on a single axis (a - /// [VerticalDragGestureRecognizer] or [HorizontalDragGestureRecognizer]), - /// then `details` will reflect movement only on that axis and its - /// [DragEndDetails.primaryVelocity] will be non-null. - /// If this gesture recognizer recognizes movement in all directions - /// (a [PanGestureRecognizer]), then `details` will reflect movement on - /// both axes and its [DragEndDetails.primaryVelocity] will be null. - /// - /// See also: - /// - /// * [allowedButtonsFilter], which decides which button will be allowed. - /// * [DragEndDetails], which is passed as an argument to this callback. - GestureDragEndCallback? onEnd; - - /// The pointer that previously triggered [onDown] did not complete. - /// - /// See also: - /// - /// * [allowedButtonsFilter], which decides which button will be allowed. - GestureDragCancelCallback? onCancel; - - /// The minimum distance an input pointer drag must have moved - /// to be considered a fling gesture. - /// - /// This value is typically compared with the distance traveled along the - /// scrolling axis. If null then [kTouchSlop] is used. - double? minFlingDistance; - - /// The minimum velocity for an input pointer drag to be considered fling. - /// - /// This value is typically compared with the magnitude of fling gesture's - /// velocity along the scrolling axis. If null then [kMinFlingVelocity] - /// is used. - double? minFlingVelocity; - - /// Fling velocity magnitudes will be clamped to this value. - /// - /// If null then [kMaxFlingVelocity] is used. - double? maxFlingVelocity; - - /// Whether the drag threshold should be met before dispatching any drag callbacks. - /// - /// The drag threshold is met when the global distance traveled by a pointer has - /// exceeded the defined threshold on the relevant axis, i.e. y-axis for the - /// [VerticalDragGestureRecognizer], x-axis for the [HorizontalDragGestureRecognizer], - /// and the entire plane for [PanGestureRecognizer]. The threshold for both - /// [VerticalDragGestureRecognizer] and [HorizontalDragGestureRecognizer] are - /// calculated by [computeHitSlop], while [computePanSlop] is used for - /// [PanGestureRecognizer]. - /// - /// If true, the drag callbacks will only be dispatched when this recognizer has - /// won the arena and the drag threshold has been met. - /// - /// If false, the drag callbacks will be dispatched immediately when this recognizer - /// has won the arena. - /// - /// This value defaults to false. - bool onlyAcceptDragOnThreshold; - - /// Determines the type of velocity estimation method to use for a potential - /// drag gesture, when a new pointer is added. - /// - /// To estimate the velocity of a gesture, [DragGestureRecognizer] calls - /// [velocityTrackerBuilder] when it starts to track a new pointer in - /// [addAllowedPointer], and add subsequent updates on the pointer to the - /// resulting velocity tracker, until the gesture recognizer stops tracking - /// the pointer. This allows you to specify a different velocity estimation - /// strategy for each allowed pointer added, by changing the type of velocity - /// tracker this [GestureVelocityTrackerBuilder] returns. - /// - /// If left unspecified the default [velocityTrackerBuilder] creates a new - /// [VelocityTracker] for every pointer added. - /// - /// See also: - /// - /// * [VelocityTracker], a velocity tracker that uses least squares estimation - /// on the 20 most recent pointer data samples. It's a well-rounded velocity - /// tracker and is used by default. - /// * [IOSScrollViewFlingVelocityTracker], a specialized velocity tracker for - /// determining the initial fling velocity for a [Scrollable] on iOS, to - /// match the native behavior on that platform. - GestureVelocityTrackerBuilder velocityTrackerBuilder; - - _DragState _state = _DragState.ready; - late OffsetPair _initialPosition; - late OffsetPair _pendingDragOffset; - Duration? _lastPendingEventTimestamp; - - /// When asserts are enabled, returns the last tracked pending event timestamp - /// for this recognizer. - /// - /// Otherwise, returns null. - /// - /// This getter is intended for use in framework unit tests. Applications must - /// not depend on its value. - @visibleForTesting - Duration? get debugLastPendingEventTimestamp { - Duration? lastPendingEventTimestamp; - assert(() { - lastPendingEventTimestamp = _lastPendingEventTimestamp; - return true; - }()); - return lastPendingEventTimestamp; - } - - // The buttons sent by `PointerDownEvent`. If a `PointerMoveEvent` comes with a - // different set of buttons, the gesture is canceled. - int? _initialButtons; - Matrix4? _lastTransform; - - /// Distance moved in the global coordinate space of the screen in drag direction. - /// - /// If drag is only allowed along a defined axis, this value may be negative to - /// differentiate the direction of the drag. - late double _globalDistanceMoved; - - @protected - double get globalDistanceMoved => _globalDistanceMoved; - - /// Determines if a gesture is a fling or not based on velocity. - /// - /// A fling calls its gesture end callback with a velocity, allowing the - /// provider of the callback to respond by carrying the gesture forward with - /// inertia, for example. - bool isFlingGesture(VelocityEstimate estimate, PointerDeviceKind kind); - - /// Determines if a gesture is a fling or not, and if so its effective velocity. - /// - /// A fling calls its gesture end callback with a velocity, allowing the - /// provider of the callback to respond by carrying the gesture forward with - /// inertia, for example. - DragEndDetails? _considerFling( - VelocityEstimate estimate, PointerDeviceKind kind); - - Offset _getDeltaForDetails(Offset delta); - double? _getPrimaryValueFromOffset(Offset value); - bool hasSufficientGlobalDistanceToAccept( - PointerDeviceKind pointerDeviceKind, double? deviceTouchSlop); - bool _hasDragThresholdBeenMet = false; - - final Map _velocityTrackers = {}; - - @override - bool isPointerAllowed(PointerEvent event) { - if (_initialButtons == null) { - if (onDown == null && - onStart == null && - onUpdate == null && - onEnd == null && - onCancel == null) { - return false; - } - } else { - // There can be multiple drags simultaneously. Their effects are combined. - if (event.buttons != _initialButtons) { - return false; - } - } - return super.isPointerAllowed(event as PointerDownEvent); - } - - void _addPointer(PointerEvent event) { - _velocityTrackers[event.pointer] = velocityTrackerBuilder(event); - if (_state == _DragState.ready) { - _state = _DragState.possible; - _initialPosition = - OffsetPair(global: event.position, local: event.localPosition); - _pendingDragOffset = OffsetPair.zero; - _globalDistanceMoved = 0.0; - _lastPendingEventTimestamp = event.timeStamp; - _lastTransform = event.transform; - _checkDown(); - } else if (_state == _DragState.accepted) { - resolve(GestureDisposition.accepted); - } - } - - @override - void addAllowedPointer(PointerDownEvent event) { - super.addAllowedPointer(event); - if (_state == _DragState.ready) { - _initialButtons = event.buttons; - } - _addPointer(event); - } - - @override - void addAllowedPointerPanZoom(PointerPanZoomStartEvent event) { - super.addAllowedPointerPanZoom(event); - startTrackingPointer(event.pointer, event.transform); - if (_state == _DragState.ready) { - _initialButtons = kPrimaryButton; - } - _addPointer(event); - } - - @override - void handleEvent(PointerEvent event) { - assert(_state != _DragState.ready); - if (!event.synthesized && - (event is PointerDownEvent || - event is PointerMoveEvent || - event is PointerPanZoomStartEvent || - event is PointerPanZoomUpdateEvent)) { - final VelocityTracker tracker = _velocityTrackers[event.pointer]!; - if (event is PointerPanZoomStartEvent) { - tracker.addPosition(event.timeStamp, Offset.zero); - } else if (event is PointerPanZoomUpdateEvent) { - tracker.addPosition(event.timeStamp, event.pan); - } else { - tracker.addPosition(event.timeStamp, event.localPosition); - } - } - if (event is PointerMoveEvent && event.buttons != _initialButtons) { - _giveUpPointer(event.pointer); - return; - } - if (event is PointerMoveEvent || event is PointerPanZoomUpdateEvent) { - final Offset delta = (event is PointerMoveEvent) - ? event.delta - : (event as PointerPanZoomUpdateEvent).panDelta; - final Offset localDelta = (event is PointerMoveEvent) - ? event.localDelta - : (event as PointerPanZoomUpdateEvent).localPanDelta; - final Offset position = (event is PointerMoveEvent) - ? event.position - : (event.position + (event as PointerPanZoomUpdateEvent).pan); - final Offset localPosition = (event is PointerMoveEvent) - ? event.localPosition - : (event.localPosition + - (event as PointerPanZoomUpdateEvent).localPan); - if (_state == _DragState.accepted) { - _checkUpdate( - sourceTimeStamp: event.timeStamp, - delta: _getDeltaForDetails(localDelta), - primaryDelta: _getPrimaryValueFromOffset(localDelta), - globalPosition: position, - localPosition: localPosition, - ); - } else { - _pendingDragOffset += OffsetPair(local: localDelta, global: delta); - _lastPendingEventTimestamp = event.timeStamp; - _lastTransform = event.transform; - final Offset movedLocally = _getDeltaForDetails(localDelta); - final Matrix4? localToGlobalTransform = event.transform == null - ? null - : Matrix4.tryInvert(event.transform!); - _globalDistanceMoved += PointerEvent.transformDeltaViaPositions( - transform: localToGlobalTransform, - untransformedDelta: movedLocally, - untransformedEndPosition: localPosition) - .distance * - (_getPrimaryValueFromOffset(movedLocally) ?? 1).sign; - if (hasSufficientGlobalDistanceToAccept( - event.kind, gestureSettings?.touchSlop)) { - _hasDragThresholdBeenMet = true; - if (_acceptedActivePointers.contains(event.pointer)) { - _checkDrag(event.pointer); - } else { - resolve(GestureDisposition.accepted); - } - } - } - } - if (event is PointerUpEvent || - event is PointerCancelEvent || - event is PointerPanZoomEndEvent) { - _giveUpPointer(event.pointer); - } - } - - final Set _acceptedActivePointers = {}; - - @override - void acceptGesture(int pointer) { - assert(!_acceptedActivePointers.contains(pointer)); - _acceptedActivePointers.add(pointer); - if (!onlyAcceptDragOnThreshold || _hasDragThresholdBeenMet) { - _checkDrag(pointer); - } - } - - @override - void rejectGesture(int pointer) { - _giveUpPointer(pointer); - } - - @override - void didStopTrackingLastPointer(int pointer) { - assert(_state != _DragState.ready); - switch (_state) { - case _DragState.ready: - break; - - case _DragState.possible: - resolve(GestureDisposition.rejected); - _checkCancel(); - - case _DragState.accepted: - _checkEnd(pointer); - } - _hasDragThresholdBeenMet = false; - _velocityTrackers.clear(); - _initialButtons = null; - _state = _DragState.ready; - } - - void _giveUpPointer(int pointer) { - stopTrackingPointer(pointer); - // If we never accepted the pointer, we reject it since we are no longer - // interested in winning the gesture arena for it. - if (!_acceptedActivePointers.remove(pointer)) { - resolvePointer(pointer, GestureDisposition.rejected); - } - } - - void _checkDown() { - if (onDown != null) { - final DragDownDetails details = DragDownDetails( - globalPosition: _initialPosition.global, - localPosition: _initialPosition.local, - ); - invokeCallback('onDown', () => onDown!(details)); - } - } - - void _checkDrag(int pointer) { - if (_state == _DragState.accepted) { - return; - } - _state = _DragState.accepted; - final OffsetPair delta = _pendingDragOffset; - final Duration? timestamp = _lastPendingEventTimestamp; - final Matrix4? transform = _lastTransform; - final Offset localUpdateDelta; - switch (dragStartBehavior) { - case DragStartBehavior.start: - _initialPosition = _initialPosition + delta; - localUpdateDelta = Offset.zero; - case DragStartBehavior.down: - localUpdateDelta = _getDeltaForDetails(delta.local); - } - _pendingDragOffset = OffsetPair.zero; - _lastPendingEventTimestamp = null; - _lastTransform = null; - _checkStart(timestamp, pointer); - if (localUpdateDelta != Offset.zero && onUpdate != null) { - final Matrix4? localToGlobal = - transform != null ? Matrix4.tryInvert(transform) : null; - final Offset correctedLocalPosition = - _initialPosition.local + localUpdateDelta; - final Offset globalUpdateDelta = PointerEvent.transformDeltaViaPositions( - untransformedEndPosition: correctedLocalPosition, - untransformedDelta: localUpdateDelta, - transform: localToGlobal, - ); - final OffsetPair updateDelta = - OffsetPair(local: localUpdateDelta, global: globalUpdateDelta); - final OffsetPair correctedPosition = _initialPosition + updateDelta; - _checkUpdate( - sourceTimeStamp: timestamp, - delta: localUpdateDelta, - primaryDelta: _getPrimaryValueFromOffset(localUpdateDelta), - globalPosition: correctedPosition.global, - localPosition: correctedPosition.local, - ); - } - // This acceptGesture might have been called only for one pointer, instead - // of all pointers. Resolve all pointers to `accepted`. This won't cause - // infinite recursion because an accepted pointer won't be accepted again. - resolve(GestureDisposition.accepted); - } - - void _checkStart(Duration? timestamp, int pointer) { - if (onStart != null) { - final DragStartDetails details = DragStartDetails( - sourceTimeStamp: timestamp, - globalPosition: _initialPosition.global, - localPosition: _initialPosition.local, - kind: getKindForPointer(pointer), - ); - invokeCallback('onStart', () => onStart!(details)); - } - } - - void _checkUpdate({ - Duration? sourceTimeStamp, - required Offset delta, - double? primaryDelta, - required Offset globalPosition, - Offset? localPosition, - }) { - if (onUpdate != null) { - final DragUpdateDetails details = DragUpdateDetails( - sourceTimeStamp: sourceTimeStamp, - delta: delta, - primaryDelta: primaryDelta, - globalPosition: globalPosition, - localPosition: localPosition, - ); - invokeCallback('onUpdate', () => onUpdate!(details)); - } - } - - void _checkEnd(int pointer) { - if (onEnd == null) { - return; - } - - final VelocityTracker tracker = _velocityTrackers[pointer]!; - final VelocityEstimate? estimate = tracker.getVelocityEstimate(); - - DragEndDetails? details; - final String Function() debugReport; - if (estimate == null) { - debugReport = () => 'Could not estimate velocity.'; - } else { - details = _considerFling(estimate, tracker.kind); - debugReport = (details != null) - ? () => '$estimate; fling at ${details!.velocity}.' - : () => '$estimate; judged to not be a fling.'; - } - details ??= DragEndDetails(primaryVelocity: 0.0); - - invokeCallback('onEnd', () => onEnd!(details!), - debugReport: debugReport); - } - - void _checkCancel() { - if (onCancel != null) { - invokeCallback('onCancel', onCancel!); - } - } - - @override - void dispose() { - _velocityTrackers.clear(); - super.dispose(); - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add( - EnumProperty('start behavior', dragStartBehavior)); - } -} - -/// Recognizes movement in the vertical direction. -/// -/// Used for vertical scrolling. -/// -/// See also: -/// -/// * [HorizontalDragGestureRecognizer], for a similar recognizer but for -/// horizontal movement. -/// * [MultiDragGestureRecognizer], for a family of gesture recognizers that -/// track each touch point independently. -class VerticalDragGestureRecognizer extends _DragGestureRecognizer { - /// Create a gesture recognizer for interactions in the vertical axis. - /// - /// {@macro flutter.gestures.GestureRecognizer.supportedDevices} - VerticalDragGestureRecognizer({ - super.debugOwner, - super.supportedDevices, - super.allowedButtonsFilter, - }); - - @override - bool isFlingGesture(VelocityEstimate estimate, PointerDeviceKind kind) { - final double minVelocity = minFlingVelocity ?? kMinFlingVelocity; - final double minDistance = - minFlingDistance ?? computeHitSlop(kind, gestureSettings); - return estimate.pixelsPerSecond.dy.abs() > minVelocity && - estimate.offset.dy.abs() > minDistance; - } - - @override - DragEndDetails? _considerFling( - VelocityEstimate estimate, PointerDeviceKind kind) { - if (!isFlingGesture(estimate, kind)) { - return null; - } - final double maxVelocity = maxFlingVelocity ?? kMaxFlingVelocity; - final double dy = - clampDouble(estimate.pixelsPerSecond.dy, -maxVelocity, maxVelocity); - return DragEndDetails( - velocity: Velocity(pixelsPerSecond: Offset(0, dy)), - primaryVelocity: dy, - ); - } - - @override - bool hasSufficientGlobalDistanceToAccept( - PointerDeviceKind pointerDeviceKind, double? deviceTouchSlop) { - return _globalDistanceMoved.abs() > - computeHitSlop(pointerDeviceKind, gestureSettings); - } - - @override - Offset _getDeltaForDetails(Offset delta) => Offset(0.0, delta.dy); - - @override - double _getPrimaryValueFromOffset(Offset value) => value.dy; - - @override - String get debugDescription => 'vertical drag'; -}