Merge pull request #1777 from vlidholt/master
Fitness demo, initial version
diff --git a/examples/material_gallery/flutter.yaml b/examples/material_gallery/flutter.yaml
index 1b0ab27..4452dbd 100644
--- a/examples/material_gallery/flutter.yaml
+++ b/examples/material_gallery/flutter.yaml
@@ -20,7 +20,10 @@
- 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 @@
- 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 @@
- 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 0000000..e2e12ec
--- /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(<String>[
+ '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: <Widget>[
+ 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: <Widget>[
+ _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: <Widget>[
+ 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(<Action>[
+ _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<double> 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<double> d0 = _dataForPose(startPose);
+ List<double> d1 = _dataForPose(endPose);
+
+ List<ActionTween> tweens = <ActionTween>[
+ _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<double> _dataForPose(int pose) {
+ if (pose == null)
+ return _dataForCurrentPose();
+
+ if (pose == 0) {
+ return <double>[
+ -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 <double>[
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ -70.0
+ ];
+ } else {
+ return <double>[
+ 40.0,
+ 30.0,
+ 10.0,
+ 20.0,
+ -20.0,
+ 15.0,
+ 40.0
+ ];
+ }
+ }
+
+ List<double> _dataForCurrentPose() {
+ return <double>[
+ 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 bf80312..5d1e07f 100644
--- a/examples/material_gallery/lib/gallery/home.dart
+++ b/examples/material_gallery/lib/gallery/home.dart
@@ -30,6 +30,7 @@
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 @@
image: 'assets/section_animation.png',
colors: Colors.purple,
demos: <GalleryDemo>[
- new GalleryDemo(title: 'Weathers', builder: () => new WeathersDemo())
+ new GalleryDemo(title: 'Weathers', builder: () => new WeathersDemo()),
+ new GalleryDemo(title: 'Fitness', builder: () => new FitnessDemo())
]
),
new GallerySection(