Skip to content

Commit

Permalink
Add iOS 15 style modal sheet transition (#21)
Browse files Browse the repository at this point in the history
This PR introduces `CupertinoModalSheetRoute` and
`CupertinoModalSheetPage`, which emulate the iOS 15 style modal sheet
transition mentioned in #5.

## Changes

- Add new components for iOS-style modal sheets
  - `CupertinoModalSheetRoute`
  - `CupertinoModalSheetPage`
  - `CupertinoStackedTransition`
  - `CupertinoStackedModalTransition`
- Add a flag `fireImmediately` to `SheetController.addListener`
- This enables the listeners to handle sheet metrics changes even if
they are fired during a layout phase
- Add `MaybeSheetMetrics.viewPixels` getter
- Which represents the current visual height of a sheet (extent +
keyboard height)
- Make `SheetExtent` to notify its listeners whenever the `pixels` or
the `viewPixels` are changed
- Make `SheetExtent.animateTo` not to run an animation if it is already
at the destination
- Make `ModalSheetRoute` always create a `SheetController` and expose it
to the descendant widgets

## Documentation

- Add a tutorial code for the new stuff
- Add a Safari clone app to the showcases
- Add descriptions of `CupertinoModalSheetRoute` and
`CupertinoModalSheetPage` to the README
- Add a description of the Safari clone app to the README
  • Loading branch information
fujidaiti authored Feb 5, 2024
1 parent a7596a8 commit a90a600
Show file tree
Hide file tree
Showing 24 changed files with 1,508 additions and 61 deletions.
14 changes: 14 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@
"program": "lib/showcase/todo_list/main.dart",
"cwd": "./cookbook"
},
{
"name": "Safari",
"request": "launch",
"type": "dart",
"program": "lib/showcase/safari/main.dart",
"cwd": "./cookbook"
},
{
"name": "Scrollable Sheet",
"request": "launch",
Expand Down Expand Up @@ -95,6 +102,13 @@
"program": "lib/tutorial/imperative_modal_sheet.dart",
"cwd": "./cookbook"
},
{
"name": "Cupertino Modal Sheet",
"request": "launch",
"type": "dart",
"program": "lib/tutorial/cupertino_modal_sheet.dart",
"cwd": "./cookbook"
},
{
"name": "Extent Driven Animation",
"request": "launch",
Expand Down
Binary file added cookbook/assets/apple_website.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion cookbook/ios/Flutter/AppFrameworkInfo.plist
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>11.0</string>
<string>12.0</string>
</dict>
</plist>
6 changes: 3 additions & 3 deletions cookbook/ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
Expand Down Expand Up @@ -473,7 +473,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
Expand Down Expand Up @@ -522,7 +522,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
Expand Down
132 changes: 132 additions & 0 deletions cookbook/lib/showcase/safari/actions.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import 'package:cookbook/showcase/safari/common.dart';
import 'package:flutter/cupertino.dart';
import 'package:smooth_sheets/smooth_sheets.dart';

void showEditActionsSheet(BuildContext context) {
Navigator.push(
context,
CupertinoModalSheetRoute(
builder: (context) => const EditActionsSheet(),
),
);
}

class EditActionsSheet extends StatelessWidget {
const EditActionsSheet({super.key});

@override
Widget build(BuildContext context) {
return ScrollableSheet(
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: SheetContentScaffold(
backgroundColor: CupertinoColors.systemGroupedBackground,
appBar: CupertinoAppBar(
title: const Text('Edit Actions'),
trailing: CupertinoButton(
onPressed: () => Navigator.pop(context),
child: const Text('Done'),
),
),
body: const _ActionList(),
),
),
);
}
}

class _ActionList extends StatelessWidget {
const _ActionList();

@override
Widget build(BuildContext context) {
return ListView(
children: const [
SizedBox(height: 16),
_ActionListSection(
header: Text('Favorites'),
children: [
_ActionListItem(title: 'Copy', isFavorite: true),
_ActionListItem(title: 'Save in Keep', isFavorite: true),
],
),
SizedBox(height: 16),
_ActionListSection(
header: Text('Safari'),
children: [
_ActionListItem(title: 'Add to Reading List'),
_ActionListItem(title: 'Add Bookmark'),
_ActionListItem(title: 'Add to Favorites'),
_ActionListItem(title: 'Add to Quick Note'),
_ActionListItem(title: 'Find on Page'),
_ActionListItem(title: 'Add to Home Screen'),
],
),
SizedBox(height: 16),
_ActionListSection(
header: Text('Other actions'),
children: [
_ActionListItem(title: 'Markup'),
_ActionListItem(title: 'Print'),
_ActionListItem(title: 'Analyze with Bing Chat'),
_ActionListItem(title: 'Open in Chrome'),
_ActionListItem(title: 'Open using Mastodon'),
_ActionListItem(title: 'Save to Pinterest'),
_ActionListItem(title: 'Save to Dropbox'),
],
),
],
);
}
}

