From d46fb09c078c8a2695871a1622eaf593bd7acfe1 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Thu, 5 Mar 2020 15:50:12 +0900 Subject: [PATCH] album: scaling overlay grid --- lib/widgets/album/collection_scaling.dart | 112 ++++++++++++++++++---- lib/widgets/album/thumbnail.dart | 9 +- 2 files changed, 101 insertions(+), 20 deletions(-) diff --git a/lib/widgets/album/collection_scaling.dart b/lib/widgets/album/collection_scaling.dart index cbb787b6a..a9a6c0f42 100644 --- a/lib/widgets/album/collection_scaling.dart +++ b/lib/widgets/album/collection_scaling.dart @@ -1,4 +1,5 @@ -import 'dart:ui'; +import 'dart:math'; +import 'dart:ui' as ui; import 'package:aves/model/image_entry.dart'; import 'package:aves/widgets/album/collection_section.dart'; @@ -63,7 +64,7 @@ class _GridScaleGestureDetectorState extends State { builder: (context) { return ScaleOverlay( imageEntry: _metadata.entry, - thumbnailCenter: thumbnailCenter, + center: thumbnailCenter, gridWidth: gridWidth, scaledCountNotifier: _scaledCountNotifier, ); @@ -74,7 +75,7 @@ class _GridScaleGestureDetectorState extends State { onScaleUpdate: (details) { if (_scaledCountNotifier == null) return; final s = details.scale; - _scaledCountNotifier.value = (s <= 1 ? lerpDouble(_start * 2, _start, s) : lerpDouble(_start, _start / 2, s / 6)).clamp(columnCountMin, columnCountMax); + _scaledCountNotifier.value = (s <= 1 ? ui.lerpDouble(_start * 2, _start, s) : ui.lerpDouble(_start, _start / 2, s / 6)).clamp(columnCountMin, columnCountMax); }, onScaleEnd: (details) { if (_overlayEntry != null) { @@ -102,7 +103,7 @@ class _GridScaleGestureDetectorState extends State { // `Scrollable.ensureVisible` only works on already rendered objects // `RenderViewport.showOnScreen` can find any `RenderSliver`, but not always a `RenderMetadata` final scrollOffset = viewportClosure.scrollOffsetOf(sliverClosure, (row + 1) * newExtent - gridSize.height / 2); - viewportClosure.offset.jumpTo(scrollOffset); + viewportClosure.offset.jumpTo(scrollOffset.clamp(0, double.infinity)); }); }, child: widget.child, @@ -112,13 +113,13 @@ class _GridScaleGestureDetectorState extends State { class ScaleOverlay extends StatefulWidget { final ImageEntry imageEntry; - final Offset thumbnailCenter; + final Offset center; final double gridWidth; final ValueNotifier scaledCountNotifier; const ScaleOverlay({ @required this.imageEntry, - @required this.thumbnailCenter, + @required this.center, @required this.gridWidth, @required this.scaledCountNotifier, }); @@ -130,6 +131,10 @@ class ScaleOverlay extends StatefulWidget { class _ScaleOverlayState extends State { bool _init = false; + Offset get center => widget.center; + + double get gridWidth => widget.gridWidth; + @override void initState() { super.initState(); @@ -141,23 +146,58 @@ class _ScaleOverlayState extends State { return MediaQueryDataProvider( child: IgnorePointer( child: AnimatedContainer( - color: _init ? Colors.black54 : Colors.transparent, + decoration: _init + ? BoxDecoration( + gradient: RadialGradient( + center: FractionalOffset.fromOffsetAndSize(center, MediaQuery.of(context).size), + radius: 1, + colors: [ + Colors.black, + Colors.black54, + ], + ), + ) + : const BoxDecoration( + // provide dummy gradient to lerp to the other one during animation + gradient: RadialGradient( + colors: [ + Colors.transparent, + Colors.transparent, + ], + ), + ), duration: const Duration(milliseconds: 300), child: ValueListenableBuilder( valueListenable: widget.scaledCountNotifier, builder: (context, columnCount, child) { - final extent = widget.gridWidth / columnCount; - return Stack( - children: [ - Positioned( - left: widget.thumbnailCenter.dx - extent / 2, - top: widget.thumbnailCenter.dy - extent / 2, - child: Thumbnail( - entry: widget.imageEntry, - extent: extent, + final extent = gridWidth / columnCount; + + // keep scaled thumbnail within the screen + var dx = .0; + if (center.dx - extent / 2 < 0) { + dx = extent / 2 - center.dx; + } else if (center.dx + extent / 2 > gridWidth) { + dx = gridWidth - (center.dx + extent / 2); + } + final clampedCenter = center.translate(dx, 0); + + return CustomPaint( + painter: GridPainter( + center: clampedCenter, + extent: extent, + ), + child: Stack( + children: [ + Positioned( + left: clampedCenter.dx - extent / 2, + top: clampedCenter.dy - extent / 2, + child: Thumbnail( + entry: widget.imageEntry, + extent: extent, + ), ), - ), - ], + ], + ), ); }, ), @@ -166,3 +206,39 @@ class _ScaleOverlayState extends State { ); } } + +class GridPainter extends CustomPainter { + final Offset center; + final double extent; + + const GridPainter({ + @required this.center, + @required this.extent, + }); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..strokeWidth = Thumbnail.borderWidth + ..shader = ui.Gradient.radial( + center, + size.width / 2, + [ + Thumbnail.borderColor, + Colors.transparent, + ], + [ + min(.5, 2 * extent / size.width), + 1, + ], + ); + final topLeft = center.translate(-extent / 2, -extent / 2); + for (int i = -1; i <= 2; i++) { + canvas.drawLine(Offset(0, topLeft.dy + extent * i), Offset(size.width, topLeft.dy + extent * i), paint); + canvas.drawLine(Offset(topLeft.dx + extent * i, 0), Offset(topLeft.dx + extent * i, size.height), paint); + } + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => true; +} diff --git a/lib/widgets/album/thumbnail.dart b/lib/widgets/album/thumbnail.dart index 0cd8b0e7e..e4fece9b6 100644 --- a/lib/widgets/album/thumbnail.dart +++ b/lib/widgets/album/thumbnail.dart @@ -9,6 +9,9 @@ class Thumbnail extends StatelessWidget { final ImageEntry entry; final double extent; + static final Color borderColor = Colors.grey.shade700; + static const double borderWidth = .5; + const Thumbnail({ Key key, @required this.entry, @@ -55,10 +58,12 @@ class Thumbnail extends StatelessWidget { return Container( decoration: BoxDecoration( border: Border.all( - color: Colors.grey.shade700, - width: 0.5, + color: borderColor, + width: borderWidth, ), ), + width: extent, + height: extent, child: Stack( alignment: AlignmentDirectional.bottomStart, children: [