diff --git a/examples/material_gallery/flutter.yaml b/examples/material_gallery/flutter.yaml index 1b0ab27013..4452dbd1ad 100644 --- a/examples/material_gallery/flutter.yaml +++ b/examples/material_gallery/flutter.yaml @@ -20,7 +20,10 @@ assets: - packages/flutter_gallery_assets/icon-snow.png - packages/flutter_gallery_assets/kangaroo_valley_safari.png - packages/flutter_gallery_assets/top_10_australian_beaches.png + - packages/flutter_gallery_assets/jumpingjack.json + - packages/flutter_gallery_assets/jumpingjack.png material-design-icons: + - name: action/accessibility - name: action/account_circle - name: action/alarm - name: action/android @@ -29,6 +32,8 @@ material-design-icons: - name: action/home - name: action/hourglass_empty - name: action/language + - name: av/play_arrow + - name: av/stop - name: communication/call - name: communication/email - name: communication/location_on @@ -39,6 +44,8 @@ material-design-icons: - name: content/create - name: image/brightness_5 - name: image/brightness_7 + - name: image/flash_on + - name: image/timer - name: navigation/arrow_back - name: navigation/arrow_drop_down - name: navigation/arrow_forward diff --git a/examples/material_gallery/lib/demo/fitness_demo.dart b/examples/material_gallery/lib/demo/fitness_demo.dart new file mode 100644 index 0000000000..e2e12ecfea --- /dev/null +++ b/examples/material_gallery/lib/demo/fitness_demo.dart @@ -0,0 +1,491 @@ +// Copyright 2015 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 'dart:math' as math; +import 'dart:ui' as ui; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_sprites/flutter_sprites.dart'; + +ImageMap _images; +SpriteSheet _sprites; + +class FitnessDemo extends StatelessComponent { + FitnessDemo({ Key key }) : super(key: key); + + Widget build(BuildContext context) { + return new Scaffold( + toolBar: new ToolBar( + center: new Text("Fitness") + ), + body: new _FitnessDemoContents() + ); + } +} + +class _FitnessDemoContents extends StatefulComponent { + _FitnessDemoContents({ Key key }) : super(key: key); + _FitnessDemoContentsState createState() => new _FitnessDemoContentsState(); +} + +class _FitnessDemoContentsState extends State<_FitnessDemoContents> { + + Future _loadAssets(AssetBundle bundle) async { + _images = new ImageMap(bundle); + await _images.load([ + 'packages/flutter_gallery_assets/jumpingjack.png', + ]); + + String json = await DefaultAssetBundle.of(context).loadString('packages/flutter_gallery_assets/jumpingjack.json'); + _sprites = new SpriteSheet(_images['packages/flutter_gallery_assets/jumpingjack.png'], json); + } + + void initState() { + super.initState(); + + AssetBundle bundle = DefaultAssetBundle.of(context); + _loadAssets(bundle).then((_) { + setState(() { + assetsLoaded = true; + workoutAnimation = new _WorkoutAnimationNode( + onPerformedJumpingJack: () { + setState(() { + count += 1; + }); + }, + onSecondPassed: (int seconds) { + setState(() { + time = seconds; + }); + } + ); + }); + }); + } + + bool assetsLoaded = false; + int count = 0; + int time = 0; + int get kcal => (count * 0.2).toInt(); + + _WorkoutAnimationNode workoutAnimation; + + Widget build(BuildContext context) { + if (!assetsLoaded) + return new Container(); + + Color buttonColor; + String buttonText; + VoidCallback onButtonPressed; + + if (workoutAnimation.workingOut) { + buttonColor = Colors.red[500]; + buttonText = "STOP WORKOUT"; + onButtonPressed = endWorkout; + } else { + buttonColor = Theme.of(context).primaryColor; + buttonText = "START WORKOUT"; + onButtonPressed = startWorkout; + } + + return new Material( + child: new Column( + justifyContent: FlexJustifyContent.center, + children: [ + new Flexible( + child: new Container( + decoration: new BoxDecoration(backgroundColor: Colors.grey[800]), + child: new SpriteWidget(workoutAnimation, SpriteBoxTransformMode.scaleToFit) + ) + ), + new Padding( + padding: new EdgeDims.only(top: 20.0), + child: new Text("JUMPING JACKS", style: Theme.of(context).text.title) + ), + new Padding( + padding: new EdgeDims.only(top: 20.0, bottom: 20.0), + child: new Row( + justifyContent: FlexJustifyContent.center, + children: [ + _createInfoPanelCell("action/accessibility", "$count", "COUNT"), + _createInfoPanelCell("image/timer", _formatSeconds(time), "TIME"), + _createInfoPanelCell("image/flash_on", "$kcal", "KCAL") + ] + ) + ), + new Padding( + padding: new EdgeDims.only(bottom: 16.0), + child: new SizedBox( + width: 300.0, + height: 72.0, + child: new RaisedButton ( + onPressed: onButtonPressed, + color: buttonColor, + child: new Text( + buttonText, + style: new TextStyle(color: Colors.white, fontSize: 20.0) + ) + ) + ) + ) + ] + ) + ); + } + + Widget _createInfoPanelCell(String icon, String value, String description) { + Color color; + if (workoutAnimation.workingOut) + color = Colors.black87; + else + color = Theme.of(context).disabledColor; + + return new Container( + width: 100.0, + child: new Center( + child: new Column( + children: [ + new Icon(icon: icon, size: IconSize.s48, color: color), + new Text(value, style: new TextStyle(fontSize: 24.0, color: color)), + new Text(description, style: new TextStyle(color: color)) + ] + ) + ) + ); + } + + String _formatSeconds(int seconds) { + int minutes = seconds ~/ 60; + String secondsStr = "${seconds % 60}".padLeft(2, "0"); + return "$minutes:$secondsStr"; + } + + void startWorkout() { + setState(() { + count = 0; + time = 0; + workoutAnimation.start(); + }); + } + + void endWorkout() { + setState(() { + workoutAnimation.stop(); + }); + } +} + +typedef void _SecondPassedCallback(int seconds); + +class _WorkoutAnimationNode extends NodeWithSize { + _WorkoutAnimationNode({ + this.onPerformedJumpingJack, + this.onSecondPassed + }) : super(const Size(1024.0, 1024.0)) { + reset(); + + _progress = new _ProgressCircle(const Size(800.0, 800.0)); + _progress.pivot = const Point(0.5, 0.5); + _progress.position = const Point(512.0, 512.0); + addChild(_progress); + + _jumpingJack = new _JumpingJack((){ + onPerformedJumpingJack(); + }); + _jumpingJack.scale = 0.5; + _jumpingJack.position = const Point(512.0, 550.0); + addChild(_jumpingJack); + } + + final VoidCallback onPerformedJumpingJack; + final _SecondPassedCallback onSecondPassed; + + int seconds; + + bool workingOut; + + static const int _kTargetMillis = 1000 * 30; + int _startTimeMillis; + _ProgressCircle _progress; + _JumpingJack _jumpingJack; + + void reset() { + seconds = 0; + workingOut = false; + } + + void start() { + reset(); + _startTimeMillis = new DateTime.now().millisecondsSinceEpoch; + workingOut = true; + _jumpingJack.animateJumping(); + } + + void stop() { + workingOut = false; + _jumpingJack.neutralPose(); + } + + void update(double dt) { + if (workingOut) { + int millis = new DateTime.now().millisecondsSinceEpoch - _startTimeMillis; + int newSeconds = (millis) ~/ 1000; + if (newSeconds != seconds) { + seconds = newSeconds; + onSecondPassed(seconds); + } + + _progress.value = millis / _kTargetMillis; + } else { + _progress.value = 0.0; + } + } +} + +class _ProgressCircle extends NodeWithSize { + _ProgressCircle(Size size, [this.value = 0.0]) : super(size); + + static const _kTwoPI = math.PI * 2.0; + static const _kEpsilon = .0000001; + static const _kSweep = _kTwoPI - _kEpsilon; + + double value; + + void paint(Canvas canvas) { + applyTransformForPivot(canvas); + + Paint circlePaint = new Paint() + ..color = Colors.white30 + ..strokeWidth = 24.0 + ..style = ui.PaintingStyle.stroke; + + canvas.drawCircle( + new Point(size.width / 2.0, size.height / 2.0), + size.width / 2.0, + circlePaint + ); + + Paint pathPaint = new Paint() + ..color = Colors.purple[500] + ..strokeWidth = 25.0 + ..style = ui.PaintingStyle.stroke; + + double angle = value.clamp(0.0, 1.0) * _kSweep; + Path path = new Path() + ..arcTo(Point.origin & size, -math.PI / 2.0, angle, false); + canvas.drawPath(path, pathPaint); + } +} + +class _JumpingJack extends Node { + _JumpingJack(VoidCallback onPerformedJumpingJack) { + left = new _JumpingJackSide(false, onPerformedJumpingJack); + right = new _JumpingJackSide(true, null); + addChild(left); + addChild(right); + } + + void animateJumping() { + left.animateJumping(); + right.animateJumping(); + } + + void neutralPose() { + left.neutralPosition(true); + right.neutralPosition(true); + } + + _JumpingJackSide left; + _JumpingJackSide right; +} + +class _JumpingJackSide extends Node { + _JumpingJackSide(bool right, this.onPerformedJumpingJack) { + // Torso and head + torso = _createPart('torso.png', const Point(512.0, 512.0)); + addChild(torso); + + head = _createPart('head.png', const Point(512.0, 160.0)); + torso.addChild(head); + + if (right) { + torso.opacity = 0.0; + head.opacity = 0.0; + torso.scaleX = -1.0; + } + + // Left side movable parts + upperArm = _createPart('upper-arm.png', const Point(445.0, 220.0)); + torso.addChild(upperArm); + lowerArm = _createPart('lower-arm.png', const Point(306.0, 200.0)); + upperArm.addChild(lowerArm); + hand = _createPart('hand.png', const Point(215.0, 127.0)); + lowerArm.addChild(hand); + upperLeg = _createPart('upper-leg.png', const Point(467.0, 492.0)); + torso.addChild(upperLeg); + lowerLeg = _createPart('lower-leg.png', const Point(404.0, 660.0)); + upperLeg.addChild(lowerLeg); + foot = _createPart('foot.png', const Point(380.0, 835.0)); + lowerLeg.addChild(foot); + + torso.setPivotAndPosition(Point.origin); + + neutralPosition(false); + } + + _JumpingJackPart torso; + _JumpingJackPart head; + _JumpingJackPart upperArm; + _JumpingJackPart lowerArm; + _JumpingJackPart hand; + _JumpingJackPart lowerLeg; + _JumpingJackPart upperLeg; + _JumpingJackPart foot; + + final VoidCallback onPerformedJumpingJack; + + _JumpingJackPart _createPart(String textureName, Point pivotPosition) { + return new _JumpingJackPart(_sprites[textureName], pivotPosition); + } + + void animateJumping() { + actions.stopAll(); + actions.run(new ActionSequence([ + _createPoseAction(null, 0, 0.5), + new ActionCallFunction(_animateJumpingLoop) + ])); + } + + void _animateJumpingLoop() { + actions.run(new ActionRepeatForever( + new ActionSequence([ + _createPoseAction(0, 1, 0.30), + _createPoseAction(1, 2, 0.30), + _createPoseAction(2, 1, 0.30), + _createPoseAction(1, 0, 0.30), + new ActionCallFunction(() { + if (onPerformedJumpingJack != null) + onPerformedJumpingJack(); + }) + ]) + )); + } + + void neutralPosition(bool animate) { + actions.stopAll(); + if (animate) { + actions.run(_createPoseAction(null, 1, 0.5)); + } else { + List d = _dataForPose(1); + upperArm.rotation = d[0]; + lowerArm.rotation = d[1]; + hand.rotation = d[2]; + upperLeg.rotation = d[3]; + lowerLeg.rotation = d[4]; + foot.rotation = d[5]; + torso.position = new Point(0.0, d[6]); + } + } + + ActionInterval _createPoseAction(int startPose, int endPose, double duration) { + List d0 = _dataForPose(startPose); + List d1 = _dataForPose(endPose); + + List tweens = [ + _tweenRotation(upperArm, d0[0], d1[0], duration), + _tweenRotation(lowerArm, d0[1], d1[1], duration), + _tweenRotation(hand, d0[2], d1[2], duration), + _tweenRotation(upperLeg, d0[3], d1[3], duration), + _tweenRotation(lowerLeg, d0[4], d1[4], duration), + _tweenRotation(foot, d0[5], d1[5], duration), + new ActionTween( + (Point a) => torso.position = a, + new Point(0.0, d0[6]), + new Point(0.0, d1[6]), + duration + ) + ]; + + return new ActionGroup(tweens); + } + + ActionTween _tweenRotation(_JumpingJackPart part, double r0, double r1, double duration) { + return new ActionTween( + (double a) => part.rotation = a, + r0, + r1, + duration + ); + } + + List _dataForPose(int pose) { + if (pose == null) + return _dataForCurrentPose(); + + if (pose == 0) { + return [ + -80.0, // Upper arm rotation + -30.0, // Lower arm rotation + -10.0, // Hand rotation + -15.0, // Upper leg rotation + 5.0, // Lower leg rotation + 15.0, // Foot rotation + 0.0 // Torso y offset + ]; + } else if (pose == 1) { + return [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -70.0 + ]; + } else { + return [ + 40.0, + 30.0, + 10.0, + 20.0, + -20.0, + 15.0, + 40.0 + ]; + } + } + + List _dataForCurrentPose() { + return [ + upperArm.rotation, + lowerArm.rotation, + hand.rotation, + upperLeg.rotation, + lowerLeg.rotation, + foot.rotation, + torso.position.y + ]; + } +} + +class _JumpingJackPart extends Sprite { + _JumpingJackPart(Texture texture, this.pivotPosition) : super(texture); + final Point pivotPosition; + + void setPivotAndPosition(Point newPosition) { + pivot = new Point(pivotPosition.x / 1024.0, pivotPosition.y / 1024.0); + position = newPosition; + + for (Node child in children) { + _JumpingJackPart subPart = child; + subPart.setPivotAndPosition( + new Point( + subPart.pivotPosition.x - pivotPosition.x, + subPart.pivotPosition.y - pivotPosition.y + ) + ); + } + } +} diff --git a/examples/material_gallery/lib/gallery/home.dart b/examples/material_gallery/lib/gallery/home.dart index bf80312523..5d1e07f60c 100644 --- a/examples/material_gallery/lib/gallery/home.dart +++ b/examples/material_gallery/lib/gallery/home.dart @@ -30,6 +30,7 @@ import '../demo/time_picker_demo.dart'; import '../demo/two_level_list_demo.dart'; import '../demo/typography_demo.dart'; import '../demo/weathers_demo.dart'; +import '../demo/fitness_demo.dart'; class GalleryHome extends StatefulComponent { GalleryHome({ Key key }) : super(key: key); @@ -65,7 +66,8 @@ class GalleryHomeState extends State { image: 'assets/section_animation.png', colors: Colors.purple, demos: [ - new GalleryDemo(title: 'Weathers', builder: () => new WeathersDemo()) + new GalleryDemo(title: 'Weathers', builder: () => new WeathersDemo()), + new GalleryDemo(title: 'Fitness', builder: () => new FitnessDemo()) ] ), new GallerySection(