Files
leaf_widgets/README.md
T

131 lines
4.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# leaf_widgets
Leaf UI widgets — zero external dependencies.
- `LeafViewer` — a self-contained, photo_view-style fullscreen viewer:
clamped zoom + pan, swipe-to-dismiss, double-tap zoom, mouse-wheel zoom,
native keyboard controls and tap-outside dismissal. Desktop + mobile
behaviour out of the box.
- `LeafZoomable` — opt-in per-page zoom wrapper for `LeafViewer` (also usable
standalone); auto-sizes itself from an image's pixel dimensions via
`LeafZoomable.image`.
- `LeafZoomController` — the zoom/pan state machine behind both (clamped,
fling, double-tap staging, wheel zoom).
- Plus: `LeafContextMenu`, `LeafPopover`, `LeafImage`, `AnimatedFavoriteButton`,
`AnimatedStar`.
## Features
- Clamped zoom (1x–maxScale) and pan (corners cannot escape the viewport)
- Edge-release: a drag at a fully zoomed-out, fully clamped position pages or
dismisses instead of panning (gesture-arena based, no raw pointer routing)
- Mobile: swipe to page, drag perpendicular to dismiss, double-tap zooms
(1x → contained → 1:1 when dimensions are known)
- Desktop (Windows/Linux/macOS): mouse-wheel zoom (cursor-anchored), no drag
paging/dismiss/double-tap by default, default on-screen controls
(previous/next, close, page counter) — or your own via `controlsBuilder`
- Keyboard when focused: ←/→ (or ↑/↓ for vertical paging) + Esc dismiss
(opt-out with `enableKeyboard: false`)
- Tap outside the active image fires `onTapOutside`
- `LeafZoomable.image`: pixel dimensions extracted from the image stream at
load time — contained size and 1:1 double-tap step work without passing
anything (falls back to viewport clamping until decoded)
## Getting started
Add to `pubspec.yaml`:
```yaml
dependencies:
leaf_widgets:
path: ../leaf_widgets
```
## Usage
```dart
import 'package:flutter/material.dart';
import 'package:leaf_widgets/leaf_widgets.dart';
class PostViewer extends StatelessWidget {
const PostViewer({super.key, required this.posts});
final List<String> urls;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: LeafViewer(
pageCount: urls.length,
onDismissed: () => Navigator.of(context).maybePop(),
pageBuilder: (context, index) => LeafZoomable.image(
image: Image.network(
urls[index],
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) =>
const Center(child: Icon(Icons.broken_image, color: Colors.white38)),
),
),
),
);
}
}
```
That is the full viewer: paging, clamped zoom/pan, swipe-dismiss, double-tap
(1:1 included — the pixel dimensions are resolved automatically), wheel zoom,
keyboard, tap-outside, and desktop controls.
### Sizing control
`LeafZoomable` sizes the zoom clamp itself when possible:
- `LeafZoomable.image` resolves the image's pixel dimensions from its stream
(no second download — the image cache dedupes) and uses them for the
contained size and the 1:1 stage.
- Plain `LeafZoomable(child: ...)` clamps against the viewport until you pass
explicit sizes:
```dart
// Contained (fit-to-screen) render size + full-resolution size.
LeafZoomable(
childSize: myContainedSize, // optional — defaults to the viewport
originalSize: myFullSize, // optional — null disables the 1:1 stage
child: myWidget,
)
```
### Custom chrome
```dart
LeafViewer(
pageCount: urls.length,
controlsBuilder: (context, controls) => Row(
children: [
IconButton(
onPressed: controls.canGoPrevious ? () => controls.goToPage(controls.currentIndex - 1) : null,
icon: const Icon(Icons.chevron_left),
),
Text('${controls.currentIndex + 1} / ${controls.pageCount}'),
],
),
pageBuilder: (context, index) => /* ... */,
)
```
### Platforms
- Touch: swipe paging, perpendicular drag dismiss, double-tap zoom.
- Desktop: wheel zoom, keyboard, default controls; swipe gestures are off
unless `enableSwipeGestures: true` forces them on.
- `scrollDirection` flips paging to vertical; the dismiss gesture then runs
along the other axis.
## Additional information
- Example: `example/lib/viewer_demo.dart`
- All viewer code lives in `lib/src/viewer/` — zoom math in
`leaf_zoom_controller.dart` (property-tested), gesture arbitration in
`leaf_pan_zoom_recognizer.dart` (ported from photo_view, MIT).