Commit 202f99d7 authored by Rafael Weinstein's avatar Rafael Weinstein

Initial commit of Effen reactive framework experiment for Sky

This is just a proof of concept. If we like this direction, it will move out of the examples directory (likely re-written) and be committed in smaller pieces with unit tests and formal reviews.

TBR=abarth
BUG=

Review URL: https://codereview.chromium.org/971183002
parent 6f92088f
This diff is collapsed.
part of fn;
List<Component> _dirtyComponents = new List<Component>();
bool _renderScheduled = false;
void _renderDirtyComponents() {
Stopwatch sw = new Stopwatch()..start();
_dirtyComponents.sort((a, b) => a._order - b._order);
for (var comp in _dirtyComponents) {
comp._renderIfDirty();
}
_dirtyComponents.clear();
_renderScheduled = false;
sw.stop();
print("Render took ${sw.elapsedMicroseconds} microseconds");
}
void _scheduleComponentForRender(Component c) {
_dirtyComponents.add(c);
if (!_renderScheduled) {
_renderScheduled = true;
new Future.microtask(_renderDirtyComponents);
}
}
abstract class Component extends Node {
bool _dirty = true; // components begin dirty because they haven't rendered.
Node _rendered = null;
bool _removed = false;
int _order;
static int _currentOrder = 0;
bool _stateful;
static Component _currentlyRendering;
Component({ Object key, bool stateful })
: _stateful = stateful != null ? stateful : false,
_order = _currentOrder + 1,
super(key:key);
void willUnmount() {}
void _remove() {
assert(_rendered != null);
assert(_root != null);
willUnmount();
_rendered._remove();
_rendered = null;
_root = null;
_removed = true;
}
// TODO(rafaelw): It seems wrong to expose DOM at all. This is presently
// needed to get sizing info.
sky.Node getRoot() => _root;
bool _sync(Node old, sky.Node host, sky.Node insertBefore) {
Component oldComponent = old as Component;
if (oldComponent == null || oldComponent == this) {
_renderInternal(host, insertBefore);
return false;
}
assert(oldComponent != null);
assert(_dirty);
assert(_rendered == null);
if (oldComponent._stateful) {
_stateful = false; // prevent iloop from _renderInternal below.
reflect.copyPublicFields(this, oldComponent);
oldComponent._dirty = true;
_dirty = false;
oldComponent._renderInternal(host, insertBefore);
return true; // Must retain old component
}
_rendered = oldComponent._rendered;
_renderInternal(host, insertBefore);
return false;
}
void _renderInternal(sky.Node host, sky.Node insertBefore) {
if (!_dirty) {
assert(_rendered != null);
return;
}
var oldRendered = _rendered;
int lastOrder = _currentOrder;
_currentOrder = _order;
_currentlyRendering = this;
_rendered = render();
_currentlyRendering = null;
_currentOrder = lastOrder;
_dirty = false;
// TODO(rafaelw): This prevents components from returning different node
// types as their root node at different times. Consider relaxing.
assert(oldRendered == null ||
_rendered.runtimeType == oldRendered.runtimeType);
if (_rendered._sync(oldRendered, host, insertBefore)) {
_rendered = oldRendered; // retain stateful component
}
_root = _rendered._root;
assert(_rendered._root is sky.Node);
}
void _renderIfDirty() {
assert(_rendered != null);
assert(!_removed);
var rendered = _rendered;
while (rendered is Component) {
rendered = rendered._rendered;
}
sky.Node root = rendered._root;
_renderInternal(root.parentNode, root.nextSibling);
}
void setState(Function fn()) {
assert(_rendered != null); // cannot setState before mounting.
_stateful = true;
fn();
if (_currentlyRendering != this) {
_dirty = true;
_scheduleComponentForRender(this);
}
}
Node render();
}
abstract class App extends Component {
sky.Node _host = null;
App()
: super(stateful: true) {
_host = sky.document.createElement('div');
sky.document.appendChild(_host);
new Future.microtask(() {
Stopwatch sw = new Stopwatch()..start();
_sync(null, _host, null);
assert(_root is sky.Node);
sw.stop();
print("Initial render: ${sw.elapsedMicroseconds} microseconds");
});
}
}
import 'dart:async';
void assertHasParentNode(Node n) { assert(n.parentNode != null); }
void assertHasParentNodes(List<Node> list) {
for (var n in list) {
assertHasParentNode(n);
}
}
class Node {
ParentNode parentNode;
Node nextSibling;
Node previousSibling;
Node();
void insertBefore(List<Node> nodes) {
int count = nodes.length;
while (count-- > 0) {
parentNode._insertBefore(nodes[count], this);
}
assertHasParentNodes(nodes);
}
remove() {
if (parentNode == null) {
return;
}
if (nextSibling != null) {
nextSibling.previousSibling = previousSibling;
} else {
parentNode.lastChild = previousSibling;
}
if (previousSibling != null) {
previousSibling.nextSibling = nextSibling;
} else {
parentNode.firstChild = nextSibling;
}
parentNode = null;
nextSibling = null;
previousSibling = null;
}
}
class Text extends Node {
String data;
Text(this.data) : super();
}
class ParentNode extends Node {
Node firstChild;
Node lastChild;
ParentNode() : super();
Node setChild(Node node) {
firstChild = node;
lastChild = node;
node.parentNode = this;
assertHasParentNode(node);
return node;
}
Node _insertBefore(Node node, Node ref) {
assert(ref == null || ref.parentNode == this);
if (node.parentNode != null) {
node.remove();
}
node.parentNode = this;
if (firstChild == null && lastChild == null) {
firstChild = node;
lastChild = node;
} else if (ref == null) {
node.previousSibling = lastChild;
lastChild.nextSibling = node;
lastChild = node;
} else {
if (ref == firstChild) {
assert(ref.previousSibling == null);
firstChild = node;
}
node.previousSibling = ref.previousSibling;
ref.previousSibling = node;
node.nextSibling = ref;
}
assertHasParentNode(node);
return node;
}
Node appendChild(Node node) {
return _insertBefore(node, null);
}
}
class Element extends ParentNode {
void addEventListener(String type, EventListener listener, [bool useCapture = false]) {}
void removeEventListener(String type, EventListener listener) {}
void setAttribute(String name, [String value]) {}
}
class Document extends ParentNode {
Document();
Element createElement(String tagName) {
switch (tagName) {
case 'img' : return new HTMLImageElement();
default : return new Element();
}
}
}
class HTMLImageElement extends Element {
Image();
String src;
Object style = {};
}
class Event {
Event();
}
typedef EventListener(Event event);
void _callRAF(Function fn) {
fn(new DateTime.now().millisecondsSinceEpoch.toDouble());
}
class Window {
int requestAnimationFrame(Function fn) {
new Timer(const Duration(milliseconds: 16), () {
_callRAF(fn);
});
}
void cancelAnimationFrame(int id) {
}
}
Document document = new Document();
Window window = new Window();
library fn;
import 'dart:async';
import 'dart:collection';
import 'dart:sky' as sky;
import 'reflect.dart' as reflect;
part 'component.dart';
part 'node.dart';
part 'style.dart';
bool _checkedMode;
bool debugWarnings() {
void testFn(double i) {}
if (_checkedMode == null) {
_checkedMode = false;
try {
testFn('not a double');
} catch (ex) {
_checkedMode = true;
}
}
return _checkedMode;
}
This diff is collapsed.
library reflect;
import 'dart:mirrors';
import 'dart:collection';
HashMap<ClassMirror, List> _fieldCache = new HashMap<ClassMirror, List>();
List<Symbol> _getPublicFields(ClassMirror mirror) {
var fields = _fieldCache[mirror];
if (fields == null) {
fields = new List<Symbol>();
_fieldCache[mirror] = fields;
while (mirror != null) {
var decls = mirror.declarations;
fields.addAll(decls.keys.where((symbol) {
var mirror = decls[symbol];
if (mirror is! VariableMirror) {
return false;
}
var vMirror = mirror as VariableMirror;
return !vMirror.isPrivate && !vMirror.isStatic && !vMirror.isFinal;
}));
mirror = mirror.superclass;
}
}
return fields;
}
void copyPublicFields(Object source, Object target) {
assert(source.runtimeType == target.runtimeType);
var sourceMirror = reflect(source);
var targetMirror = reflect(target);
for (var symbol in _getPublicFields(sourceMirror.type)) {
targetMirror.setField(symbol, sourceMirror.getField(symbol).reflectee);
}
}
part of fn;
class Style {
final String _className;
static Map<String, Style> _cache = null;
static int nextStyleId = 1;
static String nextClassName(String styles) {
assert(sky.document != null);
var className = "style$nextStyleId";
nextStyleId++;
var styleNode = sky.document.createElement('style');
styleNode.setChild(new sky.Text(".$className { $styles }"));
sky.document.appendChild(styleNode);
return className;
}
factory Style(String styles) {
if (_cache == null) {
_cache = new HashMap<String, Style>();
}
var style = _cache[styles];
if (style == null) {
style = new Style._internal(nextClassName(styles));
_cache[styles] = style;
}
return style;
}
Style._internal(this._className);
}
part of widgets;
class FrameGenerator {
Function onDone;
StreamController _controller;
Stream<double> get onTick => _controller.stream;
int _animationId = 0;
bool _cancelled = false;
FrameGenerator({this.onDone}) {
_controller = new StreamController(
sync: true,
onListen: _scheduleTick,
onCancel: cancel);
}
void cancel() {
if (_cancelled) {
return;
}
if (_animationId != 0) {
sky.window.cancelAnimationFrame(_animationId);
}
_animationId = 0;
_cancelled = true;
if (onDone != null) {
onDone();
}
}
void _scheduleTick() {
assert(_animationId == 0);
_animationId = sky.window.requestAnimationFrame(_tick);
}
void _tick(double timeStamp) {
_animationId = 0;
_controller.add(timeStamp);
if (!_cancelled) {
_scheduleTick();
}
}
}
const double _kFrameTime = 1000 / 60;
class AnimationGenerator extends FrameGenerator {
Stream<double> get onTick => _stream;
final double duration;
final double begin;
final double end;
final Curve curve;
Stream<double> _stream;
AnimationGenerator(this.duration, {
this.begin: 0.0,
this.end: 1.0,
this.curve: linear,
Function onDone
}):super(onDone: onDone) {
double startTime = 0.0;
double targetTime = 0.0;
bool done = false;
_stream = super.onTick.map((timeStamp) {
if (startTime == 0.0) {
startTime = timeStamp;
targetTime = startTime + duration;
}
// Clamp the final frame to target time so we terminate the series with
// 1.0 exactly.
if ((timeStamp - targetTime).abs() <= _kFrameTime) {
return 1.0;
}
return (timeStamp - startTime) / duration;
})
.takeWhile((t) => t <= 1.0)
.map((t) => begin + (end - begin) * curve.transform(t));
}
}
double _evaluateCubic(double a, double b, double m) {
// TODO(abarth): Would Math.pow be faster?
return 3 * a * (1 - m) * (1 - m) * m + 3 * b * (1 - m) * m * m + m * m * m;
}
const double _kCubicErrorBound = 0.001;
abstract class Curve {
double transform(double t);
}
class Linear implements Curve {
const Linear();
double transform(double t) {
return t;
}
}
class Cubic implements Curve {
final double a;
final double b;
final double c;
final double d;
const Cubic(this.a, this.b, this.c, this.d);
double transform(double t) {
if (t == 0.0 || t == 1.0)
return t;
double start = 0.0;
double end = 1.0;
while (true) {
double midpoint = (start + end) / 2;
double estimate = _evaluateCubic(a, c, midpoint);
if ((t - estimate).abs() < _kCubicErrorBound)
return _evaluateCubic(b, d, midpoint);
if (estimate < t)
start = midpoint;
else
end = midpoint;
}
}
}
const Linear linear = const Linear();
const Cubic ease = const Cubic(0.25, 0.1, 0.25, 1.0);
const Cubic easeIn = const Cubic(0.42, 0.0, 1.0, 1.0);
const Cubic easeOut = const Cubic(0.0, 0.0, 0.58, 1.0);
const Cubic easeInOut = const Cubic(0.42, 0.0, 0.58, 1.0);
part of widgets;
class Box extends Component {
static Style _style = new Style('''
display: flex;
flex-direction: column;
border-radius: 4px;
border: 1px solid gray;
margin: 10px;'''
);
static Style _titleStyle = new Style('''
flex: 1;
text-align: center;
font-size: 10px;
padding: 8px 8px 4px 8px;'''
);
static Style _contentStyle = new Style('''
flex: 1;
padding: 4px 8px 8px 8px;'''
);
String title;
List<Node> children;
Box({String key, this.title, this.children }) : super(key: key);
Node render() {
return new Container(
style: _style,
children: [
new Container(
key: 'Title',
style: _titleStyle,
children: [new Text(title)]
),
new Container(
key: 'Content',
style: _contentStyle,
children: children
),
]
);
}
}
part of widgets;
class Button extends ButtonBase {
static Style _style = new Style('''
display: inline-flex;
border-radius: 4px;
justify-content: center;
align-items: center;
border: 1px solid blue;
-webkit-user-select: none;
margin: 5px;'''
);
static Style _highlightStyle = new Style('''
display: inline-flex;
border-radius: 4px;
justify-content: center;
align-items: center;
border: 1px solid blue;
-webkit-user-select: none;
margin: 5px;
background-color: orange;'''
);
Node content;
sky.EventListener onClick;
Button({ Object key, this.content, this.onClick }) : super(key: key);
Node render() {
return new Container(
key: 'Button',
style: _highlight ? _highlightStyle : _style,
onClick: onClick,
onPointerDown: _handlePointerDown,
onPointerUp: _handlePointerUp,
onPointerCancel: _handlePointerCancel,
children: [content]
);
}
}
part of widgets;
abstract class ButtonBase extends Component {
bool _highlight = false;
ButtonBase({ Object key }) : super(key: key);
void _handlePointerDown(_) {
setState(() {
_highlight = true;
});
}
void _handlePointerUp(_) {
setState(() {
_highlight = false;
});
}
void _handlePointerCancel(_) {
setState(() {
_highlight = false;
});
}
}
part of widgets;
class Checkbox extends ButtonBase {
bool checked;
ValueChanged onChanged;
static Style _style = new Style('''
display: flex;
justify-content: center;
align-items: center;
-webkit-user-select: none;
cursor: pointer;
width: 30px;
height: 30px;'''
);
static Style _containerStyle = new Style('''
border: solid 2px;
border-color: rgba(90, 90, 90, 0.25);
width: 10px;
height: 10px;'''
);
static Style _containerHighlightStyle = new Style('''
border: solid 2px;
border-color: rgba(90, 90, 90, 0.25);
width: 10px;
height: 10px;
border-radius: 10px;
background-color: orange;
border-color: orange;'''
);
static Style _uncheckedStyle = new Style('''
top: 0px;
left: 0px;'''
);
static Style _checkedStyle = new Style('''
top: 0px;
left: 0px;
transform: translate(2px, -15px) rotate(45deg);
width: 10px;
height: 20px;
border-style: solid;
border-top: none;
border-left: none;
border-right-width: 2px;
border-bottom-width: 2px;
border-color: #0f9d58;'''
);
Checkbox({ Object key, this.onChanged, this.checked }) : super(key: key);
Node render() {
return new Container(
style: _style,
onClick: _handleClick,
onPointerDown: _handlePointerDown,
onPointerUp: _handlePointerUp,
onPointerCancel: _handlePointerCancel,
children: [
new Container(
style: _highlight ? _containerHighlightStyle : _containerStyle,
children: [
new Container(
style: checked ? _checkedStyle : _uncheckedStyle
)
]
)
]
);
}
void _handleClick(sky.Event e) {
onChanged(!checked);
}
}
part of widgets;
const double _kWidth = 256.0;
const double _kMinFlingVelocity = 0.4;
const double _kMinAnimationDurationMS = 246.0;
const double _kMaxAnimationDurationMS = 600.0;
const Cubic _kAnimationCurve = easeOut;
class DrawerAnimation {
Stream<double> get onPositionChanged => _controller.stream;
StreamController _controller;
AnimationGenerator _animation;
double _position;
bool get _isAnimating => _animation != null;
bool get _isMostlyClosed => _position <= -_kWidth / 2;
DrawerAnimation() {
_controller = new StreamController(sync: true);
_setPosition(-_kWidth);
}
void toggle(_) => _isMostlyClosed ? _open() : _close();
void handleMaskTap(_) => _close();
void handlePointerDown(_) => _cancelAnimation();
void handlePointerMove(sky.PointerEvent event) {
assert(_animation == null);
_setPosition(_position + event.dx);
}
void handlePointerUp(_) {
if (!_isAnimating)
_settle();
}
void handlePointerCancel(_) {
if (!_isAnimating)
_settle();
}
void _open() => _animateToPosition(0.0);
void _close() => _animateToPosition(-_kWidth);
void _settle() => _isMostlyClosed ? _close() : _open();
void _setPosition(double value) {
_position = math.min(0.0, math.max(value, -_kWidth));
_controller.add(_position);
}
void _cancelAnimation() {
if (_animation != null) {
_animation.cancel();
_animation = null;
}
}
void _animate(double duration, double begin, double end, Curve curve) {
_cancelAnimation();
_animation = new AnimationGenerator(duration, begin: begin, end: end,
curve: curve);
_animation.onTick.listen(_setPosition, onDone: () {
_animation = null;
});
}
void _animateToPosition(double targetPosition) {
double distance = (targetPosition - _position).abs();
double duration = math.max(
_kMinAnimationDurationMS,
_kMaxAnimationDurationMS * distance / _kWidth);
_animate(duration, _position, targetPosition, _kAnimationCurve);
}
void handleFlingStart(event) {
double direction = event.velocityX.sign;
double velocityX = event.velocityX.abs() / 1000;
if (velocityX < _kMinFlingVelocity)
return;
double targetPosition = direction < 0.0 ? -_kWidth : 0.0;
double distance = (targetPosition - _position).abs();
double duration = distance / velocityX;
_animate(duration, _position, targetPosition, linear);
}
}
class Drawer extends Component {
static Style _style = new Style('''
position: absolute;
z-index: 2;
top: 0;
left: 0;
bottom: 0;
right: 0;
box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23);'''
);
static Style _maskStyle = new Style('''
background-color: black;
will-change: opacity;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;'''
);
static Style _contentStyle = new Style('''
background-color: #FAFAFA;
will-change: transform;
position: absolute;
width: 256px;
top: 0;
left: 0;
bottom: 0;'''
);
Stream<double> onPositionChanged;
sky.EventListener handleMaskFling;
sky.EventListener handleMaskTap;
sky.EventListener handlePointerCancel;
sky.EventListener handlePointerDown;
sky.EventListener handlePointerMove;
sky.EventListener handlePointerUp;
List<Node> children;
Drawer({
Object key,
this.onPositionChanged,
this.handleMaskFling,
this.handleMaskTap,
this.handlePointerCancel,
this.handlePointerDown,
this.handlePointerMove,
this.handlePointerUp,
this.children
}) : super(key: key);
double _position = -_kWidth;
bool _listening = false;
void _ensureListening() {
if (_listening)
return;
_listening = true;
onPositionChanged.listen((position) {
setState(() {
_position = position;
});
});
}
Node render() {
_ensureListening();
bool isClosed = _position <= -_kWidth;
String inlineStyle = 'display: ${isClosed ? 'none' : ''}';
String maskInlineStyle = 'opacity: ${(_position / _kWidth + 1) * 0.25}';
String contentInlineStyle = 'transform: translateX(${_position}px)';
return new Container(
style: _style,
inlineStyle: inlineStyle,
onPointerDown: handlePointerDown,
onPointerMove: handlePointerMove,
onPointerUp: handlePointerUp,
onPointerCancel: handlePointerCancel,
children: [
new Container(
key: 'Mask',
style: _maskStyle,
inlineStyle: maskInlineStyle,
onGestureTap: handleMaskTap,
onFlingStart: handleMaskFling
),
new Container(
key: 'Content',
style: _contentStyle,
inlineStyle: contentInlineStyle,
children: children
)
]
);
}
}
part of widgets;
class DrawerHeader extends Component {
static Style _style = new Style('''
display: flex;
flex-direction: column;
height: 140px;
-webkit-user-select: none;
background-color: #E3ECF5;
border-bottom: 1px solid #D1D9E1;
padding-bottom: 7px;
margin-bottom: 8px;'''
);
static Style _spacerStyle = new Style('''
flex: 1'''
);
static Style _labelStyle = new Style('''
padding: 0 16px;
font-family: 'Roboto Medium', 'Helvetica';
color: #212121;'''
);
List<Node> children;
DrawerHeader({ Object key, this.children }) : super(key: key);
Node render() {
return new Container(
style: _style,
children: [
new Container(
key: 'Spacer',
style: _spacerStyle
),
new Container(
key: 'Label',
style: _labelStyle,
children: children
)
]
);
}
}
part of widgets;
abstract class FixedHeightScrollable extends Component {
static Style _style = new Style('''
overflow: hidden;
position: relative;
will-change: transform;'''
);
static Style _scrollAreaStyle = new Style('''
position:relative;
will-change: transform;'''
);
double itemHeight;
double height;
double minOffset;
double maxOffset;
double _scrollOffset = 0.0;
FlingCurve _flingCurve;
int _flingAnimationId;
FixedHeightScrollable({
Object key,
this.itemHeight,
this.height,
this.minOffset,
this.maxOffset
}) : super(key: key) {}
List<Node> renderItems(int start, int count);
Node render() {
int drawCount = (height / itemHeight).round() + 1;
double alignmentDelta = -_scrollOffset % itemHeight;
if (alignmentDelta != 0.0) {
alignmentDelta -= itemHeight;
}
double drawStart = _scrollOffset + alignmentDelta;
int itemNumber = (drawStart / itemHeight).floor();
var transformStyle =
'transform: translateY(${(alignmentDelta).toStringAsFixed(2)}px)';
var items = renderItems(itemNumber, drawCount);
return new Container(
style: _style,
onFlingStart: _handleFlingStart,
onFlingCancel: _handleFlingCancel,
onScrollUpdate: _handleScrollUpdate,
onWheel: _handleWheel,
children: [
new Container(
style: _scrollAreaStyle,
inlineStyle: transformStyle,
children: items
)
]
);
}
void willUnmount() {
_stopFling();
}
bool _scrollBy(double scrollDelta) {
var newScrollOffset = _scrollOffset + scrollDelta;
if (minOffset != null && newScrollOffset < minOffset) {
newScrollOffset = minOffset;
} else if (maxOffset != null && newScrollOffset > maxOffset) {
newScrollOffset = maxOffset;
}
if (newScrollOffset == _scrollOffset) {
return false;
}
setState(() {
_scrollOffset = newScrollOffset;
});
return true;
}
void _scheduleFlingUpdate() {
_flingAnimationId = sky.window.requestAnimationFrame(_updateFling);
}
void _stopFling() {
if (_flingAnimationId == null) {
return;
}
sky.window.cancelAnimationFrame(_flingAnimationId);
_flingCurve = null;
_flingAnimationId = null;
}
void _updateFling(double timeStamp) {
double scrollDelta = _flingCurve.update(timeStamp);
if (!_scrollBy(scrollDelta))
return _stopFling();
_scheduleFlingUpdate();
}
void _handleScrollUpdate(sky.Event event) {
_scrollBy(-event.dy);
}
void _handleFlingStart(sky.Event event) {
setState(() {
_flingCurve = new FlingCurve(-event.velocityY, event.timeStamp);
_scheduleFlingUpdate();
});
}
void _handleFlingCancel(sky.Event event) {
_stopFling();
}
void _handleWheel(sky.Event event) {
_scrollBy(-event.offsetY);
}
}
part of widgets;
// 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.
const double _kDefaultAlpha = -5707.62;
const double _kDefaultBeta = 172.0;
const double _kDefaultGamma = 3.7;
double _positionAtTime(double t) {
return _kDefaultAlpha * math.exp(-_kDefaultGamma * t)
- _kDefaultBeta * t
- _kDefaultAlpha;
}
double _velocityAtTime(double t) {
return -_kDefaultAlpha * _kDefaultGamma * math.exp(-_kDefaultGamma * t)
- _kDefaultBeta;
}
double _timeAtVelocity(double v) {
return -math.log((v + _kDefaultBeta) / (-_kDefaultAlpha * _kDefaultGamma))
/ _kDefaultGamma;
}
final double _kMaxVelocity = _velocityAtTime(0.0);
final double _kCurveDuration = _timeAtVelocity(0.0);
class FlingCurve {
double _timeOffset;
double _positionOffset;
double _startTime;
double _previousPosition;
double _direction;
FlingCurve(double velocity, double startTime) {
double startingVelocity = math.min(_kMaxVelocity, velocity.abs());
_timeOffset = _timeAtVelocity(startingVelocity);
_positionOffset = _positionAtTime(_timeOffset);
_startTime = startTime / 1000.0;
_previousPosition = 0.0;
_direction = velocity.sign;
}
double update(double timeStamp) {
double t = timeStamp / 1000.0 - _startTime + _timeOffset;
if (t >= _kCurveDuration)
return 0.0;
double position = _positionAtTime(t) - _positionOffset;
double positionDelta = position - _previousPosition;
_previousPosition = position;
return _direction * math.max(0.0, positionDelta);
}
}
part of widgets;
const String kAssetBase = '/sky/assets/material-design-icons';
class Icon extends Component {
Style style;
int size;
String type;
sky.EventListener onClick;
Icon({
String key,
this.style,
this.size,
this.type: '',
this.onClick
}) : super(key: key);
Node render() {
String category = '';
String subtype = '';
List<String> parts = type.split('/');
if (parts.length == 2) {
category = parts[0];
subtype = parts[1];
}
return new Image(
style: style,
onClick: onClick,
width: size,
height: size,
src: '${kAssetBase}/${category}/2x_web/ic_${subtype}_${size}dp.png'
);
}
}
part of widgets;
const double _kSplashSize = 400.0;
const double _kSplashDuration = 500.0;
class SplashAnimation {
AnimationGenerator _animation;
double _offsetX;
double _offsetY;
Stream<String> _styleChanged;
Stream<String> get onStyleChanged => _styleChanged;
void cancel() => _animation.cancel();
SplashAnimation(sky.ClientRect rect, double x, double y,
{ Function onDone })
: _offsetX = x - rect.left,
_offsetY = y - rect.top {
_animation = new AnimationGenerator(_kSplashDuration,
end: _kSplashSize, curve: easeOut, onDone: onDone);
_styleChanged = _animation.onTick.map((p) => '''
top: ${_offsetY - p/2}px;
left: ${_offsetX - p/2}px;
width: ${p}px;
height: ${p}px;
border-radius: ${p}px;
opacity: ${1.0 - (p / _kSplashSize)};
''');
}
}
class InkSplash extends Component {
Stream<String> onStyleChanged;
static Style _style = new Style('''
position: absolute;
pointer-events: none;
overflow: hidden;
top: 0;
left: 0;
bottom: 0;
right: 0;
''');
static Style _splashStyle = new Style('''
position: absolute;
background-color: rgba(0, 0, 0, 0.4);
border-radius: 0;
top: 0;
left: 0;
height: 0;
width: 0;
''');
double _offsetX;
double _offsetY;
String _inlineStyle;
InkSplash(Stream<String> onStyleChanged)
: onStyleChanged = onStyleChanged,
super(stateful: true, key: onStyleChanged.hashCode);
bool _listening = false;
void _ensureListening() {
if (_listening)
return;
_listening = true;
onStyleChanged.listen((style) {
setState(() {
_inlineStyle = style;
});
});
}
Node render() {
_ensureListening();
return new Container(
style: _style,
children: [
new Container(
inlineStyle: _inlineStyle,
style: _splashStyle
)
]
);
}
}
library item;
import 'dart:sky' as sky;
import 'fn.dart';
import 'widgets.dart';
enum Color { RED, GREEN }
class Item extends Component {
String label;
Color _color = Color.GREEN;
Item({ Object key, this.label }) : super(key: key);
Node render() {
return new Container(
children: [
new Radio(
onChanged: changed,
value: Color.GREEN,
groupValue: _color
),
new Radio(
onChanged: changed,
value: Color.RED,
groupValue: _color
),
new Text("$label: ${Color.values[_color.index]}")
]
);
}
void changed(Object value) {
setState(() {
_color = value;
});
}
}
part of widgets;
class MenuDivider extends Component {
static Style _style = new Style('''
margin: 8px 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);'''
);
MenuDivider({ Object key }) : super(key: key);
Node render() {
return new Container(
style: _style
);
}
}
part of widgets;
class MenuItem extends Component {
static Style _style = new Style('''
display: flex;
align-items: center;
height: 48px;
-webkit-user-select: none;'''
);
static Style _iconStyle = new Style('''
padding: 0px 16px;'''
);
static Style _labelStyle = new Style('''
font-family: 'Roboto Medium', 'Helvetica';
color: #212121;
padding: 0px 16px;
flex: 1;'''
);
List<Node> children;
String icon;
MenuItem({ Object key, this.icon, this.children }) : super(key: key) {
}
Node render() {
return new Container(
style: _style,
children: [
new Icon(
style: _iconStyle,
size: 24,
type: "${icon}_grey600"
),
new Container(
style: _labelStyle,
children: children
)
]
);
}
}
part of widgets;
class Radio extends ButtonBase {
Object value;
Object groupValue;
ValueChanged onChanged;
static Style _style = new Style('''
display: inline-block;
-webkit-user-select: none;
width: 14px;
height: 14px;
border-radius: 7px;
border: 1px solid blue;
margin: 0 5px;'''
);
static Style _highlightStyle = new Style('''
display: inline-block;
-webkit-user-select: none;
width: 14px;
height: 14px;
border-radius: 7px;
border: 1px solid blue;
margin: 0 5px;
background-color: orange;'''
);
static Style _dotStyle = new Style('''
-webkit-user-select: none;
width: 10px;
height: 10px;
border-radius: 5px;
background-color: black;
margin: 2px;'''
);
Radio({
Object key,
this.onChanged,
this.value,
this.groupValue
}) : super(key: key);
Node render() {
return new Container(
style: _highlight ? _highlightStyle : _style,
onClick: _handleClick,
onPointerDown: _handlePointerDown,
onPointerUp: _handlePointerUp,
onPointerCancel: _handlePointerCancel,
children: value == groupValue ?
[new Container( style : _dotStyle )] : null
);
}
void _handleClick(sky.Event e) {
onChanged(value);
}
}
part of widgets;
class Toolbar extends Component {
List<Node> children;
static Style _style = new Style('''
display: flex;
align-items: center;
height: 84px;
z-index: 1;
background-color: #3F51B5;
color: white;
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);'''
);
Toolbar({String key, this.children}) : super(key: key);
Node render() {
return new Container(
style: _style,
children: children
);
}
}
library widgets;
import '../lib/fn.dart';
import 'dart:async';
import 'dart:math' as math;
import 'dart:sky' as sky;
part 'animationgenerator.dart';
part 'box.dart';
part 'button.dart';
part 'buttonbase.dart';
part 'checkbox.dart';
part 'drawer.dart';
part 'drawerheader.dart';
part 'fixedheightscrollable.dart';
part 'flingcurve.dart';
part 'icon.dart';
part 'inksplash.dart';
part 'menudivider.dart';
part 'menuitem.dart';
part 'radio.dart';
part 'toolbar.dart';
typedef void ValueChanged(value);
This diff is collapsed.
part of stocksapp;
class StockArrow extends Component {
double percentChange;
static Style _style = new Style('''
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 40px;
margin-right: 16px;
border: 1px solid transparent;'''
);
static Style _upStyle = new Style('''
width: 0;
height: 0;
border-left: 9px solid transparent;
border-right: 9px solid transparent;
margin-bottom: 3px;
border-bottom: 9px solid white;'''
);
static Style _downStyle = new Style('''
width: 0;
height: 0;
border-left: 9px solid transparent;
border-right: 9px solid transparent;
margin-top: 3px;
border-top: 9px solid white'''
);
StockArrow({ Object key, this.percentChange }) : super(key: key);
final List<String> _kRedColors = [
'#E57373',
'#EF5350',
'#F44336',
'#E53935',
'#D32F2F',
'#C62828',
'#B71C1C',
];
final List<String> _kGreenColors = [
'#81C784',
'#66BB6A',
'#4CAF50',
'#43A047',
'#388E3C',
'#2E7D32',
'#1B5E20',
];
int _colorIndexForPercentChange(double percentChange) {
// Currently the max is 10%.
double maxPercent = 10.0;
return max(0, ((percentChange.abs() / maxPercent) * _kGreenColors.length).floor());
}
String _colorForPercentChange(double percentChange) {
if (percentChange > 0)
return _kGreenColors[_colorIndexForPercentChange(percentChange)];
return _kRedColors[_colorIndexForPercentChange(percentChange)];
}
Node render() {
String border = _colorForPercentChange(percentChange).toString();
bool up = percentChange > 0;
String type = up ? 'bottom' : 'top';
return new Container(
inlineStyle: 'border-color: $border',
style: _style,
children: [
new Container(
inlineStyle: 'border-$type-color: $border',
style: up ? _upStyle : _downStyle
)
]
);
}
}
part of stocksapp;
class Stocklist extends FixedHeightScrollable {
List<Stock> stocks;
Stocklist({
Object key,
this.stocks
}) : super(key: key, itemHeight: 80.0, height: 800.0, minOffset: 0.0);
List<Node> renderItems(int start, int count) {
var items = [];
for (var i = 0; i < count; i++) {
items.add(new StockRow(stock: stocks[start + i]));
}
return items;
}
}
part of stocksapp;
class StockRow extends Component {
Stock stock;
LinkedHashSet<SplashAnimation> _splashes;
static Style _style = new Style('''
transform: translateX(0);
max-height: 48px;
display: flex;
align-items: center;
border-bottom: 1px solid #F4F4F4;
padding-top: 16px;
padding-left: 16px;
padding-right: 16px;
padding-bottom: 20px;'''
);
static Style _tickerStyle = new Style('''
flex: 1;
font-family: 'Roboto Medium', 'Helvetica';'''
);
static Style _lastSaleStyle = new Style('''
text-align: right;
padding-right: 16px;'''
);
static Style _changeStyle = new Style('''
color: #8A8A8A;
text-align: right;'''
);
StockRow({Stock stock}) : super(key: stock.symbol) {
this.stock = stock;
}
Node render() {
String lastSale = "\$${stock.lastSale.toStringAsFixed(2)}";
String changeInPrice = "${stock.percentChange.toStringAsFixed(2)}%";
if (stock.percentChange > 0)
changeInPrice = "+" + changeInPrice;
List<Node> children = [
new StockArrow(
percentChange: stock.percentChange
),
new Container(
key: 'Ticker',
style: _tickerStyle,
children: [new Text(stock.symbol)]
),
new Container(
key: 'LastSale',
style: _lastSaleStyle,
children: [new Text(lastSale)]
),
new Container(
key: 'Change',
style: _changeStyle,
children: [new Text(changeInPrice)]
)
];
if (_splashes != null) {
children.addAll(_splashes.map((s) => new InkSplash(s.onStyleChanged)));
}
return new Container(
style: _style,
onScrollStart: _cancelSplashes,
onWheel: _cancelSplashes,
onPointerDown: _handlePointerDown,
children: children
);
}
sky.ClientRect _getBoundingRect() => getRoot().getBoundingClientRect();
void _handlePointerDown(sky.Event event) {
setState(() {
if (_splashes == null) {
_splashes = new LinkedHashSet<SplashAnimation>();
}
var splash;
splash = new SplashAnimation(_getBoundingRect(), event.x, event.y,
onDone: () { _splashDone(splash); });
_splashes.add(splash);
});
}
void _cancelSplashes(sky.Event event) {
if (_splashes == null) {
return;
}
setState(() {
var splashes = _splashes;
_splashes = null;
splashes.forEach((s) { s.cancel(); });
});
}
void willUnmount() {
_cancelSplashes(null);
}
void _splashDone(SplashAnimation splash) {
if (_splashes == null) {
return;
}
setState(() {
_splashes.remove(splash);
if (_splashes.length == 0) {
_splashes = null;
}
});
}
}
#!mojo mojo:sky_viewer
<sky>
<script>
import 'stocksapp.dart';
main() {
new StocksApp();
}
</script>
</sky>
library stocksapp;
import '../fn/lib/fn.dart';
import '../fn/widgets/widgets.dart';
import 'dart:collection';
import 'dart:math';
import 'dart:sky' as sky;
part 'companylist.dart';
part 'stockarrow.dart';
part 'stocklist.dart';
part 'stockrow.dart';
class StocksApp extends App {
DrawerAnimation _drawerAnimation = new DrawerAnimation();
static Style _style = new Style('''
display: flex;
flex-direction: column;
height: -webkit-fill-available;
font-family: 'Roboto Regular', 'Helvetica';
font-size: 16px;'''
);
static Style _iconStyle = new Style('''
padding: 8px;
margin: 0 4px;'''
);
static Style _titleStyle = new Style('''
flex: 1;
margin: 0 4px;'''
);
StocksApp() : super();
Node render() {
var drawer = new Drawer(
onPositionChanged: _drawerAnimation.onPositionChanged,
handleMaskFling: _drawerAnimation.handleFlingStart,
handleMaskTap: _drawerAnimation.handleMaskTap,
handlePointerCancel: _drawerAnimation.handlePointerCancel,
handlePointerDown: _drawerAnimation.handlePointerDown,
handlePointerMove: _drawerAnimation.handlePointerMove,
handlePointerUp: _drawerAnimation.handlePointerUp,
children: [
new DrawerHeader(
children: [new Text('Stocks')]
),
new MenuItem(
key: 'Inbox',
icon: 'content/inbox',
children: [new Text('Inbox')]
),
new MenuDivider(
),
new MenuItem(
key: 'Drafts',
icon: 'content/drafts',
children: [new Text('Drafts')]
),
new MenuItem(
key: 'Settings',
icon: 'action/settings',
children: [new Text('Settings')]
),
new MenuItem(
key: 'Help & Feedback',
icon: 'action/help',
children: [new Text('Help & Feedback')]
)
]
);
var toolbar = new Toolbar(
children: [
new Icon(key: 'menu', style: _iconStyle,
onClick: _drawerAnimation.toggle,
size: 24,
type: 'navigation/menu_white'),
new Container(
style: _titleStyle,
children: [new Text('I am a stocks app')]
),
new Icon(key: 'search', style: _iconStyle,
size: 24,
type: 'action/search_white'),
new Icon(key: 'more_white', style: _iconStyle,
size: 24,
type: 'navigation/more_vert_white')
]
);
return new Container(
key: 'StocksApp',
style: _style,
children: [drawer, toolbar, new Stocklist(stocks: oracle.stocks)]
);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment