chore: add stupid_simple_sheet
176
packages/stupid_simple_sheet/CHANGELOG.md
Normal file
@@ -0,0 +1,176 @@
|
||||
## 0.9.1+1
|
||||
|
||||
- **FIX**: dragging up a sheet whose highest snapping point was lower than 1.0 would incorrectly snap it to 1.0.
|
||||
|
||||
## 0.9.1
|
||||
|
||||
- **FIX**: incorrect snapping points used for cupertino secondary transitions in some cases.
|
||||
- **FEAT**: add option to blur route behind `StupidSimpleGlassSheet` and customize `barrierColor`.
|
||||
|
||||
## 0.9.0
|
||||
|
||||
> Note: This release has breaking changes.
|
||||
|
||||
- **FEAT**: add `RouteSnapshotMode` that can be used to improve performance.
|
||||
- **FEAT**: Add `StupidSimpleGlassSheetRoute` which uses the iOS 26 style.
|
||||
- **BREAKING** **REFACTOR**: remove deprecated `topRadius` parameter.
|
||||
- **BREAKING** **FEAT**: simplify `SheetSnappingConfig` to only support relative values.
|
||||
|
||||
Remove absolute pixel-based snapping which didn't work correctly.
|
||||
|
||||
|
||||
## 0.8.0+1
|
||||
|
||||
- **FIX**: immediately start clearing overscroll when letting go.
|
||||
|
||||
## 0.8.0
|
||||
|
||||
> Note: This release has breaking changes.
|
||||
|
||||
- **FIX**: dragging up from a snap point doesn't perform one frame of scroll before transitioning to dragging anymore.
|
||||
- **DOCS**: updated docs with more cookbooks and all settings.
|
||||
- **BREAKING** **FEAT**: removed clip, background color and shape from `StupidSimpleSheetRoute`.
|
||||
|
||||
Added a `SheetBackground` widget as a replacement.
|
||||
Wrap your content in `SheetBackground` to get the same effect as before.
|
||||
|
||||
This was done to allow more flexibility in customizing the sheet's appearance.
|
||||
|
||||
|
||||
## 0.7.0
|
||||
|
||||
> Note: This release has breaking changes.
|
||||
|
||||
- **FIX**: clip behavior wasn't respected in sheets.
|
||||
- **BREAKING** **FEAT**: add `backgroundColor` to `StupidSimpleSheetRoute`.
|
||||
|
||||
## 0.6.3
|
||||
|
||||
- **FIX**: improve clipping of overlayed sheets.
|
||||
- **FEAT**: add `shape` parameter to `StupidSimpleCupertinoSheetRoute`.
|
||||
|
||||
Deprecated `topRadius` parameter in favor of it.
|
||||
|
||||
|
||||
## 0.6.2+3
|
||||
|
||||
- **FIX**: don't interfere with `pop()` animation while sheet is being dragged.
|
||||
|
||||
## 0.6.2+2
|
||||
|
||||
- **FIX**: stop bounce when scrolling down while sheet is at top.
|
||||
|
||||
## 0.6.2+1
|
||||
|
||||
- **DOCS**: fixed an incorrect changelog entry.
|
||||
|
||||
## 0.6.2
|
||||
|
||||
- **FEAT**: add `topRadius` to cupertino sheet.
|
||||
|
||||
This allows matching the iOS 26.0 appearance more closely
|
||||
|
||||
- **FEAT**: add `originateAboveBottomViewInset` to `StupidSimpleSheet` and its mixin.
|
||||
|
||||
## 0.6.1
|
||||
|
||||
- **FEAT**: add `draggable` parameter to sheets that can be used to disable user drags (#206).
|
||||
|
||||
## 0.6.0+1
|
||||
|
||||
- **FIX**: `overrideSnappingConfig` and `animateToRelative` could interrupt sheet dismissal and break Navigator (#205).
|
||||
|
||||
## 0.6.0
|
||||
|
||||
> Note: This release has breaking changes.
|
||||
|
||||
- **BREAKING** **FEAT**: allow overriding the current sheet's snapping config from its controller (#203).
|
||||
|
||||
For this, `effectiveSnappingConfig` was introduced on `StupidSimpleSheetTransitionMixin`, which should be used for all snapping configuration calculations instead of `snappingConfig`.
|
||||
If you only set `snappingConfig` and didn't do your own calculations with its values, you don't need to change anything.
|
||||
|
||||
## 0.5.0
|
||||
|
||||
> Note: This release has breaking changes.
|
||||
|
||||
- **BREAKING** **FIX**: don't call `Navigator.did[Start|Stop]UserGesture` by default to match Flutter sheets.
|
||||
|
||||
You can restore the old behavior by passing `callNavigatorUserGestureMethods = true` to your sheet route
|
||||
|
||||
|
||||
## 0.4.2
|
||||
|
||||
- **FEAT**: add `StupidSimpleSheetController` that can be used from a sheets subtree for imperative control.
|
||||
|
||||
## 0.4.1
|
||||
|
||||
- **FIX**: velocity scaling when overdragging with resistance.
|
||||
- **FIX**: secondary animation in cupertino sheet.
|
||||
- **FEAT**: cupertino sheet can now be dragged over its limits with resistance.
|
||||
- **FEAT**: sheets support snapping points now.
|
||||
- **FEAT**: cupertino sheet top padding is based on safe area now.
|
||||
|
||||
## 0.4.0+2
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.4.0+1
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.4.0
|
||||
|
||||
> Note: This release has breaking changes.
|
||||
|
||||
- **BREAKING** **FEAT**: add `onlyDragWhenScrollWasAtTop` and default to true.
|
||||
|
||||
This allows sheets to prevent closing accidentally when the user just wanted to scroll to the top, especially in short lists. It matches iOS default behavior.
|
||||
|
||||
|
||||
## 0.3.1
|
||||
|
||||
- **FEAT**: add `clearBarrierImmediately` setting that allows the route to make underlying routes interactible straight away (#183).
|
||||
|
||||
## 0.3.0+1
|
||||
|
||||
- **FIX**: routes below `StupidSimpleCupertinoSheetRoute` took too long to become interactable (#180).
|
||||
|
||||
## 0.3.0
|
||||
|
||||
- Graduate package to a stable release. See pre-releases prior to this version for changelog entries.
|
||||
|
||||
## 0.3.0-dev.3
|
||||
|
||||
- **FIX**: import internal from package:meta again.
|
||||
|
||||
## 0.3.0-dev.2
|
||||
|
||||
- **FEAT**: add clip behavior option to sheet (#173).
|
||||
|
||||
## 0.3.0-dev.1
|
||||
|
||||
- **FIX**: don't render overscroll indicators while we drag.
|
||||
- **FIX**: only pay attention to drags on the axis we care about.
|
||||
- **FIX**: only pay attention to relevant axes.
|
||||
|
||||
## 0.3.0-dev.0
|
||||
|
||||
- **BUILD**: fixed package versioning
|
||||
|
||||
## 0.0.2-dev.2
|
||||
|
||||
- **FIX**: default to `snapToEnd` in sheet motions for compatibility with the latest motor update.
|
||||
|
||||
## 0.0.2-dev.1
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 0.0.2-dev.0+1
|
||||
|
||||
- **FIX**: sheets don't incorrectly use an old drag end velocity anymore.
|
||||
|
||||
## 0.0.2
|
||||
|
||||
- **FIX**: remove hard-coded padding from sheet (#155).
|
||||
- **FEAT**: stupid simple sheet and drag detector (#154).
|
||||
|
||||
21
packages/stupid_simple_sheet/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Tim Lehmann for whynotmake.it
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
361
packages/stupid_simple_sheet/README.md
Normal file
@@ -0,0 +1,361 @@
|
||||
# Stupid Simple Sheet
|
||||
|
||||
[](https://pub.dev/packages/stupid_simple_sheet)
|
||||
[](./test/)
|
||||
[![lintervention_badge]]([lintervention_link])
|
||||
[](https://bsky.app/profile/i.madethese.works)
|
||||
|
||||
A simple yet powerful sheet widget for Flutter with seamless scroll-to-drag transitions.
|
||||
|
||||
## What makes it unique
|
||||
|
||||
**Smooth transitioning from any scrolling child to the drag gestures of the mobile sheet.** The sheet automatically detects when scrollable content reaches its bounds and seamlessly transitions to sheet dragging behavior - no complex gesture coordination required.
|
||||
|
||||
**Powered by Motor physics simulations** to make the sheet feel incredibly natural and responsive. The spring physics create smooth, realistic motion that feels right at home on any device.
|
||||
|
||||
The sheet works perfectly with:
|
||||
- `ListView`
|
||||
- `CustomScrollView`
|
||||
- `PageView`
|
||||
- Any scrollable widget
|
||||
|
||||
## Important Warning
|
||||
|
||||
**Content inside the sheet should not define any custom `ScrollConfiguration`.** The sheet relies on the default Flutter scroll behavior to properly detect scroll boundaries and transition between scrolling and dragging states.
|
||||
|
||||
## Installation
|
||||
|
||||
**In order to start using Stupid Simple Sheet you must have the [Flutter SDK][flutter_install_link] installed on your machine.**
|
||||
|
||||
|
||||
Install via `flutter pub`:
|
||||
|
||||
```sh
|
||||
flutter pub add stupid_simple_sheet
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Understanding the Base Sheet
|
||||
|
||||
`StupidSimpleSheetRoute` is intentionally minimal - it provides **no background, shape, or SafeArea by default**. This gives you complete freedom to build any style of sheet:
|
||||
|
||||
```dart
|
||||
Navigator.of(context).push(
|
||||
StupidSimpleSheetRoute(
|
||||
child: YourSheetContent(), // You control all styling
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
This design lets you create sheets that don't look like traditional sheets at all - floating cards, full-bleed content, custom shapes, or anything else you can imagine.
|
||||
|
||||
### Common Use Cases
|
||||
|
||||
#### 1. Standard Modal Sheet with Background
|
||||
|
||||
For a typical modal sheet with rounded corners and a background, wrap your content in `SheetBackground`:
|
||||
|
||||
```dart
|
||||
Navigator.of(context).push(
|
||||
StupidSimpleSheetRoute(
|
||||
child: SafeArea(
|
||||
// Most sheets should only avoid the top safe area, the rest should be avoided
|
||||
// inside the sheet content as needed.
|
||||
bottom: false,
|
||||
left: false,
|
||||
right: false,
|
||||
child: SheetBackground(
|
||||
child: YourContent(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
`SheetBackground` provides:
|
||||
- Rounded superellipse shape (24px radius at top)
|
||||
- Theme's surface color as background
|
||||
- Anti-aliased clipping
|
||||
- Automatic background extension to handle overdrag
|
||||
|
||||
You can customize it:
|
||||
|
||||
```dart
|
||||
SheetBackground(
|
||||
backgroundColor: Colors.blue.shade50,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: YourContent(),
|
||||
)
|
||||
```
|
||||
|
||||
#### 2. Cupertino-style Sheet
|
||||
|
||||
For iOS-style modal sheets that push the previous screen back:
|
||||
|
||||

|
||||
|
||||
```dart
|
||||
Navigator.of(context).push(
|
||||
StupidSimpleCupertinoSheetRoute(
|
||||
child: CupertinoPageScaffold(
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
CupertinoSliverNavigationBar(
|
||||
largeTitle: Text('Sheet'),
|
||||
leading: CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
child: Text('Close'),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
),
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => CupertinoListTile(
|
||||
title: Text('Item #$index'),
|
||||
),
|
||||
childCount: 50,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
#### 3. Small Floating Sheet (Resizing Content)
|
||||
|
||||
For sheets that size to fit their content and can grow/shrink dynamically:
|
||||
|
||||

|
||||
|
||||
```dart
|
||||
Navigator.of(context).push(
|
||||
StupidSimpleSheetRoute(
|
||||
motion: CupertinoMotion.smooth(),
|
||||
originateAboveBottomViewInset: true, // Stays above keyboard
|
||||
child: SafeArea(
|
||||
child: Card(
|
||||
margin: EdgeInsets.all(8),
|
||||
shape: RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min, // Size to content
|
||||
children: [
|
||||
// Your content here
|
||||
CupertinoTextField(placeholder: 'Type something...'),
|
||||
// Content can grow dynamically
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
#### 4. Sheet with Snapping Points
|
||||
|
||||
Create sheets that snap to specific positions (e.g., half-open, full):
|
||||
|
||||
```dart
|
||||
Navigator.of(context).push(
|
||||
StupidSimpleCupertinoSheetRoute(
|
||||
snappingConfig: SheetSnappingConfig(
|
||||
[0.5, 1.0], // Snap at 50% and 100%
|
||||
initialSnap: 0.5, // Start half-open
|
||||
),
|
||||
child: YourContent(),
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
#### 5. Non-Draggable Sheet
|
||||
|
||||
For sheets that can only be closed programmatically:
|
||||
|
||||
```dart
|
||||
Navigator.of(context).push(
|
||||
StupidSimpleCupertinoSheetRoute(
|
||||
draggable: false,
|
||||
child: YourContent(), // Must include a close button
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
#### 6. Sheet with PageView
|
||||
|
||||
The sheet handles horizontal paging seamlessly:
|
||||
|
||||
```dart
|
||||
Navigator.of(context).push(
|
||||
StupidSimpleCupertinoSheetRoute(
|
||||
child: CupertinoPageScaffold(
|
||||
child: PageView(
|
||||
children: [
|
||||
CustomScrollView(/* Page 1 content */),
|
||||
CustomScrollView(/* Page 2 content */),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
### Customizing Motion
|
||||
|
||||
Control the sheet's animation physics:
|
||||
|
||||
```dart
|
||||
Navigator.of(context).push(
|
||||
StupidSimpleSheetRoute(
|
||||
motion: CupertinoMotion.bouncy(snapToEnd: true),
|
||||
child: YourContent(),
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
### Programmatic Control with StupidSimpleSheetController
|
||||
|
||||
Control the sheet's position from within its content:
|
||||
|
||||
```dart
|
||||
Navigator.of(context).push(
|
||||
StupidSimpleSheetRoute(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final controller = StupidSimpleSheetController.maybeOf<void>(context);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
// Animate to half-open position
|
||||
controller?.animateToRelative(0.5);
|
||||
},
|
||||
child: Text('Half Open'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
// Animate to fully open with snapping
|
||||
controller?.animateToRelative(0.8, snap: true);
|
||||
},
|
||||
child: Text('Almost Full (with snap)'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
#### Controller Methods
|
||||
|
||||
- **`maybeOf<T>(BuildContext context)`**: Retrieves the controller from a context within the sheet. Returns `null` if called from outside a sheet.
|
||||
- **`animateToRelative(double position, {bool snap = false})`**: Animates the sheet to a relative position between 0.0 (closed) and 1.0 (fully open).
|
||||
- **`overrideSnappingConfig(SheetSnappingConfig? config, {bool animateToComply = false})`**: Dynamically change or disable snapping behavior.
|
||||
|
||||
**Note**: The controller cannot close the sheet programmatically. To close the sheet, use `Navigator.pop(context)`.
|
||||
|
||||
### Custom Routes with Maximum Control
|
||||
|
||||
For advanced use cases, create custom routes using `StupidSimpleSheetTransitionMixin`:
|
||||
|
||||
```dart
|
||||
class MyCustomSheetRoute<T> extends PopupRoute<T>
|
||||
with StupidSimpleSheetTransitionMixin<T> {
|
||||
MyCustomSheetRoute({
|
||||
required this.child,
|
||||
this.motion = const CupertinoMotion.smooth(snapToEnd: true),
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
final Motion motion;
|
||||
|
||||
@override
|
||||
Widget buildContent(BuildContext context) {
|
||||
return SafeArea(
|
||||
bottom: false,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
bottom: -1000,
|
||||
child: Material(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
|
||||
),
|
||||
),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
|
||||
child: child,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
double get overshootResistance => 50;
|
||||
|
||||
@override
|
||||
Color? get barrierColor => Colors.black26;
|
||||
}
|
||||
```
|
||||
|
||||
### Background Snapshotting
|
||||
|
||||
Improve transition performance by rasterizing the route behind the sheet into a GPU texture via `backgroundSnapshotMode`:
|
||||
|
||||
```dart
|
||||
Navigator.of(context).push(
|
||||
StupidSimpleCupertinoSheetRoute(
|
||||
backgroundSnapshotMode: RouteSnapshotMode.openAndForward,
|
||||
child: YourContent(),
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
| Mode | Snapshots when |
|
||||
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `never` | Never (default). Background is always painted live. |
|
||||
| `always` | Entire lifetime of the sheet. Best for static backgrounds. |
|
||||
| `animating` | During animations and drags only. Live when settled. |
|
||||
| `settled` | When settled only. Live during animations and drags. |
|
||||
| `openAndForward` | During the opening animation to the max snap point, and while settled there. Live during drags, closing, and animations to intermediate snap points. |
|
||||
|
||||
When sheets stack (e.g. glass-on-glass), the mixin automatically wires up snapshotting between them via `maybeSnapshotChild`.
|
||||
|
||||
## Features
|
||||
|
||||
- **Seamless scroll transitions**: Automatically handles the transition between scrolling content and sheet dragging
|
||||
- **Spring physics**: Natural motion using the `motor` package physics engine
|
||||
- **Programmatic control**: Use `StupidSimpleSheetController` to animate the sheet position
|
||||
- **Flexible styling**: Build any sheet style with `SheetBackground` or custom widgets
|
||||
- **Cupertino integration**: Native iOS-style sheets with `StupidSimpleCupertinoSheetRoute`
|
||||
- **Snapping**: Configure snap points for multi-stop sheets
|
||||
- **Background snapshotting**: Rasterize the background route for better transition performance
|
||||
- **Gesture coordination**: No need to manually handle gesture conflicts
|
||||
- **Multiple scroll types**: Supports all Flutter scrollable widgets
|
||||
- **Extensible architecture**: Use the mixin to create custom routes with full control
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
Check out the [example app](./example/) to see the sheet in action with:
|
||||
- Cupertino-style sheets with navigation bars
|
||||
- Paged content with PageView
|
||||
- Dynamically resizing sheets
|
||||
- Snapping sheets with multiple stops
|
||||
- Non-draggable sheets
|
||||
|
||||
---
|
||||
|
||||
[flutter_install_link]: https://flutter.dev/docs/get-started/install
|
||||
[lintervention_link]: https://github.com/whynotmake-it/lintervention
|
||||
[lintervention_badge]: https://img.shields.io/badge/lints_by-lintervention-3A5A40
|
||||
1
packages/stupid_simple_sheet/analysis_options.yaml
Normal file
@@ -0,0 +1 @@
|
||||
include: package:lintervention/analysis_options.yaml
|
||||
1
packages/stupid_simple_sheet/coverage.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="230" height="20" role="img" aria-label="stupid_simple_sheet coverage: 88.24%"><title>stupid_simple_sheet coverage: 88.24%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="230" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="177" height="20" fill="#555"/><rect x="177" width="53" height="20" fill="#4c1"/><rect width="230" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="895" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="1670">stupid_simple_sheet coverage</text><text x="895" y="140" transform="scale(.1)" fill="#fff" textLength="1670">stupid_simple_sheet coverage</text><text aria-hidden="true" x="2025" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">88.24%</text><text x="2025" y="140" transform="scale(.1)" fill="#fff" textLength="430">88.24%</text></g></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
563
packages/stupid_simple_sheet/coverage/lcov.info
Normal file
@@ -0,0 +1,563 @@
|
||||
SF:lib/stupid_simple_sheet.dart
|
||||
DA:41,1
|
||||
DA:94,1
|
||||
DA:96,2
|
||||
DA:98,0
|
||||
DA:99,0
|
||||
DA:100,0
|
||||
DA:107,1
|
||||
DA:109,1
|
||||
DA:114,2
|
||||
DA:133,2
|
||||
DA:135,2
|
||||
DA:136,2
|
||||
DA:137,2
|
||||
DA:138,6
|
||||
DA:139,2
|
||||
DA:140,4
|
||||
DA:141,12
|
||||
DA:145,2
|
||||
DA:146,4
|
||||
DA:147,8
|
||||
DA:151,2
|
||||
DA:179,1
|
||||
DA:190,0
|
||||
DA:201,3
|
||||
DA:210,0
|
||||
DA:216,0
|
||||
DA:223,0
|
||||
DA:236,0
|
||||
DA:243,2
|
||||
DA:245,2
|
||||
DA:247,4
|
||||
DA:248,4
|
||||
DA:258,0
|
||||
DA:260,0
|
||||
DA:281,4
|
||||
DA:282,8
|
||||
DA:294,4
|
||||
DA:295,4
|
||||
DA:297,4
|
||||
DA:298,8
|
||||
DA:301,1
|
||||
DA:302,2
|
||||
DA:306,2
|
||||
DA:307,2
|
||||
DA:308,1
|
||||
DA:309,1
|
||||
DA:310,2
|
||||
DA:311,3
|
||||
DA:313,1
|
||||
DA:314,4
|
||||
DA:317,3
|
||||
DA:318,2
|
||||
DA:320,2
|
||||
DA:321,2
|
||||
DA:322,1
|
||||
DA:323,1
|
||||
DA:324,1
|
||||
DA:329,4
|
||||
DA:330,4
|
||||
DA:331,4
|
||||
DA:332,8
|
||||
DA:336,4
|
||||
DA:337,8
|
||||
DA:339,4
|
||||
DA:341,8
|
||||
DA:343,4
|
||||
DA:345,4
|
||||
DA:346,4
|
||||
DA:352,8
|
||||
DA:357,4
|
||||
DA:358,4
|
||||
DA:359,4
|
||||
DA:360,8
|
||||
DA:362,8
|
||||
DA:363,4
|
||||
DA:367,4
|
||||
DA:373,4
|
||||
DA:375,8
|
||||
DA:377,4
|
||||
DA:380,4
|
||||
DA:381,4
|
||||
DA:382,8
|
||||
DA:383,8
|
||||
DA:384,6
|
||||
DA:385,3
|
||||
DA:386,3
|
||||
DA:387,3
|
||||
DA:388,3
|
||||
DA:392,4
|
||||
DA:396,4
|
||||
DA:399,8
|
||||
DA:400,0
|
||||
DA:402,4
|
||||
DA:403,4
|
||||
DA:404,4
|
||||
DA:407,4
|
||||
DA:408,4
|
||||
DA:409,12
|
||||
DA:413,1
|
||||
DA:420,1
|
||||
DA:421,1
|
||||
DA:422,1
|
||||
DA:423,2
|
||||
DA:425,1
|
||||
DA:429,1
|
||||
DA:430,1
|
||||
DA:431,1
|
||||
DA:432,2
|
||||
DA:436,1
|
||||
DA:437,1
|
||||
DA:438,2
|
||||
DA:448,4
|
||||
DA:450,4
|
||||
DA:451,4
|
||||
DA:452,4
|
||||
DA:453,4
|
||||
DA:454,3
|
||||
DA:455,4
|
||||
DA:461,3
|
||||
DA:464,6
|
||||
DA:465,3
|
||||
DA:466,3
|
||||
DA:469,3
|
||||
DA:472,3
|
||||
DA:473,3
|
||||
DA:474,3
|
||||
DA:475,0
|
||||
DA:479,3
|
||||
DA:480,6
|
||||
DA:481,6
|
||||
DA:484,6
|
||||
DA:486,6
|
||||
DA:488,4
|
||||
DA:491,1
|
||||
DA:492,2
|
||||
DA:493,2
|
||||
DA:497,4
|
||||
DA:499,2
|
||||
DA:500,6
|
||||
DA:502,2
|
||||
DA:505,3
|
||||
DA:507,6
|
||||
DA:508,3
|
||||
DA:511,3
|
||||
DA:516,3
|
||||
DA:517,6
|
||||
DA:518,3
|
||||
DA:519,0
|
||||
DA:523,6
|
||||
DA:525,3
|
||||
DA:527,6
|
||||
DA:530,6
|
||||
DA:531,0
|
||||
DA:534,0
|
||||
DA:535,0
|
||||
DA:536,0
|
||||
DA:538,0
|
||||
DA:540,0
|
||||
DA:542,0
|
||||
DA:543,0
|
||||
DA:547,9
|
||||
DA:553,3
|
||||
DA:554,2
|
||||
DA:557,4
|
||||
DA:560,4
|
||||
DA:562,4
|
||||
DA:563,2
|
||||
DA:566,3
|
||||
DA:575,3
|
||||
DA:577,3
|
||||
DA:579,2
|
||||
DA:587,4
|
||||
DA:590,4
|
||||
DA:591,4
|
||||
DA:592,4
|
||||
DA:594,4
|
||||
DA:598,0
|
||||
DA:601,0
|
||||
DA:602,0
|
||||
DA:605,4
|
||||
DA:608,8
|
||||
DA:609,8
|
||||
DA:610,4
|
||||
DA:625,1
|
||||
DA:626,1
|
||||
DA:627,1
|
||||
DA:644,2
|
||||
DA:646,6
|
||||
DA:651,2
|
||||
DA:656,3
|
||||
DA:662,1
|
||||
DA:663,1
|
||||
DA:664,1
|
||||
DA:671,4
|
||||
DA:672,4
|
||||
DA:674,4
|
||||
DA:677,2
|
||||
DA:678,4
|
||||
DA:701,1
|
||||
DA:706,1
|
||||
DA:710,1
|
||||
DA:713,1
|
||||
DA:714,2
|
||||
DA:717,2
|
||||
DA:719,2
|
||||
DA:724,3
|
||||
DA:725,0
|
||||
DA:728,2
|
||||
DA:731,2
|
||||
DA:734,1
|
||||
DA:736,2
|
||||
DA:739,1
|
||||
LF:212
|
||||
LH:187
|
||||
end_of_record
|
||||
SF:lib/src/clamped_animation.dart
|
||||
DA:7,2
|
||||
DA:12,2
|
||||
DA:13,6
|
||||
DA:18,4
|
||||
DA:24,2
|
||||
DA:37,2
|
||||
DA:38,12
|
||||
DA:43,2
|
||||
DA:47,2
|
||||
LF:9
|
||||
LH:9
|
||||
end_of_record
|
||||
SF:lib/src/cupertino_sheet_copy.dart
|
||||
DA:36,4
|
||||
DA:37,4
|
||||
DA:39,1
|
||||
DA:44,1
|
||||
DA:45,2
|
||||
DA:47,1
|
||||
DA:52,4
|
||||
DA:55,2
|
||||
DA:62,4
|
||||
DA:66,4
|
||||
DA:70,2
|
||||
DA:72,2
|
||||
DA:73,2
|
||||
DA:74,2
|
||||
DA:75,2
|
||||
DA:77,2
|
||||
DA:78,2
|
||||
DA:87,1
|
||||
DA:92,1
|
||||
DA:93,1
|
||||
DA:94,1
|
||||
DA:95,1
|
||||
DA:99,1
|
||||
DA:100,1
|
||||
DA:101,1
|
||||
DA:111,1
|
||||
DA:120,1
|
||||
DA:122,1
|
||||
DA:124,1
|
||||
DA:129,3
|
||||
DA:132,1
|
||||
DA:134,1
|
||||
DA:139,1
|
||||
DA:141,2
|
||||
DA:145,1
|
||||
DA:149,1
|
||||
DA:152,1
|
||||
DA:156,1
|
||||
DA:158,1
|
||||
DA:162,2
|
||||
DA:164,1
|
||||
DA:167,1
|
||||
DA:175,4
|
||||
DA:177,1
|
||||
DA:178,1
|
||||
DA:179,1
|
||||
DA:184,1
|
||||
DA:186,1
|
||||
DA:188,1
|
||||
DA:192,1
|
||||
DA:195,1
|
||||
DA:196,1
|
||||
DA:197,1
|
||||
DA:208,1
|
||||
DA:218,1
|
||||
DA:220,1
|
||||
DA:224,1
|
||||
DA:225,1
|
||||
DA:226,1
|
||||
DA:227,1
|
||||
DA:229,4
|
||||
DA:234,1
|
||||
DA:236,1
|
||||
DA:240,2
|
||||
DA:243,1
|
||||
DA:245,2
|
||||
DA:246,1
|
||||
DA:247,1
|
||||
DA:253,1
|
||||
DA:256,1
|
||||
DA:267,1
|
||||
DA:281,1
|
||||
DA:282,1
|
||||
DA:283,1
|
||||
DA:286,1
|
||||
DA:288,2
|
||||
DA:292,1
|
||||
DA:296,1
|
||||
DA:298,1
|
||||
DA:300,1
|
||||
DA:301,1
|
||||
DA:302,1
|
||||
DA:307,4
|
||||
DA:308,1
|
||||
DA:310,1
|
||||
DA:312,1
|
||||
LF:86
|
||||
LH:86
|
||||
end_of_record
|
||||
SF:lib/src/optimized_clip.dart
|
||||
DA:12,2
|
||||
DA:25,2
|
||||
DA:27,2
|
||||
DA:29,0
|
||||
DA:30,6
|
||||
DA:31,2
|
||||
DA:33,2
|
||||
DA:35,3
|
||||
DA:36,1
|
||||
DA:38,1
|
||||
DA:40,1
|
||||
DA:41,0
|
||||
DA:42,0
|
||||
DA:44,2
|
||||
DA:45,1
|
||||
DA:46,1
|
||||
DA:48,0
|
||||
DA:49,0
|
||||
DA:50,0
|
||||
DA:53,0
|
||||
LF:20
|
||||
LH:13
|
||||
end_of_record
|
||||
SF:lib/src/glass_sheet_transitions.dart
|
||||
DA:16,0
|
||||
DA:25,0
|
||||
DA:36,2
|
||||
DA:49,2
|
||||
DA:53,2
|
||||
DA:54,2
|
||||
DA:55,2
|
||||
DA:56,2
|
||||
DA:58,8
|
||||
DA:62,2
|
||||
DA:63,2
|
||||
DA:66,2
|
||||
DA:68,2
|
||||
DA:72,4
|
||||
DA:75,2
|
||||
DA:77,2
|
||||
DA:81,2
|
||||
DA:84,2
|
||||
DA:95,2
|
||||
DA:110,2
|
||||
DA:111,2
|
||||
DA:112,2
|
||||
DA:115,2
|
||||
DA:117,2
|
||||
DA:118,2
|
||||
DA:119,2
|
||||
DA:124,8
|
||||
DA:125,2
|
||||
DA:127,2
|
||||
DA:128,2
|
||||
DA:130,2
|
||||
LF:31
|
||||
LH:29
|
||||
end_of_record
|
||||
SF:lib/src/sheet_background.dart
|
||||
DA:11,2
|
||||
DA:52,2
|
||||
DA:55,6
|
||||
DA:56,2
|
||||
DA:57,2
|
||||
DA:58,4
|
||||
DA:59,2
|
||||
DA:60,4
|
||||
DA:61,2
|
||||
DA:62,2
|
||||
DA:63,2
|
||||
DA:64,2
|
||||
DA:65,2
|
||||
DA:66,2
|
||||
LF:14
|
||||
LH:14
|
||||
end_of_record
|
||||
SF:lib/src/snapping_point.dart
|
||||
DA:20,1
|
||||
DA:33,2
|
||||
DA:35,2
|
||||
DA:36,2
|
||||
DA:39,4
|
||||
DA:44,2
|
||||
DA:49,2
|
||||
DA:50,5
|
||||
DA:51,2
|
||||
DA:56,4
|
||||
DA:58,0
|
||||
DA:60,0
|
||||
DA:63,2
|
||||
DA:67,2
|
||||
DA:69,2
|
||||
DA:71,4
|
||||
DA:72,4
|
||||
DA:73,2
|
||||
DA:87,2
|
||||
DA:88,2
|
||||
DA:89,0
|
||||
DA:92,2
|
||||
DA:93,6
|
||||
DA:94,2
|
||||
DA:96,4
|
||||
DA:100,2
|
||||
DA:101,2
|
||||
DA:103,4
|
||||
DA:105,10
|
||||
DA:111,2
|
||||
DA:112,2
|
||||
DA:114,4
|
||||
DA:115,6
|
||||
DA:121,2
|
||||
DA:122,2
|
||||
DA:123,4
|
||||
DA:127,0
|
||||
DA:128,0
|
||||
DA:129,0
|
||||
DA:134,2
|
||||
DA:135,10
|
||||
DA:138,1
|
||||
DA:141,0
|
||||
DA:142,0
|
||||
DA:143,0
|
||||
DA:145,0
|
||||
DA:146,0
|
||||
DA:148,0
|
||||
DA:149,0
|
||||
LF:49
|
||||
LH:36
|
||||
end_of_record
|
||||
SF:lib/src/stupid_simple_cupertino_sheet.dart
|
||||
DA:15,1
|
||||
DA:39,1
|
||||
DA:61,1
|
||||
DA:64,1
|
||||
DA:65,2
|
||||
DA:67,1
|
||||
DA:70,1
|
||||
DA:73,1
|
||||
DA:93,1
|
||||
DA:95,1
|
||||
DA:97,2
|
||||
DA:99,1
|
||||
DA:101,1
|
||||
DA:102,1
|
||||
DA:103,1
|
||||
DA:104,1
|
||||
DA:105,1
|
||||
DA:106,1
|
||||
DA:107,1
|
||||
DA:115,1
|
||||
DA:117,1
|
||||
DA:120,1
|
||||
DA:124,1
|
||||
DA:131,1
|
||||
DA:133,1
|
||||
DA:134,1
|
||||
DA:135,1
|
||||
DA:137,2
|
||||
DA:139,2
|
||||
DA:140,2
|
||||
DA:141,1
|
||||
DA:142,1
|
||||
DA:143,1
|
||||
DA:150,0
|
||||
DA:152,0
|
||||
DA:153,0
|
||||
DA:156,1
|
||||
DA:159,1
|
||||
DA:161,1
|
||||
DA:162,0
|
||||
DA:164,1
|
||||
DA:169,1
|
||||
DA:171,0
|
||||
DA:175,0
|
||||
DA:178,0
|
||||
DA:180,0
|
||||
DA:182,0
|
||||
LF:47
|
||||
LH:38
|
||||
end_of_record
|
||||
SF:lib/src/stupid_simple_glass_sheet.dart
|
||||
DA:26,2
|
||||
DA:61,0
|
||||
DA:90,2
|
||||
DA:91,4
|
||||
DA:93,2
|
||||
DA:94,4
|
||||
DA:96,2
|
||||
DA:99,2
|
||||
DA:102,2
|
||||
DA:120,2
|
||||
DA:122,4
|
||||
DA:124,1
|
||||
DA:125,1
|
||||
DA:126,1
|
||||
DA:137,2
|
||||
DA:139,2
|
||||
DA:140,2
|
||||
DA:141,2
|
||||
DA:142,4
|
||||
DA:143,2
|
||||
DA:144,2
|
||||
DA:145,2
|
||||
DA:146,2
|
||||
DA:147,2
|
||||
DA:148,2
|
||||
DA:149,2
|
||||
DA:159,2
|
||||
DA:161,2
|
||||
DA:164,2
|
||||
DA:168,2
|
||||
DA:175,2
|
||||
DA:177,2
|
||||
DA:178,2
|
||||
DA:180,6
|
||||
DA:181,2
|
||||
DA:183,4
|
||||
DA:185,2
|
||||
DA:186,2
|
||||
DA:187,2
|
||||
DA:188,2
|
||||
DA:189,2
|
||||
DA:190,2
|
||||
DA:197,2
|
||||
DA:199,2
|
||||
DA:200,0
|
||||
DA:203,2
|
||||
DA:205,4
|
||||
DA:206,2
|
||||
DA:209,2
|
||||
DA:212,2
|
||||
DA:214,2
|
||||
DA:215,2
|
||||
DA:217,2
|
||||
DA:220,2
|
||||
DA:224,2
|
||||
DA:228,0
|
||||
DA:231,0
|
||||
DA:232,0
|
||||
DA:234,0
|
||||
LF:59
|
||||
LH:53
|
||||
end_of_record
|
||||
29
packages/stupid_simple_sheet/example/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# example
|
||||
|
||||
A new Flutter project.
|
||||
|
||||
## Getting Started
|
||||
|
||||
This project is a starting point for a Flutter application that follows the
|
||||
[simple app state management
|
||||
tutorial](https://flutter.dev/to/state-management-sample).
|
||||
|
||||
For help getting started with Flutter development, view the
|
||||
[online documentation](https://docs.flutter.dev), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
||||
|
||||
## Assets
|
||||
|
||||
The `assets` directory houses images, fonts, and any other files you want to
|
||||
include with your application.
|
||||
|
||||
The `assets/images` directory contains [resolution-aware
|
||||
images](https://flutter.dev/to/resolution-aware-images).
|
||||
|
||||
## Localization
|
||||
|
||||
This project generates localized messages based on arb files found in
|
||||
the `lib/src/localization` directory.
|
||||
|
||||
To support additional languages, please visit the tutorial on
|
||||
[Internationalizing Flutter apps](https://flutter.dev/to/internationalization).
|
||||
@@ -0,0 +1,3 @@
|
||||
linter:
|
||||
rules:
|
||||
public_member_api_docs: false
|
||||
610
packages/stupid_simple_sheet/example/lib/main.dart
Normal file
@@ -0,0 +1,610 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:liquid_glass_renderer/liquid_glass_renderer.dart';
|
||||
import 'package:stupid_simple_sheet/stupid_simple_sheet.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
runApp(
|
||||
CupertinoApp.router(
|
||||
routerConfig: router.config(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final stupidSimpleSheetRoutes = [
|
||||
NamedRouteDef(
|
||||
name: 'Stupid Simple Sheet Examples',
|
||||
path: '',
|
||||
type: RouteType.cupertino(),
|
||||
builder: (context, state) => const MotorExample(),
|
||||
),
|
||||
NamedRouteDef(
|
||||
name: 'Glass Sheet',
|
||||
path: 'glass-sheet',
|
||||
builder: (context, data) => _GlassSheetContent(),
|
||||
type: RouteType.custom(
|
||||
customRouteBuilder: <T>(context, child, page) =>
|
||||
StupidSimpleGlassSheetRoute<T>(
|
||||
backgroundSnapshotMode: RouteSnapshotMode.openAndForward,
|
||||
settings: page,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
NamedRouteDef(
|
||||
name: 'Cupertino Sheet',
|
||||
path: 'cupertino-sheet',
|
||||
builder: (context, data) => _CupertinoSheetContent(),
|
||||
type: RouteType.custom(
|
||||
customRouteBuilder: <T>(context, child, page) =>
|
||||
StupidSimpleCupertinoSheetRoute<T>(
|
||||
backgroundSnapshotMode: RouteSnapshotMode.openAndForward,
|
||||
settings: page,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
NamedRouteDef(
|
||||
name: 'Paged Sheet',
|
||||
path: 'paged-sheet',
|
||||
builder: (context, data) => _PagedSheetContent(),
|
||||
type: RouteType.custom(
|
||||
customRouteBuilder: <T>(context, child, page) =>
|
||||
StupidSimpleCupertinoSheetRoute<T>(
|
||||
backgroundSnapshotMode: RouteSnapshotMode.openAndForward,
|
||||
settings: page,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
NamedRouteDef(
|
||||
name: 'Small Sheet',
|
||||
path: 'small-sheet',
|
||||
builder: (context, data) => _SmallSheetContent(),
|
||||
type: RouteType.custom(
|
||||
customRouteBuilder: <T>(context, child, page) =>
|
||||
StupidSimpleSheetRoute<T>(
|
||||
backgroundSnapshotMode: RouteSnapshotMode.openAndForward,
|
||||
settings: page,
|
||||
motion: CupertinoMotion.smooth(),
|
||||
originateAboveBottomViewInset: true,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
NamedRouteDef(
|
||||
name: 'Snapping Sheet',
|
||||
path: 'snapping-sheet',
|
||||
builder: (context, data) => _SnappingSheetContent(),
|
||||
type: RouteType.custom(
|
||||
customRouteBuilder: <T>(context, child, page) =>
|
||||
StupidSimpleCupertinoSheetRoute<T>(
|
||||
backgroundSnapshotMode: RouteSnapshotMode.openAndForward,
|
||||
settings: page,
|
||||
snappingConfig: SheetSnappingConfig(
|
||||
[0.5, 1.0],
|
||||
initialSnap: .5,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
NamedRouteDef(
|
||||
name: 'Non-Draggable Sheet',
|
||||
path: 'non-draggable-sheet',
|
||||
builder: (context, data) => _NonDraggableSheetContent(),
|
||||
type: RouteType.custom(
|
||||
customRouteBuilder: <T>(context, child, page) =>
|
||||
StupidSimpleCupertinoSheetRoute<T>(
|
||||
backgroundSnapshotMode: RouteSnapshotMode.openAndForward,
|
||||
settings: page,
|
||||
draggable: false,
|
||||
child: child,
|
||||
shape: RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(28),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
final router = RootStackRouter.build(
|
||||
routes: [
|
||||
NamedRouteDef.shell(
|
||||
name: 'Home',
|
||||
path: '/',
|
||||
type: RouteType.cupertino(),
|
||||
children: stupidSimpleSheetRoutes,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
class MotorExample extends StatelessWidget {
|
||||
const MotorExample({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: const CupertinoNavigationBar.large(
|
||||
largeTitle: Text('Stupid Simple Sheet'),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
spacing: 16,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CupertinoButton.filled(
|
||||
child: Text('Glass Sheet'),
|
||||
onPressed: () => context.navigateTo(NamedRoute('Glass Sheet')),
|
||||
),
|
||||
CupertinoButton.filled(
|
||||
child: Text('Cupertino Sheet'),
|
||||
onPressed: () =>
|
||||
context.navigateTo(NamedRoute('Cupertino Sheet')),
|
||||
),
|
||||
CupertinoButton.filled(
|
||||
child: Text('Paged Sheet'),
|
||||
onPressed: () => context.navigateTo(NamedRoute('Paged Sheet')),
|
||||
),
|
||||
CupertinoButton.filled(
|
||||
child: Text('Resizing Sheet'),
|
||||
onPressed: () => context.navigateTo(NamedRoute('Small Sheet')),
|
||||
),
|
||||
CupertinoButton.filled(
|
||||
child: Text('Snapping Sheet'),
|
||||
onPressed: () => context.navigateTo(NamedRoute('Snapping Sheet')),
|
||||
),
|
||||
CupertinoButton.filled(
|
||||
child: Text('Non-Draggable Sheet'),
|
||||
onPressed: () =>
|
||||
context.navigateTo(NamedRoute('Non-Draggable Sheet')),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GlassSheetContent extends StatelessWidget {
|
||||
const _GlassSheetContent();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
PinnedHeaderSliver(
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: IntrinsicHeight(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
child: _GlassSurface(
|
||||
inLayer: false,
|
||||
borderRadius: BorderRadius.circular(200),
|
||||
child: Padding(
|
||||
padding: EdgeInsetsGeometry.all(10),
|
||||
child: Icon(
|
||||
CupertinoIcons.xmark,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () => context.pushRoute(
|
||||
NamedRoute('Glass Sheet'),
|
||||
),
|
||||
child: _GlassSurface(
|
||||
color: CupertinoColors.activeBlue,
|
||||
inLayer: false,
|
||||
borderRadius: BorderRadius.circular(200),
|
||||
child: Padding(
|
||||
padding: EdgeInsetsGeometry.symmetric(
|
||||
vertical: 10, horizontal: 16),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Another',
|
||||
style: CupertinoTheme.of(context)
|
||||
.textTheme
|
||||
.actionTextStyle
|
||||
.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: CupertinoColors.white.withValues(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverSafeArea(
|
||||
sliver: SliverMainAxisGroup(slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: CupertinoTextField(
|
||||
placeholder: 'Type something...',
|
||||
),
|
||||
),
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => CupertinoListTile(
|
||||
title: Text('Item #$index'),
|
||||
),
|
||||
childCount: 50,
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: CupertinoTextField(),
|
||||
),
|
||||
]))
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CupertinoSheetContent extends StatelessWidget {
|
||||
const _CupertinoSheetContent();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
CupertinoSliverNavigationBar(
|
||||
largeTitle: Text('Sheet'),
|
||||
leading: CupertinoButton(
|
||||
child: Text("Close"),
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
),
|
||||
SliverSafeArea(
|
||||
sliver: SliverMainAxisGroup(slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: CupertinoTextField(
|
||||
placeholder: 'Type something...',
|
||||
),
|
||||
),
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => CupertinoListTile(
|
||||
title: Text('Item #$index'),
|
||||
),
|
||||
childCount: 50,
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: CupertinoTextField(),
|
||||
),
|
||||
]))
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GlassSurface extends StatelessWidget {
|
||||
const _GlassSurface({
|
||||
required this.borderRadius,
|
||||
required this.inLayer,
|
||||
required this.child,
|
||||
this.color,
|
||||
});
|
||||
|
||||
final BorderRadius borderRadius;
|
||||
final bool inLayer;
|
||||
final Color? color;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LiquidStretch(
|
||||
child: DecoratedBox(
|
||||
decoration: ShapeDecoration(
|
||||
shape: RoundedSuperellipseBorder(borderRadius: borderRadius),
|
||||
shadows: [
|
||||
BoxShadow(
|
||||
color: CupertinoColors.black.withValues(alpha: 0.1),
|
||||
blurStyle: BlurStyle.outer,
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 0),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: _buildContent(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent(
|
||||
BuildContext context,
|
||||
) {
|
||||
if (inLayer) {
|
||||
return LiquidGlass(
|
||||
shape: LiquidRoundedSuperellipse(
|
||||
borderRadius: borderRadius.topLeft.x,
|
||||
),
|
||||
child: GlassGlow(child: child),
|
||||
);
|
||||
}
|
||||
return LiquidGlass.withOwnLayer(
|
||||
fake: true,
|
||||
settings: LiquidGlassSettings(
|
||||
glassColor: color ??
|
||||
CupertinoTheme.of(context).barBackgroundColor.withValues(alpha: .7),
|
||||
thickness: 30,
|
||||
ambientStrength: .1,
|
||||
saturation: 4,
|
||||
lightIntensity: .4,
|
||||
blur: 4,
|
||||
),
|
||||
shape: LiquidRoundedSuperellipse(
|
||||
borderRadius: borderRadius.topLeft.x,
|
||||
),
|
||||
child: GlassGlow(
|
||||
child: IconTheme(
|
||||
data: IconThemeData(
|
||||
color: CupertinoTheme.of(context).textTheme.textStyle.color),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SnappingSheetContent extends StatefulWidget {
|
||||
const _SnappingSheetContent();
|
||||
|
||||
@override
|
||||
State<_SnappingSheetContent> createState() => _SnappingSheetContentState();
|
||||
}
|
||||
|
||||
class _SnappingSheetContentState extends State<_SnappingSheetContent> {
|
||||
bool _snapDisabled = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
CupertinoSliverNavigationBar(
|
||||
largeTitle: Text('Sheet'),
|
||||
trailing: CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
child: Text(_snapDisabled ? 'Enable Snaps' : 'Disable Snaps'),
|
||||
onPressed: () {
|
||||
final controller =
|
||||
StupidSimpleSheetController.maybeOf(context);
|
||||
controller
|
||||
?.overrideSnappingConfig(
|
||||
_snapDisabled ? null : SheetSnappingConfig.full,
|
||||
animateToComply: true,
|
||||
)
|
||||
.ignore();
|
||||
setState(() {
|
||||
_snapDisabled = !_snapDisabled;
|
||||
});
|
||||
}),
|
||||
leading: CupertinoButton(
|
||||
child: Text("Close"),
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
),
|
||||
SliverFillRemaining(
|
||||
child: Center(
|
||||
child: CupertinoButton.tinted(
|
||||
child: Text('Another'),
|
||||
onPressed: () =>
|
||||
context.pushRoute(NamedRoute('Snapping Sheet')),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PagedSheetContent extends StatelessWidget {
|
||||
const _PagedSheetContent();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
child: PageView(
|
||||
children: [
|
||||
CustomScrollView(
|
||||
slivers: [
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => CupertinoListTile(
|
||||
title: Text('Item #$index'),
|
||||
),
|
||||
childCount: 50,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
CustomScrollView(
|
||||
slivers: [
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => CupertinoListTile(
|
||||
title: Text('Item #$index'),
|
||||
),
|
||||
childCount: 50,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NonDraggableSheetContent extends StatelessWidget {
|
||||
const _NonDraggableSheetContent();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
CupertinoSliverNavigationBar(
|
||||
largeTitle: Text('Non-Draggable Sheet'),
|
||||
leading: CupertinoButton(
|
||||
child: Text("Close"),
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
),
|
||||
SliverFillRemaining(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 16,
|
||||
children: [
|
||||
Text(
|
||||
'This sheet cannot be dragged!',
|
||||
style: CupertinoTheme.of(context).textTheme.textStyle,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Text(
|
||||
'Use the Close button to dismiss.',
|
||||
style: CupertinoTheme.of(context)
|
||||
.textTheme
|
||||
.textStyle
|
||||
.copyWith(color: CupertinoColors.secondaryLabel),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SmallSheetContent extends StatefulWidget {
|
||||
const _SmallSheetContent();
|
||||
|
||||
@override
|
||||
State<_SmallSheetContent> createState() => _SmallSheetContentState();
|
||||
}
|
||||
|
||||
class _SmallSheetContentState extends State<_SmallSheetContent> {
|
||||
List<String> items = List.generate(
|
||||
5,
|
||||
(index) => 'Item ${index + 1}',
|
||||
);
|
||||
|
||||
late final textController = TextEditingController();
|
||||
late final focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
textController.dispose();
|
||||
focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
margin: EdgeInsets.all(16),
|
||||
shape: RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
color: CupertinoColors.secondarySystemGroupedBackground
|
||||
.resolveFrom(context),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
child: AnimatedSize(
|
||||
alignment: Alignment.topCenter,
|
||||
duration: CupertinoMotion.smooth().duration,
|
||||
curve: CupertinoMotion.smooth().toCurve,
|
||||
child: Column(
|
||||
children: [
|
||||
CupertinoTextField.borderless(
|
||||
focusNode: focusNode,
|
||||
controller: textController,
|
||||
padding: EdgeInsetsGeometry.all(16),
|
||||
autofocus: true,
|
||||
placeholder: 'Type something...',
|
||||
onSubmitted: (_) => _addItem(),
|
||||
),
|
||||
for (var i = 0; i < items.length; i++)
|
||||
CupertinoListTile(
|
||||
title: Text(items[i]),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Divider(
|
||||
color: CupertinoColors.opaqueSeparator.resolveFrom(context),
|
||||
height: 1,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CupertinoButton(
|
||||
foregroundColor: CupertinoColors.destructiveRed,
|
||||
child: Text('Close'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: CupertinoButton(
|
||||
child: Text('Add Item'),
|
||||
onPressed: () => _addItem(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _addItem() {
|
||||
setState(() {
|
||||
final text = textController.text.isEmpty
|
||||
? 'Item ${items.length + 1}'
|
||||
: textController.text;
|
||||
items.add(text);
|
||||
textController.clear();
|
||||
focusNode.requestFocus();
|
||||
});
|
||||
}
|
||||
}
|
||||
31
packages/stupid_simple_sheet/example/pubspec.yaml
Normal file
@@ -0,0 +1,31 @@
|
||||
name: stupid_simple_sheet_example
|
||||
description: "A new Flutter project."
|
||||
|
||||
# Prevent accidental publishing to pub.dev.
|
||||
publish_to: 'none'
|
||||
|
||||
version: 1.0.0+1
|
||||
resolution: workspace
|
||||
|
||||
environment:
|
||||
sdk: ^3.5.4
|
||||
flutter: ^3.27.0
|
||||
|
||||
dependencies:
|
||||
auto_route: ^10.0.1
|
||||
flutter:
|
||||
sdk: flutter
|
||||
liquid_glass_renderer:
|
||||
git:
|
||||
url: https://github.com/whynotmake-it/flutter_liquid_glass
|
||||
path: packages/liquid_glass_renderer
|
||||
ref: 2c5f88acefea99d4d7e22663946b623adc06bc3c
|
||||
stupid_simple_sheet:
|
||||
path: ../
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
48
packages/stupid_simple_sheet/lib/src/clamped_animation.dart
Normal file
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter/animation.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
@internal
|
||||
class ClampedAnimation extends Animation<double>
|
||||
with AnimationWithParentMixin<double> {
|
||||
ClampedAnimation(this.parent);
|
||||
|
||||
@override
|
||||
final Animation<double> parent;
|
||||
|
||||
@override
|
||||
double get value => parent.value.clamp(0.0, 1.0);
|
||||
}
|
||||
|
||||
@internal
|
||||
extension ClampedAnimationX on Animation<double> {
|
||||
Animation<double> get clamped => ClampedAnimation(this);
|
||||
}
|
||||
|
||||
@internal
|
||||
class RemappedAnimation extends Animation<double>
|
||||
with AnimationWithParentMixin<double> {
|
||||
RemappedAnimation(
|
||||
this.parent, {
|
||||
this.start = 0.0,
|
||||
this.end = 1.0,
|
||||
});
|
||||
|
||||
@override
|
||||
final Animation<double> parent;
|
||||
|
||||
final double start;
|
||||
|
||||
final double end;
|
||||
|
||||
@override
|
||||
double get value => Interval(start, end).transform(parent.value);
|
||||
}
|
||||
|
||||
@internal
|
||||
extension RemappedAnimationX on Animation<double> {
|
||||
Animation<double> remapped({
|
||||
double start = 0.0,
|
||||
double end = 1.0,
|
||||
}) =>
|
||||
RemappedAnimation(this, start: start, end: end);
|
||||
}
|
||||
330
packages/stupid_simple_sheet/lib/src/cupertino_sheet_copy.dart
Normal file
@@ -0,0 +1,330 @@
|
||||
/// A place for pasting source from cupertinos `sheets.dart`.
|
||||
/// We ignore lints here so that we can copy the code without modification.
|
||||
library;
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:stupid_simple_sheet/src/clamped_animation.dart';
|
||||
import 'package:stupid_simple_sheet/src/optimized_clip.dart';
|
||||
import 'package:stupid_simple_sheet/stupid_simple_sheet.dart';
|
||||
|
||||
// Smoothing factor applied to the device's top padding (which approximates the corner radius)
|
||||
// to achieve a smoother end to the corner radius animation. A value of 1.0 would use
|
||||
// the full top padding. Values less than 1.0 reduce the effective corner radius, improving
|
||||
// the animation's appearance. Determined through empirical testing.
|
||||
const double kDeviceCornerRadiusSmoothingFactor = 0.9;
|
||||
|
||||
// Threshold in logical pixels. If the calculated device corner radius (after applying
|
||||
// the smoothing factor) is below this value, the corner radius transition animation will
|
||||
// start from zero. This prevents abrupt transitions for devices with small or negligible
|
||||
// corner radii. This value, combined with the smoothing factor, corresponds roughly
|
||||
// to double the targeted radius of 12. Determined through testing and visual inspection.
|
||||
const double kRoundedDeviceCornersThreshold = 20.0;
|
||||
|
||||
// Amount the sheet in the background scales down. Found by measuring the width
|
||||
// of the sheet in the background and comparing against the screen width on the
|
||||
// iOS simulator showing an iPhone 16 pro running iOS 18.0. The scale transition
|
||||
// will go from a default of 1.0 to 1.0 - _kSheetScaleFactor.
|
||||
const double kSheetScaleFactor = 0.0835;
|
||||
|
||||
const kSheetPaddingToPrevious = 11.0;
|
||||
|
||||
const double kSheetSlideDownFactor = 0.03;
|
||||
|
||||
final Animatable<double> kScaleTween =
|
||||
Tween<double>(begin: 1.0, end: 1.0 - kSheetScaleFactor);
|
||||
|
||||
double getRelativeTopPadding(
|
||||
BuildContext context, {
|
||||
double extraPadding = 0,
|
||||
double minFraction = 0.05,
|
||||
}) {
|
||||
final safeArea = MediaQuery.paddingOf(context);
|
||||
final height = MediaQuery.sizeOf(context).height;
|
||||
|
||||
if (height == 0) {
|
||||
return minFraction;
|
||||
}
|
||||
// Ensure that the sheet moves down by at least 5% of the screen height if
|
||||
// the safe area is very small (e.g. no notch).
|
||||
return max((safeArea.top + extraPadding) / height, minFraction);
|
||||
}
|
||||
|
||||
Widget getOverlayedChild(
|
||||
BuildContext context,
|
||||
Widget? child,
|
||||
Animation<double> animation,
|
||||
bool secondLayer,
|
||||
) {
|
||||
final bool isDarkMode =
|
||||
CupertinoTheme.brightnessOf(context) == Brightness.dark;
|
||||
final overlayColor = isDarkMode && !secondLayer
|
||||
? const Color(0xFFc8c8c8)
|
||||
: const Color(0xFF000000);
|
||||
final opacity = animation.drive(Tween(
|
||||
begin: 0.0,
|
||||
end: secondLayer && isDarkMode ? 0.15 : 0.1,
|
||||
));
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: <Widget>[
|
||||
if (child != null) child,
|
||||
IgnorePointer(
|
||||
child: FadeTransition(
|
||||
opacity: opacity,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(color: overlayColor),
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
ShapeBorder getDeviceShape({
|
||||
required ShapeBorder sheetShape,
|
||||
required double deviceCornerRadius,
|
||||
}) {
|
||||
switch (sheetShape) {
|
||||
case RoundedSuperellipseBorder():
|
||||
return RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(deviceCornerRadius),
|
||||
),
|
||||
);
|
||||
default:
|
||||
return RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(deviceCornerRadius),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class CopiedCupertinoSheetTransitions {
|
||||
/// The primary delegated transition. Will slide a non [CupertinoSheetRoute] page down.
|
||||
///
|
||||
/// Provided to the previous route to coordinate transitions between routes.
|
||||
static Widget? secondarySlideDownTransition(
|
||||
BuildContext context, {
|
||||
required Animation<double> animation,
|
||||
required Animation<double> secondaryAnimation,
|
||||
required (double, double) opacityRange,
|
||||
required (double, double) slideBackRange,
|
||||
required ShapeBorder primaryShape,
|
||||
Widget? child,
|
||||
}) {
|
||||
final Animatable<Offset> topDownTween = Tween<Offset>(
|
||||
begin: Offset.zero,
|
||||
end: Offset(
|
||||
0,
|
||||
getRelativeTopPadding(context),
|
||||
),
|
||||
);
|
||||
|
||||
final double deviceCornerRadius =
|
||||
(MediaQuery.maybeViewPaddingOf(context)?.top ?? 0) *
|
||||
kDeviceCornerRadiusSmoothingFactor;
|
||||
final bool roundedDeviceCorners =
|
||||
deviceCornerRadius > kRoundedDeviceCornersThreshold;
|
||||
|
||||
final deviceShape = getDeviceShape(
|
||||
sheetShape: primaryShape,
|
||||
deviceCornerRadius: roundedDeviceCorners ? deviceCornerRadius : 0,
|
||||
);
|
||||
|
||||
final decorationTween = ShapeBorderTween(
|
||||
begin: deviceShape,
|
||||
end: primaryShape.scale(1 / 1.5),
|
||||
);
|
||||
|
||||
final shapeAnimation = secondaryAnimation
|
||||
.remapped(
|
||||
start: slideBackRange.$1,
|
||||
end: slideBackRange.$2,
|
||||
)
|
||||
.drive(decorationTween);
|
||||
|
||||
final Animation<Offset> slideAnimation = secondaryAnimation
|
||||
.remapped(
|
||||
start: slideBackRange.$1,
|
||||
end: slideBackRange.$2,
|
||||
)
|
||||
.drive(topDownTween);
|
||||
final Animation<double> scaleAnimation = secondaryAnimation
|
||||
.remapped(
|
||||
start: slideBackRange.$1,
|
||||
end: slideBackRange.$2,
|
||||
)
|
||||
.drive(kScaleTween);
|
||||
|
||||
final Widget? contrastedChild = getOverlayedChild(
|
||||
context,
|
||||
child,
|
||||
secondaryAnimation.remapped(
|
||||
start: opacityRange.$1,
|
||||
end: opacityRange.$2,
|
||||
),
|
||||
false,
|
||||
);
|
||||
|
||||
final double topGapHeight =
|
||||
MediaQuery.sizeOf(context).height * getRelativeTopPadding(context);
|
||||
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: const SystemUiOverlayStyle(
|
||||
statusBarBrightness: Brightness.dark,
|
||||
statusBarIconBrightness: Brightness.light,
|
||||
),
|
||||
child: SizedBox(height: topGapHeight, width: double.infinity),
|
||||
),
|
||||
SlideTransition(
|
||||
position: slideAnimation,
|
||||
child: ScaleTransition(
|
||||
scale: scaleAnimation,
|
||||
filterQuality: FilterQuality.medium,
|
||||
alignment: Alignment.topCenter,
|
||||
child: AnimatedBuilder(
|
||||
animation: shapeAnimation,
|
||||
child: contrastedChild,
|
||||
builder: (BuildContext context, Widget? child) {
|
||||
return OptimizedClip(
|
||||
shape: shapeAnimation.value,
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
static Widget secondarySlideUpTransition(
|
||||
BuildContext context, {
|
||||
required Animation<double> animation,
|
||||
required Animation<double> secondaryAnimation,
|
||||
required Animation<ShapeBorder?> shapeAnimation,
|
||||
required (double, double) opacityRange,
|
||||
required (double, double) slideBackRange,
|
||||
required Color backgroundColor,
|
||||
Widget? child,
|
||||
}) {
|
||||
return SlideTransition(
|
||||
position: secondaryAnimation
|
||||
.remapped(
|
||||
start: slideBackRange.$1,
|
||||
end: slideBackRange.$2,
|
||||
)
|
||||
.drive(
|
||||
Tween<Offset>(
|
||||
begin: Offset(0, 0),
|
||||
end: Offset(
|
||||
0,
|
||||
-kSheetPaddingToPrevious / MediaQuery.sizeOf(context).height,
|
||||
),
|
||||
),
|
||||
),
|
||||
transformHitTests: false,
|
||||
child: ScaleTransition(
|
||||
scale: secondaryAnimation
|
||||
.remapped(
|
||||
start: slideBackRange.$1,
|
||||
end: slideBackRange.$2,
|
||||
)
|
||||
.drive(kScaleTween),
|
||||
alignment: Alignment.topCenter,
|
||||
filterQuality: FilterQuality.medium,
|
||||
child: AnimatedBuilder(
|
||||
animation: shapeAnimation,
|
||||
builder: (context, child) => SheetBackground(
|
||||
shape: shapeAnimation.value!,
|
||||
backgroundColor: CupertinoDynamicColor.resolve(
|
||||
backgroundColor,
|
||||
context,
|
||||
),
|
||||
child: child!,
|
||||
),
|
||||
child: getOverlayedChild(
|
||||
context,
|
||||
child,
|
||||
secondaryAnimation.remapped(
|
||||
start: opacityRange.$1,
|
||||
end: opacityRange.$2,
|
||||
),
|
||||
true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Widget fullTransition(
|
||||
BuildContext context, {
|
||||
required Animation<double> animation,
|
||||
required Animation<double> secondaryAnimation,
|
||||
required (double, double) opacityRange,
|
||||
required (double, double) slideBackRange,
|
||||
required Color backgroundColor,
|
||||
ShapeBorder shape = const RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
Widget? child,
|
||||
}) {
|
||||
final offsetTween = Tween<Offset>(
|
||||
begin: Offset(0, 1),
|
||||
end: Offset(0, 0),
|
||||
);
|
||||
|
||||
final shapeTween = ShapeBorderTween(
|
||||
begin: shape,
|
||||
end: shape.scale(1 / 1.5),
|
||||
);
|
||||
|
||||
final shapeAnimation = secondaryAnimation
|
||||
.remapped(
|
||||
start: slideBackRange.$1,
|
||||
end: slideBackRange.$2,
|
||||
)
|
||||
.drive(shapeTween);
|
||||
|
||||
final Animation<Offset> positionAnimation = animation.drive(offsetTween);
|
||||
|
||||
return Builder(
|
||||
builder: (context) {
|
||||
return SafeArea(
|
||||
left: false,
|
||||
right: false,
|
||||
bottom: false,
|
||||
minimum:
|
||||
EdgeInsets.only(top: MediaQuery.sizeOf(context).height * 0.05),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: kSheetPaddingToPrevious),
|
||||
child: SlideTransition(
|
||||
position: positionAnimation,
|
||||
child: secondarySlideUpTransition(
|
||||
context,
|
||||
animation: animation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
shapeAnimation: shapeAnimation,
|
||||
opacityRange: opacityRange,
|
||||
slideBackRange: slideBackRange,
|
||||
backgroundColor: backgroundColor,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/// Sheet transitions for the iOS 26 liquid glass style.
|
||||
///
|
||||
/// Reuses shared constants and helpers from [cupertino_sheet_copy.dart].
|
||||
/// Only the methods that differ from [CopiedCupertinoSheetTransitions] are
|
||||
/// defined here.
|
||||
library;
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:stupid_simple_sheet/src/clamped_animation.dart';
|
||||
import 'package:stupid_simple_sheet/src/cupertino_sheet_copy.dart';
|
||||
import 'package:stupid_simple_sheet/stupid_simple_sheet.dart';
|
||||
|
||||
abstract class GlassSheetTransitions {
|
||||
/// Reuses the identical implementation from the iOS 18 transitions.
|
||||
static Widget? secondarySlideDownTransition(
|
||||
BuildContext context, {
|
||||
required Animation<double> animation,
|
||||
required Animation<double> secondaryAnimation,
|
||||
required (double, double) opacityRange,
|
||||
required (double, double) slideBackRange,
|
||||
required ShapeBorder primaryShape,
|
||||
Widget? child,
|
||||
}) {
|
||||
return CopiedCupertinoSheetTransitions.secondarySlideDownTransition(
|
||||
context,
|
||||
animation: animation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
opacityRange: opacityRange,
|
||||
slideBackRange: slideBackRange,
|
||||
primaryShape: primaryShape,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
static Widget secondarySlideUpTransition(
|
||||
BuildContext context, {
|
||||
required Animation<double> animation,
|
||||
required Animation<double> secondaryAnimation,
|
||||
required ShapeBorder shape,
|
||||
required (double, double) opacityRange,
|
||||
required (double, double) slideBackRange,
|
||||
required Color backgroundColor,
|
||||
required bool secondSheet,
|
||||
Widget? child,
|
||||
}) {
|
||||
final slideAnimation = secondSheet
|
||||
? secondaryAnimation
|
||||
.remapped(
|
||||
start: slideBackRange.$1,
|
||||
end: slideBackRange.$2,
|
||||
)
|
||||
.drive(
|
||||
Tween<Offset>(
|
||||
begin: Offset(0, 0),
|
||||
end: Offset(
|
||||
0,
|
||||
-kSheetPaddingToPrevious / MediaQuery.sizeOf(context).height,
|
||||
),
|
||||
),
|
||||
)
|
||||
: secondaryAnimation
|
||||
.remapped(
|
||||
start: slideBackRange.$1,
|
||||
end: slideBackRange.$2,
|
||||
)
|
||||
.drive(
|
||||
Tween<Offset>(
|
||||
begin: Offset(0, 0),
|
||||
end: Offset(
|
||||
0,
|
||||
kSheetSlideDownFactor,
|
||||
),
|
||||
),
|
||||
);
|
||||
return SlideTransition(
|
||||
position: slideAnimation,
|
||||
transformHitTests: false,
|
||||
child: ScaleTransition(
|
||||
scale: secondaryAnimation
|
||||
.remapped(
|
||||
start: slideBackRange.$1,
|
||||
end: slideBackRange.$2,
|
||||
)
|
||||
.drive(kScaleTween),
|
||||
alignment: Alignment.topCenter,
|
||||
filterQuality: FilterQuality.medium,
|
||||
child: SheetBackground(
|
||||
shape: shape,
|
||||
backgroundColor: CupertinoDynamicColor.resolve(
|
||||
backgroundColor,
|
||||
context,
|
||||
),
|
||||
child: getOverlayedChild(
|
||||
context,
|
||||
child,
|
||||
secondaryAnimation.remapped(
|
||||
start: opacityRange.$1,
|
||||
end: opacityRange.$2,
|
||||
),
|
||||
true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Widget fullTransition(
|
||||
BuildContext context, {
|
||||
required Animation<double> animation,
|
||||
required Animation<double> secondaryAnimation,
|
||||
required (double, double) opacityRange,
|
||||
required (double, double) slideBackRange,
|
||||
required Color backgroundColor,
|
||||
required bool secondSheet,
|
||||
ShapeBorder shape = const RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(36),
|
||||
),
|
||||
),
|
||||
Widget? child,
|
||||
}) {
|
||||
final offsetTween = Tween<Offset>(
|
||||
begin: Offset(0, 1),
|
||||
end: Offset(0, 0),
|
||||
);
|
||||
|
||||
final Animation<Offset> positionAnimation = animation.drive(offsetTween);
|
||||
|
||||
return Builder(
|
||||
builder: (context) {
|
||||
return SafeArea(
|
||||
left: false,
|
||||
right: false,
|
||||
bottom: false,
|
||||
minimum:
|
||||
EdgeInsets.only(top: MediaQuery.sizeOf(context).height * 0.05),
|
||||
child: Padding(
|
||||
padding:
|
||||
EdgeInsets.only(top: secondSheet ? kSheetPaddingToPrevious : 0),
|
||||
child: SlideTransition(
|
||||
position: positionAnimation,
|
||||
child: secondarySlideUpTransition(
|
||||
context,
|
||||
animation: animation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
shape: shape,
|
||||
opacityRange: opacityRange,
|
||||
slideBackRange: slideBackRange,
|
||||
backgroundColor: backgroundColor,
|
||||
secondSheet: secondSheet,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
57
packages/stupid_simple_sheet/lib/src/optimized_clip.dart
Normal file
@@ -0,0 +1,57 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// Clips its child using the given [shape].
|
||||
///
|
||||
/// Compared to using [ClipPath] directly, this widget optimizes for
|
||||
/// performance by using more efficient clipping methods when possible.
|
||||
///
|
||||
/// If [shape] is null, no clipping is applied.
|
||||
@internal
|
||||
class OptimizedClip extends StatelessWidget {
|
||||
const OptimizedClip({
|
||||
required this.shape,
|
||||
required this.child,
|
||||
this.clipBehavior = Clip.antiAlias,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final ShapeBorder? shape;
|
||||
|
||||
final Clip clipBehavior;
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final shape = this.shape;
|
||||
return switch (shape) {
|
||||
null => child,
|
||||
RoundedSuperellipseBorder(:final borderRadius) => ClipRSuperellipse(
|
||||
clipBehavior: clipBehavior,
|
||||
borderRadius: borderRadius,
|
||||
child: child,
|
||||
),
|
||||
RoundedRectangleBorder(:final borderRadius) => ClipRRect(
|
||||
clipBehavior: clipBehavior,
|
||||
borderRadius: borderRadius,
|
||||
child: child,
|
||||
),
|
||||
OvalBorder() => ClipOval(
|
||||
clipBehavior: clipBehavior,
|
||||
child: child,
|
||||
),
|
||||
LinearBorder() => ClipRect(
|
||||
clipBehavior: clipBehavior,
|
||||
child: child,
|
||||
),
|
||||
_ => ClipPath(
|
||||
clipBehavior: clipBehavior,
|
||||
clipper: ShapeBorderClipper(
|
||||
shape: shape,
|
||||
),
|
||||
child: child,
|
||||
)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
/// Controls when the route behind the sheet is rasterized to a GPU texture
|
||||
/// instead of being painted live.
|
||||
///
|
||||
/// Snapshotting replaces the background route's widget tree with a frozen
|
||||
/// image, eliminating the cost of rebuilding and painting complex widgets
|
||||
/// during sheet transitions and while the sheet is open.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SnapshotWidget], the Flutter widget that performs the rasterization.
|
||||
enum RouteSnapshotMode {
|
||||
/// Never snapshot. The background route is always painted live.
|
||||
///
|
||||
/// Use this when the content behind the sheet contains animations, video, or
|
||||
/// other dynamic content that must remain live at all times.
|
||||
never,
|
||||
|
||||
/// Snapshot only while the sheet animation is playing (opening, closing,
|
||||
/// or being dragged). Reverts to the live widget tree when the animation
|
||||
/// settles.
|
||||
///
|
||||
/// Good for cases where transition performance matters but background content
|
||||
/// must stay live when the sheet is idle.
|
||||
animating,
|
||||
|
||||
/// Snapshot while the sheet is open and settled. Reverts to live during
|
||||
/// all animations (opening, closing, and drag gestures).
|
||||
///
|
||||
/// Useful when the transition involves visual effects that don't rasterize
|
||||
/// well, but idle performance matters.
|
||||
settled,
|
||||
|
||||
/// Snapshot while the sheet animates toward its fully-open (max extent)
|
||||
/// position, and while it is settled there. Reverts to the live widget tree
|
||||
/// during dismiss gestures, closing animations, and animations toward
|
||||
/// intermediate snap points.
|
||||
///
|
||||
/// "Forward" only counts when the animation target is the maximum snapping
|
||||
/// point. Animations to intermediate snap points are not snapshotted.
|
||||
///
|
||||
/// Best default for most sheets: maximum performance during the opening
|
||||
/// transition and while idle, with real content visible as the user drags to
|
||||
/// dismiss.
|
||||
openAndForward,
|
||||
|
||||
/// Always snapshot while the sheet is present.
|
||||
///
|
||||
/// Maximum performance. The background is completely frozen for the lifetime
|
||||
/// of the sheet. Best for static background content.
|
||||
always,
|
||||
}
|
||||
72
packages/stupid_simple_sheet/lib/src/sheet_background.dart
Normal file
@@ -0,0 +1,72 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:motor/motor.dart';
|
||||
import 'package:stupid_simple_sheet/src/optimized_clip.dart';
|
||||
|
||||
/// A widget that provides a background for a sheet, including shape and color.
|
||||
///
|
||||
/// Will also extend the background color below the sheet to account for
|
||||
/// dragging the sheet further than its content height.
|
||||
class SheetBackground extends StatelessWidget {
|
||||
/// Creates a [SheetBackground].
|
||||
const SheetBackground({
|
||||
required this.child,
|
||||
super.key,
|
||||
this.backgroundColor,
|
||||
this.shape = const RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||
),
|
||||
this.clipBehavior = Clip.antiAlias,
|
||||
this.extensionAtBottom,
|
||||
});
|
||||
|
||||
/// The shape that the sheet should have.
|
||||
///
|
||||
/// The child will be clipped to fit that shape, if [clipBehavior] is not
|
||||
/// [Clip.none].
|
||||
/// Defaults to a rounded superellipse with 24px radius at the top.
|
||||
final ShapeBorder shape;
|
||||
|
||||
/// The background color of the sheet.
|
||||
///
|
||||
/// If null, the default background color from the current [Theme]s
|
||||
/// surface color is used.
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// The [Clip] behavior to use for the sheet's content.
|
||||
///
|
||||
/// Defaults to [Clip.antiAlias].
|
||||
/// If you set this to [Clip.none], the sheet's content will not be clipped.
|
||||
final Clip clipBehavior;
|
||||
|
||||
/// How much to extend the sheet background below the sheet itself.
|
||||
////
|
||||
/// This is useful to cover up any content below the sheet when the user
|
||||
/// drags the sheet up further than its content height.
|
||||
///
|
||||
/// If null (default), it will extend by the full height of the screen.
|
||||
final double? extensionAtBottom;
|
||||
|
||||
/// The content of the sheet.
|
||||
final Widget? child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bottomExtension =
|
||||
extensionAtBottom ?? MediaQuery.sizeOf(context).height;
|
||||
final color = backgroundColor ?? Theme.of(context).colorScheme.surface;
|
||||
return PaddingExtended(
|
||||
padding: EdgeInsets.only(bottom: -bottomExtension),
|
||||
child: DecoratedBox(
|
||||
decoration: ShapeDecoration(shape: shape, color: color),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: bottomExtension),
|
||||
child: OptimizedClip(
|
||||
clipBehavior: clipBehavior,
|
||||
shape: shape,
|
||||
child: child ?? const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
150
packages/stupid_simple_sheet/lib/src/snapping_point.dart
Normal file
@@ -0,0 +1,150 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// Configuration for sheet snapping behavior.
|
||||
///
|
||||
/// Values are normalized between 0.0 and 1.0, where 0.0 is closed and 1.0
|
||||
/// is fully open. The 0.0 point is always implicit and doesn't need to be
|
||||
/// specified.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// SheetSnappingConfig([0.5, 1.0]) // Half and full height
|
||||
/// ```
|
||||
@immutable
|
||||
class SheetSnappingConfig {
|
||||
/// Creates a snapping configuration.
|
||||
///
|
||||
/// Values are normalized between 0.0 and 1.0, where 0.0 is closed and 1.0
|
||||
/// is fully open. The 0.0 point is always implicit and doesn't need to be
|
||||
/// specified.
|
||||
const SheetSnappingConfig(this.points, {double? initialSnap})
|
||||
: _initialSnap = initialSnap;
|
||||
|
||||
/// A snapping configuration that only includes the fully open state (1.0).
|
||||
static const SheetSnappingConfig full = SheetSnappingConfig([1.0]);
|
||||
|
||||
/// The raw snapping points as provided to the constructor.
|
||||
final List<double> points;
|
||||
|
||||
final double? _initialSnap;
|
||||
|
||||
/// Gets all snapping points including the implicit 0, resolved as relative
|
||||
/// values (0.0-1.0) for the given sheet height.
|
||||
List<double> getAllPoints() {
|
||||
final resolved = <double>{
|
||||
0.0, // Always include the implicit 0 point
|
||||
...points,
|
||||
};
|
||||
|
||||
return resolved.toList()..sort();
|
||||
}
|
||||
|
||||
/// Gets the closest snapping point to the given relative position and
|
||||
/// velocity.
|
||||
double findTargetSnapPoint(
|
||||
double currentRelativePosition,
|
||||
double velocity, {
|
||||
bool includeClosed = true,
|
||||
}) {
|
||||
final allPoints = getAllPoints()
|
||||
.where((p) => includeClosed || p > 0) // Exclude 0 if needed
|
||||
.toList();
|
||||
|
||||
// For high velocity, predict where the sheet would naturally settle
|
||||
const velocityThreshold = 0.5;
|
||||
|
||||
if (velocity.abs() > velocityThreshold) {
|
||||
// High velocity - predict the natural settling position
|
||||
final projectedPosition = currentRelativePosition - (velocity * 0.3);
|
||||
|
||||
return _findClosestPoint(allPoints, projectedPosition);
|
||||
} else {
|
||||
// Low velocity - snap to the closest point based on current position
|
||||
return _findClosestPoint(allPoints, currentRelativePosition);
|
||||
}
|
||||
}
|
||||
|
||||
double _findClosestPoint(List<double> points, double target) {
|
||||
var minDistance = double.infinity;
|
||||
var closest = points.first;
|
||||
|
||||
for (final point in points) {
|
||||
final distance = (target - point).abs();
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
closest = point;
|
||||
}
|
||||
}
|
||||
|
||||
return closest;
|
||||
}
|
||||
|
||||
/// Gets the initial snap point as a relative value.
|
||||
///
|
||||
/// If [initialSnap] is set, returns that point resolved to
|
||||
/// relative.
|
||||
/// Otherwise, returns the lowest non-zero snap point, or 1.0 as fallback.
|
||||
double get initialSnap {
|
||||
if (_initialSnap case final p?) {
|
||||
return p.clamp(0.0, 1.0);
|
||||
}
|
||||
|
||||
final relativePoints = getAllPoints()
|
||||
.where((value) => value > 0.001) // Exclude values effectively zero
|
||||
.toList();
|
||||
|
||||
return relativePoints.isNotEmpty ? relativePoints.first : 1.0;
|
||||
}
|
||||
|
||||
/// Gets the top two snap points for transition calculations.
|
||||
(double, double) get topTwoPoints {
|
||||
final allPoints = getAllPoints();
|
||||
|
||||
final lastPoint = allPoints.isNotEmpty ? allPoints.last : 1.0;
|
||||
final secondLastPoint =
|
||||
allPoints.length > 1 ? allPoints[allPoints.length - 2] : 0.0;
|
||||
|
||||
return (secondLastPoint, lastPoint);
|
||||
}
|
||||
|
||||
/// Gets the bottom two snap points for opacity range calculations.
|
||||
(double, double) get bottomTwoPoints {
|
||||
final allPoints = getAllPoints();
|
||||
|
||||
final firstPoint = allPoints.isNotEmpty ? allPoints.first : 0.0;
|
||||
final secondPoint = allPoints.length > 1 ? allPoints[1] : 1.0;
|
||||
|
||||
return (firstPoint, secondPoint);
|
||||
}
|
||||
|
||||
/// Gets the maximum extent as a relative value.
|
||||
double get maxExtent {
|
||||
final allPoints = getAllPoints();
|
||||
return allPoints.isNotEmpty ? allPoints.last : 1.0;
|
||||
}
|
||||
|
||||
/// Gets the minimum extent as a relative value.
|
||||
double get minExtent {
|
||||
final allPoints = getAllPoints();
|
||||
return allPoints.isNotEmpty ? allPoints.first : 0.0;
|
||||
}
|
||||
|
||||
/// Whether this configuration has any in-between snap points
|
||||
/// (not just 0 and 1).
|
||||
bool get hasInbetweenSnaps {
|
||||
return points.any((p) => p < 1.0 && p > 0.0);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is SheetSnappingConfig &&
|
||||
runtimeType == other.runtimeType &&
|
||||
listEquals(points, other.points);
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hashAll([runtimeType, ...points.toList()..sort()]);
|
||||
|
||||
@override
|
||||
String toString() => 'SheetSnappingConfig($points)';
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
// ignore_for_file: avoid_positional_boolean_parameters
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:stupid_simple_sheet/src/clamped_animation.dart';
|
||||
import 'package:stupid_simple_sheet/src/cupertino_sheet_copy.dart';
|
||||
import 'package:stupid_simple_sheet/stupid_simple_sheet.dart';
|
||||
|
||||
/// Simular to [CupertinoSheetRoute] but with the drag gesture improvements from
|
||||
/// this package.
|
||||
class StupidSimpleCupertinoSheetRoute<T> extends PopupRoute<T>
|
||||
with StupidSimpleSheetTransitionMixin<T>, StupidSimpleSheetController<T> {
|
||||
/// Creates a sheet route for displaying modal content.
|
||||
///
|
||||
/// The [motion] and [child] arguments must not be null.
|
||||
StupidSimpleCupertinoSheetRoute({
|
||||
required this.child,
|
||||
super.settings,
|
||||
this.motion = const CupertinoMotion.smooth(
|
||||
duration: Duration(milliseconds: 350),
|
||||
snapToEnd: true,
|
||||
),
|
||||
this.clearBarrierImmediately = true,
|
||||
this.backgroundColor = CupertinoColors.systemBackground,
|
||||
this.callNavigatorUserGestureMethods = false,
|
||||
this.snappingConfig = SheetSnappingConfig.full,
|
||||
this.draggable = true,
|
||||
this.originateAboveBottomViewInset = false,
|
||||
this.backgroundSnapshotMode = RouteSnapshotMode.never,
|
||||
this.shape = iOS18Shape,
|
||||
});
|
||||
|
||||
/// The default iOS 18 shape for sheet controllers.
|
||||
static const iOS18Shape = RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(12),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
double get overshootResistance => 5000;
|
||||
|
||||
@override
|
||||
final Motion motion;
|
||||
|
||||
@override
|
||||
final bool clearBarrierImmediately;
|
||||
|
||||
/// The background color of the sheet.
|
||||
///
|
||||
/// Will default [CupertinoColors.secondarySystemBackground] if not provided.
|
||||
final Color backgroundColor;
|
||||
|
||||
/// The widget to display in the sheet.
|
||||
final Widget child;
|
||||
|
||||
/// The shape of the sheet.
|
||||
///
|
||||
/// Defaults to [iOS18Shape].
|
||||
final ShapeBorder shape;
|
||||
|
||||
@override
|
||||
Color? get barrierColor => CupertinoColors.transparent;
|
||||
|
||||
@override
|
||||
bool get barrierDismissible => effectiveSnappingConfig.hasInbetweenSnaps;
|
||||
|
||||
@override
|
||||
String? get barrierLabel => null;
|
||||
|
||||
@override
|
||||
bool get maintainState => true;
|
||||
|
||||
@override
|
||||
bool get opaque => false;
|
||||
|
||||
@override
|
||||
final bool callNavigatorUserGestureMethods;
|
||||
|
||||
@override
|
||||
final SheetSnappingConfig snappingConfig;
|
||||
|
||||
@override
|
||||
final bool draggable;
|
||||
|
||||
@override
|
||||
final bool originateAboveBottomViewInset;
|
||||
|
||||
@override
|
||||
final RouteSnapshotMode backgroundSnapshotMode;
|
||||
|
||||
StupidSimpleSheetTransitionMixin<dynamic>? _nextSheet;
|
||||
|
||||
@override
|
||||
DelegatedTransitionBuilder? get delegatedTransition =>
|
||||
(context, animation, secondaryAnimation, canSnapshot, child) {
|
||||
final snappingConfigForTransition =
|
||||
_nextSheet?.effectiveSnappingConfig ?? effectiveSnappingConfig;
|
||||
|
||||
return CopiedCupertinoSheetTransitions.secondarySlideDownTransition(
|
||||
context,
|
||||
animation: animation.clamped,
|
||||
secondaryAnimation: secondaryAnimation.clamped,
|
||||
slideBackRange: snappingConfigForTransition.topTwoPoints,
|
||||
opacityRange: snappingConfigForTransition.bottomTwoPoints,
|
||||
primaryShape: shape,
|
||||
child: SnapshotWidget(
|
||||
controller: backgroundSnapshotController,
|
||||
mode: SnapshotMode.permissive,
|
||||
autoresize: true,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
@override
|
||||
Widget buildContent(BuildContext context) {
|
||||
return MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildTransitions(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
Widget child,
|
||||
) {
|
||||
return CupertinoUserInterfaceLevel(
|
||||
data: CupertinoUserInterfaceLevelData.elevated,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
return CopiedCupertinoSheetTransitions.fullTransition(
|
||||
context,
|
||||
animation: controller!.view,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
slideBackRange: effectiveSnappingConfig.topTwoPoints,
|
||||
opacityRange: effectiveSnappingConfig.bottomTwoPoints,
|
||||
backgroundColor: backgroundColor,
|
||||
shape: shape,
|
||||
child: maybeSnapshotChild(child),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool canTransitionTo(TransitionRoute<dynamic> nextRoute) {
|
||||
return nextRoute is StupidSimpleCupertinoSheetRoute ||
|
||||
super.canTransitionTo(nextRoute);
|
||||
}
|
||||
|
||||
@override
|
||||
@mustCallSuper
|
||||
void didChangeNext(Route<dynamic>? nextRoute) {
|
||||
super.didChangeNext(nextRoute);
|
||||
|
||||
if (nextRoute is StupidSimpleSheetTransitionMixin) {
|
||||
_nextSheet = nextRoute;
|
||||
} else {
|
||||
_nextSheet = null;
|
||||
}
|
||||
|
||||
// Force the internal secondary transition instead of the delegated one,
|
||||
// so this sheet's own scale-down/slide-up animation plays correctly.
|
||||
if (nextRoute is StupidSimpleCupertinoSheetRoute) {
|
||||
// ignore: invalid_use_of_visible_for_testing_member
|
||||
receivedTransition = null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@mustCallSuper
|
||||
void didPopNext(Route<dynamic> nextRoute) {
|
||||
super.didPopNext(nextRoute);
|
||||
|
||||
if (nextRoute is StupidSimpleCupertinoSheetRoute) {
|
||||
// ignore: invalid_use_of_visible_for_testing_member
|
||||
receivedTransition = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
// ignore_for_file: avoid_positional_boolean_parameters
|
||||
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:stupid_simple_sheet/src/glass_sheet_transitions.dart';
|
||||
import 'package:stupid_simple_sheet/stupid_simple_sheet.dart';
|
||||
import 'package:xianyan/core/utils/sheet_animation_notifier.dart';
|
||||
|
||||
/// A sheet route styled after the iOS 26 liquid glass aesthetic.
|
||||
///
|
||||
/// The first sheet of this kind that is pushed will blur the backdrop (if
|
||||
/// [blurBehindBarrier] is true) and apply [barrierColor] to the route behind.
|
||||
///
|
||||
/// Any subsequent sheet of this kind will not apply a barrier color or blur,
|
||||
/// since the previous sheet will transition using its own internal secondary
|
||||
/// transition.
|
||||
///
|
||||
/// Similar to [StupidSimpleCupertinoSheetRoute] but uses the newer glass-style
|
||||
/// transitions with a larger default corner radius and no delegated transition
|
||||
/// to the previous route.
|
||||
class StupidSimpleGlassSheetRoute<T> extends PopupRoute<T>
|
||||
with StupidSimpleSheetTransitionMixin<T>, StupidSimpleSheetController<T> {
|
||||
/// Creates a glass-style sheet route for displaying modal content.
|
||||
///
|
||||
/// The [child] argument must not be null.
|
||||
StupidSimpleGlassSheetRoute({
|
||||
required this.child,
|
||||
super.settings,
|
||||
this.motion = const CupertinoMotion.smooth(
|
||||
duration: Duration(milliseconds: 350),
|
||||
snapToEnd: true,
|
||||
),
|
||||
this.clearBarrierImmediately = true,
|
||||
this.backgroundColor = CupertinoColors.systemBackground,
|
||||
this.callNavigatorUserGestureMethods = false,
|
||||
this.snappingConfig = SheetSnappingConfig.full,
|
||||
this.draggable = true,
|
||||
this.originateAboveBottomViewInset = false,
|
||||
this.backgroundSnapshotMode = RouteSnapshotMode.never,
|
||||
this.shape = glassShape,
|
||||
this.blurBehindBarrier = true,
|
||||
|
||||
/// The color applied to the route behind the first glass sheet.
|
||||
///
|
||||
/// This barrier color is only used for the first pushed glass sheet; any
|
||||
/// subsequent sheets rely on the previous sheet's internal transition and
|
||||
/// do not apply an additional barrier.
|
||||
///
|
||||
/// Defaults to black with 15% opacity.
|
||||
Color barrierColor =
|
||||
const Color.from(alpha: .15, red: 0, green: 0, blue: 0),
|
||||
}) : _barrierColor = barrierColor;
|
||||
|
||||
/// The default glass shape with a 36px superellipse corner radius.
|
||||
static const glassShape = RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(36),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
double get overshootResistance => 5000;
|
||||
|
||||
@override
|
||||
final Motion motion;
|
||||
|
||||
@override
|
||||
final bool clearBarrierImmediately;
|
||||
|
||||
/// The background color of the sheet.
|
||||
///
|
||||
/// Defaults to [CupertinoColors.systemBackground].
|
||||
final Color backgroundColor;
|
||||
|
||||
/// The widget to display in the sheet.
|
||||
final Widget child;
|
||||
|
||||
/// The shape of the sheet.
|
||||
///
|
||||
/// Defaults to [glassShape].
|
||||
final ShapeBorder shape;
|
||||
|
||||
/// Whether the first sheet will blur the backdrop when it appears.
|
||||
///
|
||||
/// Defaults to true.
|
||||
final bool blurBehindBarrier;
|
||||
|
||||
final Color _barrierColor;
|
||||
|
||||
@override
|
||||
Color? get barrierColor => _isSecondGlassSheet ? null : _barrierColor;
|
||||
|
||||
@override
|
||||
bool get barrierDismissible => effectiveSnappingConfig.hasInbetweenSnaps;
|
||||
|
||||
@override
|
||||
String? get barrierLabel => null;
|
||||
|
||||
@override
|
||||
bool get maintainState => true;
|
||||
|
||||
@override
|
||||
bool get opaque => false;
|
||||
|
||||
@override
|
||||
final bool callNavigatorUserGestureMethods;
|
||||
|
||||
@override
|
||||
final SheetSnappingConfig snappingConfig;
|
||||
|
||||
@override
|
||||
final bool draggable;
|
||||
|
||||
@override
|
||||
final bool originateAboveBottomViewInset;
|
||||
|
||||
@override
|
||||
final RouteSnapshotMode backgroundSnapshotMode;
|
||||
|
||||
@override
|
||||
DelegatedTransitionBuilder? get delegatedTransition =>
|
||||
backgroundSnapshotMode == RouteSnapshotMode.never
|
||||
? null
|
||||
: (context, animation, secondaryAnimation, canSnapshot, child) {
|
||||
return SnapshotWidget(
|
||||
controller: backgroundSnapshotController,
|
||||
mode: SnapshotMode.permissive,
|
||||
autoresize: true,
|
||||
child: child,
|
||||
);
|
||||
};
|
||||
|
||||
bool _isSecondGlassSheet = false;
|
||||
|
||||
StupidSimpleSheetTransitionMixin<dynamic>? _nextSheet;
|
||||
|
||||
@override
|
||||
Widget buildModalBarrier() {
|
||||
return Stack(
|
||||
children: [
|
||||
super.buildModalBarrier(),
|
||||
if (blurBehindBarrier && !_isSecondGlassSheet)
|
||||
Positioned.fill(
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: animation ?? kAlwaysDismissedAnimation,
|
||||
builder: (context, value, child) {
|
||||
final progress = (value as num).toDouble();
|
||||
final sigma = progress * 10;
|
||||
return BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: sigma, sigmaY: sigma),
|
||||
child: const SizedBox.expand(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get onlyDragWhenScrollWasAtTop => false;
|
||||
|
||||
@override
|
||||
Widget buildContent(BuildContext context) {
|
||||
return MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildTransitions(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
Widget child,
|
||||
) {
|
||||
return CupertinoUserInterfaceLevel(
|
||||
data: CupertinoUserInterfaceLevelData.elevated,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final snappingConfigForTransition =
|
||||
_nextSheet?.effectiveSnappingConfig ?? effectiveSnappingConfig;
|
||||
return GlassSheetTransitions.fullTransition(
|
||||
context,
|
||||
animation: controller!.view,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
slideBackRange: snappingConfigForTransition.topTwoPoints,
|
||||
opacityRange: snappingConfigForTransition.bottomTwoPoints,
|
||||
backgroundColor: backgroundColor,
|
||||
secondSheet: _isSecondGlassSheet,
|
||||
shape: shape,
|
||||
child: maybeSnapshotChild(child),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool canTransitionTo(TransitionRoute<dynamic> nextRoute) {
|
||||
return nextRoute is StupidSimpleGlassSheetRoute ||
|
||||
super.canTransitionTo(nextRoute);
|
||||
}
|
||||
|
||||
@override
|
||||
void install() {
|
||||
super.install();
|
||||
animation?.addListener(_onAnimationChanged);
|
||||
}
|
||||
|
||||
void _onAnimationChanged() {
|
||||
final value = animation?.value ?? 0.0;
|
||||
SheetAnimationNotifier.instance.updateLayer(this, value);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangePrevious(Route<dynamic>? previousRoute) {
|
||||
_isSecondGlassSheet = previousRoute is StupidSimpleGlassSheetRoute;
|
||||
super.didChangePrevious(previousRoute);
|
||||
}
|
||||
|
||||
@override
|
||||
@mustCallSuper
|
||||
void didChangeNext(Route<dynamic>? nextRoute) {
|
||||
super.didChangeNext(nextRoute);
|
||||
|
||||
if (nextRoute is StupidSimpleSheetTransitionMixin) {
|
||||
_nextSheet = nextRoute;
|
||||
} else {
|
||||
_nextSheet = null;
|
||||
}
|
||||
|
||||
if (nextRoute is StupidSimpleGlassSheetRoute) {
|
||||
// Force the internal secondary transition instead of the delegated one,
|
||||
// so this sheet's own scale-down/slide-up animation plays correctly.
|
||||
// ignore: invalid_use_of_visible_for_testing_member
|
||||
receivedTransition = null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@mustCallSuper
|
||||
void didPopNext(Route<dynamic> nextRoute) {
|
||||
super.didPopNext(nextRoute);
|
||||
if (nextRoute is StupidSimpleGlassSheetRoute) {
|
||||
// ignore: invalid_use_of_visible_for_testing_member
|
||||
receivedTransition = null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool didPop(T? result) {
|
||||
animation?.addStatusListener(_onAnimationStatusChanged);
|
||||
return super.didPop(result);
|
||||
}
|
||||
|
||||
void _onAnimationStatusChanged(AnimationStatus status) {
|
||||
if (status == AnimationStatus.dismissed) {
|
||||
SheetAnimationNotifier.instance.updateLayer(this, 0.0);
|
||||
SheetAnimationNotifier.instance.removeLayer(this);
|
||||
animation?.removeStatusListener(_onAnimationStatusChanged);
|
||||
animation?.removeListener(_onAnimationChanged);
|
||||
}
|
||||
}
|
||||
}
|
||||
742
packages/stupid_simple_sheet/lib/stupid_simple_sheet.dart
Normal file
@@ -0,0 +1,742 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:motor/motor.dart';
|
||||
import 'package:scroll_drag_detector/scroll_drag_detector.dart';
|
||||
import 'package:stupid_simple_sheet/src/clamped_animation.dart';
|
||||
import 'package:stupid_simple_sheet/src/route_snapshot_mode.dart';
|
||||
import 'package:stupid_simple_sheet/src/snapping_point.dart';
|
||||
|
||||
export 'package:motor/src/motion.dart';
|
||||
|
||||
export 'src/route_snapshot_mode.dart';
|
||||
export 'src/sheet_background.dart';
|
||||
export 'src/snapping_point.dart';
|
||||
export 'src/stupid_simple_cupertino_sheet.dart';
|
||||
export 'src/stupid_simple_glass_sheet.dart';
|
||||
|
||||
/// A modal route that displays a sheet that slides up from the bottom.
|
||||
///
|
||||
/// The sheet can be dismissed by dragging down or by tapping the barrier.
|
||||
/// The animation supports spring physics for natural motion using the
|
||||
///
|
||||
///
|
||||
/// By default, when a modal route is replaced by another, the previous route
|
||||
/// remains in memory. To free all the resources when this is not necessary, set
|
||||
/// [maintainState] to false.
|
||||
///
|
||||
/// The type `T` specifies the return type of the route which can be supplied as
|
||||
/// the route is popped from the stack via [Navigator.pop] when an optional
|
||||
/// `result` can be provided.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [StupidSimpleSheetTransitionMixin], for a mixin that provides sheet
|
||||
/// transition
|
||||
/// behavior for this modal route.
|
||||
/// * [CupertinoModalPopupRoute], for a similar iOS-style modal popup.
|
||||
class StupidSimpleSheetRoute<T> extends PopupRoute<T>
|
||||
with StupidSimpleSheetTransitionMixin<T>, StupidSimpleSheetController<T> {
|
||||
/// Creates a sheet route for displaying modal content.
|
||||
///
|
||||
/// The [motion] and [child] arguments must not be null.
|
||||
StupidSimpleSheetRoute({
|
||||
required this.child,
|
||||
super.settings,
|
||||
this.motion = const CupertinoMotion.smooth(snapToEnd: true),
|
||||
this.barrierColor = const Color.fromRGBO(0, 0, 0, 0.2),
|
||||
this.barrierDismissible = true,
|
||||
this.barrierLabel,
|
||||
this.clearBarrierImmediately = true,
|
||||
this.onlyDragWhenScrollWasAtTop = true,
|
||||
this.callNavigatorUserGestureMethods = false,
|
||||
this.snappingConfig = SheetSnappingConfig.full,
|
||||
this.draggable = true,
|
||||
this.originateAboveBottomViewInset = false,
|
||||
this.backgroundSnapshotMode = RouteSnapshotMode.never,
|
||||
});
|
||||
|
||||
@override
|
||||
final Motion motion;
|
||||
|
||||
/// The widget to display in the sheet.
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
final Color? barrierColor;
|
||||
|
||||
@override
|
||||
final bool barrierDismissible;
|
||||
|
||||
@override
|
||||
final String? barrierLabel;
|
||||
|
||||
@override
|
||||
final bool clearBarrierImmediately;
|
||||
|
||||
@override
|
||||
final bool onlyDragWhenScrollWasAtTop;
|
||||
|
||||
@override
|
||||
final bool callNavigatorUserGestureMethods;
|
||||
|
||||
/// The base snapping configuration for the sheet.
|
||||
@override
|
||||
final SheetSnappingConfig snappingConfig;
|
||||
|
||||
@override
|
||||
final bool draggable;
|
||||
|
||||
@override
|
||||
final bool originateAboveBottomViewInset;
|
||||
|
||||
@override
|
||||
final RouteSnapshotMode backgroundSnapshotMode;
|
||||
|
||||
@override
|
||||
DelegatedTransitionBuilder? get delegatedTransition =>
|
||||
backgroundSnapshotMode == RouteSnapshotMode.never
|
||||
? null
|
||||
: (context, animation, secondaryAnimation, canSnapshot, child) {
|
||||
return SnapshotWidget(
|
||||
controller: backgroundSnapshotController,
|
||||
mode: SnapshotMode.permissive,
|
||||
autoresize: true,
|
||||
child: child,
|
||||
);
|
||||
};
|
||||
|
||||
@override
|
||||
Widget buildContent(BuildContext context) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
class _RelativeGestureDetector extends StatelessWidget {
|
||||
const _RelativeGestureDetector({
|
||||
required this.scrollableCanMoveBack,
|
||||
required this.onlyDragWhenScrollWasAtTop,
|
||||
required this.onRelativeDragStart,
|
||||
required this.onRelativeDragUpdate,
|
||||
required this.onRelativeDragEnd,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
final bool scrollableCanMoveBack;
|
||||
final bool onlyDragWhenScrollWasAtTop;
|
||||
final VoidCallback onRelativeDragStart;
|
||||
// ignore: avoid_positional_boolean_parameters
|
||||
final void Function(double, bool) onRelativeDragUpdate;
|
||||
// ignore: avoid_positional_boolean_parameters
|
||||
final void Function(double, bool) onRelativeDragEnd;
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ScrollDragDetector(
|
||||
onlyDragWhenScrollWasAtTop: onlyDragWhenScrollWasAtTop,
|
||||
scrollableCanMoveBack: scrollableCanMoveBack,
|
||||
onVerticalDragStart: (details, _) => onRelativeDragStart(),
|
||||
onVerticalDragEnd: (details, willScroll) {
|
||||
onRelativeDragEnd(
|
||||
details.velocity.pixelsPerSecond.dy / context.size!.height,
|
||||
willScroll,
|
||||
);
|
||||
},
|
||||
onVerticalDragUpdate: (details, wouldScroll) {
|
||||
onRelativeDragUpdate(
|
||||
details.primaryDelta! / context.size!.height,
|
||||
wouldScroll,
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A mixin that provides sheet transition behavior for a [PopupRoute].
|
||||
///
|
||||
/// This mixin handles the slide-up animation, drag gesture detection,
|
||||
/// and dismissal logic for sheet-style popups.
|
||||
///
|
||||
/// The sheet slides in from the bottom and can be dismissed by dragging down
|
||||
/// or by tapping the barrier. The animation supports spring physics for
|
||||
/// natural motion.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [StupidSimpleSheetRoute], which is a [PopupRoute] that leverages this
|
||||
/// mixin.
|
||||
mixin StupidSimpleSheetTransitionMixin<T> on PopupRoute<T> {
|
||||
/// Builds the primary contents of the sheet.
|
||||
@protected
|
||||
Widget buildContent(BuildContext context);
|
||||
|
||||
/// The motion configuration for the sheet animation.
|
||||
Motion get motion;
|
||||
|
||||
/// How much resistance the sheet should give when the user tries to drag
|
||||
/// it past it's fully opened state.
|
||||
double get overshootResistance => 100;
|
||||
|
||||
/// {@template clearBarrierImmediately}
|
||||
/// Whether this route should clear the modal barrier immediately when
|
||||
/// dismissed.
|
||||
///
|
||||
/// This can make your app feel more responsive by letting the user interact
|
||||
/// with the underlying content, while this sheet is still animating out.
|
||||
///
|
||||
/// Defaults to true.
|
||||
/// {@endtemplate}
|
||||
bool get clearBarrierImmediately => true;
|
||||
|
||||
/// {@template onlyDragWhenScrollWasAtTop}
|
||||
/// Whether the sheet should only start being draggable when its scrollable
|
||||
/// content was at the top whenever the user initiates a drag.
|
||||
///
|
||||
/// If this is true, and the user starts scrolling up from somewhere other
|
||||
/// than the top, the scroll view will perform a normal overscroll.
|
||||
///
|
||||
/// This matches iOS sheet behavior and defaults to true.
|
||||
/// {@endtemplate}
|
||||
bool get onlyDragWhenScrollWasAtTop => true;
|
||||
|
||||
/// Whether the sheet can be dragged.
|
||||
///
|
||||
/// When false, the sheet cannot be moved by dragging and applies
|
||||
/// resistance in both directions, similar to the resistance applied when
|
||||
/// dragging past 1.0.
|
||||
///
|
||||
/// Defaults to true.
|
||||
bool get draggable => true;
|
||||
|
||||
/// Whether the navigator's user gesture methods should be called when
|
||||
/// dragging starts and ends.
|
||||
///
|
||||
/// Defaults to false.
|
||||
bool get callNavigatorUserGestureMethods => false;
|
||||
|
||||
/// Whether the sheet's origin should be moved up by the bottom view inset of
|
||||
/// the current [MediaQuery].
|
||||
///
|
||||
/// If this is true, and the keyboard is opened, the sheet will originate from
|
||||
/// above the keyboard.
|
||||
bool get originateAboveBottomViewInset => false;
|
||||
|
||||
/// Controls when the route behind this sheet is rasterized to a GPU texture.
|
||||
///
|
||||
/// When enabled, the previous route's widget tree is replaced with a frozen
|
||||
/// image during the configured phases, eliminating rebuild/paint costs.
|
||||
///
|
||||
/// Defaults to [RouteSnapshotMode.never].
|
||||
///
|
||||
/// To use this, subclasses must also provide a [delegatedTransition] that
|
||||
/// wraps the previous route's child in a [SnapshotWidget] using
|
||||
/// [backgroundSnapshotController]. See [StupidSimpleSheetRoute] for an
|
||||
/// example.
|
||||
RouteSnapshotMode get backgroundSnapshotMode => RouteSnapshotMode.never;
|
||||
|
||||
/// The [SnapshotController] that toggles snapshotting of the background
|
||||
/// route.
|
||||
///
|
||||
/// Subclasses should pass this to a [SnapshotWidget] wrapping the previous
|
||||
/// route's child inside their [delegatedTransition].
|
||||
@protected
|
||||
SnapshotController get backgroundSnapshotController =>
|
||||
_backgroundSnapshotController;
|
||||
|
||||
late final SnapshotController _backgroundSnapshotController =
|
||||
SnapshotController();
|
||||
|
||||
/// The [SnapshotController] from the sheet route stacked on top of this one.
|
||||
///
|
||||
/// Set automatically when another [StupidSimpleSheetTransitionMixin] route
|
||||
/// pushes on top. Used by [maybeSnapshotChild] to wrap this route's content.
|
||||
SnapshotController? _coveredBySnapshotController;
|
||||
|
||||
bool _isUserDragging = false;
|
||||
|
||||
@override
|
||||
bool get allowSnapshotting =>
|
||||
backgroundSnapshotMode != RouteSnapshotMode.never;
|
||||
|
||||
SheetSnappingConfig? _snappingConfigOverride;
|
||||
|
||||
/// The base snapping configuration for the sheet, as provided by the
|
||||
/// implementing class.
|
||||
///
|
||||
/// This is used as the fallback when no override is set via
|
||||
/// [StupidSimpleSheetController.overrideSnappingConfig].
|
||||
///
|
||||
/// Defaults to only containing a relative point at 1.0 (fully open).
|
||||
///
|
||||
/// A fully closed point of 0.0 is always added implicitly.
|
||||
@protected
|
||||
SheetSnappingConfig get snappingConfig;
|
||||
|
||||
/// The currently active snapping configuration for the sheet.
|
||||
///
|
||||
/// This will return the override if one has been set via
|
||||
/// [StupidSimpleSheetController.overrideSnappingConfig], otherwise it returns
|
||||
/// [snappingConfig].
|
||||
SheetSnappingConfig get effectiveSnappingConfig =>
|
||||
_snappingConfigOverride ?? snappingConfig;
|
||||
|
||||
double? _dragEndVelocity;
|
||||
|
||||
double? _animationTargetValue;
|
||||
|
||||
/// Where the sheet should stick if [draggable] is false, or when
|
||||
/// overshooting.
|
||||
double? _stickingPoint;
|
||||
|
||||
/// Updates [_backgroundSnapshotController] based on the current
|
||||
/// [backgroundSnapshotMode], animation state, and drag state.
|
||||
void _updateSnapshotState() {
|
||||
final mode = backgroundSnapshotMode;
|
||||
|
||||
if (mode == RouteSnapshotMode.never) {
|
||||
_backgroundSnapshotController.allowSnapshotting = false;
|
||||
return;
|
||||
}
|
||||
if (mode == RouteSnapshotMode.always) {
|
||||
_backgroundSnapshotController.allowSnapshotting = true;
|
||||
return;
|
||||
}
|
||||
|
||||
final isAnimating = controller?.isAnimating ?? false;
|
||||
final value = controller?.value ?? 0.0;
|
||||
final isVisible = value > 0.001;
|
||||
final isSettled = !isAnimating && !_isUserDragging && isVisible;
|
||||
final maxExtent = effectiveSnappingConfig.maxExtent;
|
||||
final isFullyOpen = (value - maxExtent).abs() < 0.001;
|
||||
|
||||
final isTargetingMax = _animationTargetValue != null &&
|
||||
(_animationTargetValue! - maxExtent).abs() < 0.001;
|
||||
|
||||
final isMovingForward = isTargetingMax &&
|
||||
((controller?.status.isAnimating ?? false) ||
|
||||
(_animationTargetValue! > value));
|
||||
|
||||
_backgroundSnapshotController.allowSnapshotting = switch (mode) {
|
||||
RouteSnapshotMode.animating => isAnimating || _isUserDragging,
|
||||
RouteSnapshotMode.settled => isSettled,
|
||||
RouteSnapshotMode.openAndForward => (isSettled && isFullyOpen) ||
|
||||
(isAnimating && isMovingForward && !_isUserDragging),
|
||||
_ => false, // never/always handled above
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Duration get transitionDuration => switch (motion) {
|
||||
CurvedMotion(:final duration) => duration,
|
||||
CupertinoMotion(:final duration) => duration,
|
||||
_ => const Duration(milliseconds: 500),
|
||||
};
|
||||
|
||||
@override
|
||||
Animation<double>? get animation => super.animation?.clamped;
|
||||
|
||||
@override
|
||||
Animation<double>? get secondaryAnimation =>
|
||||
super.secondaryAnimation?.clamped;
|
||||
|
||||
@override
|
||||
Simulation? createSimulation({required bool forward}) {
|
||||
final v = _dragEndVelocity;
|
||||
_dragEndVelocity = null;
|
||||
|
||||
// Get the appropriate end value
|
||||
double endValue;
|
||||
if (forward) {
|
||||
// Opening: use initial snap point or default
|
||||
endValue = effectiveSnappingConfig.initialSnap;
|
||||
} else {
|
||||
// Closing
|
||||
endValue = 0.0;
|
||||
}
|
||||
_animationTargetValue = endValue;
|
||||
_stickingPoint = endValue;
|
||||
_updateSnapshotState();
|
||||
return motion.createSimulation(
|
||||
end: endValue,
|
||||
start: animation?.value ?? 0,
|
||||
velocity: -(v ?? 0),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildPage(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
return AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (context, child) => MediaQuery.removeViewInsets(
|
||||
context: context,
|
||||
removeBottom: originateAboveBottomViewInset,
|
||||
// ^ The sheet is already moved up by the bottom view inset, so we make
|
||||
// sure the content inside the sheet doesn't add extra padding
|
||||
child: _RelativeGestureDetector(
|
||||
onlyDragWhenScrollWasAtTop: onlyDragWhenScrollWasAtTop,
|
||||
scrollableCanMoveBack: (_animationTargetValue ?? animation.value) <
|
||||
effectiveSnappingConfig.maxExtent,
|
||||
onRelativeDragStart: () => _handleDragStart(context),
|
||||
onRelativeDragUpdate: (delta, wouldScroll) =>
|
||||
_handleDragUpdate(context, delta, wouldScroll),
|
||||
onRelativeDragEnd: (velocity, willScroll) =>
|
||||
_handleDragEnd(context, velocity, willScroll),
|
||||
child: child!,
|
||||
),
|
||||
),
|
||||
child: buildContent(context),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AnimationController createAnimationController() {
|
||||
assert(
|
||||
!debugTransitionCompleted(),
|
||||
'Cannot reuse a $runtimeType after disposing it.',
|
||||
);
|
||||
final duration = transitionDuration;
|
||||
final reverseDuration = reverseTransitionDuration;
|
||||
final animationController = AnimationController.unbounded(
|
||||
duration: duration,
|
||||
reverseDuration: reverseDuration,
|
||||
debugLabel: debugLabel,
|
||||
vsync: navigator!,
|
||||
)..addStatusListener((_) => _updateSnapshotState());
|
||||
return animationController;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildTransitions(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
Widget child,
|
||||
) {
|
||||
return AnimatedBuilder(
|
||||
animation: controller!,
|
||||
builder: (context, _) {
|
||||
final value = controller!.value;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Flexible(
|
||||
child: FractionalTranslation(
|
||||
translation: Offset(0, 1 - value),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
if (originateAboveBottomViewInset)
|
||||
SizedBox(
|
||||
height: MediaQuery.viewInsetsOf(context).bottom,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final _poppedNotifier = ValueNotifier(false);
|
||||
|
||||
@override
|
||||
Widget buildModalBarrier() {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: _poppedNotifier,
|
||||
builder: (context, value, child) {
|
||||
return IgnorePointer(
|
||||
ignoring: value && clearBarrierImmediately,
|
||||
child: super.buildModalBarrier(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@mustCallSuper
|
||||
bool didPop(T? result) {
|
||||
_poppedNotifier.value = true;
|
||||
_updateSnapshotState();
|
||||
return super.didPop(result);
|
||||
}
|
||||
|
||||
void _handleDragStart(
|
||||
BuildContext context,
|
||||
) {
|
||||
_isUserDragging = true;
|
||||
_updateSnapshotState();
|
||||
if (callNavigatorUserGestureMethods) {
|
||||
navigator?.didStartUserGesture();
|
||||
}
|
||||
}
|
||||
|
||||
void _handleDragUpdate(BuildContext context, double delta, bool wouldScroll) {
|
||||
if (_poppedNotifier.value) return;
|
||||
final currentValue = controller?.value ?? 0;
|
||||
var adjustedDelta = delta;
|
||||
|
||||
final maxExtent = effectiveSnappingConfig.maxExtent;
|
||||
|
||||
final applyResistance = !draggable || currentValue > maxExtent;
|
||||
|
||||
if (wouldScroll && (currentValue - delta) > maxExtent) {
|
||||
// If the scrollable would scroll, and the sheet will be dragged past its
|
||||
// max, we don't allow that.
|
||||
adjustedDelta = currentValue - maxExtent;
|
||||
} else if (applyResistance && delta != 0) {
|
||||
final stickingPoint = _stickingPoint ?? 1.0;
|
||||
// When dragging up past fully open, reduce the delta with diminishing
|
||||
// returns
|
||||
|
||||
final overshoot = (stickingPoint - currentValue).abs();
|
||||
|
||||
final resistance = 1.0 /
|
||||
(1.0 + overshoot * overshootResistance); // Exponential resistance
|
||||
|
||||
adjustedDelta = delta * resistance;
|
||||
}
|
||||
|
||||
final newValue = currentValue - adjustedDelta;
|
||||
|
||||
controller?.value = newValue;
|
||||
_animationTargetValue = newValue;
|
||||
}
|
||||
|
||||
void _handleDragEnd(
|
||||
BuildContext context,
|
||||
double velocity,
|
||||
bool willScroll,
|
||||
) {
|
||||
_isUserDragging = false;
|
||||
final currentValue = controller!.value;
|
||||
if (callNavigatorUserGestureMethods) {
|
||||
navigator?.didStopUserGesture();
|
||||
}
|
||||
|
||||
// If the route has been popped, don't interfere with the closing animation
|
||||
if (_poppedNotifier.value) return;
|
||||
|
||||
_dragEndVelocity = velocity;
|
||||
|
||||
final maxExtent = effectiveSnappingConfig.maxExtent;
|
||||
|
||||
// If dragged past fully open, always snap back to 1.0
|
||||
if (currentValue > maxExtent || !draggable) {
|
||||
final stickingPoint = _stickingPoint ?? maxExtent;
|
||||
// Scale the velocity by the same resistance factor that was applied
|
||||
//during dragging
|
||||
final overshoot = (currentValue - stickingPoint).abs();
|
||||
final resistance = 1.0 / (maxExtent + overshoot * overshootResistance);
|
||||
final adjustedVelocity = velocity * resistance;
|
||||
|
||||
final backSim = motion.createSimulation(
|
||||
start: currentValue,
|
||||
end: maxExtent,
|
||||
velocity: -adjustedVelocity,
|
||||
);
|
||||
controller!.animateWith(backSim);
|
||||
_dragEndVelocity = null;
|
||||
} else {
|
||||
// Find the target snap point based on position and velocity
|
||||
final targetValue =
|
||||
_animationTargetValue = effectiveSnappingConfig.findTargetSnapPoint(
|
||||
currentValue,
|
||||
velocity,
|
||||
);
|
||||
|
||||
// If target is 0 (closed), dismiss the sheet
|
||||
if (targetValue <= 0.001) {
|
||||
navigator?.pop();
|
||||
} else {
|
||||
// Animate to the target snap point
|
||||
final snapSim = motion.createSimulation(
|
||||
start: currentValue,
|
||||
end: targetValue,
|
||||
velocity: -_dragEndVelocity!,
|
||||
);
|
||||
controller!.animateWith(snapSim);
|
||||
_dragEndVelocity = null;
|
||||
}
|
||||
}
|
||||
_updateSnapshotState();
|
||||
}
|
||||
|
||||
/// Wraps [child] in a [SnapshotWidget] if another sheet route with
|
||||
/// snapshotting is stacked on top of this one.
|
||||
///
|
||||
/// Call this in [buildTransitions] around the content that should be
|
||||
/// snapshotted when covered by another sheet. Returns [child] unchanged
|
||||
/// when no snapshotting is active.
|
||||
@protected
|
||||
Widget maybeSnapshotChild(Widget child) {
|
||||
final controller = _coveredBySnapshotController;
|
||||
if (controller == null) return child;
|
||||
return SnapshotWidget(
|
||||
controller: controller,
|
||||
mode: SnapshotMode.permissive,
|
||||
autoresize: true,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@mustCallSuper
|
||||
void didChangeNext(Route<dynamic>? nextRoute) {
|
||||
super.didChangeNext(nextRoute);
|
||||
if (nextRoute is StupidSimpleSheetTransitionMixin) {
|
||||
_coveredBySnapshotController = nextRoute._backgroundSnapshotController;
|
||||
} else {
|
||||
_coveredBySnapshotController = null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@mustCallSuper
|
||||
void didPopNext(Route<dynamic> nextRoute) {
|
||||
super.didPopNext(nextRoute);
|
||||
_coveredBySnapshotController = null;
|
||||
}
|
||||
|
||||
@override
|
||||
@mustCallSuper
|
||||
void dispose() {
|
||||
_backgroundSnapshotController.dispose();
|
||||
_poppedNotifier.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// A mixin that provides imperative control over a sheet's animation.
|
||||
///
|
||||
/// Mix this into your [PopupRoute] that also uses
|
||||
/// [StupidSimpleSheetTransitionMixin] to allow children of the sheet to
|
||||
/// imperatively control the sheet's position.
|
||||
mixin StupidSimpleSheetController<T> on StupidSimpleSheetTransitionMixin<T> {
|
||||
/// Retrieves the current [StupidSimpleSheetController] from the given
|
||||
/// [BuildContext].
|
||||
///
|
||||
/// This will only work if called from a context that is inside the
|
||||
/// [StupidSimpleSheetRoute].
|
||||
static StupidSimpleSheetController<T>? maybeOf<T>(BuildContext context) {
|
||||
final route = ModalRoute.of(context);
|
||||
if (route case final StupidSimpleSheetController<T> route) {
|
||||
return route;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Can be used to imperatively animate the sheet to a relative position, but
|
||||
/// can't close it.
|
||||
///
|
||||
/// The [relativePosition] must be larger than 0.0 (fully closed) and
|
||||
/// lower than or equal to 1.0 (fully open).
|
||||
///
|
||||
/// If [snap] is true, the sheet will snap to the nearest snapping point
|
||||
/// after reaching the target position.
|
||||
///
|
||||
/// If you want to close the sheet, use [Navigator.pop] instead.
|
||||
TickerFuture animateToRelative(double relativePosition, {bool snap = false}) {
|
||||
assert(
|
||||
relativePosition > 0.0 && relativePosition <= 1.0,
|
||||
'Relative position must be larger than 0.0 and less than or equal to 1.0',
|
||||
);
|
||||
|
||||
assert(
|
||||
controller != null,
|
||||
'Controller is null. Make sure the route is pushed before calling.',
|
||||
);
|
||||
|
||||
// We only animate if this route is still current
|
||||
if (!isCurrent) return TickerFuture.complete();
|
||||
|
||||
final double target;
|
||||
|
||||
if (snap) {
|
||||
// Find the closest snapping point that isn't 0.0
|
||||
final config = effectiveSnappingConfig;
|
||||
target = switch (config.findTargetSnapPoint(relativePosition, 0)) {
|
||||
0.0 => config.points.first,
|
||||
final v => v,
|
||||
};
|
||||
} else {
|
||||
target = relativePosition;
|
||||
}
|
||||
|
||||
final simulation = motion.createSimulation(
|
||||
start: controller!.value,
|
||||
end: target,
|
||||
velocity: controller!.velocity,
|
||||
);
|
||||
|
||||
_animationTargetValue = target;
|
||||
return controller!.animateWith(simulation);
|
||||
}
|
||||
|
||||
/// Updates the snapping configuration to an override for the sheet.
|
||||
///
|
||||
/// Pass `null` to [newConfig] to reset the configuration to the one
|
||||
/// originally passed to the route constructor.
|
||||
///
|
||||
/// If [animateToComply] is `true`, the sheet will immediately animate to
|
||||
/// comply with the new snapping configuration. This will snap the sheet to
|
||||
/// the nearest valid snapping point in the new configuration.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// // Update to a new configuration and animate to comply
|
||||
/// controller.updateSheetSnappingConfig(
|
||||
/// SheetSnappingConfig([0.3, 0.6, 1.0]),
|
||||
/// animateToComply: true,
|
||||
/// );
|
||||
///
|
||||
/// // Reset to the original configuration
|
||||
/// controller.updateSheetSnappingConfig(null);
|
||||
/// ```
|
||||
TickerFuture overrideSnappingConfig(
|
||||
SheetSnappingConfig? newConfig, {
|
||||
bool animateToComply = false,
|
||||
}) {
|
||||
assert(
|
||||
controller != null,
|
||||
'Controller is null. Make sure the route is pushed before calling.',
|
||||
);
|
||||
|
||||
_snappingConfigOverride = newConfig;
|
||||
|
||||
// Only animate to comply if requested and if this route is still current
|
||||
if (animateToComply && isCurrent) {
|
||||
final currentPosition = controller!.value;
|
||||
|
||||
// Find the nearest snapping point in the new configuration
|
||||
final targetPosition = effectiveSnappingConfig.findTargetSnapPoint(
|
||||
currentPosition,
|
||||
controller!.velocity,
|
||||
includeClosed: false,
|
||||
);
|
||||
|
||||
// If the current position is already at a valid snap point, don't animate
|
||||
if ((targetPosition - currentPosition).abs() < 0.001) {
|
||||
return TickerFuture.complete();
|
||||
}
|
||||
|
||||
final simulation = motion.createSimulation(
|
||||
start: currentPosition,
|
||||
end: targetPosition,
|
||||
velocity: controller!.velocity,
|
||||
);
|
||||
|
||||
_animationTargetValue = targetPosition;
|
||||
|
||||
return controller!.animateWith(simulation);
|
||||
}
|
||||
|
||||
return TickerFuture.complete();
|
||||
}
|
||||
}
|
||||
28
packages/stupid_simple_sheet/pubspec.yaml
Normal file
@@ -0,0 +1,28 @@
|
||||
name: stupid_simple_sheet
|
||||
description: A simple sheet widget for Flutter.
|
||||
version: 0.9.1-ohos.1
|
||||
resolution: workspace
|
||||
|
||||
repository: https://github.com/whynotmake-it/rivership/tree/main/packages/stupid_simple_sheet
|
||||
homepage: https://whynotmake.it
|
||||
|
||||
topics: [animation, motion, physics, spring, curve]
|
||||
|
||||
environment:
|
||||
sdk: ">=3.5.0 <4.0.0"
|
||||
flutter: ">=3.10.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
meta: ">=1.15.0 <2.0.0"
|
||||
motor: ^1.1.0
|
||||
scroll_drag_detector: ^0.1.0+2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
lintervention: ^0.3.1
|
||||
mocktail: ^1.0.3
|
||||
snaptest: ^0.3.0
|
||||
spot: ^0.18.0
|
||||
BIN
packages/stupid_simple_sheet/test/goldens/default radius.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
packages/stupid_simple_sheet/test/goldens/dragged down.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
packages/stupid_simple_sheet/test/goldens/fully extended.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
BIN
packages/stupid_simple_sheet/test/goldens/glass large radius.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
packages/stupid_simple_sheet/test/goldens/glass small radius.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 23 KiB |
BIN
packages/stupid_simple_sheet/test/goldens/large radius rrect.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
packages/stupid_simple_sheet/test/goldens/large radius.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
packages/stupid_simple_sheet/test/goldens/zero radius.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
208
packages/stupid_simple_sheet/test/route_snapshot_mode_test.dart
Normal file
@@ -0,0 +1,208 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:stupid_simple_sheet/stupid_simple_sheet.dart';
|
||||
|
||||
void main() {
|
||||
const motion = CupertinoMotion.smooth(
|
||||
duration: Duration(milliseconds: 300),
|
||||
snapToEnd: true,
|
||||
);
|
||||
|
||||
Widget buildApp({
|
||||
required RouteSnapshotMode mode,
|
||||
SheetSnappingConfig snappingConfig = SheetSnappingConfig.full,
|
||||
bool enableSecondSheet = false,
|
||||
}) {
|
||||
return MaterialApp(
|
||||
home: Builder(
|
||||
builder: (context) => Scaffold(
|
||||
body: Center(
|
||||
child: TextButton(
|
||||
key: const ValueKey('open'),
|
||||
onPressed: () => Navigator.of(context).push(
|
||||
StupidSimpleGlassSheetRoute<void>(
|
||||
motion: motion,
|
||||
backgroundSnapshotMode: mode,
|
||||
snappingConfig: snappingConfig,
|
||||
child: Builder(
|
||||
builder: (context) => Scaffold(
|
||||
key: const ValueKey('sheet'),
|
||||
body: enableSecondSheet
|
||||
? Center(
|
||||
child: TextButton(
|
||||
key: const ValueKey('open2'),
|
||||
onPressed: () => Navigator.of(context).push(
|
||||
StupidSimpleGlassSheetRoute<void>(
|
||||
motion: motion,
|
||||
backgroundSnapshotMode: mode,
|
||||
child: const Scaffold(
|
||||
key: ValueKey('sheet2'),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: const Text('Open 2'),
|
||||
),
|
||||
)
|
||||
: const SizedBox.expand(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: const Text('Open'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the route's [SnapshotController.allowSnapshotting] value.
|
||||
bool isSnapshotting(WidgetTester tester) {
|
||||
final route = ModalRoute.of(
|
||||
tester.element(find.byKey(const ValueKey('sheet'))),
|
||||
)! as StupidSimpleGlassSheetRoute;
|
||||
// ignore: invalid_use_of_protected_member
|
||||
return route.backgroundSnapshotController.allowSnapshotting;
|
||||
}
|
||||
|
||||
group('RouteSnapshotMode', () {
|
||||
testWidgets('never: no SnapshotWidget in delegated transition',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(buildApp(mode: RouteSnapshotMode.never));
|
||||
await tester.tap(find.byKey(const ValueKey('open')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// With never, delegatedTransition returns null → no SnapshotWidget
|
||||
expect(find.byType(SnapshotWidget), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('always: snapshotting stays on throughout', (tester) async {
|
||||
await tester.pumpWidget(buildApp(mode: RouteSnapshotMode.always));
|
||||
await tester.tap(find.byKey(const ValueKey('open')));
|
||||
// Mid-animation
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
expect(isSnapshotting(tester), isTrue);
|
||||
|
||||
// Settled
|
||||
await tester.pumpAndSettle();
|
||||
expect(isSnapshotting(tester), isTrue);
|
||||
});
|
||||
|
||||
testWidgets('openAndForward: on during open animation and when settled',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(
|
||||
buildApp(mode: RouteSnapshotMode.openAndForward),
|
||||
);
|
||||
await tester.tap(find.byKey(const ValueKey('open')));
|
||||
|
||||
// Mid open-animation
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
expect(isSnapshotting(tester), isTrue);
|
||||
|
||||
// Settled
|
||||
await tester.pumpAndSettle();
|
||||
expect(isSnapshotting(tester), isTrue);
|
||||
});
|
||||
|
||||
testWidgets('openAndForward: off during user drag', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
buildApp(mode: RouteSnapshotMode.openAndForward),
|
||||
);
|
||||
await tester.tap(find.byKey(const ValueKey('open')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(isSnapshotting(tester), isTrue);
|
||||
|
||||
// Start drag
|
||||
final gesture = await tester.startGesture(
|
||||
tester.getCenter(find.byKey(const ValueKey('sheet'))),
|
||||
);
|
||||
await gesture.moveBy(const Offset(0, 50));
|
||||
await tester.pump();
|
||||
|
||||
expect(isSnapshotting(tester), isFalse);
|
||||
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('openAndForward: off during animation to intermediate snap',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(
|
||||
buildApp(
|
||||
mode: RouteSnapshotMode.openAndForward,
|
||||
// Initial snap is 0.5 (the lowest non-zero point).
|
||||
// The sheet opens to 0.5, not 1.0.
|
||||
snappingConfig: const SheetSnappingConfig([0.5, 1.0]),
|
||||
),
|
||||
);
|
||||
await tester.tap(find.byKey(const ValueKey('open')));
|
||||
|
||||
// Mid-animation toward 0.5 — target is not max extent so no snapshot
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
expect(isSnapshotting(tester), isFalse);
|
||||
|
||||
// Settled at 0.5, still not the final snap point (1.0)
|
||||
await tester.pumpAndSettle();
|
||||
expect(isSnapshotting(tester), isFalse);
|
||||
});
|
||||
|
||||
testWidgets('animating: on during animation, off when settled',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(buildApp(mode: RouteSnapshotMode.animating));
|
||||
await tester.tap(find.byKey(const ValueKey('open')));
|
||||
|
||||
// Mid-animation
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
expect(isSnapshotting(tester), isTrue);
|
||||
|
||||
// Settled
|
||||
await tester.pumpAndSettle();
|
||||
expect(isSnapshotting(tester), isFalse);
|
||||
});
|
||||
|
||||
testWidgets('settled: off during animation, on when settled',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(buildApp(mode: RouteSnapshotMode.settled));
|
||||
await tester.tap(find.byKey(const ValueKey('open')));
|
||||
|
||||
// Mid-animation
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
expect(isSnapshotting(tester), isFalse);
|
||||
|
||||
// Settled
|
||||
await tester.pumpAndSettle();
|
||||
expect(isSnapshotting(tester), isTrue);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'sheet-on-sheet: second sheet snapshots the first via '
|
||||
'maybeSnapshotChild', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
buildApp(
|
||||
mode: RouteSnapshotMode.always,
|
||||
enableSecondSheet: true,
|
||||
),
|
||||
);
|
||||
|
||||
// Open first sheet
|
||||
await tester.tap(find.byKey(const ValueKey('open')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// No SnapshotWidget wrapping the first sheet's own content yet
|
||||
// (maybeSnapshotChild returns child as-is when not covered)
|
||||
final snapshotsBefore =
|
||||
tester.widgetList<SnapshotWidget>(find.byType(SnapshotWidget));
|
||||
final countBefore = snapshotsBefore.length;
|
||||
|
||||
// Open second sheet on top
|
||||
await tester.tap(find.byKey(const ValueKey('open2')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Now there should be an additional SnapshotWidget from
|
||||
// maybeSnapshotChild wrapping the first sheet's content
|
||||
final snapshotsAfter =
|
||||
tester.widgetList<SnapshotWidget>(find.byType(SnapshotWidget));
|
||||
expect(snapshotsAfter.length, greaterThan(countBefore));
|
||||
});
|
||||
});
|
||||
}
|
||||