album: scaling overlay
This commit is contained in:
parent
aa697f3a37
commit
5fc1510982
3 changed files with 182 additions and 48 deletions
159
lib/widgets/album/collection_scaling.dart
Normal file
159
lib/widgets/album/collection_scaling.dart
Normal file
|
@ -0,0 +1,159 @@
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/widgets/album/collection_section.dart';
|
||||
import 'package:aves/widgets/album/thumbnail.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
|
||||
|
||||
class GridScaleGestureDetector extends StatefulWidget {
|
||||
final GlobalKey scrollableKey;
|
||||
final ValueNotifier<double> columnCountNotifier;
|
||||
final Widget child;
|
||||
|
||||
const GridScaleGestureDetector({
|
||||
this.scrollableKey,
|
||||
@required this.columnCountNotifier,
|
||||
@required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
_GridScaleGestureDetectorState createState() => _GridScaleGestureDetectorState();
|
||||
}
|
||||
|
||||
class _GridScaleGestureDetectorState extends State<GridScaleGestureDetector> {
|
||||
double _start;
|
||||
ValueNotifier<double> _scaledCountNotifier;
|
||||
OverlayEntry _overlayEntry;
|
||||
ThumbnailMetadata _metadata;
|
||||
RenderSliver _renderSliver;
|
||||
RenderViewport _renderViewport;
|
||||
|
||||
ValueNotifier<double> get countNotifier => widget.columnCountNotifier;
|
||||
|
||||
static const columnCountMin = 2.0;
|
||||
static const columnCountMax = 8.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onScaleStart: (details) {
|
||||
final scrollableContext = widget.scrollableKey.currentContext;
|
||||
final RenderBox scrollableBox = scrollableContext.findRenderObject();
|
||||
final result = BoxHitTestResult();
|
||||
scrollableBox.hitTest(result, position: details.localFocalPoint);
|
||||
|
||||
// find `RenderObject`s at the gesture focal point
|
||||
final firstOf = <T>(BoxHitTestResult result) => result.path.firstWhere((el) => el.target is T, orElse: () => null)?.target as T;
|
||||
final renderMetaData = firstOf<RenderMetaData>(result);
|
||||
// abort if we cannot find an image to show on overlay
|
||||
if (renderMetaData == null) return;
|
||||
_renderSliver = firstOf<RenderSliverStickyHeader>(result);
|
||||
_renderViewport = firstOf<RenderViewport>(result);
|
||||
_metadata = renderMetaData.metaData;
|
||||
_start = countNotifier.value;
|
||||
_scaledCountNotifier = ValueNotifier(_start);
|
||||
|
||||
final gridWidth = scrollableBox.size.width;
|
||||
final halfExtent = gridWidth / _start / 2;
|
||||
final thumbnailCenter = renderMetaData.localToGlobal(Offset(halfExtent, halfExtent));
|
||||
_overlayEntry = OverlayEntry(
|
||||
builder: (context) {
|
||||
return ScaleOverlay(
|
||||
imageEntry: _metadata.entry,
|
||||
thumbnailCenter: thumbnailCenter,
|
||||
gridWidth: gridWidth,
|
||||
scaledCountNotifier: _scaledCountNotifier,
|
||||
);
|
||||
},
|
||||
);
|
||||
Overlay.of(scrollableContext).insert(_overlayEntry);
|
||||
},
|
||||
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);
|
||||
},
|
||||
onScaleEnd: (details) {
|
||||
if (_overlayEntry != null) {
|
||||
_overlayEntry.remove();
|
||||
_overlayEntry = null;
|
||||
}
|
||||
if (_scaledCountNotifier == null) return;
|
||||
|
||||
final newColumnCount = _scaledCountNotifier.value.roundToDouble();
|
||||
_scaledCountNotifier = null;
|
||||
if (newColumnCount == countNotifier.value) return;
|
||||
|
||||
// update grid layout
|
||||
countNotifier.value = newColumnCount;
|
||||
|
||||
// scroll to show the focal point thumbnail at its new position
|
||||
final sliverClosure = _renderSliver;
|
||||
final viewportClosure = _renderViewport;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final scrollableContext = widget.scrollableKey.currentContext;
|
||||
final gridSize = (scrollableContext.findRenderObject() as RenderBox).size;
|
||||
final newExtent = gridSize.width / newColumnCount;
|
||||
final row = _metadata.index ~/ newColumnCount;
|
||||
// `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);
|
||||
});
|
||||
},
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ScaleOverlay extends StatefulWidget {
|
||||
final ImageEntry imageEntry;
|
||||
final Offset thumbnailCenter;
|
||||
final double gridWidth;
|
||||
final ValueNotifier<double> scaledCountNotifier;
|
||||
|
||||
const ScaleOverlay({
|
||||
@required this.imageEntry,
|
||||
@required this.thumbnailCenter,
|
||||
@required this.gridWidth,
|
||||
@required this.scaledCountNotifier,
|
||||
});
|
||||
|
||||
@override
|
||||
_ScaleOverlayState createState() => _ScaleOverlayState();
|
||||
}
|
||||
|
||||
class _ScaleOverlayState extends State<ScaleOverlay> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MediaQueryDataProvider(
|
||||
child: IgnorePointer(
|
||||
child: AnimatedContainer(
|
||||
color: Colors.black54,
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -39,9 +39,12 @@ class SectionSliver extends StatelessWidget {
|
|||
child: Selector<MediaQueryData, double>(
|
||||
selector: (c, mq) => mq.size.width,
|
||||
builder: (c, mqWidth, child) {
|
||||
return Thumbnail(
|
||||
return MetaData(
|
||||
metaData: ThumbnailMetadata(index, entry),
|
||||
child: Thumbnail(
|
||||
entry: entry,
|
||||
extent: mqWidth / columnCount,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@ -80,6 +83,14 @@ class SectionSliver extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
// metadata to identify entry from RenderObject hit test during collection scaling
|
||||
class ThumbnailMetadata {
|
||||
final int index;
|
||||
final ImageEntry entry;
|
||||
|
||||
const ThumbnailMetadata(this.index, this.entry);
|
||||
}
|
||||
|
||||
class SectionHeader extends StatelessWidget {
|
||||
final ImageCollection collection;
|
||||
final Map<dynamic, List<ImageEntry>> sections;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:aves/model/image_collection.dart';
|
||||
import 'package:aves/widgets/album/collection_scaling.dart';
|
||||
import 'package:aves/widgets/album/collection_section.dart';
|
||||
import 'package:draggable_scrollbar/draggable_scrollbar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -10,6 +9,7 @@ class ThumbnailCollection extends StatelessWidget {
|
|||
final Widget appBar;
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
final ValueNotifier<double> _columnCountNotifier = ValueNotifier(4);
|
||||
final GlobalKey _scrollableKey = GlobalKey();
|
||||
|
||||
ThumbnailCollection({
|
||||
Key key,
|
||||
|
@ -36,11 +36,12 @@ class ThumbnailCollection extends StatelessWidget {
|
|||
child: Selector<MediaQueryData, double>(
|
||||
selector: (c, mq) => mq.viewInsets.bottom,
|
||||
builder: (c, mqViewInsetsBottom, child) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: _columnCountNotifier,
|
||||
builder: (context, columnCount, child) => GridScaleGestureDetector(
|
||||
return GridScaleGestureDetector(
|
||||
scrollableKey: _scrollableKey,
|
||||
columnCountNotifier: _columnCountNotifier,
|
||||
child: DraggableScrollbar(
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: _columnCountNotifier,
|
||||
builder: (context, columnCount, child) => DraggableScrollbar(
|
||||
heightScrollThumb: 48,
|
||||
backgroundColor: Colors.white,
|
||||
scrollThumbBuilder: _thumbArrowBuilder(false),
|
||||
|
@ -51,6 +52,7 @@ class ThumbnailCollection extends StatelessWidget {
|
|||
bottom: mqViewInsetsBottom,
|
||||
),
|
||||
child: CustomScrollView(
|
||||
key: _scrollableKey,
|
||||
controller: _scrollController,
|
||||
slivers: [
|
||||
if (appBar != null) appBar,
|
||||
|
@ -120,41 +122,3 @@ class ThumbnailCollection extends StatelessWidget {
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
class GridScaleGestureDetector extends StatefulWidget {
|
||||
final ValueNotifier<double> columnCountNotifier;
|
||||
final Widget child;
|
||||
|
||||
const GridScaleGestureDetector({
|
||||
@required this.columnCountNotifier,
|
||||
@required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
_GridScaleGestureDetectorState createState() => _GridScaleGestureDetectorState();
|
||||
}
|
||||
|
||||
class _GridScaleGestureDetectorState extends State<GridScaleGestureDetector> {
|
||||
double _start;
|
||||
|
||||
ValueNotifier<double> get countNotifier => widget.columnCountNotifier;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onScaleStart: (details) => _start = countNotifier.value,
|
||||
onScaleUpdate: (details) {
|
||||
final s = details.scale;
|
||||
_updateColumnCount(s <= 1 ? lerpDouble(_start * 2, _start, s) : lerpDouble(_start, _start / 2, s / 6));
|
||||
},
|
||||
onScaleEnd: (details) {
|
||||
_updateColumnCount(countNotifier.value.roundToDouble());
|
||||
},
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
void _updateColumnCount(double count) {
|
||||
countNotifier.value = count.clamp(2.0, 8.0);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue