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,13 +18,14 @@ 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(
scale: null, position: Offset.zero,
source: ChangeSource.internal, scale: null,
); source: ChangeSource.internal,
);
previousState = initial; previousState = initial;
_currentState = initial; _currentState = initial;
_setState(initial); _setState(initial);

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,91 +184,110 @@ 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 Stack( return ValueListenableBuilder<double>(
fit: StackFit.expand, valueListenable: videoController.sarNotifier,
children: [ builder: (context, sar, child) {
ValueListenableBuilder<double>( final videoDisplaySize = entry.videoDisplaySize(sar);
valueListenable: videoController.sarNotifier, return Stack(
builder: (context, sar, child) { fit: StackFit.expand,
return Stack( children: [
children: [ Stack(
_buildMagnifier( children: [
displaySize: entry.videoDisplaySize(sar), _buildMagnifier(
child: VideoView( displaySize: videoDisplaySize,
entry: entry, child: VideoView(
controller: videoController, entry: entry,
), controller: videoController,
), ),
),
VideoSubtitles(
controller: videoController,
viewStateNotifier: _viewStateNotifier,
),
if (settings.videoShowRawTimedText)
VideoSubtitles( VideoSubtitles(
controller: videoController, controller: videoController,
viewStateNotifier: _viewStateNotifier, viewStateNotifier: _viewStateNotifier,
debugMode: true,
), ),
if (settings.videoShowRawTimedText) ],
VideoSubtitles( ),
controller: videoController, _buildVideoCover(videoController, videoDisplaySize),
viewStateNotifier: _viewStateNotifier, ],
debugMode: true, );
), },
], );
); }
}),
// fade out image to ease transition with the player
StreamBuilder<VideoStatus>(
stream: videoController.statusStream,
builder: (context, snapshot) {
final showCover = !videoController.isReady;
return IgnorePointer(
ignoring: !showCover,
child: AnimatedOpacity(
opacity: showCover ? 1 : 0,
curve: Curves.easeInCirc,
duration: Durations.viewerVideoPlayerTransition,
child: ValueListenableBuilder<ImageInfo?>(
valueListenable: _videoCoverInfoNotifier,
builder: (context, videoCoverInfo, child) {
if (videoCoverInfo == null) {
return GestureDetector(
onTap: _onTap,
child: ThumbnailImage(
entry: entry,
extent: context.select<MediaQueryData, double>((mq) => mq.size.shortestSide),
fit: BoxFit.contain,
showLoadingBackground: false,
),
);
}
// full cover image StreamBuilder<VideoStatus> _buildVideoCover(AvesVideoController videoController, Size videoDisplaySize) {
return _buildMagnifier( // fade out image to ease transition with the player
displaySize: Size( return StreamBuilder<VideoStatus>(
videoCoverInfo.image.width.toDouble(), stream: videoController.statusStream,
videoCoverInfo.image.height.toDouble(), builder: (context, snapshot) {
), final showCover = !videoController.isReady;
child: Image( return IgnorePointer(
image: entry.uriImage, ignoring: !showCover,
), child: AnimatedOpacity(
); opacity: showCover ? 1 : 0,
}, curve: Curves.easeInCirc,
), duration: Durations.viewerVideoPlayerTransition,
), child: ValueListenableBuilder<ImageInfo?>(
); valueListenable: _videoCoverInfoNotifier,
}, builder: (context, videoCoverInfo, child) {
), 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(
onTap: _onTap,
child: ThumbnailImage(
entry: entry,
extent: extent,
fit: BoxFit.contain,
showLoadingBackground: false,
),
);
}
return const SizedBox();
},
),
),
);
},
); );
} }
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,