Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 77 additions & 9 deletions packages/cupertino_ui/lib/src/sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,11 @@ typedef _GetSheetDragged = bool Function();
/// When `showDragHandle` is set to `true`, then a drag handle will be placed at
/// the top of the sheet. This flag will default to false.
///
/// The `hasPlatformViews` parameter should be set to `true` when the sheet or
/// the route underneath it contains a platform view that is composited by the
/// host platform. See [CupertinoSheetTransition.hasPlatformViews] for
/// platform-specific details.
///
/// iOS sheet widgets are generally designed to be tightly coupled to the context
/// of the widget that opened the sheet. As such, it is not recommended to push
/// a non-sheet route that covers the sheet without first popping the sheet. If
Expand Down Expand Up @@ -194,6 +199,7 @@ Future<T?> showCupertinoSheet<T>({
RouteSettings? settings,
double? topGap,
bool showDragHandle = false,
bool hasPlatformViews = false,
}) {
assert(topGap == null || (topGap >= 0.0 && topGap <= 0.9), 'topGap must be between 0.0 and 0.9');
assert(pageBuilder != null || builder != null || scrollableBuilder != null);
Expand All @@ -211,6 +217,7 @@ Future<T?> showCupertinoSheet<T>({
settings: settings,
enableDrag: enableDrag,
topGap: topGap,
hasPlatformViews: hasPlatformViews,
);

return Navigator.of(context, rootNavigator: true).push<T>(route);
Expand Down Expand Up @@ -255,6 +262,7 @@ Future<T?> showCupertinoSheet<T>({
settings: settings,
enableDrag: enableDrag,
topGap: topGap,
hasPlatformViews: hasPlatformViews,
);
return Navigator.of(context, rootNavigator: true).push<T>(route);
}
Expand All @@ -273,6 +281,7 @@ class CupertinoSheetTransition extends StatefulWidget {
required this.secondaryRouteAnimation,
required this.child,
required this.linearTransition,
this.hasPlatformViews = false,
this.topGap = _kTopGapRatio,
});

Expand Down Expand Up @@ -304,21 +313,61 @@ class CupertinoSheetTransition extends StatefulWidget {
/// {@endtemplate}
final double topGap;

/// {@template cupertino_ui.CupertinoSheetTransition.hasPlatformViews}
/// Whether the sheet or the route underneath it contains a platform view
/// that is composited by the host platform.
///
/// On iOS and macOS, set this to `true` when the sheet or the route underneath
/// it contains a platform view. On Android, this flag is generally not needed
/// unless the platform view uses a native-view composition mode such as hybrid
/// composition.
///
/// When set to `false` (the default), the transition uses image-filtered
/// scaling for high-quality rendering. This can cause a native-composited
/// platform view to appear detached from the rest of the route. Set this to
/// `true` to keep it synchronized with the rest of the transition.
/// {@endtemplate}
final bool hasPlatformViews;

static Widget _delegateTransitionWithPlatformViews(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
bool allowSnapshotting,
Widget? child,
) => delegateTransition(
context,
animation,
secondaryAnimation,
allowSnapshotting,
child,
hasPlatformViews: true,
);

/// The primary delegated transition. Will slide a non [CupertinoSheetRoute] page down.
///
/// Provided to the previous route to coordinate transitions between routes.
///
/// If a [CupertinoSheetRoute] already exists in the stack, then it will
/// slide the previous sheet upwards instead.
///
/// Set [hasPlatformViews] to `true` when the sheet or the route underneath it
/// contains a platform view that is composited by the host platform. See
/// [CupertinoSheetTransition.hasPlatformViews] for platform-specific details.
static Widget delegateTransition(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
bool allowSnapshotting,
Widget? child,
) {
Widget? child, {
bool hasPlatformViews = false,
}) {
if (CupertinoSheetRoute.hasParentSheet(context)) {
return _delegatedCoverSheetSecondaryTransition(secondaryAnimation, child);
return _delegatedCoverSheetSecondaryTransition(
secondaryAnimation,
child,
hasPlatformViews: hasPlatformViews,
);
}
final bool linear = Navigator.of(context).userGestureInProgress;

Expand Down Expand Up @@ -377,7 +426,12 @@ class CupertinoSheetTransition extends StatefulWidget {
position: slideAnimation,
child: ScaleTransition(
scale: scaleAnimation,
filterQuality: FilterQuality.medium,
// During animation, FilterQuality.medium uses an ImageFilterLayer, which is
// usually worthwhile for animated transforms and avoids the observed subpixel
// drift. A null filter quality always uses a TransformLayer to keep platform
// views synchronized with the host. When not animating, filterQuality is ignored
// and both options have the same effect.
filterQuality: hasPlatformViews ? null : FilterQuality.medium,
Comment thread
victorsanni marked this conversation as resolved.
alignment: Alignment.topCenter,
child: AnimatedBuilder(
animation: radiusAnimation,
Expand All @@ -399,8 +453,9 @@ class CupertinoSheetTransition extends StatefulWidget {

static Widget _delegatedCoverSheetSecondaryTransition(
Animation<double> secondaryAnimation,
Widget? child,
) {
Widget? child, {
bool hasPlatformViews = false,
}) {
const Curve curve = Curves.linearToEaseOut;
const Curve reverseCurve = Curves.easeInToLinear;
final curvedAnimation = CurvedAnimation(
Expand All @@ -418,7 +473,7 @@ class CupertinoSheetTransition extends StatefulWidget {
transformHitTests: false,
child: ScaleTransition(
scale: scaleAnimation,
filterQuality: FilterQuality.medium,
filterQuality: hasPlatformViews ? null : FilterQuality.medium,
alignment: Alignment.topCenter,
child: ClipRSuperellipse(
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
Expand Down Expand Up @@ -537,7 +592,7 @@ class _CupertinoSheetTransitionState extends State<CupertinoSheetTransition>
transformHitTests: false,
child: ScaleTransition(
scale: _secondaryScaleAnimation,
filterQuality: FilterQuality.medium,
filterQuality: widget.hasPlatformViews ? null : FilterQuality.medium,
alignment: Alignment.topCenter,
child: child,
),
Expand Down Expand Up @@ -667,6 +722,7 @@ class CupertinoSheetRoute<T> extends PageRoute<T> with _CupertinoSheetRouteTrans
this.scrollableBuilder,
this.enableDrag = true,
this.showDragHandle = false,
this.hasPlatformViews = false,
double? topGap,
}) : assert(
topGap == null || (topGap >= 0.0 && topGap <= 0.9),
Expand Down Expand Up @@ -712,6 +768,10 @@ class CupertinoSheetRoute<T> extends PageRoute<T> with _CupertinoSheetRouteTrans
@override
final bool enableDrag;

/// {@macro cupertino_ui.CupertinoSheetTransition.hasPlatformViews}
@override
final bool hasPlatformViews;

// The gap between the top of the screen and the top of the sheet.
final double? _topGap;

Expand Down Expand Up @@ -850,7 +910,9 @@ mixin _CupertinoSheetRouteTransitionMixin<T> on PageRoute<T> {
if (_hasCustomTopGap) {
return null;
}
return CupertinoSheetTransition.delegateTransition;
return hasPlatformViews
? CupertinoSheetTransition._delegateTransitionWithPlatformViews
: CupertinoSheetTransition.delegateTransition;
}

/// Determines whether the content can be dragged.
Expand All @@ -864,6 +926,9 @@ mixin _CupertinoSheetRouteTransitionMixin<T> on PageRoute<T> {
/// {@macro cupertino_ui.CupertinoSheetTransition.topGap}
double get topGap;

/// {@macro cupertino_ui.CupertinoSheetTransition.hasPlatformViews}
bool get hasPlatformViews;

/// Whether a custom top gap has been set.
bool get _hasCustomTopGap;

Expand Down Expand Up @@ -898,13 +963,15 @@ mixin _CupertinoSheetRouteTransitionMixin<T> on PageRoute<T> {
Widget child,
bool enableDrag,
double topGap,
bool hasPlatformViews,
) {
final bool linearTransition = route.popGestureInProgress;
return CupertinoSheetTransition(
primaryRouteAnimation: animation,
secondaryRouteAnimation: secondaryAnimation,
linearTransition: linearTransition,
topGap: topGap,
hasPlatformViews: hasPlatformViews,
child: _CupertinoDragGestureDetector<T>(
enabledCallback: () => enableDrag,
onStartPopGesture: () => _startPopGesture<T>(route, topGap),
Expand Down Expand Up @@ -941,6 +1008,7 @@ mixin _CupertinoSheetRouteTransitionMixin<T> on PageRoute<T> {
child,
enableDrag,
topGap,
hasPlatformViews,
);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
changelog: |
- Add `hasPlatformViews` to Cupertino sheet transitions to keep platform views synchronized with the underlying route.
version: minor
93 changes: 93 additions & 0 deletions packages/cupertino_ui/test/sheet_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import 'dart:async' show unawaited;

import 'package:cupertino_ui/cupertino_ui.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart' show GlobalWidgetsLocalizations;
import 'package:flutter_test/flutter_test.dart';
Expand Down Expand Up @@ -2298,6 +2299,98 @@ void main() {
await tester.pumpAndSettle();
});
});

group('hasPlatformViews parameter tests', () {
final GlobalKey scaffoldKey = GlobalKey();

bool checkHasImageFilterLayer() {
// The transition widget is the ancestor of the scaffold.
final BuildContext context = scaffoldKey.currentContext!;
var found = false;
context.visitAncestorElements((element) {
final RenderObject? renderObject = element.findRenderObject();
if (renderObject?.debugLayer is ImageFilterLayer) {
found = true;
return false;
}
return true;
});
return found;
}

Widget dragGestureApp(bool hasPlatformViews) {
return CupertinoApp(
home: CupertinoPageScaffold(
key: scaffoldKey,
child: Center(
child: Column(
children: <Widget>[
const Text('Page 1'),
CupertinoButton(
onPressed: () {
showCupertinoSheet<void>(
context: scaffoldKey.currentContext!,
hasPlatformViews: hasPlatformViews,
scrollableBuilder: (BuildContext context, ScrollController controller) {
return const CupertinoPageScaffold(child: Center(child: Text('Page 2')));
},
);
},
child: const Text('Push Page 2'),
),
],
),
),
),
);
}

Future<void> pumpSheet(WidgetTester tester, bool hasPlatformViews) async {
await tester.pumpWidget(dragGestureApp(hasPlatformViews));
await tester.tap(find.text('Push Page 2'));
await tester.pumpAndSettle();
expect(find.text('Page 2'), findsOneWidget);
}

testWidgets('hasPlatformViews=false should add ImageFilterLayer during transition', (
WidgetTester tester,
) async {
await pumpSheet(tester, false);

// ImageFilterLayer should not be found after the transition is completed
// because ScaleTransition will set the Transform.filterQuality to null.
expect(checkHasImageFilterLayer(), false);

final TestGesture gesture = await tester.startGesture(const Offset(100, 300));
await gesture.moveBy(const Offset(0, 100));
await tester.pump();

// ImageFilterLayer should be found when the sheet is dragged.
expect(checkHasImageFilterLayer(), true);

await gesture.up();
await tester.pumpAndSettle();
});

testWidgets('hasPlatformViews=true should not add ImageFilterLayer during transition', (
WidgetTester tester,
) async {
await pumpSheet(tester, true);

expect(checkHasImageFilterLayer(), false);

final TestGesture gesture = await tester.startGesture(const Offset(100, 300));
await gesture.moveBy(const Offset(0, 100));
await tester.pump();

// ImageFilterLayer should not be found even when the sheet is dragged.
expect(checkHasImageFilterLayer(), false);

await gesture.up();
await tester.pumpAndSettle();
});
});

testWidgets('didUpdateWidget in sheet transition does not try and use multiple tickers', (
WidgetTester tester,
) async {
Expand Down
Loading