In this article, I will show you how to create custom gesture control for flutter video player using chewie package. First let see what gesture we are going to build:
Gestures
- Double tap
- double tap in the middle 20% of the screen to play/pause video.
- double tap in the left 40% of the screen to seek 10sec backward.
- double tap in the right 40% of the screen to seek 10sec forward.
- Vertical swipe
- vertical swipe from left 50% of the screen to change brightness of screen.
- vertical swipe from right 50% of the screen to change volume of video.
- Horizontal swipe to seek video forward/backward according to the swipe length.
Before we start
Please take a look at how to setup chewie first because this tutorial will proceed after setting up chewie player.
I hope you got the idea of what gesture we are going to be building. I assume that you have done setup for chewie package. Now let’s import the necessary dependencies.
dependencies:
---
screen_brightness: ^0.2.1
---
flutter:
assets:
- file.mp4 # short video file for testing is ./assets directory
Project Structure
/
├── assets/
│ └── file.mp4
├── lib/
│ ├── main.dart
│ ├── custom_controls.dart
│ └── video_page.dart
└── pubspec.yaml
Now pass the CustomControls
widget to customControls
named parameter of ChewieController.
_chewieController = ChewieController(
...
customControls: const CustomControls(),
);
We are done with video_page.dart
, Now head over to custom_controls.dart
file and create a CustomControls
statefulwidget.
Adding variables
Now add variables which are going to be used in the widget. I have written the purpose of each variable in the comments.
Code: Variable used
bool _fastForward = false; // To show/hide fast forward ui
bool _fastRewind = false; // To show/hide fast rewind ui
bool _volumeChange = false; // To show/hide volume ui
bool _brightnessChange = false; // To show/hide brightness ui
double _volume = 0.0; // current volume
double _brightness = 1.0; // current brightness
double _volumeDragStart = 0.0; // position from where volume drag started
// position from where brightness drag started
double _brightnessDragStart = 0.0;
double _initialBrightness = 0.0; // initial brightness
double _lastVolume = 0.0; // last volume after the volume changed
double _lastBrightness = 0.0; // last volume after the brightness changed
// last difference calculated from volume/brightness
// to check if the last difference is equal to current
// difference.
double _lastDiff = 0;
double _seekDragStart = 0.0; // horizontal drag starting position
double _seekDuration = 10; // seek duration
// package:screen_brightness
final ScreenBrightness _screenBrightness = ScreenBrightness();
// to control the video
ChewieController? _chewieController;
// a geetter to check with _chewieController on didChangeDependencies override method
ChewieController get chewieController => _chewieController!;
Initializing and disposing
Now lets set initial brightness in _setInitialBrightness
method where we assign _initialBrightness
, _lastBrightness
and _brightness
to _screenBrightness.current
inside try-catch block. This function will called initState
override method. After that lets assign _chewieController
, _volume
and _lastVolume
in didChangeDependencies method. Compare _chewieController
with its getter and if it didn’t matches then call _dispose()
method where we dispose _chewieController
and reset screen brightness inside try catch-block. And also call _dispose()
method in dispose()
override method.
Code: Initialize - dispose
@override
void didChangeDependencies() {
final oldController = _chewieController;
_chewieController = ChewieController.of(context);
_volume = _lastVolume = _chewieController!.videoPlayerController.value.volume;
if (oldController != chewieController) {
_dispose();
}
super.didChangeDependencies();
}
_dispose() async {
_chewieController!.dispose();
try {
await _screenBrightness.resetScreenBrightness();
} on PlatformException catch (e) {
debugPrint(e.message);
await _screenBrightness.setScreenBrightness(_initialBrightness);
} catch (e) {
debugPrint(e.toString());
}
}
@override
void dispose() {
_dispose();
super.dispose();
}
@override
void initState() {
_setInitialBrightness();
super.initState();
}
_setInitialBrightness() async {
try {
_initialBrightness = _lastBrightness = _brightness = await _screenBrightness.current;
setState(() {});
} catch (e) {
debugPrint(e.toString());
throw 'Failed to get system brightness';
}
}
Build
In our build method we will get the screen size and then adding GestureDetector to the child of SizedBox. There are 8 callback functions which we are going to use in the tutorial from GestureDetector. All methods are extracted. If you want to build the controls from scratch then start with Container and it is necessary to pass color
to container. If you want to use built-in Chewie controls then import AdaptiveControls from chewie package. The UI for gesture feedback are extracted into methods for reusability. I have written the purpose of each method in the comments.
Code: Build method
// chewie built in controls
import 'package:chewie/src/helpers/adaptive_controls.dart';
...
@override
Widget build(BuildContext context) {
// get screen size
Size size = MediaQuery.of(context).size;
return SizedBox(
width: size.width,
height: size.height,
child: GestureDetector( // to detect gestures
onDoubleTap: () {}, // this is needed in order to use `onDoubleTapDown`
onDoubleTapDown: (details) { // get the position of double tap
_handleDoubleTapDown(details, size);
},
onVerticalDragStart: (DragStartDetails details) { // get the start position from where vertical drag started
_handleVerticalDragStart(details, size);
},
onVerticalDragUpdate: _handleVerticalDragUpdate, // get the position where drag is happening
onVerticalDragEnd: _handleVerticalDragEnd, // do something after drag is done, finger/mouse-pointer lifted
onHorizontalDragStart: _handleHorizontalDragStart, // get the start position from where horizontal drag started
onHorizontalDragUpdate: _handleHorizontalDragUpdate, // get the position where drag is happening
onHorizontalDragEnd: _handleHorizontalDragEnd, // do something after drag is done, finger/mouse-pointer lifted
child: Stack(
alignment: Alignment.center,
children: [
// if you want to make controls from scratch
// Container(
// width: size.width,
// height: size.height,
// color: Colors.transparent, // this is important
// ),
// use default controls that comes with chewie player
// if you use this import from `import 'package:chewie/src/helpers/adaptive_controls.dart';`
const AdaptiveControls(),
if (_fastRewind) _buildSeekUi(Icons.fast_rewind, size.width * 0.2), // fast rewind ui
if (_fastForward) _buildSeekUi(Icons.fast_forward, size.width * 0.2), // fast forward ui
if (_brightnessChange) _buildVolumeBrightnessUi(context, Icons.wb_sunny_outlined, size.width * 0.2), // brightness ui
if (_volumeChange) _buildVolumeBrightnessUi(context, Icons.volume_up_outlined, size.width * 0.2), // volume ui
],
),
),
);
}
handleDoubleTapDown
- Play/Pause
In this method we will get the position of double tap and then we will check if the position is in the middle 20% of the screen. If it is then we will check if the video is playing. If it is then we will pause the video. If it is not then we will play the video.
- 10 seconds backward
If the tapped position is not in the middle 20% of the screen then we will check for next condition i.e. left 40% of the screen. If it is then we will subtract video’s current position with 10 seconds to seek backward.
- 10 seconds forward
And if this condition didn’t matches than we will check for next condition i.e. right 40% of the screen. If it is then we will add video’s current position with 10 seconds to seek forward. At last of this method we wait for 600 milliseconds to hide the ui.
Code: _handleDoubleTapDown
void _handleDoubleTapDown(TapDownDetails details, Size size) {
// handle play/pause on double tap on the middle 20% of the screen
if (details.globalPosition.dx >= size.width * 0.4 && details.globalPosition.dx <= size.width * 0.6) {
// check if video is playing
_chewieController!.isPlaying ?
_chewieController!.videoPlayerController.pause() : _chewieController!.videoPlayerController.play();
}
else if (details.globalPosition.dx < size.width * 0.4) {
// handle rewind on double tap on the left 40% of the screen
setState(() {
// make the fastForward UI visible
_fastRewind = true;
});
// get current position of the video
Duration currentPosition = _chewieController!.videoPlayerController.value.position;
// seek 10 seconds back
_chewieController!.seekTo(currentPosition - const Duration(seconds: 10));
}
else if (details.globalPosition.dx > size.width * 0.6) {
// handle forward on double tap on the right 40% of the screen
setState(() {
_fastForward = true;
});
Duration currentPosition = _chewieController!.videoPlayerController.value.position;
_chewieController!.seekTo(currentPosition + const Duration(seconds: 10));
}
// wait 600ms to hide seekUi
Future.delayed(const Duration(milliseconds: 600), () {
setState(() {
_fastForward = false;
_fastRewind = false;
});
});
}
handleVerticalDragStart
- Change brightness
User slide on left 50% of the screen to change brightness.
- change volume
User slide on right 50% of the screen to change volume.
Code: _handleVerticalDragStart
void _handleVerticalDragStart(DragStartDetails details, Size size) {
// handle slide left 50% of the screen
if (details.globalPosition.dx < size.width * 0.5) {
// set the brightness ui visible and store the drag start position
setState(() {
_brightnessChange = true;
_brightnessDragStart = details.globalPosition.dy;
});
}
else if (details.globalPosition.dx >= size.width * 0.5) {
// handle slide right 50% of the screen
// set the volume ui visible and store the drag start position
setState(() {
_volumeChange = true;
_volumeDragStart = details.globalPosition.dy;
});
}
}
handleVerticalDragUpdate
First check either brightness or volume was triggered. Then calculate the difference between current drag position with the property to change. If the finger/mouse-pointer is not dragging than simply return. Then calculate brightness/volume according to difference with last brightness/volume.
Code: _handleVerticalDragUpdate
void _handleVerticalDragUpdate(DragUpdateDetails details) {
// check whether volume or brightness to update
double toChange = _volumeChange ? _volumeDragStart : _brightnessDragStart;
// get the difference from drag start position to current drag position
double diff = details.globalPosition.dy - toChange;
// don't update if there is no change in difference
if (_lastDiff == diff) {
return;
}
// Brightness
if (_brightness >= 0.0 && _brightnessChange && diff.abs() < 100) {
// don't update if brightness become 0 and difference is positive
// to avoid increasing brightness when it is already 0
if (_brightness == 0.0 && !diff.isNegative) {
return;
}
setState(() {
_lastDiff = diff;
// get diff in decimal Ex: 500/ 100 = 0.5
// then add diff to last brightness
// Ex: -(0.5) + 0.3 = 0.2
// change the result to positive
// get the result upto one digit after decimal Ex: 0.56564654 = 0.5
_brightness = _parseOneDecimal((-(diff * 0.01) + _lastBrightness).abs());
});
}
// Volume
if (_volume >= 0.0 && _volumeChange && diff.abs() < 100) {
if (_volume == 0.0 && !diff.isNegative) {
return;
}
setState(() {
_lastDiff = diff;
_volume = _parseOneDecimal((-(diff / 100) + _lastVolume).abs());
});
}
}
// convert: 0.5737845748 to 0.5
double _parseOneDecimal(double value) {
return double.parse(value.toStringAsFixed(1));
}
handleVerticalDragEnd
First Set brightness/volume to 0.0 if it is greater or less than 0. Then set volume for volume change and brightness for brightness change. Note: brightness package only support android and ios platform. Lastly set current brightness/volume to last brightness/volume for updating from.
Code: _handleVerticalDragEnd
Future<void> _handleVerticalDragEnd(DragEndDetails details) async {
if (_volume > 1.0) {
_volume = 1.0;
}
if (_volume < 0.0) {
_volume = 0.0;
}
if (_brightness > 1.0) {
_brightness = 1.0;
}
if (_brightness < 0.0) {
_brightness = 0.0;
}
if (_volumeChange) {
await _chewieController!.setVolume(_volume);
}
if (_brightnessChange) {
if (Platform.isAndroid || Platform.isIOS) {
await _screenBrightness.setScreenBrightness(_brightness);
}
}
setState(() {
_lastVolume = _volume;
_lastBrightness = _brightness;
_brightnessChange = false;
_volumeChange = false;
});
}
handleHorizontalDragStart
In this method we only need the drag start position.
Code: _handleHorizontalDragStart
void _handleHorizontalDragStart(DragStartDetails details) {
setState(() {
_seekDragStart = details.globalPosition.dx;
});
}
handleHorizontalDragUpdate
We need position difference between drag start and current drag position to determine the duration to seek.
Code: _handleHorizontalDragUpdate
void _handleHorizontalDragUpdate(DragUpdateDetails details) {
// get the difference from drag start position to current drag position
double diff = _seekDragStart - details.globalPosition.dx;
// user is sliding to left
if (!diff.isNegative) {
if (!_fastRewind) {
setState(() {
_fastRewind = true;
_fastForward = false;
});
}
} else {
// user is sliding to right
if (!_fastForward) {
setState(() {
_fastForward = true;
_fastRewind = false;
});
}
}
setState(() {
// Ex: 50/10 = 5
_seekDuration = diff.abs() / 10;
});
}
handleHorizontalDragEnd
First we get current position of the video. Then convert the difference between drag start and current drag position to duration. Then seek to the duration.
Code: _handleHorizontalDragEnd
Future<void> _handleHorizontalDragEnd(DragEndDetails details) async {
Duration currentPosition = _chewieController!.videoPlayerController.value.position;
Duration position = Duration(seconds: _seekDuration.toInt());
if (_fastForward) {
await _chewieController!.seekTo(currentPosition + position);
} else if (_fastRewind) {
await _chewieController!.seekTo(currentPosition - position);
}
setState(() {
// reset to default value
_seekDuration = 10;
_fastForward = false;
_fastRewind = false;
});
}
Building seek UI
Code: Seek UI Widget
Positioned _buildSeekUi(IconData icon, double position) {
return Positioned(
left: _fastRewind ? position : null, // position left for fast rewind
right: _fastForward ? position : null, // position right for fast forward
child: AnimatedOpacity(
duration: const Duration(milliseconds: 300),
opacity: (_fastForward || _fastRewind) ? 1 : 0, // hide in idle state
child: Container(
width: 70,
height: 40,
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 3),
decoration: BoxDecoration(
color: Colors.grey[800]!.withOpacity(0.5),
borderRadius: BorderRadius.circular(5),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// left arrow icon
if (_fastRewind) ...[
Icon(
icon,
size: 18,
color: Colors.white,
),
const SizedBox(
width: 2,
),
],
// duration(seconds) to change
Text(
'${_seekDuration.round()}s',
style: const TextStyle(color: Colors.white),
),
// right arrow icon
if (_fastForward) ...[
const SizedBox(
width: 2,
),
Icon(
icon,
size: 18,
color: Colors.white,
),
],
],
),
),
),
);
}
Building Brightness and Volume UI
Code: Building Brightness and Volume UI
Positioned _buildVolumeBrightnessUi(BuildContext context, IconData icon, double position) {
return Positioned(
left: _brightnessChange ? position : null, // position left for brightness change
right: _volumeChange ? position : null, // position right for volume change
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 3),
height: 150,
width: 24,
alignment: Alignment.bottomCenter,
decoration: BoxDecoration(
color: Colors.grey[800],
borderRadius: BorderRadius.circular(20),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 300),
constraints: const BoxConstraints(maxHeight: 120), // max height for container
height: (_brightnessChange ? _brightness : _volume) * 120, // Ex: 0.5 * 120 = 60
width: 14,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(20),
),
),
Icon(
icon,
color: Colors.white,
size: 19,
)
],
),
),
);
}