Move example fn widgets into sky/framework/components

Moving these files into sky/framework will make them easier to use from the
SDK. Also, this CL also splits up the giant "widgets" library into smaller
libraries, one per component.

TBR=eseidel@chromium.org

Review URL: https://codereview.chromium.org/993033003
This commit is contained in:
Adam Barth 2015-03-10 15:55:24 -07:00
parent 0f99e272d8
commit 5d27d7b223
18 changed files with 10 additions and 991 deletions

View file

@ -1,47 +0,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 build() {
return new Container(
style: _style,
children: [
new Container(
key: 'Title',
style: _titleStyle,
children: [new Text(title)]
),
new Container(
key: 'Content',
style: _contentStyle,
children: children
),
]
);
}
}

View file

@ -1,39 +0,0 @@
part of widgets;
class Button extends ButtonBase {
static Style _style = new Style('''
transform: translateX(0);
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('''
transform: translateX(0);
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;
Button({ Object key, this.content }) : super(key: key);
Node build() {
return new Container(
key: 'Button',
style: _highlight ? _highlightStyle : _style,
children: [super.build(), content]
);
}
}

View file

@ -1,28 +0,0 @@
part of widgets;
abstract class ButtonBase extends MaterialComponent {
bool _highlight = false;
ButtonBase({ Object key }) : super(key: key) {
events.listen('pointerdown', _handlePointerDown);
events.listen('pointerup', _handlePointerUp);
events.listen('pointercancel', _handlePointerCancel);
}
void _handlePointerDown(_) {
setState(() {
_highlight = true;
});
}
void _handlePointerUp(_) {
setState(() {
_highlight = false;
});
}
void _handlePointerCancel(_) {
setState(() {
_highlight = false;
});
}
}

View file

@ -1,77 +0,0 @@
part of widgets;
class Checkbox extends ButtonBase {
bool checked;
ValueChanged onChanged;
static Style _style = new Style('''
transform: translateX(0);
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 build() {
return new Container(
style: _style,
children: [
super.build(),
new Container(
style: _highlight ? _containerHighlightStyle : _containerStyle,
children: [
new Container(
style: checked ? _checkedStyle : _uncheckedStyle
)
]
)
]
)..events.listen('click', _handleClick);
}
void _handleClick(sky.Event e) {
onChanged(!checked);
}
}

View file

@ -1,159 +0,0 @@
part of widgets;
const double _kWidth = 256.0;
const double _kMinFlingVelocity = 0.4;
const double _kBaseSettleDurationMS = 246.0;
const double _kMaxSettleDurationMS = 600.0;
const Cubic _kAnimationCurve = easeOut;
class DrawerAnimation extends Animation {
Stream<double> get onPositionChanged => onValueChanged;
bool get _isMostlyClosed => value <= -_kWidth / 2;
DrawerAnimation() {
value = -_kWidth;
}
void toggle(_) => _isMostlyClosed ? _open() : _close();
void handleMaskTap(_) => _close();
void handlePointerDown(_) => stop();
void handlePointerMove(sky.PointerEvent event) {
assert(!isAnimating);
value = math.min(0.0, math.max(value + event.dx, -_kWidth));
}
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 _animateToPosition(double targetPosition) {
double distance = (targetPosition - value).abs();
if (distance != 0) {
double targetDuration = distance / _kWidth * _kBaseSettleDurationMS;
double duration = math.min(targetDuration, _kMaxSettleDurationMS);
animateTo(targetPosition, duration, curve: _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 - value).abs();
double duration = distance / velocityX;
animateTo(targetPosition, duration, curve: linear);
}
}
class Drawer extends Component {
static Style _style = new Style('''
position: absolute;
z-index: 2;
top: 0;
left: 0;
bottom: 0;
right: 0;
box-shadpw: ${Shadow[3]};'''
);
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: ${Grey[50]};
will-change: transform;
position: absolute;
z-index: 3;
width: 256px;
top: 0;
left: 0;
bottom: 0;'''
);
DrawerAnimation animation;
List<Node> children;
Drawer({
Object key,
this.animation,
this.children
}) : super(key: key);
double _position = -_kWidth;
bool _listening = false;
void _ensureListening() {
if (_listening)
return;
_listening = true;
animation.onPositionChanged.listen((position) {
setState(() {
_position = position;
});
});
}
Node build() {
_ensureListening();
bool isClosed = _position <= -_kWidth;
String inlineStyle = 'display: ${isClosed ? 'none' : ''}';
String maskInlineStyle = 'opacity: ${(_position / _kWidth + 1) * 0.25}';
String contentInlineStyle = 'transform: translateX(${_position}px)';
Container mask = new Container(
key: 'Mask',
style: _maskStyle,
inlineStyle: maskInlineStyle
)..events.listen('gesturetap', animation.handleMaskTap)
..events.listen('gestureflingstart', animation.handleFlingStart);
Container content = new Container(
key: 'Content',
style: _contentStyle,
inlineStyle: contentInlineStyle,
children: children
);
return new Container(
style: _style,
inlineStyle: inlineStyle,
children: [ mask, content ]
)..events.listen('pointerdown', animation.handlePointerDown)
..events.listen('pointermove', animation.handlePointerMove)
..events.listen('pointerup', animation.handlePointerUp)
..events.listen('pointercancel', animation.handlePointerCancel);
}
}

View file

@ -1,46 +0,0 @@
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: ${BlueGrey[50]};
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 build() {
return new Container(
style: _style,
children: [
new Container(
key: 'Spacer',
style: _spacerStyle
),
new Container(
key: 'Label',
style: _labelStyle,
children: children
)
]
);
}
}

