Skip to content

Commit 3a11fe8

Browse files
authored
✨ Support live photos display (#251)
1 parent dd771a4 commit 3a11fe8

File tree

1 file changed

+127
-25
lines changed

1 file changed

+127
-25
lines changed

lib/src/widget/builder/image_page_builder.dart

Lines changed: 127 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
///
55
import 'package:extended_image/extended_image.dart';
66
import 'package:flutter/material.dart';
7+
import 'package:flutter/services.dart';
78
import 'package:photo_manager/photo_manager.dart';
9+
import 'package:video_player/video_player.dart';
810

911
import '../../delegates/asset_picker_viewer_builder_delegate.dart';
1012
import 'locally_available_builder.dart';
@@ -30,40 +32,140 @@ class ImagePageBuilder extends StatefulWidget {
3032
}
3133

3234
class _ImagePageBuilderState extends State<ImagePageBuilder> {
35+
bool _isLocallyAvailable = false;
36+
VideoPlayerController? _controller;
37+
38+
bool get _isLivePhoto => widget.asset.isLivePhoto;
39+
40+
@override
41+
void dispose() {
42+
_controller?.dispose();
43+
super.dispose();
44+
}
45+
46+
Future<void> _initializeLivePhoto() async {
47+
final String? url = await widget.asset.getMediaUrl();
48+
if (!mounted || url == null) {
49+
return;
50+
}
51+
final VideoPlayerController c = VideoPlayerController.network(
52+
url,
53+
videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
54+
);
55+
setState(() => _controller = c);
56+
c
57+
..initialize()
58+
..setVolume(0)
59+
..addListener(() {
60+
if (mounted) {
61+
setState(() {});
62+
}
63+
});
64+
}
65+
66+
void _play() {
67+
if (_controller?.value.isInitialized == true) {
68+
// Only impact when initialized.
69+
HapticFeedback.lightImpact();
70+
_controller?.play();
71+
}
72+
}
73+
74+
Future<void> _stop() async {
75+
await _controller?.pause();
76+
await _controller?.seekTo(Duration.zero);
77+
}
78+
79+
Widget _imageBuilder(BuildContext context, AssetEntity asset) {
80+
return ExtendedImage(
81+
image: AssetEntityImageProvider(
82+
asset,
83+
isOriginal: widget.previewThumbSize == null,
84+
thumbSize: widget.previewThumbSize,
85+
),
86+
fit: BoxFit.contain,
87+
mode: ExtendedImageMode.gesture,
88+
onDoubleTap: widget.delegate.updateAnimation,
89+
initGestureConfigHandler: (ExtendedImageState state) {
90+
return GestureConfig(
91+
initialScale: 1.0,
92+
minScale: 1.0,
93+
maxScale: 3.0,
94+
animationMinScale: 0.6,
95+
animationMaxScale: 4.0,
96+
cacheGesture: false,
97+
inPageView: true,
98+
);
99+
},
100+
loadStateChanged: (ExtendedImageState state) {
101+
return widget.delegate.previewWidgetLoadStateChanged(
102+
context,
103+
state,
104+
hasLoaded: state.extendedImageLoadState == LoadState.completed,
105+
);
106+
},
107+
);
108+
}
109+
33110
@override
34111
Widget build(BuildContext context) {
35112
return LocallyAvailableBuilder(
36113
asset: widget.asset,
37114
isOriginal: widget.previewThumbSize == null,
38115
builder: (BuildContext context, AssetEntity asset) {
116+
// Initialize the video controller when the asset is a Live photo
117+
// and available for further use.
118+
if (!_isLocallyAvailable && _isLivePhoto) {
119+
_initializeLivePhoto();
120+
}
121+
_isLocallyAvailable = true;
122+
// TODO(Alex): Wait until `extended_image` support synchronized zooming.
39123
return GestureDetector(
40124
behavior: HitTestBehavior.opaque,
41125
onTap: widget.delegate.switchDisplayingDetail,
42-
child: ExtendedImage(
43-
image: AssetEntityImageProvider(
44-
asset,
45-
isOriginal: widget.previewThumbSize == null,
46-
thumbSize: widget.previewThumbSize,
47-
),
48-
fit: BoxFit.contain,
49-
mode: ExtendedImageMode.gesture,
50-
onDoubleTap: widget.delegate.updateAnimation,
51-
initGestureConfigHandler: (ExtendedImageState state) {
52-
return GestureConfig(
53-
initialScale: 1.0,
54-
minScale: 1.0,
55-
maxScale: 3.0,
56-
animationMinScale: 0.6,
57-
animationMaxScale: 4.0,
58-
cacheGesture: false,
59-
inPageView: true,
60-
);
61-
},
62-
loadStateChanged: (ExtendedImageState state) {
63-
return widget.delegate.previewWidgetLoadStateChanged(
64-
context,
65-
state,
66-
hasLoaded: state.extendedImageLoadState == LoadState.completed,
126+
onLongPress: _isLivePhoto ? () => _play() : null,
127+
onLongPressEnd: _isLivePhoto ? (_) => _stop() : null,
128+
child: Builder(
129+
builder: (BuildContext context) {
130+
if (!_isLivePhoto) {
131+
return _imageBuilder(context, asset);
132+
}
133+
return Stack(
134+
children: <Widget>[
135+
if (_controller == null)
136+
_imageBuilder(context, asset)
137+
else ...<Widget>[
138+
if (_controller!.value.isInitialized)
139+
Center(
140+
child: AspectRatio(
141+
aspectRatio: _controller!.value.aspectRatio,
142+
child: ValueListenableBuilder<VideoPlayerValue>(
143+
valueListenable: _controller!,
144+
builder:
145+
(_, VideoPlayerValue value, Widget? child) {
146+
return Opacity(
147+
opacity: value.isPlaying ? 1 : 0,
148+
child: child,
149+
);
150+
},
151+
child: VideoPlayer(_controller!),
152+
),
153+
),
154+
),
155+
Positioned.fill(
156+
child: ValueListenableBuilder<VideoPlayerValue>(
157+
valueListenable: _controller!,
158+
builder: (_, VideoPlayerValue value, Widget? child) {
159+
return Opacity(
160+
opacity: value.isPlaying ? 0 : 1,
161+
child: child,
162+
);
163+
},
164+
child: _imageBuilder(context, asset),
165+
),
166+
),
167+
],
168+
],
67169
);
68170
},
69171
),

0 commit comments

Comments
 (0)