blob: de932aa83204b7bcfba4a3bf330a3b5b85b6d5c5 [file] [log] [blame]
// Copyright 2015 The Vanadium 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 '../logic/card.dart' as logic_card;
import 'dart:async';
import 'package:flutter/animation.dart';
import 'package:flutter/material.dart' as widgets;
import 'package:flutter/rendering.dart';
import 'package:vector_math/vector_math_64.dart' as vector_math;
enum CardAnimationType {
NONE, OLD_TO_NEW, IN_TOP
}
enum CardUIType {
CARD, ZCARD
}
class GlobalCardKey extends widgets.GlobalKey {
logic_card.Card card;
CardUIType type;
GlobalCardKey(this.card, this.type): super.constructor();
bool operator ==(Object other) {
if (other is! GlobalCardKey) {
return false;
}
GlobalCardKey k = other;
return k.card == card && k.type == type;
}
int get hashCode {
return 17 * card.hashCode + 33 * type.hashCode;
}
}
class ZCard extends widgets.StatefulComponent {
final logic_card.Card card;
final bool faceUp;
final double width;
final double height;
final double rotation;
final bool animateEntrance;
final double z;
final Point startingPosition;
final Point endingPosition;
ZCard(Card dataComponent, this.startingPosition, this.endingPosition) :
super(key: new GlobalCardKey(dataComponent.card, CardUIType.ZCARD)),
card = dataComponent.card,
faceUp = dataComponent.faceUp,
width = dataComponent.width ?? 40.0,
height = dataComponent.height ?? 40.0,
rotation = dataComponent.rotation,
animateEntrance = dataComponent.animateEntrance,
z = dataComponent.z;
_ZCardState createState() => new _ZCardState();
}
class Card extends widgets.StatefulComponent {
final logic_card.Card card;
final bool faceUp;
final double width;
final double height;
final double rotation;
final bool useKey;
final bool visible;
final bool animateEntrance;
final double z;
Card(logic_card.Card card, this.faceUp,
{double width, double height, this.rotation: 0.0, bool useKey: false, this.visible: true, this.animateEntrance: true, this.z})
: card = card,
width = width ?? 40.0,
height = height ?? 40.0,
useKey = useKey,
super(key: useKey ? new GlobalCardKey(card, CardUIType.CARD) : null);
// Use this helper to help create a Card clone.
// Used by the drag and drop layer.
Card clone({bool visible}) {
return new Card(this.card, this.faceUp,
width: width, height: height, rotation: rotation,
useKey: false, visible: visible ?? this.visible, animateEntrance: false,
z: z);
}
CardState createState() => new CardState();
}
class CardState extends widgets.State<Card> {
// TODO(alexfandrianto): This bug is why some cards appear slightly off.
// https://github.com/flutter/engine/issues/1939
Point getGlobalPosition() {
RenderBox box = context.findRenderObject();
return box.localToGlobal(Point.origin);
}
widgets.Widget build(widgets.BuildContext context) {
widgets.Widget image = null;
if (config.visible) {
image = new widgets.Transform(
child: _imageFromCard(config.card, config.faceUp),
transform: new vector_math.Matrix4.identity()
.rotateZ(config.rotation),
alignment: new FractionalOffset(0.5, 0.5));
}
return new widgets.Container(
width: config.width,
height: config.height,
child: image);
}
}
widgets.Widget _imageFromCard(logic_card.Card c, bool faceUp) {
// TODO(alexfandrianto): Instead of 'default', what if we were told which theme to use?
String imageName =
"images/default/${c.deck}/${faceUp ? 'up' : 'down'}/${c.identifier}.png";
return new widgets.NetworkImage(src: imageName);
}
class _ZCardState extends widgets.State<ZCard> {
ValuePerformance<Point> _performance;
List<Point> _pointQueue; // at least 1 longer than the current animation index.
int _animationIndex;
bool _cardUpdateScheduled = false;
@override
void initState() {
super.initState();
_initialize();
scheduleUpdatePosition();
}
void _initialize() {
_pointQueue = new List<Point>();
_animationIndex = 0;
if (config.startingPosition != null) {
_pointQueue.add(config.startingPosition);
}
_pointQueue.add(config.endingPosition);
_performance = new ValuePerformance<Point>(
variable: new AnimatedValue<Point>(Point.origin, curve: Curves.ease),
duration: const Duration(milliseconds: 250)
);
_performance.addStatusListener((PerformanceStatus status) {
if (status == PerformanceStatus.completed) {
_animationIndex++;
_tryAnimate();
}
});
}
void scheduleUpdatePosition() {
if (!_cardUpdateScheduled) {
_cardUpdateScheduled = true;
scheduleMicrotask(_updatePosition);
}
}
// These microtasks are being scheduled on every build change.
// Theoretically, this is too often, but to be safe, it is also good to do it.
@override
void didUpdateConfig(ZCard oldConfig) {
if (config.key != oldConfig.key) {
_initialize();
} else {
// Do we need to animate to a new location? If so, add it to the queue.
if (config.endingPosition != _pointQueue.last) {
setState(() {
_pointQueue.add(config.endingPosition);
_tryAnimate();
});
}
}
scheduleUpdatePosition();
}
// A callback that sets up the animation from point a to point b.
void _updatePosition() {
_cardUpdateScheduled = false; // allow the next attempt to schedule _updatePosition to succeed.
if (!config.animateEntrance || _pointQueue.length == 1) {
RenderBox box = context.findRenderObject();
Point endingLocation = box.globalToLocal(config.endingPosition);
_performance.variable
..begin = endingLocation
..value = endingLocation
..end = endingLocation;
_performance.progress = 0.0;
return;
}
_tryAnimate();
}
bool _needsAnimation() {
return _animationIndex < _pointQueue.length - 1;
}
void _tryAnimate() {
// Let animations finish... (Is this a good idea?)
if (!_performance.isAnimating && _needsAnimation()) {
RenderBox box = context.findRenderObject();
Point globalStart = _pointQueue[_animationIndex];
Point globalEnd = _pointQueue[_animationIndex + 1];
Point startingLocation = box.globalToLocal(globalStart);
Point endingLocation = box.globalToLocal(globalEnd);
_performance.variable
..begin = startingLocation
..value = startingLocation
..end = endingLocation;
_performance.progress = 0.0;
_performance.play();
}
}
widgets.Widget build(widgets.BuildContext context) {
widgets.Widget image = new widgets.Transform(
child: _imageFromCard(config.card, config.faceUp),
transform: new vector_math.Matrix4.identity()
.rotateZ(config.rotation),
alignment: new FractionalOffset(0.5, 0.5));
// Set up the drag listener.
widgets.Widget listeningCard = new widgets.Listener(
child: new widgets.Container(
width: config.width,
height: config.height,
child: image));
// Set up the slide transition.
// During animation, we must ignore all events.
widgets.Widget retWidget = new widgets.IgnorePointer(
ignoring: _performance.isAnimating,
child: new widgets.SlideTransition(
performance: _performance.view,
position: _performance.variable,
child: listeningCard
)
);
return retWidget;
}
}