View file

@ -1,146 +0,0 @@
part of widgets;
abstract class FixedHeightScrollable extends Component {
// TODO(rafaelw): This component really shouldn't have an opinion
// about how it is sized. The owning component should decide whether
// it's explicitly sized or flexible or whatever...
static Style _style = new Style('''
overflow: hidden;
position: relative;
flex: 1;
will-change: transform;'''
);
static Style _scrollAreaStyle = new Style('''
position:relative;
will-change: transform;'''
);
double minOffset;
double maxOffset;
double _scrollOffset = 0.0;
FlingCurve _flingCurve;
int _flingAnimationId;
double _height = 0.0;
double _itemHeight;
FixedHeightScrollable({
Object key,
this.minOffset,
this.maxOffset
}) : super(key: key) {}
List<Node> buildItems(int start, int count);
void didMount() {
var root = getRoot();
var item = root.firstChild.firstChild;
sky.ClientRect scrollRect = root.getBoundingClientRect();
sky.ClientRect itemRect = item.getBoundingClientRect();
assert(scrollRect.height > 0);
assert(itemRect.height > 0);
setState(() {
_height = scrollRect.height;
_itemHeight = itemRect.height;
});
}
Node build() {
var itemNumber = 0;
var drawCount = 1;
var transformStyle = '';
if (_height > 0.0) {
drawCount = (_height / _itemHeight).round() + 1;
double alignmentDelta = -_scrollOffset % _itemHeight;
if (alignmentDelta != 0.0) {
alignmentDelta -= _itemHeight;
}
double drawStart = _scrollOffset + alignmentDelta;
itemNumber = (drawStart / _itemHeight).floor();
transformStyle =
'transform: translateY(${(alignmentDelta).toStringAsFixed(2)}px)';
}
return new Container(
style: _style,
children: [
new Container(
style: _scrollAreaStyle,
inlineStyle: transformStyle,
children: buildItems(itemNumber, drawCount)
)
]
)
..events.listen('gestureflingstart', _handleFlingStart)
..events.listen('gestureflingcancel', _handleFlingCancel)
..events.listen('gesturescrollupdate', _handleScrollUpdate)
..events.listen('wheel', _handleWheel);
}
void didUnmount() {
_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.GestureEvent event) {
_scrollBy(-event.dy);
}
void _handleFlingStart(sky.GestureEvent event) {
setState(() {
_flingCurve = new FlingCurve(-event.velocityY, event.timeStamp);
_scheduleFlingUpdate();
});
}
void _handleFlingCancel(sky.GestureEvent event) {
_stopFling();
}
void _handleWheel(sky.WheelEvent event) {
_scrollBy(-event.offsetY);
}
}

View file

@ -1,51 +0,0 @@
part of widgets;
class FloatingActionButton extends MaterialComponent {
static final Style _style = new Style('''
position: absolute;
bottom: 16px;
right: 16px;
z-index: 5;
transform: translateX(0);
width: 56px;
height: 56px;
background-color: ${Red[500]};
color: white;
box-shadow: ${Shadow[3]};
border-radius: 28px;'''
);
static final Style _clipStyle = new Style('''
transform: translateX(0);
position: absolute;
display: flex;
justify-content: center;
align-items: center;
top: 0;
left: 0;
right: 0;
bottom: 0;
-webkit-clip-path: circle(28px at center);''');
Node content;
FloatingActionButton({ Object key, this.content }) : super(key: key);
Node build() {
List<Node> children = [super.build()];
if (content != null)
children.add(content);
return new Container(
key: "Container",
style: _style,
children: [
new Container(
key: "Clip",
style: _clipStyle,
children: children
)
]
);
}
}

View file

@ -1,34 +0,0 @@
part of widgets;
const String kAssetBase = '/sky/assets/material-design-icons';
class Icon extends Component {
Style style;
int size;
String type;
Icon({
String key,
this.style,
this.size,
this.type: ''
}) : super(key: key);
Node build() {
String category = '';
String subtype = '';
List<String> parts = type.split('/');
if (parts.length == 2) {
category = parts[0];
subtype = parts[1];
}
return new Image(
style: style,
width: size,
height: size,
src: '${kAssetBase}/${category}/2x_web/ic_${subtype}_${size}dp.png'
);
}
}

