From ea06ee22464381072aa71545601a41f80e90d6bb Mon Sep 17 00:00:00 2001 From: Alex Li Date: Mon, 23 Sep 2024 20:38:22 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20Make=20Live=20Photos=20gesture?= =?UTF-8?q?=20consist=20when=20scaling=20and=20panning=20(#631)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 4 +- example/pubspec.yaml | 2 +- .../widget/builder/image_page_builder.dart | 325 ++++++++++-------- pubspec.yaml | 4 +- 4 files changed, 188 insertions(+), 147 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cfd8cce..e1b636d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,9 @@ that can be found in the LICENSE file. --> ## Unreleased -*None.* +### Improvements + +- Make Live Photos gesture consist when scaling and panning. ## 9.3.0 diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 7be081bc..00abb433 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -17,7 +17,7 @@ dependencies: path: ../ wechat_camera_picker: ^4.2.0 - extended_image: ^8.2.0 + extended_image: ^8.3.0 package_info_plus: '>=5.0.0 <9.0.0' path: ^1.8.0 path_provider: ^2.0.15 diff --git a/lib/src/widget/builder/image_page_builder.dart b/lib/src/widget/builder/image_page_builder.dart index f441531d..62de6eb8 100644 --- a/lib/src/widget/builder/image_page_builder.dart +++ b/lib/src/widget/builder/image_page_builder.dart @@ -13,6 +13,7 @@ import 'package:video_player/video_player.dart'; import 'package:wechat_picker_library/wechat_picker_library.dart'; import '../../constants/constants.dart'; +import '../../delegates/asset_picker_text_delegate.dart'; import '../../delegates/asset_picker_viewer_builder_delegate.dart'; class ImagePageBuilder extends StatefulWidget { @@ -42,7 +43,6 @@ class ImagePageBuilder extends StatefulWidget { class _ImagePageBuilderState extends State { bool _isLocallyAvailable = false; - bool _showLivePhotoIndicator = true; VideoPlayerController? _livePhotoVideoController; bool get _isOriginal => widget.previewThumbnailSize == null; @@ -82,59 +82,17 @@ class _ImagePageBuilderState extends State { file, videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true), ); + await c.initialize(); safeSetState(() { _livePhotoVideoController = c; }); c - ..initialize().then((_) { - _play(); - }) ..setVolume(0) ..addListener(() { safeSetState(() {}); }); } - void _play() { - if (_livePhotoVideoController?.value.isInitialized ?? false) { - // Only impact when initialized. - HapticFeedback.lightImpact(); - _livePhotoVideoController?.play(); - } - } - - Future _stop() async { - await _livePhotoVideoController?.pause(); - await _livePhotoVideoController?.seekTo(Duration.zero); - } - - Widget _buildLivePhotoIndicator(BuildContext context) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Image.asset( - 'assets/icon/indicator-live-photos.png', - width: 24.0, - height: 24.0, - package: packageName, - gaplessPlayback: true, - color: Colors.white, - ), - const SizedBox(width: 2.0), - Text( - widget.delegate.textDelegate.livePhotoIndicator, - style: const TextStyle( - color: Colors.white, - fontSize: 14.0, - ), - semanticsLabel: - widget.delegate.semanticsTextDelegate.livePhotoIndicator, - strutStyle: const StrutStyle(forceStrutHeight: true, height: 1), - ), - ], - ); - } - Widget _imageBuilder(BuildContext context, AssetEntity asset) { return ExtendedImage( image: AssetEntityImageProvider( @@ -151,103 +109,23 @@ class _ImagePageBuilderState extends State { animationMinScale: 0.6, animationMaxScale: 4.0, inPageView: true, - gestureDetailsIsChanged: (details) { - final scale = details?.totalScale; - if (scale == null) { - return; - } - if (scale != 1.0 && _showLivePhotoIndicator) { - safeSetState(() { - _showLivePhotoIndicator = false; - }); - } else if (scale == 1.0 && !_showLivePhotoIndicator) { - safeSetState(() { - _showLivePhotoIndicator = true; - }); - } - }, + initialAlignment: InitialAlignment.center, ), loadStateChanged: (ExtendedImageState state) { - final Size? imageSize; - final double? aspectRatio; - if (state.extendedImageInfo case final imageInfo?) { - final dpr = MediaQuery.devicePixelRatioOf(context); - imageSize = Size( - imageInfo.image.width / dpr, - imageInfo.image.height / dpr, - ); - aspectRatio = imageSize.aspectRatio; - } else { - imageSize = null; - aspectRatio = _livePhotoVideoController?.value.aspectRatio; - } - Widget imageWidget = widget.delegate.previewWidgetLoadStateChanged( + final imageWidget = widget.delegate.previewWidgetLoadStateChanged( context, state, hasLoaded: state.extendedImageLoadState == LoadState.completed, ); - if (_isLivePhoto && _showLivePhotoIndicator) { - imageWidget = Stack( - alignment: Alignment.center, - children: [ - imageWidget, - PositionedDirectional( - start: 20.0, - bottom: 20.0, - child: _buildLivePhotoIndicator(context), - ), - ], + if (_isLivePhoto && _livePhotoVideoController != null) { + return _LivePhotoWidget( + controller: _livePhotoVideoController!, + fit: BoxFit.contain, + state: state, + textDelegate: widget.delegate.textDelegate, ); } - if (imageSize case final size?) { - imageWidget = Center( - child: AspectRatio( - aspectRatio: size.aspectRatio, - child: imageWidget, - ), - ); - } - return Stack( - alignment: Alignment.center, - fit: StackFit.expand, - children: [ - if (_livePhotoVideoController case final controller?) ...[ - ValueListenableBuilder( - valueListenable: controller, - builder: (context, value, child) => AnimatedSwitcher( - duration: kThemeChangeDuration, - child: AspectRatio( - aspectRatio: aspectRatio!, - child: child, - ), - ), - child: Stack( - alignment: Alignment.center, - children: [ - PositionedDirectional( - start: 0, - end: 0, - child: AspectRatio( - aspectRatio: controller.value.aspectRatio, - child: IgnorePointer(child: VideoPlayer(controller)), - ), - ), - ], - ), - ), - ValueListenableBuilder( - valueListenable: controller, - builder: (context, value, child) => AnimatedOpacity( - opacity: value.isPlaying ? 0 : 1, - duration: kThemeChangeDuration, - child: child, - ), - child: imageWidget, - ), - ] else - imageWidget, - ], - ); + return imageWidget; }, ); } @@ -265,21 +143,182 @@ class _ImagePageBuilderState extends State { _initializeLivePhoto(); } _isLocallyAvailable = true; - // TODO(Alex): Wait until `extended_image` support synchronized zooming. return GestureDetector( behavior: HitTestBehavior.opaque, onTap: widget.delegate.switchDisplayingDetail, - onLongPress: _isLivePhoto ? _play : null, - onLongPressEnd: _isLivePhoto - ? (_) { - _stop(); - } - : null, - child: Builder( - builder: (context) => _imageBuilder(context, asset), - ), + child: _imageBuilder(context, asset), ); }, ); } } + +class _LivePhotoWidget extends StatefulWidget { + const _LivePhotoWidget({ + required this.controller, + required this.state, + required this.fit, + required this.textDelegate, + }); + + final VideoPlayerController controller; + final ExtendedImageState state; + final BoxFit fit; + final AssetPickerTextDelegate textDelegate; + + @override + State<_LivePhotoWidget> createState() => _LivePhotoWidgetState(); +} + +class _LivePhotoWidgetState extends State<_LivePhotoWidget> { + final _showVideo = ValueNotifier(false); + late final _controller = widget.controller; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _controller.play().then((_) { + HapticFeedback.lightImpact(); + _showVideo.value = true; + }); + }); + _controller.addListener(_notify); + } + + @override + void dispose() { + _controller.pause(); + _controller.removeListener(_notify); + super.dispose(); + } + + Future continuePlay() async { + if (_showVideo.value && _controller.value.position != Duration.zero) { + HapticFeedback.lightImpact(); + await _controller.play(); + } + } + + Future _notify() async { + if (_controller.value.position >= _controller.value.duration) { + await _controller.pause(); + await _controller.seekTo(Duration.zero); + _showVideo.value = false; + } + } + + Future _showVideoAndPlay() async { + if (_controller.value.isPlaying) { + return; + } + HapticFeedback.lightImpact(); + _showVideo.value = true; + await _controller.play(); + } + + Future _hideVideoAndStop() async { + await _controller.pause(); + await _controller.seekTo(Duration.zero); + _showVideo.value = false; + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onLongPress: () { + _showVideoAndPlay(); + }, + onLongPressUp: () { + _hideVideoAndStop(); + }, + child: ExtendedImageGesture( + widget.state, + imageBuilder: ( + Widget image, { + ExtendedImageGestureState? imageGestureState, + }) { + return ValueListenableBuilder( + valueListenable: _showVideo, + builder: (context, showVideo, child) { + if (imageGestureState == null || + widget.state.extendedImageInfo == null) { + return child!; + } + final size = MediaQuery.sizeOf(context); + final rect = GestureWidgetDelegateFromState.getRectFormState( + Offset.zero & size, + imageGestureState, + width: _controller.value.size.width, + height: _controller.value.size.height, + copy: true, + ); + return Stack( + children: [ + imageGestureState.wrapGestureWidget( + FittedBox( + fit: BoxFit.cover, + clipBehavior: Clip.hardEdge, + child: SizedBox( + width: rect.width, + height: rect.height, + child: VideoPlayer(_controller), + ), + ), + ), + Positioned.fill( + child: AnimatedOpacity( + duration: kThemeChangeDuration, + opacity: showVideo ? 0.0 : 1.0, + child: child!, + ), + ), + Positioned.fromRect( + rect: rect, + child: AnimatedOpacity( + duration: kThemeChangeDuration, + opacity: showVideo ? 0.0 : 1.0, + child: _buildLivePhotoIndicator(context), + ), + ), + ], + ); + }, + child: image, + ); + }, + ), + ); + } + + Widget _buildLivePhotoIndicator(BuildContext context) { + return Container( + alignment: AlignmentDirectional.bottomStart, + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset( + 'assets/icon/indicator-live-photos.png', + width: 24.0, + height: 24.0, + package: packageName, + gaplessPlayback: true, + color: Colors.white, + ), + const SizedBox(width: 2.0), + Text( + widget.textDelegate.livePhotoIndicator, + style: const TextStyle( + color: Colors.white, + fontSize: 14.0, + ), + semanticsLabel: + widget.textDelegate.semanticsTextDelegate.livePhotoIndicator, + strutStyle: const StrutStyle(forceStrutHeight: true, height: 1), + ), + ], + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index a44e16f2..825ce532 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,8 +24,8 @@ dependencies: wechat_picker_library: ^1.0.2 - extended_image: ^8.2.0 - photo_manager: ^3.0.0 + extended_image: ^8.3.0 + photo_manager: ^3.4.0 photo_manager_image_provider: ^2.0.0 provider: ^6.0.5 video_player: ^2.7.0