// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:video_player/video_player.dart'; class VideoCard extends StatelessWidget { const VideoCard({ super.key, this.controller, this.title, this.subtitle }); final VideoPlayerController? controller; final String? title; final String? subtitle; Widget _buildInlineVideo() { return Padding( padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 30.0), child: Center( child: AspectRatio( aspectRatio: 3 / 2, child: Hero( tag: controller!, child: VideoPlayerLoading(controller), ), ), ), ); } Widget _buildFullScreenVideo() { return Scaffold( appBar: AppBar( title: Text(title!), ), body: Center( child: AspectRatio( aspectRatio: 3 / 2, child: Hero( tag: controller!, child: VideoPlayPause(controller), ), ), ), ); } @override Widget build(BuildContext context) { Widget fullScreenRoutePageBuilder( BuildContext context, Animation animation, Animation secondaryAnimation, ) { return _buildFullScreenVideo(); } void pushFullScreenWidget() { final TransitionRoute route = PageRouteBuilder( settings: RouteSettings(name: title), pageBuilder: fullScreenRoutePageBuilder, ); route.completed.then((void value) { controller!.setVolume(0.0); }); controller!.setVolume(1.0); Navigator.of(context).push(route); } return SafeArea( top: false, bottom: false, child: Card( child: Column( children: [ ListTile(title: Text(title!), subtitle: Text(subtitle!)), GestureDetector( onTap: pushFullScreenWidget, child: _buildInlineVideo(), ), ], ), ), ); } } class VideoPlayerLoading extends StatefulWidget { const VideoPlayerLoading(this.controller, {super.key}); final VideoPlayerController? controller; @override State createState() => _VideoPlayerLoadingState(); } class _VideoPlayerLoadingState extends State { bool? _initialized; @override void initState() { super.initState(); _initialized = widget.controller!.value.isInitialized; widget.controller!.addListener(() { if (!mounted) { return; } final bool controllerInitialized = widget.controller!.value.isInitialized; if (_initialized != controllerInitialized) { setState(() { _initialized = controllerInitialized; }); } }); } @override Widget build(BuildContext context) { if (_initialized!) { return VideoPlayer(widget.controller!); } return Stack( fit: StackFit.expand, children: [ VideoPlayer(widget.controller!), const Center(child: CircularProgressIndicator()), ], ); } } class VideoPlayPause extends StatefulWidget { const VideoPlayPause(this.controller, {super.key}); final VideoPlayerController? controller; @override State createState() => _VideoPlayPauseState(); } class _VideoPlayPauseState extends State { _VideoPlayPauseState() { listener = () { if (mounted) { setState(() { }); } }; } FadeAnimation? imageFadeAnimation; late VoidCallback listener; VideoPlayerController? get controller => widget.controller; @override void initState() { super.initState(); controller!.addListener(listener); } @override void deactivate() { controller!.removeListener(listener); super.deactivate(); } @override Widget build(BuildContext context) { return Stack( alignment: Alignment.bottomCenter, fit: StackFit.expand, children: [ GestureDetector( child: VideoPlayerLoading(controller), onTap: () { if (!controller!.value.isInitialized) { return; } if (controller!.value.isPlaying) { imageFadeAnimation = const FadeAnimation( child: Icon(Icons.pause, size: 100.0), ); controller!.pause(); } else { imageFadeAnimation = const FadeAnimation( child: Icon(Icons.play_arrow, size: 100.0), ); controller!.play(); } }, ), Center(child: imageFadeAnimation), ], ); } } class FadeAnimation extends StatefulWidget { const FadeAnimation({ super.key, this.child, this.duration = const Duration(milliseconds: 500), }); final Widget? child; final Duration duration; @override State createState() => _FadeAnimationState(); } class _FadeAnimationState extends State with SingleTickerProviderStateMixin { late AnimationController animationController; @override void initState() { super.initState(); animationController = AnimationController( duration: widget.duration, vsync: this, ); animationController.addListener(() { if (mounted) { setState(() { }); } }); animationController.forward(from: 0.0); } @override void deactivate() { animationController.stop(); super.deactivate(); } @override void didUpdateWidget(FadeAnimation oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.child != widget.child) { animationController.forward(from: 0.0); } } @override void dispose() { animationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return animationController.isAnimating ? Opacity( opacity: 1.0 - animationController.value, child: widget.child, ) : Container(); } } class VideoDemo extends StatefulWidget { const VideoDemo({ super.key }); static const String routeName = '/video'; @override State createState() => _VideoDemoState(); } class _VideoDemoState extends State with SingleTickerProviderStateMixin { final VideoPlayerController butterflyController = VideoPlayerController.asset( 'videos/butterfly.mp4', package: 'flutter_gallery_assets', videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true), ); final VideoPlayerController beeController = VideoPlayerController.asset( 'videos/bee.mp4', package: 'flutter_gallery_assets', videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true), ); bool isDisposed = false; // Only non-test mobile environments are supported for this demo. bool isSupported = Platform.isAndroid || Platform.isIOS; @override void initState() { super.initState(); if (!isSupported) { return; } Future initController(VideoPlayerController controller, String name) async { controller.setLooping(true); controller.setVolume(0.0); controller.play(); await controller.initialize(); if (mounted) { setState(() { }); } } initController(butterflyController, 'butterfly'); initController(beeController, 'bee'); } @override void dispose() { isDisposed = true; butterflyController.dispose(); beeController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Videos'), ), body: isSupported ? Scrollbar( child: ListView( primary: true, children: [ VideoCard( title: 'Butterfly', subtitle: '… flutters by', controller: butterflyController, ), VideoCard( title: 'Bee', subtitle: '… gently buzzing', controller: beeController, ), ], ), ) : const Placeholder(), ); } }