Gallery video demo (#13195)
diff --git a/examples/flutter_gallery/lib/demo/all.dart b/examples/flutter_gallery/lib/demo/all.dart
index a4a680a..93d03fe 100644
--- a/examples/flutter_gallery/lib/demo/all.dart
+++ b/examples/flutter_gallery/lib/demo/all.dart
@@ -12,3 +12,4 @@
export 'pesto_demo.dart';
export 'shrine_demo.dart';
export 'typography_demo.dart';
+export 'video_demo.dart';
diff --git a/examples/flutter_gallery/lib/demo/video_demo.dart b/examples/flutter_gallery/lib/demo/video_demo.dart
new file mode 100644
index 0000000..d24e71a
--- /dev/null
+++ b/examples/flutter_gallery/lib/demo/video_demo.dart
@@ -0,0 +1,376 @@
+// Copyright 2017 The Chromium 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 'package:connectivity/connectivity.dart';
+import 'package:flutter/material.dart';
+import 'package:video_player/video_player.dart';
+
+// TODO(sigurdm): These should not be stored here.
+const String butterflyUri =
+ 'https://flutter.github.io/assets-for-api-docs/videos/butterfly.mp4';
+
+const String beeUri =
+ 'https://flutter.github.io/assets-for-api-docs/videos/bee.mp4';
+
+class VideoCard extends StatelessWidget {
+ final VideoPlayerController controller;
+ final String title;
+ final String subtitle;
+
+ const VideoCard({Key key, this.controller, this.title, this.subtitle})
+ : super(key: key);
+
+ Widget _buildInlineVideo() {
+ return new Padding(
+ padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 30.0),
+ child: new Center(
+ child: new AspectRatio(
+ aspectRatio: 3 / 2,
+ child: new Hero(
+ tag: controller,
+ child: new VideoPlayer(controller),
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildFullScreenVideo() {
+ return new Scaffold(
+ appBar: new AppBar(
+ title: new Text(title),
+ ),
+ body: new Center(
+ child: new AspectRatio(
+ aspectRatio: 3 / 2,
+ child: new Hero(
+ tag: controller,
+ child: new VideoPlayPause(controller),
+ ),
+ ),
+ ),
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ Widget fullScreenRoutePageBuilder(BuildContext context,
+ Animation<double> animation, Animation<double> secondaryAnimation) {
+ return new AnimatedBuilder(
+ child: _buildFullScreenVideo(),
+ animation: animation,
+ builder: (BuildContext context, Widget child) {
+ // TODO(sigurdm): It seems we get a animation.value of 1.0
+ // at first when entering the route. Find out how to avoid
+ // this.
+ controller.setVolume(animation.value);
+ return child;
+ },
+ );
+ }
+
+ void pushFullScreenWidget() {
+ final TransitionRoute<Null> route = new PageRouteBuilder<Null>(
+ settings: new RouteSettings(name: title, isInitialRoute: false),
+ pageBuilder: fullScreenRoutePageBuilder,
+ );
+
+ route.completed.then((Null _) {
+ controller.setVolume(0.0);
+ });
+ Navigator.of(context).push(route);
+ }
+
+ return new Card(
+ child: new Column(
+ children: <Widget>[
+ new ListTile(title: new Text(title), subtitle: new Text(subtitle)),
+ new GestureDetector(
+ onTap: pushFullScreenWidget,
+ child: _buildInlineVideo(),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class VideoPlayPause extends StatefulWidget {
+ final VideoPlayerController controller;
+
+ const VideoPlayPause(this.controller);
+
+ @override
+ State createState() => new _VideoPlayPauseState();
+}
+
+class _VideoPlayPauseState extends State<VideoPlayPause> {
+ FadeAnimation imageFadeAnimation;
+ VoidCallback listener;
+
+ _VideoPlayPauseState() {
+ listener = () {
+ setState(() {});
+ };
+ }
+
+ 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) {
+ final List<Widget> children = <Widget>[
+ new GestureDetector(
+ child: new VideoPlayer(controller),
+ onTap: () {
+ if (!controller.value.initialized) {
+ return;
+ }
+ if (controller.value.isPlaying) {
+ imageFadeAnimation = new FadeAnimation(
+ child: new Icon(Icons.pause, size: 100.0),
+ );
+ controller.pause();
+ } else {
+ imageFadeAnimation = new FadeAnimation(
+ child: new Icon(Icons.play_arrow, size: 100.0),
+ );
+ controller.play();
+ }
+ },
+ ),
+ new Center(child: imageFadeAnimation),
+ ];
+
+ if (!controller.value.initialized) {
+ children.add(new Container());
+ }
+
+ return new Stack(
+ alignment: Alignment.bottomCenter,
+ fit: StackFit.passthrough,
+ children: children,
+ );
+ }
+}
+
+class FadeAnimation extends StatefulWidget {
+ final Widget child;
+ final Duration duration;
+
+ const FadeAnimation({
+ this.child,
+ this.duration: const Duration(milliseconds: 500),
+ });
+
+ @override
+ _FadeAnimationState createState() => new _FadeAnimationState();
+}
+
+class _FadeAnimationState extends State<FadeAnimation>
+ with SingleTickerProviderStateMixin {
+ AnimationController animationController;
+
+ @override
+ void initState() {
+ super.initState();
+ animationController = new 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
+ ? new Opacity(
+ opacity: 1.0 - animationController.value,
+ child: widget.child,
+ )
+ : new Container();
+ }
+}
+
+class ConnectivityOverlay extends StatefulWidget {
+ final Widget child;
+ final Completer<Null> connectedCompleter;
+ final GlobalKey<ScaffoldState> scaffoldKey;
+
+ const ConnectivityOverlay({
+ this.child,
+ this.connectedCompleter,
+ this.scaffoldKey,
+ });
+
+ @override
+ _ConnectivityOverlayState createState() => new _ConnectivityOverlayState();
+}
+
+class _ConnectivityOverlayState extends State<ConnectivityOverlay> {
+ StreamSubscription<ConnectivityResult> connectivitySubscription;
+ bool connected = true;
+
+ static const Widget errorSnackBar = const SnackBar(
+ backgroundColor: Colors.red,
+ content: const ListTile(
+ title: const Text('No network'),
+ subtitle: const Text(
+ 'To load the videos you must have an active network connection',
+ ),
+ ),
+ );
+
+ Stream<ConnectivityResult> connectivityStream() async* {
+ final Connectivity connectivity = new Connectivity();
+ ConnectivityResult previousResult = await connectivity.checkConnectivity();
+ yield previousResult;
+ await for (ConnectivityResult result
+ in connectivity.onConnectivityChanged) {
+ if (result != previousResult) {
+ yield result;
+ previousResult = result;
+ }
+ }
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ connectivitySubscription = connectivityStream().listen(
+ (ConnectivityResult connectivityResult) {
+ if (!mounted) {
+ return;
+ }
+ if (connectivityResult == ConnectivityResult.none) {
+ widget.scaffoldKey.currentState.showSnackBar(errorSnackBar);
+ } else {
+ if (!widget.connectedCompleter.isCompleted) {
+ widget.connectedCompleter.complete(null);
+ }
+ }
+ },
+ );
+ }
+
+ @override
+ void dispose() {
+ connectivitySubscription.cancel();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) => widget.child;
+}
+
+class VideoDemo extends StatefulWidget {
+ const VideoDemo({Key key}) : super(key: key);
+
+ static const String routeName = '/video';
+
+ @override
+ _VideoDemoState createState() => new _VideoDemoState();
+}
+
+class _VideoDemoState extends State<VideoDemo>
+ with SingleTickerProviderStateMixin {
+ final VideoPlayerController butterflyController = new VideoPlayerController(
+ butterflyUri,
+ );
+ final VideoPlayerController beeController = new VideoPlayerController(
+ beeUri,
+ );
+
+ final GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>();
+ final Completer<Null> connectedCompleter = new Completer<Null>();
+
+ @override
+ void initState() {
+ super.initState();
+
+ Future<Null> initController(VideoPlayerController controller) async {
+ controller.setLooping(true);
+ controller.setVolume(0.0);
+ controller.play();
+ await connectedCompleter.future;
+ await controller.initialize();
+ setState(() {});
+ }
+
+ initController(butterflyController);
+ initController(beeController);
+ }
+
+ @override
+ void dispose() {
+ butterflyController.dispose();
+ beeController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return new Scaffold(
+ key: scaffoldKey,
+ appBar: new AppBar(
+ title: const Text('Videos'),
+ ),
+ body: new ConnectivityOverlay(
+ child: new ListView(
+ children: <Widget>[
+ new VideoCard(
+ title: 'Butterfly',
+ subtitle: '… flutters by',
+ controller: butterflyController,
+ ),
+ new VideoCard(
+ title: 'Bee',
+ subtitle: '… gently buzzing',
+ controller: beeController,
+ ),
+ ],
+ ),
+ connectedCompleter: connectedCompleter,
+ scaffoldKey: scaffoldKey,
+ ),
+ );
+ }
+}
diff --git a/examples/flutter_gallery/lib/gallery/item.dart b/examples/flutter_gallery/lib/gallery/item.dart
index 5aaf644..2a66be7 100644
--- a/examples/flutter_gallery/lib/gallery/item.dart
+++ b/examples/flutter_gallery/lib/gallery/item.dart
@@ -54,7 +54,7 @@
// Demos
new GalleryItem(
title: 'Shrine',
- subtitle:'Basic shopping app',
+ subtitle: 'Basic shopping app',
category: 'Demos',
routeName: ShrineDemo.routeName,
buildRoute: (BuildContext context) => new ShrineDemo(),
@@ -73,6 +73,13 @@
routeName: AnimationDemo.routeName,
buildRoute: (BuildContext context) => const AnimationDemo(),
),
+ new GalleryItem(
+ title: 'Video',
+ subtitle: 'Video playback',
+ category: 'Demos',
+ routeName: VideoDemo.routeName,
+ buildRoute: (BuildContext context) => const VideoDemo(),
+ ),
// Material Components
new GalleryItem(
title: 'Bottom navigation',
diff --git a/examples/flutter_gallery/pubspec.yaml b/examples/flutter_gallery/pubspec.yaml
index eac3305..4da5e75 100644
--- a/examples/flutter_gallery/pubspec.yaml
+++ b/examples/flutter_gallery/pubspec.yaml
@@ -4,9 +4,11 @@
sdk: flutter
collection: 1.14.3
intl: 0.15.2
+ connectivity: 0.1.0
string_scanner: 1.0.2
url_launcher: 0.4.2+5
cupertino_icons: 0.1.1
+ video_player: 0.0.5
# Also update dev/benchmarks/complex_layout/pubspec.yaml
flutter_gallery_assets: