viewer: video cover magnifier state transition

This commit is contained in:
Thibault Deckers 2021-12-01 13:18:59 +09:00
parent a1b8d313e8
commit 2628192e06
2 changed files with 101 additions and 73 deletions

View file

@ -18,10 +18,11 @@ class MagnifierController {
late ScaleStateChange _currentScaleState, previousScaleState; late ScaleStateChange _currentScaleState, previousScaleState;
MagnifierController({ MagnifierController({
Offset initialPosition = Offset.zero, MagnifierState? initialState,
}) : super() { }) : super() {
initial = MagnifierState( initial = initialState ??
position: initialPosition, const MagnifierState(
position: Offset.zero,
scale: null, scale: null,
source: ChangeSource.internal, source: ChangeSource.internal,
); );

View file

@ -23,6 +23,7 @@ import 'package:aves/widgets/viewer/visual/state.dart';
import 'package:aves/widgets/viewer/visual/subtitle/subtitle.dart'; import 'package:aves/widgets/viewer/visual/subtitle/subtitle.dart';
import 'package:aves/widgets/viewer/visual/vector.dart'; import 'package:aves/widgets/viewer/visual/vector.dart';
import 'package:aves/widgets/viewer/visual/video.dart'; import 'package:aves/widgets/viewer/visual/video.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -51,6 +52,13 @@ class _EntryPageViewState extends State<EntryPageView> {
late ImageStreamListener _videoCoverStreamListener; late ImageStreamListener _videoCoverStreamListener;
final ValueNotifier<ImageInfo?> _videoCoverInfoNotifier = ValueNotifier(null); final ValueNotifier<ImageInfo?> _videoCoverInfoNotifier = ValueNotifier(null);
MagnifierController? _dismissedCoverMagnifierController;
MagnifierController get dismissedCoverMagnifierController {
_dismissedCoverMagnifierController ??= MagnifierController();
return _dismissedCoverMagnifierController!;
}
AvesEntry get mainEntry => widget.mainEntry; AvesEntry get mainEntry => widget.mainEntry;
AvesEntry get entry => widget.pageEntry; AvesEntry get entry => widget.pageEntry;
@ -176,16 +184,17 @@ class _EntryPageViewState extends State<EntryPageView> {
Widget _buildVideoView() { Widget _buildVideoView() {
final videoController = context.read<VideoConductor>().getController(entry); final videoController = context.read<VideoConductor>().getController(entry);
if (videoController == null) return const SizedBox(); if (videoController == null) return const SizedBox();
return ValueListenableBuilder<double>(
valueListenable: videoController.sarNotifier,
builder: (context, sar, child) {
final videoDisplaySize = entry.videoDisplaySize(sar);
return Stack( return Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
ValueListenableBuilder<double>( Stack(
valueListenable: videoController.sarNotifier,
builder: (context, sar, child) {
return Stack(
children: [ children: [
_buildMagnifier( _buildMagnifier(
displaySize: entry.videoDisplaySize(sar), displaySize: videoDisplaySize,
child: VideoView( child: VideoView(
entry: entry, entry: entry,
controller: videoController, controller: videoController,
@ -202,10 +211,17 @@ class _EntryPageViewState extends State<EntryPageView> {
debugMode: true, debugMode: true,
), ),
], ],
),
_buildVideoCover(videoController, videoDisplaySize),
],
); );
}), },
);
}
StreamBuilder<VideoStatus> _buildVideoCover(AvesVideoController videoController, Size videoDisplaySize) {
// fade out image to ease transition with the player // fade out image to ease transition with the player
StreamBuilder<VideoStatus>( return StreamBuilder<VideoStatus>(
stream: videoController.statusStream, stream: videoController.statusStream,
builder: (context, snapshot) { builder: (context, snapshot) {
final showCover = !videoController.isReady; final showCover = !videoController.isReady;
@ -218,49 +234,60 @@ class _EntryPageViewState extends State<EntryPageView> {
child: ValueListenableBuilder<ImageInfo?>( child: ValueListenableBuilder<ImageInfo?>(
valueListenable: _videoCoverInfoNotifier, valueListenable: _videoCoverInfoNotifier,
builder: (context, videoCoverInfo, child) { builder: (context, videoCoverInfo, child) {
if (videoCoverInfo == null) { if (videoCoverInfo != null) {
// full cover image may have a different size and different aspect ratio
final coverSize = Size(
videoCoverInfo.image.width.toDouble(),
videoCoverInfo.image.height.toDouble(),
);
// when the cover is the same size as the video itself
// (which is often the case when the cover is not embedded but just a frame),
// we can reuse the same magnifier and preserve its state when switching from cover to video
final coverController = showCover || coverSize == videoDisplaySize ? _magnifierController : dismissedCoverMagnifierController;
return _buildMagnifier(
controller: coverController,
displaySize: coverSize,
child: Image(
image: entry.uriImage,
),
);
}
// default to cached thumbnail, if any
final extent = entry.cachedThumbnails.firstOrNull?.key.extent;
if (extent != null && extent > 0) {
return GestureDetector( return GestureDetector(
onTap: _onTap, onTap: _onTap,
child: ThumbnailImage( child: ThumbnailImage(
entry: entry, entry: entry,
extent: context.select<MediaQueryData, double>((mq) => mq.size.shortestSide), extent: extent,
fit: BoxFit.contain, fit: BoxFit.contain,
showLoadingBackground: false, showLoadingBackground: false,
), ),
); );
} }
// full cover image return const SizedBox();
return _buildMagnifier(
displaySize: Size(
videoCoverInfo.image.width.toDouble(),
videoCoverInfo.image.height.toDouble(),
),
child: Image(
image: entry.uriImage,
),
);
}, },
), ),
), ),
); );
}, },
),
],
); );
} }
Widget _buildMagnifier({ Widget _buildMagnifier({
MagnifierController? controller,
Size? displaySize,
ScaleLevel maxScale = maxScale, ScaleLevel maxScale = maxScale,
ScaleStateCycle scaleStateCycle = defaultScaleStateCycle, ScaleStateCycle scaleStateCycle = defaultScaleStateCycle,
bool applyScale = true, bool applyScale = true,
Size? displaySize,
required Widget child, required Widget child,
}) { }) {
return Magnifier( return Magnifier(
// key includes modified date to refresh when the image is modified by metadata (e.g. rotated) // key includes modified date to refresh when the image is modified by metadata (e.g. rotated)
key: ValueKey('${entry.uri}_${entry.pageId}_${entry.dateModifiedSecs}'), key: ValueKey('${entry.uri}_${entry.pageId}_${entry.dateModifiedSecs}'),
controller: _magnifierController, controller: controller ?? _magnifierController,
childSize: displaySize ?? entry.displaySize, childSize: displaySize ?? entry.displaySize,
minScale: minScale, minScale: minScale,
maxScale: maxScale, maxScale: maxScale,