highlight thumbnail after scaling
This commit is contained in:
parent
e9d12ed3f3
commit
c0e909937d
6 changed files with 105 additions and 27 deletions
|
@ -16,6 +16,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
|
||||||
GroupFactor groupFactor;
|
GroupFactor groupFactor;
|
||||||
SortFactor sortFactor;
|
SortFactor sortFactor;
|
||||||
final AChangeNotifier filterChangeNotifier = AChangeNotifier();
|
final AChangeNotifier filterChangeNotifier = AChangeNotifier();
|
||||||
|
final StreamController<ImageEntry> _highlightController = StreamController.broadcast();
|
||||||
|
|
||||||
List<ImageEntry> _filteredEntries;
|
List<ImageEntry> _filteredEntries;
|
||||||
List<StreamSubscription> _subscriptions = [];
|
List<StreamSubscription> _subscriptions = [];
|
||||||
|
@ -74,6 +75,10 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
|
||||||
return _sortedEntries;
|
return _sortedEntries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Stream<ImageEntry> get highlightStream => _highlightController.stream;
|
||||||
|
|
||||||
|
void highlight(ImageEntry entry) => _highlightController.add(entry);
|
||||||
|
|
||||||
bool get showHeaders {
|
bool get showHeaders {
|
||||||
if (sortFactor == SortFactor.size) return false;
|
if (sortFactor == SortFactor.size) return false;
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ class GridScaleGestureDetector extends StatefulWidget {
|
||||||
final ValueNotifier<double> extentNotifier;
|
final ValueNotifier<double> extentNotifier;
|
||||||
final Size mqSize;
|
final Size mqSize;
|
||||||
final double mqHorizontalPadding;
|
final double mqHorizontalPadding;
|
||||||
|
final void Function(ImageEntry entry) onScaled;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
const GridScaleGestureDetector({
|
const GridScaleGestureDetector({
|
||||||
|
@ -25,6 +26,7 @@ class GridScaleGestureDetector extends StatefulWidget {
|
||||||
@required this.extentNotifier,
|
@required this.extentNotifier,
|
||||||
@required this.mqSize,
|
@required this.mqSize,
|
||||||
@required this.mqHorizontalPadding,
|
@required this.mqHorizontalPadding,
|
||||||
|
this.onScaled,
|
||||||
@required this.child,
|
@required this.child,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -112,7 +114,11 @@ class _GridScaleGestureDetectorState extends State<GridScaleGestureDetector> {
|
||||||
} else {
|
} else {
|
||||||
// scroll to show the focal point thumbnail at its new position
|
// scroll to show the focal point thumbnail at its new position
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_scrollToEntry(_metadata.entry);
|
final entry = _metadata.entry;
|
||||||
|
_scrollToEntry(entry);
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
widget.onScaled?.call(entry);
|
||||||
|
});
|
||||||
_applyingScale = false;
|
_applyingScale = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:aves/model/collection_lens.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/widgets/album/thumbnail/overlay.dart';
|
import 'package:aves/widgets/album/thumbnail/overlay.dart';
|
||||||
import 'package:aves/widgets/album/thumbnail/raster.dart';
|
import 'package:aves/widgets/album/thumbnail/raster.dart';
|
||||||
|
@ -7,39 +8,42 @@ import 'package:flutter/material.dart';
|
||||||
class DecoratedThumbnail extends StatelessWidget {
|
class DecoratedThumbnail extends StatelessWidget {
|
||||||
final ImageEntry entry;
|
final ImageEntry entry;
|
||||||
final double extent;
|
final double extent;
|
||||||
final Object heroTag;
|
final CollectionLens collection;
|
||||||
final ValueNotifier<bool> isScrollingNotifier;
|
final ValueNotifier<bool> isScrollingNotifier;
|
||||||
final bool showOverlay;
|
final bool showOverlay;
|
||||||
|
final Object heroTag;
|
||||||
|
|
||||||
static final Color borderColor = Colors.grey.shade700;
|
static final Color borderColor = Colors.grey.shade700;
|
||||||
static const double borderWidth = .5;
|
static const double borderWidth = .5;
|
||||||
|
|
||||||
const DecoratedThumbnail({
|
DecoratedThumbnail({
|
||||||
Key key,
|
Key key,
|
||||||
@required this.entry,
|
@required this.entry,
|
||||||
@required this.extent,
|
@required this.extent,
|
||||||
this.heroTag,
|
this.collection,
|
||||||
this.isScrollingNotifier,
|
this.isScrollingNotifier,
|
||||||
this.showOverlay = true,
|
this.showOverlay = true,
|
||||||
}) : super(key: key);
|
}) : heroTag = collection?.heroTag(entry),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final child = Stack(
|
var child = entry.isSvg
|
||||||
children: [
|
? ThumbnailVectorImage(
|
||||||
entry.isSvg
|
entry: entry,
|
||||||
? ThumbnailVectorImage(
|
extent: extent,
|
||||||
entry: entry,
|
heroTag: heroTag,
|
||||||
extent: extent,
|
)
|
||||||
heroTag: heroTag,
|
: ThumbnailRasterImage(
|
||||||
)
|
entry: entry,
|
||||||
: ThumbnailRasterImage(
|
extent: extent,
|
||||||
entry: entry,
|
isScrollingNotifier: isScrollingNotifier,
|
||||||
extent: extent,
|
heroTag: heroTag,
|
||||||
isScrollingNotifier: isScrollingNotifier,
|
);
|
||||||
heroTag: heroTag,
|
if (showOverlay) {
|
||||||
),
|
child = Stack(
|
||||||
if (showOverlay)
|
children: [
|
||||||
|
child,
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
|
@ -48,13 +52,17 @@ class DecoratedThumbnail extends StatelessWidget {
|
||||||
extent: extent,
|
extent: extent,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (showOverlay)
|
|
||||||
ThumbnailSelectionOverlay(
|
ThumbnailSelectionOverlay(
|
||||||
entry: entry,
|
entry: entry,
|
||||||
extent: extent,
|
extent: extent,
|
||||||
),
|
),
|
||||||
],
|
ThumbnailHighlightOverlay(
|
||||||
);
|
highlightedStream: collection.highlightStream.map((highlighted) => highlighted == entry),
|
||||||
|
extent: extent,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'dart:math';
|
||||||
|
|
||||||
import 'package:aves/model/collection_lens.dart';
|
import 'package:aves/model/collection_lens.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
|
import 'package:aves/widgets/common/fx/sweeper.dart';
|
||||||
import 'package:aves/widgets/common/icons.dart';
|
import 'package:aves/widgets/common/icons.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
|
@ -103,3 +104,43 @@ class ThumbnailSelectionOverlay extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ThumbnailHighlightOverlay extends StatefulWidget {
|
||||||
|
final double extent;
|
||||||
|
final Stream<bool> highlightedStream;
|
||||||
|
|
||||||
|
const ThumbnailHighlightOverlay({
|
||||||
|
Key key,
|
||||||
|
@required this.extent,
|
||||||
|
@required this.highlightedStream,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_ThumbnailHighlightOverlayState createState() => _ThumbnailHighlightOverlayState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ThumbnailHighlightOverlayState extends State<ThumbnailHighlightOverlay> {
|
||||||
|
final ValueNotifier<bool> _highlightedNotifier = ValueNotifier(false);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return StreamBuilder<bool>(
|
||||||
|
stream: widget.highlightedStream,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
_highlightedNotifier.value = snapshot.hasData && snapshot.data;
|
||||||
|
return Sweeper(
|
||||||
|
builder: (context) => Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).accentColor,
|
||||||
|
width: widget.extent * .1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
toggledNotifier: _highlightedNotifier,
|
||||||
|
onSweepEnd: () => _highlightedNotifier.value = false,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -55,6 +55,7 @@ class ThumbnailCollection extends StatelessWidget {
|
||||||
extentNotifier: _tileExtentNotifier,
|
extentNotifier: _tileExtentNotifier,
|
||||||
mqSize: mqSize,
|
mqSize: mqSize,
|
||||||
mqHorizontalPadding: mqHorizontalPadding,
|
mqHorizontalPadding: mqHorizontalPadding,
|
||||||
|
onScaled: collection.highlight,
|
||||||
child: scrollView,
|
child: scrollView,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -210,7 +211,6 @@ class _CollectionScrollViewState extends State<CollectionScrollView> {
|
||||||
widget.isScrollingNotifier.value = true;
|
widget.isScrollingNotifier.value = true;
|
||||||
_stopScrollMonitoringTimer();
|
_stopScrollMonitoringTimer();
|
||||||
_scrollMonitoringTimer = Timer(const Duration(milliseconds: 100), () {
|
_scrollMonitoringTimer = Timer(const Duration(milliseconds: 100), () {
|
||||||
debugPrint('$runtimeType _onScrollChange is scrolling false');
|
|
||||||
widget.isScrollingNotifier.value = false;
|
widget.isScrollingNotifier.value = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ class Sweeper extends StatefulWidget {
|
||||||
final double sweepAngle;
|
final double sweepAngle;
|
||||||
final Curve curve;
|
final Curve curve;
|
||||||
final ValueNotifier<bool> toggledNotifier;
|
final ValueNotifier<bool> toggledNotifier;
|
||||||
|
final VoidCallback onSweepEnd;
|
||||||
|
|
||||||
const Sweeper({
|
const Sweeper({
|
||||||
Key key,
|
Key key,
|
||||||
|
@ -17,6 +18,7 @@ class Sweeper extends StatefulWidget {
|
||||||
this.sweepAngle = pi / 4,
|
this.sweepAngle = pi / 4,
|
||||||
this.curve = Curves.easeInOutCubic,
|
this.curve = Curves.easeInOutCubic,
|
||||||
@required this.toggledNotifier,
|
@required this.toggledNotifier,
|
||||||
|
this.onSweepEnd,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -96,6 +98,9 @@ class _SweeperState extends State<Sweeper> with SingleTickerProviderStateMixin {
|
||||||
|
|
||||||
void _onAnimationStatusChange(AnimationStatus status) {
|
void _onAnimationStatusChange(AnimationStatus status) {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
|
if (status == AnimationStatus.completed) {
|
||||||
|
widget.onSweepEnd?.call();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onToggle() async {
|
Future<void> _onToggle() async {
|
||||||
|
@ -121,10 +126,23 @@ class _SweepClipPath extends CustomClipper<Path> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Path getClip(Size size) {
|
Path getClip(Size size) {
|
||||||
|
final width = size.width;
|
||||||
|
final height = size.height;
|
||||||
|
final centerX = width / 2;
|
||||||
|
final centerY = height / 2;
|
||||||
|
final diagonal = sqrt(width * width + height * height);
|
||||||
return Path()
|
return Path()
|
||||||
..moveTo(size.width / 2, size.height / 2)
|
..moveTo(centerX, centerY)
|
||||||
..addArc(Rect.fromLTWH(0, 0, size.width, size.height), startAngle, sweepAngle)
|
..addArc(
|
||||||
..lineTo(size.width / 2, size.height / 2);
|
Rect.fromCenter(
|
||||||
|
center: Offset(centerX, centerY),
|
||||||
|
width: diagonal,
|
||||||
|
height: diagonal,
|
||||||
|
),
|
||||||
|
startAngle,
|
||||||
|
sweepAngle,
|
||||||
|
)
|
||||||
|
..lineTo(centerX, centerY);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
Loading…
Reference in a new issue