4
4
///
5
5
import 'package:extended_image/extended_image.dart' ;
6
6
import 'package:flutter/material.dart' ;
7
+ import 'package:flutter/services.dart' ;
7
8
import 'package:photo_manager/photo_manager.dart' ;
9
+ import 'package:video_player/video_player.dart' ;
8
10
9
11
import '../../delegates/asset_picker_viewer_builder_delegate.dart' ;
10
12
import 'locally_available_builder.dart' ;
@@ -30,40 +32,140 @@ class ImagePageBuilder extends StatefulWidget {
30
32
}
31
33
32
34
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
+
33
110
@override
34
111
Widget build (BuildContext context) {
35
112
return LocallyAvailableBuilder (
36
113
asset: widget.asset,
37
114
isOriginal: widget.previewThumbSize == null ,
38
115
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.
39
123
return GestureDetector (
40
124
behavior: HitTestBehavior .opaque,
41
125
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
+ ],
67
169
);
68
170
},
69
171
),
0 commit comments