Skip to content

Commit 8d38508

Browse files
authored
🚀 Use visibility_detector and scroll observer to improve media playing experiences (#633)
1 parent 4c464a5 commit 8d38508

File tree

5 files changed

+112
-79
lines changed

5 files changed

+112
-79
lines changed

‎CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ that can be found in the LICENSE file. -->
1313

1414
- Make Live Photos gesture consist when scaling and panning.
1515
- Integrate `LocallyAvailableBuilder` with thumbnail options to improve the thumbnail loading speed.
16+
- Use `visibility_detector` and scroll observer to improve media playing experiences.
1617

1718
## 9.3.0
1819

‎lib/src/delegates/asset_picker_builder_delegate.dart

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2435,15 +2435,15 @@ class DefaultAssetPickerBuilderDelegate
24352435
}
24362436
});
24372437
}
2438-
return AnnotatedRegion<SystemUiOverlayStyle>(
2439-
value: overlayStyle,
2440-
child: Theme(
2441-
data: theme,
2438+
return Theme(
2439+
data: theme,
2440+
child: AnnotatedRegion<SystemUiOverlayStyle>(
2441+
value: overlayStyle,
24422442
child: CNP<DefaultAssetPickerProvider>.value(
24432443
value: provider,
2444-
builder: (BuildContext context, _) => Material(
2445-
color: theme.canvasColor,
2446-
child: Stack(
2444+
builder: (BuildContext context, _) => Scaffold(
2445+
backgroundColor: theme.scaffoldBackgroundColor,
2446+
body: Stack(
24472447
fit: StackFit.expand,
24482448
children: <Widget>[
24492449
if (isAppleOS(context))

‎lib/src/delegates/asset_picker_viewer_builder_delegate.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,9 +1010,9 @@ class DefaultAssetPickerViewerBuilderDelegate
10101010
(themeData.effectiveBrightness.isDark
10111011
? SystemUiOverlayStyle.light
10121012
: SystemUiOverlayStyle.dark),
1013-
child: Material(
1014-
color: themeData.scaffoldBackgroundColor,
1015-
child: Stack(
1013+
child: Scaffold(
1014+
resizeToAvoidBottomInset: false,
1015+
body: Stack(
10161016
children: <Widget>[
10171017
Positioned.fill(child: _pageViewBuilder(context)),
10181018
if (isWeChatMoment && hasVideo) ...<Widget>[

‎lib/src/widget/builder/image_page_builder.dart

Lines changed: 100 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import 'package:flutter/services.dart';
1010
import 'package:photo_manager/photo_manager.dart';
1111
import 'package:photo_manager_image_provider/photo_manager_image_provider.dart';
1212
import 'package:video_player/video_player.dart';
13+
import 'package:visibility_detector/visibility_detector.dart';
1314
import 'package:wechat_picker_library/wechat_picker_library.dart';
1415

1516
import '../../constants/constants.dart';
@@ -179,29 +180,45 @@ class _LivePhotoWidgetState extends State<_LivePhotoWidget> {
179180
final _showVideo = ValueNotifier<bool>(false);
180181
late final _controller = widget.controller;
181182

183+
ScrollNotificationObserverState? _scrollNotificationObserver;
184+
bool _scrolling = false;
185+
182186
@override
183187
void initState() {
184188
super.initState();
185-
WidgetsBinding.instance.addPostFrameCallback((_) {
186-
_controller.play().then((_) {
187-
HapticFeedback.lightImpact();
188-
_showVideo.value = true;
189-
});
190-
});
191189
_controller.addListener(_notify);
192190
}
193191

192+
@override
193+
void didChangeDependencies() {
194+
super.didChangeDependencies();
195+
_scrollNotificationObserver?.removeListener(_handleScrollNotification);
196+
_scrollNotificationObserver = ScrollNotificationObserver.maybeOf(context);
197+
_scrollNotificationObserver?.addListener(_handleScrollNotification);
198+
}
199+
194200
@override
195201
void dispose() {
202+
_scrollNotificationObserver?.removeListener(_handleScrollNotification);
196203
_controller.pause();
197204
_controller.removeListener(_notify);
198205
super.dispose();
199206
}
200207

201-
Future<void> continuePlay() async {
202-
if (_showVideo.value && _controller.value.position != Duration.zero) {
203-
HapticFeedback.lightImpact();
204-
await _controller.play();
208+
void _handleScrollNotification(ScrollNotification notification) {
209+
if (notification is ScrollStartNotification) {
210+
_scrolling = true;
211+
} else if (notification is ScrollEndNotification) {
212+
_scrolling = false;
213+
}
214+
}
215+
216+
void _onVisibilityChanged(VisibilityInfo info) {
217+
final fraction = info.visibleFraction;
218+
if (fraction == 1 && !_showVideo.value && !_scrolling) {
219+
_showVideoAndPlay();
220+
} else if (fraction < 1 && _showVideo.value) {
221+
_hideVideoAndStop();
205222
}
206223
}
207224

@@ -223,75 +240,89 @@ class _LivePhotoWidgetState extends State<_LivePhotoWidget> {
223240
}
224241

225242
Future<void> _hideVideoAndStop() async {
243+
_showVideo.value = false;
244+
await Future.delayed(kThemeChangeDuration);
226245
await _controller.pause();
227246
await _controller.seekTo(Duration.zero);
228-
_showVideo.value = false;
229247
}
230248

231249
@override
232250
Widget build(BuildContext context) {
233-
return GestureDetector(
234-
onLongPress: () {
235-
_showVideoAndPlay();
236-
},
237-
onLongPressUp: () {
238-
_hideVideoAndStop();
239-
},
240-
child: ExtendedImageGesture(
241-
widget.state,
242-
imageBuilder: (
243-
Widget image, {
244-
ExtendedImageGestureState? imageGestureState,
245-
}) {
246-
return ValueListenableBuilder(
247-
valueListenable: _showVideo,
248-
builder: (context, showVideo, child) {
249-
if (imageGestureState == null ||
250-
widget.state.extendedImageInfo == null) {
251-
return child!;
252-
}
253-
final size = MediaQuery.sizeOf(context);
254-
final rect = GestureWidgetDelegateFromState.getRectFormState(
255-
Offset.zero & size,
256-
imageGestureState,
257-
width: _controller.value.size.width,
258-
height: _controller.value.size.height,
259-
copy: true,
260-
);
261-
return Stack(
262-
children: <Widget>[
263-
imageGestureState.wrapGestureWidget(
264-
FittedBox(
265-
fit: BoxFit.cover,
266-
clipBehavior: Clip.hardEdge,
267-
child: SizedBox(
268-
width: rect.width,
269-
height: rect.height,
270-
child: VideoPlayer(_controller),
251+
return VisibilityDetector(
252+
key: ValueKey(widget.state),
253+
onVisibilityChanged: _onVisibilityChanged,
254+
child: GestureDetector(
255+
onLongPress: () {
256+
_showVideoAndPlay();
257+
},
258+
onLongPressUp: () {
259+
_hideVideoAndStop();
260+
},
261+
child: ExtendedImageGesture(
262+
widget.state,
263+
imageBuilder: (
264+
Widget image, {
265+
ExtendedImageGestureState? imageGestureState,
266+
}) {
267+
return ValueListenableBuilder(
268+
valueListenable: _showVideo,
269+
builder: (context, showVideo, child) {
270+
if (imageGestureState == null ||
271+
widget.state.extendedImageInfo == null) {
272+
return child!;
273+
}
274+
final scaled = imageGestureState.gestureDetails?.totalScale !=
275+
imageGestureState.imageGestureConfig?.initialScale;
276+
final size = MediaQuery.sizeOf(context);
277+
final imageRect =
278+
GestureWidgetDelegateFromState.getRectFormState(
279+
Offset.zero & size,
280+
imageGestureState,
281+
copy: true,
282+
);
283+
final videoRect =
284+
GestureWidgetDelegateFromState.getRectFormState(
285+
Offset.zero & size,
286+
imageGestureState,
287+
width: _controller.value.size.width,
288+
height: _controller.value.size.height,
289+
copy: true,
290+
);
291+
return Stack(
292+
children: <Widget>[
293+
imageGestureState.wrapGestureWidget(
294+
FittedBox(
295+
fit: BoxFit.cover,
296+
clipBehavior: Clip.hardEdge,
297+
child: SizedBox(
298+
width: videoRect.width,
299+
height: videoRect.height,
300+
child: VideoPlayer(_controller),
301+
),
271302
),
272303
),
273-
),
274-
Positioned.fill(
275-
child: AnimatedOpacity(
276-
duration: kThemeChangeDuration,
277-
opacity: showVideo ? 0.0 : 1.0,
278-
child: child!,
304+
Positioned.fill(
305+
child: AnimatedOpacity(
306+
duration: kThemeChangeDuration,
307+
opacity: showVideo ? 0.0 : 1.0,
308+
child: child!,
309+
),
279310
),
280-
),
281-
Positioned.fromRect(
282-
rect: rect,
283-
child: AnimatedOpacity(
284-
duration: kThemeChangeDuration,
285-
opacity: showVideo ? 0.0 : 1.0,
286-
child: _buildLivePhotoIndicator(context),
311+
Positioned.fromRect(
312+
rect: imageRect,
313+
child: AnimatedOpacity(
314+
duration: kThemeChangeDuration,
315+
opacity: showVideo || scaled ? 0.0 : 1.0,
316+
child: _buildLivePhotoIndicator(context),
317+
),
287318
),
288-
),
289-
],
290-
);
291-
},
292-
child: image,
293-
);
294-
},
319+
],
320+
);
321+
},
322+
child: image,
323+
);
324+
},
325+
),
295326
),
296327
);
297328
}

‎pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ dependencies:
2929
photo_manager_image_provider: ^2.1.2
3030
provider: ^6.0.5
3131
video_player: ^2.7.0
32+
visibility_detector: ^0.4.0
3233

3334
dev_dependencies:
3435
flutter_lints: any

0 commit comments

Comments
 (0)