View file

@ -1,96 +0,0 @@
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 build() {
_ensureListening();
return new Container(
style: _style,
children: [
new Container(
inlineStyle: _inlineStyle,
style: _splashStyle
)
]
);
}
}

View file

@ -1,80 +0,0 @@
part of widgets;
abstract class MaterialComponent extends Component {
static const _splashesKey = const Object();
static Style _style = new Style('''
transform: translateX(0);
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0'''
);
LinkedHashSet<SplashAnimation> _splashes;
MaterialComponent({ Object key }) : super(key: key);
Node build() {
List<Node> children = [];
if (_splashes != null) {
children.addAll(_splashes.map((s) => new InkSplash(s.onStyleChanged)));
}
return new Container(
style: _style,
children: children,
key: _splashesKey
)..events.listen('gesturescrollstart', _cancelSplashes)
..events.listen('wheel', _cancelSplashes)
..events.listen('pointerdown', _startSplash);
}
sky.ClientRect _getBoundingRect() => (getRoot() as sky.Element).getBoundingClientRect();
void _startSplash(sky.PointerEvent 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 didUnmount() {
_cancelSplashes(null);
}
void _splashDone(SplashAnimation splash) {
if (_splashes == null) {
return;
}
setState(() {
_splashes.remove(splash);
if (_splashes.length == 0) {
_splashes = null;
}
});
}
}

View file

@ -1,17 +0,0 @@
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 build() {
return new Container(
style: _style
);
}
}

View file

@ -1,55 +0,0 @@
part of widgets;
class MenuItem extends ButtonBase {
static Style _style = new Style('''
transform: translateX(0);
display: flex;
align-items: center;
height: 48px;
-webkit-user-select: none;'''
);
static Style _highlightStyle = new Style('''
transform: translateX(0);
display: flex;
align-items: center;
height: 48px;
background: rgba(153, 153, 153, 0.4);
-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 build() {
return new Container(
style: _highlight ? _highlightStyle : _style,
children: [
super.build(),
new Icon(
style: _iconStyle,
size: 24,
type: "${icon}_grey600"
),
new Container(
style: _labelStyle,
children: children
)
]
);
}
}

View file

@ -1,59 +0,0 @@
part of widgets;
class Radio extends ButtonBase {
Object value;
Object groupValue;
ValueChanged onChanged;
static Style _style = new Style('''
transform: translateX(0);
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('''
transform: translateX(0);
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 build() {
return new Container(
style: _highlight ? _highlightStyle : _style,
children: value == groupValue ?
[super.build(), new Container( style : _dotStyle )] : [super.build()]
)..events.listen('click', _handleClick);
}
void _handleClick(_) {
onChanged(value);
}
}

View file

@ -1,25 +0,0 @@
part of widgets;
class Toolbar extends Component {
List<Node> children;
static final Style _style = new Style('''
display: flex;
align-items: center;
height: 56px;
z-index: 1;
background-color: ${Purple[500]};
color: white;
box-shadow: ${Shadow[2]};'''
);
Toolbar({String key, this.children}) : super(key: key);
Node build() {
return new Container(
style: _style,
children: children
);
}
}

View file

@ -1,30 +0,0 @@
library widgets;
import '../../../framework/animation/curves.dart';
import '../../../framework/animation/fling-curve.dart';
import '../../../framework/animation/generator.dart';
import '../../../framework/fn.dart';
import '../../../framework/theme/colors.dart';
import '../../../framework/theme/shadows.dart';
import 'dart:collection';
import 'dart:async';
import 'dart:math' as math;
import 'dart:sky' as sky;
part 'box.dart';
part 'button.dart';
part 'buttonbase.dart';
part 'checkbox.dart';
part 'drawer.dart';
part 'drawerheader.dart';
part 'fixedheightscrollable.dart';
part 'icon.dart';
part 'inksplash.dart';
part 'material.dart';
part 'menudivider.dart';
part 'menuitem.dart';
part 'radio.dart';
part 'toolbar.dart';
part 'floating_action_button.dart';
typedef void ValueChanged(value);

View file

@ -1,6 +1,6 @@
part of stocksapp;
class StockRow extends MaterialComponent {
class StockRow extends Material {
Stock stock;

View file

@ -1,8 +1,16 @@
library stocksapp;
import '../../framework/fn.dart';
import '../../framework/components/drawer.dart';
import '../../framework/components/drawer_header.dart';
import '../../framework/components/fixed_height_scrollable.dart';
import '../../framework/components/floating_action_button.dart';
import '../../framework/components/icon.dart';
import '../../framework/components/material.dart';
import '../../framework/components/menu_divider.dart';
import '../../framework/components/menu_item.dart';
import '../../framework/components/toolbar.dart';
import '../data/stocks.dart';
import '../fn/widgets/widgets.dart';
import 'dart:math';
part 'stockarrow.dart';