From 71a35d4398a80f8690509de13c6f72e7013862ec Mon Sep 17 00:00:00 2001 From: Ortes Date: Fri, 19 Sep 2025 16:25:27 -0300 Subject: [PATCH 1/4] Add swipedown exists fullscreen feature --- lib/src/chewie_player.dart | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/src/chewie_player.dart b/lib/src/chewie_player.dart index c693d8508..3b23076bf 100644 --- a/lib/src/chewie_player.dart +++ b/lib/src/chewie_player.dart @@ -170,10 +170,19 @@ class ChewieState extends State { ) { return Scaffold( resizeToAvoidBottomInset: false, - body: Container( - alignment: Alignment.center, - color: Colors.black, - child: controllerProvider, + body: GestureDetector( + onVerticalDragEnd: (DragEndDetails details) { + // A positive dy indicates a downward swipe. Use a threshold to avoid accidental triggers. + final double dy = details.primaryVelocity ?? 0; + if (dy > 300) { + widget.controller.exitFullScreen(); + } + }, + child: Container( + alignment: Alignment.center, + color: Colors.black, + child: controllerProvider, + ), ), ); } From ab4bb9ad8ba1a57c7e2ee7487f357fe91ad51194 Mon Sep 17 00:00:00 2001 From: Ortes Date: Mon, 22 Sep 2025 07:04:18 -0300 Subject: [PATCH 2/4] Make swipe down to exit fullscreen an option --- lib/src/chewie_player.dart | 47 ++++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/lib/src/chewie_player.dart b/lib/src/chewie_player.dart index 3b23076bf..357a87350 100644 --- a/lib/src/chewie_player.dart +++ b/lib/src/chewie_player.dart @@ -170,20 +170,26 @@ class ChewieState extends State { ) { return Scaffold( resizeToAvoidBottomInset: false, - body: GestureDetector( - onVerticalDragEnd: (DragEndDetails details) { - // A positive dy indicates a downward swipe. Use a threshold to avoid accidental triggers. - final double dy = details.primaryVelocity ?? 0; - if (dy > 300) { - widget.controller.exitFullScreen(); - } - }, - child: Container( - alignment: Alignment.center, - color: Colors.black, - child: controllerProvider, - ), - ), + body: widget.controller.swipeToExitFullscreen + ? GestureDetector( + onVerticalDragEnd: (DragEndDetails details) { + // A positive dy indicates a downward swipe. Use a threshold to avoid accidental triggers. + final double dy = details.primaryVelocity ?? 0; + if (dy > widget.controller.swipeThreshold) { + widget.controller.exitFullScreen(); + } + }, + child: Container( + alignment: Alignment.center, + color: Colors.black, + child: controllerProvider, + ), + ) + : Container( + alignment: Alignment.center, + color: Colors.black, + child: controllerProvider, + ), ); } @@ -434,6 +440,8 @@ class ChewieController extends ChangeNotifier { this.castOverlayBuilder, this.additionalControls, this.hideCursorInFullScreen = true, + this.swipeToExitFullscreen = true, + this.swipeThreshold = 300, }) : assert( playbackSpeeds.every((speed) => speed > 0), 'The playbackSpeeds values must all be greater than 0', @@ -515,6 +523,8 @@ class ChewieController extends ChangeNotifier { Widget Function(BuildContext, CastDevice?)? castOverlayBuilder, List Function(BuildContext)? additionalControls, bool? hideCursorInFullScreen, + bool? swipeToExitFullscreen, + double? swipeThreshold, }) { return ChewieController( draggableProgressBar: draggableProgressBar ?? this.draggableProgressBar, @@ -595,6 +605,9 @@ class ChewieController extends ChangeNotifier { additionalControls: additionalControls ?? this.additionalControls, hideCursorInFullScreen: hideCursorInFullScreen ?? this.hideCursorInFullScreen, + swipeToExitFullscreen: + swipeToExitFullscreen ?? this.swipeToExitFullscreen, + swipeThreshold: swipeThreshold ?? this.swipeThreshold, ); } @@ -889,6 +902,12 @@ class ChewieController extends ChangeNotifier { /// takes its size, tint, padding and chrome from there matches the buttons /// beside it on all three. final List Function(BuildContext context)? additionalControls; + /// Defines if the player allows swipe to exit fullscreen + final bool swipeToExitFullscreen; + + /// Defines the minimum velocity threshold for swipe to exit fullscreen gesture + /// The velocity is measured in pixels per second + final double swipeThreshold; /// Whether the mouse cursor auto-hides together with the controls while in /// fullscreen (and reappears on mouse movement), like most video players. From cc0c4d061be00e12ff25dab41d06817550633653 Mon Sep 17 00:00:00 2001 From: Ortes Date: Thu, 27 Aug 2026 11:18:34 +0200 Subject: [PATCH 3/4] test: cover swipe to exit fullscreen Covers the fullscreen swipe gesture added in #930: a downward fling above swipeThreshold exits fullscreen, one below it and any upward fling do not, a custom swipeThreshold shifts the cutoff, and swipeToExitFullscreen: false leaves the gesture unwired. Also covers the two new ChewieController fields in the constructor defaults and in copyWith. --- test/swipe_to_exit_fullscreen_test.dart | 185 ++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 test/swipe_to_exit_fullscreen_test.dart diff --git a/test/swipe_to_exit_fullscreen_test.dart b/test/swipe_to_exit_fullscreen_test.dart new file mode 100644 index 000000000..58d3dd8e1 --- /dev/null +++ b/test/swipe_to_exit_fullscreen_test.dart @@ -0,0 +1,185 @@ +import 'package:chewie/chewie.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:video_player/video_player.dart'; + +ChewieController buildController({ + bool? swipeToExitFullscreen, + double? swipeThreshold, +}) { + return ChewieController( + videoPlayerController: VideoPlayerController.networkUrl( + Uri.parse('https://example.com/video.mp4'), + ), + autoPlay: false, + looping: false, + swipeToExitFullscreen: swipeToExitFullscreen ?? true, + swipeThreshold: swipeThreshold ?? 300, + ); +} + +/// The [GestureDetector] the fullscreen route wraps the video in when +/// [ChewieController.swipeToExitFullscreen] is enabled. +final Finder swipeArea = find.byWidgetPredicate( + (widget) => widget is GestureDetector && widget.onVerticalDragEnd != null, +); + +/// The black backdrop the fullscreen route always paints behind the video, +/// with or without the swipe gesture. The player's own route is offstage while +/// the fullscreen route is up, so this only ever matches the fullscreen one. +final Finder fullScreenBackground = find.byWidgetPredicate( + (widget) => widget is Container && widget.color == Colors.black, +); + +extension on WidgetTester { + Future pumpFullScreen(ChewieController controller) async { + await pumpWidget( + MaterialApp( + home: Scaffold(body: Chewie(controller: controller)), + ), + ); + await pump(); + + controller.enterFullScreen(); + // Let the fullscreen route finish its push transition. + await pump(); + await pump(const Duration(seconds: 1)); + } + + /// Flings vertically over the fullscreen video with the given velocity in + /// pixels per second. A positive [velocity] is a downward swipe. + Future swipeVertically(double velocity) async { + // A long enough travel for the velocity tracker to settle on [velocity], + // while staying inside the 800x600 test surface. + const double distance = 250; + await fling( + swipeArea, + Offset(0, velocity.isNegative ? -distance : distance), + velocity.abs(), + ); + await pump(); + await pump(const Duration(seconds: 1)); + } +} + +void main() { + group('swipe to exit fullscreen', () { + testWidgets('a downward swipe above the threshold exits fullscreen', ( + tester, + ) async { + final controller = buildController(); + addTearDown(controller.dispose); + + await tester.pumpFullScreen(controller); + expect(controller.isFullScreen, isTrue); + expect(swipeArea, findsOneWidget); + + await tester.swipeVertically(1000); + + expect(controller.isFullScreen, isFalse); + expect(swipeArea, findsNothing); + }); + + testWidgets('a downward swipe below the threshold stays in fullscreen', ( + tester, + ) async { + final controller = buildController(); + addTearDown(controller.dispose); + + await tester.pumpFullScreen(controller); + + await tester.swipeVertically(200); + + expect(controller.isFullScreen, isTrue); + expect(swipeArea, findsOneWidget); + }); + + testWidgets('an upward swipe never exits fullscreen', (tester) async { + final controller = buildController(); + addTearDown(controller.dispose); + + await tester.pumpFullScreen(controller); + + await tester.swipeVertically(-1000); + + expect(controller.isFullScreen, isTrue); + expect(swipeArea, findsOneWidget); + }); + + testWidgets('swipeThreshold defines the velocity that exits fullscreen', ( + tester, + ) async { + final controller = buildController(swipeThreshold: 2000); + addTearDown(controller.dispose); + + await tester.pumpFullScreen(controller); + + // Fast enough for the default threshold, too slow for this one. + await tester.swipeVertically(1000); + expect(controller.isFullScreen, isTrue); + + await tester.swipeVertically(3000); + expect(controller.isFullScreen, isFalse); + }); + + testWidgets('swipeToExitFullscreen: false leaves the gesture unwired', ( + tester, + ) async { + final controller = buildController(swipeToExitFullscreen: false); + addTearDown(controller.dispose); + + await tester.pumpFullScreen(controller); + expect(controller.isFullScreen, isTrue); + expect(swipeArea, findsNothing); + + // Same downward fling as the passing case, over the fullscreen video. + await tester.fling(fullScreenBackground, const Offset(0, 400), 1000); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + + expect(controller.isFullScreen, isTrue); + }); + }); + + group('ChewieController swipe options', () { + test('default to an enabled gesture at 300 px/s', () { + final controller = ChewieController( + videoPlayerController: VideoPlayerController.networkUrl( + Uri.parse('https://example.com/video.mp4'), + ), + ); + addTearDown(controller.dispose); + + expect(controller.swipeToExitFullscreen, isTrue); + expect(controller.swipeThreshold, 300); + }); + + test('copyWith overrides both options', () { + final controller = buildController(); + addTearDown(controller.dispose); + + final copy = controller.copyWith( + swipeToExitFullscreen: false, + swipeThreshold: 750, + ); + addTearDown(copy.dispose); + + expect(copy.swipeToExitFullscreen, isFalse); + expect(copy.swipeThreshold, 750); + }); + + test('copyWith preserves both options when they are omitted', () { + final controller = buildController( + swipeToExitFullscreen: false, + swipeThreshold: 750, + ); + addTearDown(controller.dispose); + + final copy = controller.copyWith(); + addTearDown(copy.dispose); + + expect(copy.swipeToExitFullscreen, isFalse); + expect(copy.swipeThreshold, 750); + }); + }); +} From e776ce7976cb71bdffe81f023e2214ef7c479a6e Mon Sep 17 00:00:00 2001 From: Ortes Date: Mon, 14 Sep 2026 23:04:49 +0200 Subject: [PATCH 4/4] style: dart format after rebase onto master --- lib/src/chewie_player.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/chewie_player.dart b/lib/src/chewie_player.dart index 357a87350..3eb64a500 100644 --- a/lib/src/chewie_player.dart +++ b/lib/src/chewie_player.dart @@ -902,6 +902,7 @@ class ChewieController extends ChangeNotifier { /// takes its size, tint, padding and chrome from there matches the buttons /// beside it on all three. final List Function(BuildContext context)? additionalControls; + /// Defines if the player allows swipe to exit fullscreen final bool swipeToExitFullscreen;