class _ActionListSection extends StatelessWidget {
const _ActionListSection({
this.header,
required this.children,
});

final Widget? header;
final List<Widget> children;

@override
Widget build(BuildContext context) {
return CupertinoListSection.insetGrouped(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
header: header,
children: children,
);
}
}

class _ActionListItem extends StatelessWidget {
const _ActionListItem({
required this.title,
this.isFavorite = false,
});

final String title;
final bool isFavorite;

@override
Widget build(BuildContext context) {
return CupertinoListTile.notched(
title: Text(title),
leading: isFavorite
? const Icon(
CupertinoIcons.minus_circle_fill,
color: CupertinoColors.systemRed,
)
: const Icon(
CupertinoIcons.plus_circle_fill,
color: CupertinoColors.systemGreen,
),
trailing: isFavorite
? const Icon(
CupertinoIcons.line_horizontal_3,
color: CupertinoColors.systemGrey4,
)
: null,
);
}
}
105 changes: 105 additions & 0 deletions cookbook/lib/showcase/safari/bookmark.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import 'package:cookbook/showcase/safari/common.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:smooth_sheets/smooth_sheets.dart';

void showEditBookmarkSheet(BuildContext context) {
Navigator.push(
context,
CupertinoModalSheetRoute(
builder: (context) => const EditBookmarkSheet(
pageUrl: 'https://www.apple.com',
faviconUrl: 'https://www.apple.com/favicon.ico',
),
),
);
}

class EditBookmarkSheet extends StatelessWidget {
const EditBookmarkSheet({
super.key,
required this.pageUrl,
required this.faviconUrl,
});

final String pageUrl;
final String faviconUrl;

@override
Widget build(BuildContext context) {
return DraggableSheet(
keyboardDismissBehavior: const SheetKeyboardDismissBehavior.onDragDown(),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: SheetContentScaffold(
backgroundColor: CupertinoColors.systemGroupedBackground,
appBar: CupertinoAppBar(
title: const Text('Add Bookmark'),
leading: CupertinoButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
trailing: CupertinoButton(
onPressed: () =>
Navigator.popUntil(context, (route) => route.isFirst),
child: const Text('Save'),
),
),
body: SizedBox.expand(
child: CupertinoListSection.insetGrouped(
children: [
_BookmarkEditor(
pageUrl: pageUrl,
faviconUrl: faviconUrl,
),
],
),
),
),
),
);
}
}

class _BookmarkEditor extends StatelessWidget {
const _BookmarkEditor({
required this.pageUrl,
required this.faviconUrl,
});

final String pageUrl;
final String faviconUrl;

@override
Widget build(BuildContext context) {
return Row(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: SiteIcon(url: faviconUrl),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const CupertinoTextField.borderless(
padding: EdgeInsets.zero,
autofocus: true,
),
const Divider(color: CupertinoColors.systemGrey5),
Text(
pageUrl,
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: CupertinoColors.secondaryLabel),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
);
}
}
65 changes: 65 additions & 0 deletions cookbook/lib/showcase/safari/common.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

class CupertinoAppBar extends StatelessWidget implements PreferredSizeWidget {
const CupertinoAppBar({
super.key,
required this.title,
this.leading,
this.trailing,
});

final Widget title;
final Widget? leading;
final Widget? trailing;

@override
Size get preferredSize => const Size.fromHeight(56);

@override
Widget build(BuildContext context) {
return Container(
height: preferredSize.height,
decoration: const BoxDecoration(
color: CupertinoColors.systemGroupedBackground,
border: Border(bottom: BorderSide(color: CupertinoColors.systemGrey5)),
),
child: Stack(
alignment: Alignment.center,
children: [
if (leading case final leading?)
Positioned(
left: 1,
child: leading,
),
DefaultTextStyle(
style: Theme.of(context).textTheme.titleMedium!,
child: title,
),
if (trailing case final trailing?)
Positioned(
right: 1,
child: trailing,
),
],
),
);
}
}

class SiteIcon extends StatelessWidget {
const SiteIcon({
super.key,
required this.url,
});

final String url;

@override
Widget build(BuildContext context) {
return SizedBox.square(
dimension: 48,
child: Image.network(url),
);
}
}
Loading

0 comments on commit a90a600

Please sign in to comment.