diff --git a/packages/flutter/lib/src/fn3/dismissable.dart b/packages/flutter/lib/src/fn3/dismissable.dart new file mode 100644 index 0000000000..3745faf7e8 --- /dev/null +++ b/packages/flutter/lib/src/fn3/dismissable.dart @@ -0,0 +1,257 @@ +// 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:sky' as sky; + +import 'package:sky/animation.dart'; +import 'package:sky/src/fn3/basic.dart'; +import 'package:sky/src/fn3/transitions.dart'; +import 'package:sky/src/fn3/framework.dart'; +import 'package:sky/src/fn3/gesture_detector.dart'; + +const Duration _kCardDismissFadeout = const Duration(milliseconds: 200); +const Duration _kCardDismissResize = const Duration(milliseconds: 300); +final Interval _kCardDismissResizeInterval = new Interval(0.4, 1.0); +const double _kMinFlingVelocity = 700.0; +const double _kMinFlingVelocityDelta = 400.0; +const double _kFlingVelocityScale = 1.0 / 300.0; +const double _kDismissCardThreshold = 0.4; + +enum DismissDirection { + vertical, + horizontal, + left, + right, + up, + down +} + +typedef void ResizedCallback(); +typedef void DismissedCallback(); + +class Dismissable extends StatefulComponent { + Dismissable({ + Key key, + this.child, + this.onResized, + this.onDismissed, + this.direction: DismissDirection.horizontal + }) : super(key: key); + + Widget child; + ResizedCallback onResized; + DismissedCallback onDismissed; + DismissDirection direction; + + DismissableState createState() => new DismissableState(this); +} + +class DismissableState extends ComponentState { + DismissableState(Dismissable config) : super(config) { + _fadePerformance = new AnimationPerformance(duration: _kCardDismissFadeout); + _fadePerformance.addStatusListener((AnimationStatus status) { + if (status == AnimationStatus.completed) + _handleFadeCompleted(); + }); + } + + AnimationPerformance _fadePerformance; + AnimationPerformance _resizePerformance; + + Size _size; + double _dragExtent = 0.0; + bool _dragUnderway = false; + + bool get _directionIsYAxis { + return + config.direction == DismissDirection.vertical || + config.direction == DismissDirection.up || + config.direction == DismissDirection.down; + } + + void _handleFadeCompleted() { + if (!_dragUnderway) + _startResizePerformance(); + } + + bool get _isActive { + return _size != null && (_dragUnderway || _fadePerformance.isAnimating); + } + + void _maybeCallOnResized() { + if (config.onResized != null) + config.onResized(); + } + + void _maybeCallOnDismissed() { + if (config.onDismissed != null) + config.onDismissed(); + } + + void _startResizePerformance() { + assert(_size != null); + assert(_fadePerformance != null); + assert(_fadePerformance.isCompleted); + assert(_resizePerformance == null); + + setState(() { + _resizePerformance = new AnimationPerformance() + ..duration = _kCardDismissResize + ..addListener(_handleResizeProgressChanged); + _resizePerformance.play(); + }); + } + + void _handleResizeProgressChanged() { + if (_resizePerformance.isCompleted) + _maybeCallOnDismissed(); + else + _maybeCallOnResized(); + } + + void _handleDragStart() { + if (_fadePerformance.isAnimating) + return; + setState(() { + _dragUnderway = true; + _dragExtent = 0.0; + _fadePerformance.progress = 0.0; + }); + } + + void _handleDragUpdate(double delta) { + if (!_isActive || _fadePerformance.isAnimating) + return; + + double oldDragExtent = _dragExtent; + switch(config.direction) { + case DismissDirection.horizontal: + case DismissDirection.vertical: + _dragExtent += delta; + break; + + case DismissDirection.up: + case DismissDirection.left: + if (_dragExtent + delta < 0) + _dragExtent += delta; + break; + + case DismissDirection.down: + case DismissDirection.right: + if (_dragExtent + delta > 0) + _dragExtent += delta; + break; + } + + if (oldDragExtent.sign != _dragExtent.sign) { + setState(() { + // Rebuild to update the new drag endpoint. + // The sign of _dragExtent is part of our build state; + // the actual value is not, it's just used to configure + // the performances. + }); + } + if (!_fadePerformance.isAnimating) + _fadePerformance.progress = _dragExtent.abs() / (_size.width * _kDismissCardThreshold); + } + + bool _isFlingGesture(sky.Offset velocity) { + double vx = velocity.dx; + double vy = velocity.dy; + if (_directionIsYAxis) { + if (vy.abs() - vx.abs() < _kMinFlingVelocityDelta) + return false; + switch(config.direction) { + case DismissDirection.vertical: + return vy.abs() > _kMinFlingVelocity; + case DismissDirection.up: + return -vy > _kMinFlingVelocity; + default: + return vy > _kMinFlingVelocity; + } + } else { + if (vx.abs() - vy.abs() < _kMinFlingVelocityDelta) + return false; + switch(config.direction) { + case DismissDirection.horizontal: + return vx.abs() > _kMinFlingVelocity; + case DismissDirection.left: + return -vx > _kMinFlingVelocity; + default: + return vx > _kMinFlingVelocity; + } + } + return false; + } + + void _handleDragEnd(sky.Offset velocity) { + if (!_isActive || _fadePerformance.isAnimating) + return; + + setState(() { + _dragUnderway = false; + if (_fadePerformance.isCompleted) { + _startResizePerformance(); + } else if (_isFlingGesture(velocity)) { + double flingVelocity = _directionIsYAxis ? velocity.dy : velocity.dx; + _dragExtent = flingVelocity.sign; + _fadePerformance.fling(velocity: flingVelocity.abs() * _kFlingVelocityScale); + } else { + _fadePerformance.reverse(); + } + }); + } + + void _handleSizeChanged(Size newSize) { + setState(() { + _size = new Size.copy(newSize); + }); + } + + Point get _activeCardDragEndPoint { + if (!_isActive) + return Point.origin; + assert(_size != null); + double extent = _directionIsYAxis ? _size.height : _size.width; + return new Point(_dragExtent.sign * extent * _kDismissCardThreshold, 0.0); + } + + Widget build(BuildContext context) { + if (_resizePerformance != null) { + AnimatedValue squashAxisExtent = new AnimatedValue( + _directionIsYAxis ? _size.width : _size.height, + end: 0.0, + curve: ease, + interval: _kCardDismissResizeInterval + ); + + return new SquashTransition( + performance: _resizePerformance.view, + width: _directionIsYAxis ? squashAxisExtent : null, + height: !_directionIsYAxis ? squashAxisExtent : null + ); + } + + return new GestureDetector( + onHorizontalDragStart: _directionIsYAxis ? null : _handleDragStart, + onHorizontalDragUpdate: _directionIsYAxis ? null : _handleDragUpdate, + onHorizontalDragEnd: _directionIsYAxis ? null : _handleDragEnd, + onVerticalDragStart: _directionIsYAxis ? _handleDragStart : null, + onVerticalDragUpdate: _directionIsYAxis ? _handleDragUpdate : null, + onVerticalDragEnd: _directionIsYAxis ? _handleDragEnd : null, + child: new SizeObserver( + callback: _handleSizeChanged, + child: new FadeTransition( + performance: _fadePerformance.view, + opacity: new AnimatedValue(1.0, end: 0.0), + child: new SlideTransition( + performance: _fadePerformance.view, + position: new AnimatedValue(Point.origin, end: _activeCardDragEndPoint), + child: config.child + ) + ) + ) + ); + } +} diff --git a/packages/flutter/lib/src/fn3/focus.dart b/packages/flutter/lib/src/fn3/focus.dart new file mode 100644 index 0000000000..a8a488313d --- /dev/null +++ b/packages/flutter/lib/src/fn3/focus.dart @@ -0,0 +1,224 @@ +// 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 'package:sky/src/fn3/framework.dart'; + +typedef void FocusChanged(GlobalKey key); + +// _noFocusedScope is used by Focus to track the case where none of the Focus +// component's subscopes (e.g. dialogs) are focused. This is distinct from the +// focused scope being null, which means that we haven't yet decided which scope +// is focused and whichever is the first scope to ask for focus will get it. +final GlobalKey _noFocusedScope = new GlobalKey(); + +class _FocusScope extends InheritedWidget { + _FocusScope({ + Key key, + this.focusState, + this.scopeFocused: true, // are we focused in our ancestor scope? + this.focusedScope, // which of our descendant scopes is focused, if any? + this.focusedWidget, + Widget child + }) : super(key: key, child: child); + + final bool scopeFocused; + final FocusState focusState; + + // These are mutable because we implicitly change them when they're null in + // certain cases, basically pretending retroactively that we were constructed + // with the right keys. + GlobalKey focusedScope; + GlobalKey focusedWidget; + + // The ...IfUnset() methods don't need to notify descendants because by + // definition they are only going to make a change the very first time that + // our state is checked. + + void _setFocusedWidgetIfUnset(GlobalKey key) { + focusState._setFocusedWidgetIfUnset(key); + focusedWidget = focusState._focusedWidget; + focusedScope = focusState._focusedScope == _noFocusedScope ? null : focusState._focusedScope; + } + + void _setFocusedScopeIfUnset(GlobalKey key) { + focusState._setFocusedScopeIfUnset(key); + assert(focusedWidget == focusState._focusedWidget); + focusedScope = focusState._focusedScope == _noFocusedScope ? null : focusState._focusedScope; + } + + bool updateShouldNotify(_FocusScope oldWidget) { + if (scopeFocused != oldWidget.scopeFocused) + return true; + if (!scopeFocused) + return false; + if (focusedScope != oldWidget.focusedScope) + return true; + if (focusedScope != null) + return false; + if (focusedWidget != oldWidget.focusedWidget) + return true; + return false; + } + +} + +class Focus extends StatefulComponent { + Focus({ + GlobalKey key, // key is required if this is a nested Focus scope + this.autofocus: false, + this.child + }) : super(key: key) { + assert(!autofocus || key != null); + } + + final bool autofocus; + final Widget child; + + FocusState createState() => new FocusState(this); +} + +class FocusState extends ComponentState { + FocusState(Focus config) : super(config); + + GlobalKey _focusedWidget; // when null, the first component to ask if it's focused will get the focus + GlobalKey _currentlyRegisteredWidgetRemovalListenerKey; + + void _setFocusedWidget(GlobalKey key) { + setState(() { + _focusedWidget = key; + if (_focusedScope == null) + _focusedScope = _noFocusedScope; + }); + _updateWidgetRemovalListener(key); + } + + void _setFocusedWidgetIfUnset(GlobalKey key) { + if (_focusedWidget == null && (_focusedScope == null || _focusedScope == _noFocusedScope)) { + _focusedWidget = key; + _focusedScope = _noFocusedScope; + _updateWidgetRemovalListener(key); + } + } + + void _handleWidgetRemoved(GlobalKey key) { + assert(_focusedWidget == key); + _updateWidgetRemovalListener(null); + setState(() { + _focusedWidget = null; + }); + } + + void _updateWidgetRemovalListener(GlobalKey key) { + if (_currentlyRegisteredWidgetRemovalListenerKey != key) { + if (_currentlyRegisteredWidgetRemovalListenerKey != null) + GlobalKey.unregisterRemoveListener(_currentlyRegisteredWidgetRemovalListenerKey, _handleWidgetRemoved); + if (key != null) + GlobalKey.registerRemoveListener(key, _handleWidgetRemoved); + _currentlyRegisteredWidgetRemovalListenerKey = key; + } + } + + GlobalKey _focusedScope; // when null, the first scope to ask if it's focused will get the focus + GlobalKey _currentlyRegisteredScopeRemovalListenerKey; + + void _setFocusedScope(GlobalKey key) { + setState(() { + _focusedScope = key; + }); + _updateScopeRemovalListener(key); + } + + void _setFocusedScopeIfUnset(GlobalKey key) { + if (_focusedScope == null) { + _focusedScope = key; + _updateScopeRemovalListener(key); + } + } + + void _scopeRemoved(GlobalKey key) { + assert(_focusedScope == key); + _currentlyRegisteredScopeRemovalListenerKey = null; + setState(() { + _focusedScope = null; + }); + } + + void _updateScopeRemovalListener(GlobalKey key) { + if (_currentlyRegisteredScopeRemovalListenerKey != key) { + if (_currentlyRegisteredScopeRemovalListenerKey != null) + GlobalKey.unregisterRemoveListener(_currentlyRegisteredScopeRemovalListenerKey, _scopeRemoved); + if (key != null) + GlobalKey.registerRemoveListener(key, _scopeRemoved); + _currentlyRegisteredScopeRemovalListenerKey = key; + } + } + + void initState(BuildContext context) { + if (config.autofocus) + FocusState._moveScopeTo(context, config); + _updateWidgetRemovalListener(_focusedWidget); + _updateScopeRemovalListener(_focusedScope); + } + + void dispose() { + _updateWidgetRemovalListener(null); + _updateScopeRemovalListener(null); + } + + Widget build(BuildContext context) { + return new _FocusScope( + focusState: this, + scopeFocused: FocusState._atScope(context, config), + focusedScope: _focusedScope == _noFocusedScope ? null : _focusedScope, + focusedWidget: _focusedWidget, + child: config.child + ); + } + + static bool at(BuildContext context, Widget widget, { bool autofocus: true }) { + assert(widget != null); + assert(widget.key is GlobalKey); + _FocusScope focusScope = context.inheritedWidgetOfType(_FocusScope); + if (focusScope != null) { + if (autofocus) + focusScope._setFocusedWidgetIfUnset(widget.key); + return focusScope.scopeFocused && + focusScope.focusedScope == null && + focusScope.focusedWidget == widget.key; + } + return true; + } + + static bool _atScope(BuildContext context, Widget widget, { bool autofocus: true }) { + assert(widget != null); + _FocusScope focusScope = context.inheritedWidgetOfType(_FocusScope); + if (focusScope != null) { + if (autofocus) + focusScope._setFocusedScopeIfUnset(widget.key); + assert(widget.key != null); + return focusScope.scopeFocused && + focusScope.focusedScope == widget.key; + } + return true; + } + + // Don't call moveTo() from your build() function, it's intended to be called + // from event listeners, e.g. in response to a finger tap or tab key. + + static void moveTo(BuildContext context, Widget widget) { + assert(widget != null); + assert(widget.key is GlobalKey); + _FocusScope focusScope = context.inheritedWidgetOfType(_FocusScope); + if (focusScope != null) + focusScope.focusState._setFocusedWidget(widget.key); + } + + static void _moveScopeTo(BuildContext context, Focus component) { + assert(component != null); + assert(component.key != null); + _FocusScope focusScope = context.inheritedWidgetOfType(_FocusScope); + if (focusScope != null) + focusScope.focusState._setFocusedScope(component.key); + } +} diff --git a/packages/flutter/lib/src/fn3/framework.dart b/packages/flutter/lib/src/fn3/framework.dart index affa9914ce..aaa56f32d9 100644 --- a/packages/flutter/lib/src/fn3/framework.dart +++ b/packages/flutter/lib/src/fn3/framework.dart @@ -335,6 +335,11 @@ abstract class ComponentState { /// additional state when the config field's value is changed. void didUpdateConfig(T oldConfig) { } + /// Called when this object is inserted into the tree. Override this function + /// to perform initialization that depends on the location at which this + /// object was inserted into the tree. + void initState(BuildContext context) { } + /// Called when this object is removed from the tree. Override this to clean /// up any resources allocated by this object. void dispose() { } diff --git a/packages/flutter/lib/src/fn3/navigator.dart b/packages/flutter/lib/src/fn3/navigator.dart new file mode 100644 index 0000000000..19214301a0 --- /dev/null +++ b/packages/flutter/lib/src/fn3/navigator.dart @@ -0,0 +1,219 @@ +// 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 'package:sky/animation.dart'; +import 'package:sky/src/fn3/basic.dart'; +import 'package:sky/src/fn3/focus.dart'; +import 'package:sky/src/fn3/framework.dart'; +import 'package:sky/src/fn3/transitions.dart'; + +typedef Widget RouteBuilder(NavigatorState navigator, RouteBase route); + +typedef void NotificationCallback(); + +abstract class RouteBase { + AnimationPerformance _performance; + NotificationCallback onDismissed; + NotificationCallback onCompleted; + AnimationPerformance createPerformance() { + AnimationPerformance result = new AnimationPerformance(duration: transitionDuration); + result.addStatusListener((AnimationStatus status) { + switch (status) { + case AnimationStatus.dismissed: + if (onDismissed != null) + onDismissed(); + break; + case AnimationStatus.completed: + if (onCompleted != null) + onCompleted(); + break; + default: + ; + } + }); + return result; + } + WatchableAnimationPerformance ensurePerformance({ Direction direction }) { + assert(direction != null); + if (_performance == null) + _performance = createPerformance(); + AnimationStatus desiredStatus = direction == Direction.forward ? AnimationStatus.forward : AnimationStatus.reverse; + if (_performance.status != desiredStatus) + _performance.play(direction); + return _performance.view; + } + bool get isActuallyOpaque => _performance != null && _performance.isCompleted && isOpaque; + + bool get hasContent => true; // set to false if you have nothing useful to return from build() + + Duration get transitionDuration; + bool get isOpaque; + Widget build(Key key, NavigatorState navigator, WatchableAnimationPerformance performance); + void popState([dynamic result]) { assert(result == null); } + + String toString() => '$runtimeType()'; +} + +const Duration _kTransitionDuration = const Duration(milliseconds: 150); +const Point _kTransitionStartPoint = const Point(0.0, 75.0); +class Route extends RouteBase { + Route({ this.name, this.builder }); + + final String name; + final RouteBuilder builder; + + bool get isOpaque => true; + + Duration get transitionDuration => _kTransitionDuration; + + Widget build(Key key, NavigatorState navigator, WatchableAnimationPerformance performance) { + // TODO(jackson): Hit testing should ignore transform + // TODO(jackson): Block input unless content is interactive + return new SlideTransition( + key: key, + performance: performance, + position: new AnimatedValue(_kTransitionStartPoint, end: Point.origin, curve: easeOut), + child: new FadeTransition( + performance: performance, + opacity: new AnimatedValue(0.0, end: 1.0, curve: easeOut), + child: builder(navigator, this) + ) + ); + } + + String toString() => '$runtimeType(name="$name")'; +} + +class RouteState extends RouteBase { + RouteState({ this.callback, this.route, this.owner }); + + Function callback; + RouteBase route; + StatefulComponent owner; + + bool get isOpaque => false; + + void popState([dynamic result]) { + assert(result == null); + if (callback != null) + callback(this); + } + + bool get hasContent => false; + Duration get transitionDuration => const Duration(); + Widget build(Key key, NavigatorState navigator, WatchableAnimationPerformance performance) => null; +} + +class NavigatorHistory { + + NavigatorHistory(List routes) { + for (Route route in routes) { + if (route.name != null) + namedRoutes[route.name] = route; + } + recents.add(routes[0]); + } + + List recents = new List(); + int index = 0; + Map namedRoutes = new Map(); + + RouteBase get currentRoute => recents[index]; + bool hasPrevious() => index > 0; + + void pushNamed(String name) { + Route route = namedRoutes[name]; + assert(route != null); + push(route); + } + + void push(RouteBase route) { + assert(!_debugCurrentlyHaveRoute(route)); + recents.insert(index + 1, route); + index++; + } + + void pop([dynamic result]) { + if (index > 0) { + RouteBase route = recents[index]; + route.popState(result); + index--; + } + } + + bool _debugCurrentlyHaveRoute(RouteBase route) { + return recents.any((candidate) => candidate == route); + } +} + +class Navigator extends StatefulComponent { + Navigator(this.history, { Key key }) : super(key: key); + + final NavigatorHistory history; + + NavigatorState createState() => new NavigatorState(this); +} + +class NavigatorState extends ComponentState { + NavigatorState(Navigator config) : super(config); + + RouteBase get currentRoute => config.history.currentRoute; + + void pushState(StatefulComponent owner, Function callback) { + RouteBase route = new RouteState( + owner: owner, + callback: callback, + route: currentRoute + ); + push(route); + } + + void pushNamed(String name) { + setState(() { + config.history.pushNamed(name); + }); + } + + void push(RouteBase route) { + setState(() { + config.history.push(route); + }); + } + + void pop([dynamic result]) { + setState(() { + config.history.pop(result); + }); + } + + Widget build(BuildContext context) { + List visibleRoutes = new List(); + for (int i = config.history.recents.length-1; i >= 0; i -= 1) { + RouteBase route = config.history.recents[i]; + if (!route.hasContent) + continue; + WatchableAnimationPerformance performance = route.ensurePerformance( + direction: (i <= config.history.index) ? Direction.forward : Direction.reverse + ); + route.onDismissed = () { + setState(() { + assert(config.history.recents.contains(route)); + config.history.recents.remove(route); + }); + }; + Key key = new ObjectKey(route); + Widget widget = route.build(key, this, performance); + visibleRoutes.add(widget); + if (route.isActuallyOpaque) + break; + } + if (visibleRoutes.length > 1) { + visibleRoutes.insert(1, new Listener( + onPointerDown: (_) { pop(); }, + child: new Container() + )); + } + return new Focus(child: new Stack(visibleRoutes.reversed.toList())); + } +} diff --git a/packages/flutter/lib/src/fn3/progress_indicator.dart b/packages/flutter/lib/src/fn3/progress_indicator.dart new file mode 100644 index 0000000000..f77d31e5fc --- /dev/null +++ b/packages/flutter/lib/src/fn3/progress_indicator.dart @@ -0,0 +1,163 @@ +// 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:math' as math; +import 'dart:sky' as sky; + +import 'package:sky/animation.dart'; +import 'package:sky/src/fn3/basic.dart'; +import 'package:sky/src/fn3/theme.dart'; +import 'package:sky/src/fn3/framework.dart'; +import 'package:sky/src/fn3/transitions.dart'; + +const double _kLinearProgressIndicatorHeight = 6.0; +const double _kMinCircularProgressIndicatorSize = 15.0; +const double _kCircularProgressIndicatorStrokeWidth = 3.0; + +abstract class ProgressIndicator extends StatefulComponent { + ProgressIndicator({ + Key key, + this.value, + this.bufferValue + }) : super(key: key); + + final double value; // Null for non-determinate progress indicator. + final double bufferValue; // TODO(hansmuller) implement the support for this. + + Color _getBackgroundColor(BuildContext context) => Theme.of(context).primarySwatch[200]; + Color _getValueColor(BuildContext context) => Theme.of(context).primaryColor; + Object _getCustomPaintToken(double performanceValue) => value != null ? value : performanceValue; + + Widget _buildIndicator(BuildContext context, double performanceValue); + + ProgressIndicatorState createState() => new ProgressIndicatorState(this); +} + +class ProgressIndicatorState extends ComponentState { + ProgressIndicatorState(ProgressIndicator config) : super(config) { + _performance = new AnimationPerformance() + ..duration = const Duration(milliseconds: 1500) + ..variable = new AnimatedValue(0.0, end: 1.0, curve: ease); + _performance.addStatusListener((AnimationStatus status) { + if (status == AnimationStatus.completed) + _restartAnimation(); + }); + _performance.play(); + + } + + AnimationPerformance _performance; + double get _performanceValue => (_performance.variable as AnimatedValue).value; + + void _restartAnimation() { + _performance.progress = 0.0; + _performance.play(); + } + + Widget build(BuildContext context) { + if (config.value != null) + return config._buildIndicator(context, _performanceValue); + + return new BuilderTransition( + variables: [_performance.variable], + performance: _performance.view, + builder: (BuildContext context) { + return config._buildIndicator(context, _performanceValue); + } + ); + } +} + +class LinearProgressIndicator extends ProgressIndicator { + LinearProgressIndicator({ + Key key, + double value, + double bufferValue + }) : super(key: key, value: value, bufferValue: bufferValue); + + void _paint(BuildContext context, double performanceValue, sky.Canvas canvas, Size size) { + Paint paint = new Paint() + ..color = _getBackgroundColor(context) + ..setStyle(sky.PaintingStyle.fill); + canvas.drawRect(Point.origin & size, paint); + + paint.color = _getValueColor(context); + if (value != null) { + double width = value.clamp(0.0, 1.0) * size.width; + canvas.drawRect(Point.origin & new Size(width, size.height), paint); + } else { + double startX = size.width * (1.5 * performanceValue - 0.5); + double endX = startX + 0.5 * size.width; + double x = startX.clamp(0.0, size.width); + double width = endX.clamp(0.0, size.width) - x; + canvas.drawRect(new Point(x, 0.0) & new Size(width, size.height), paint); + } + } + + Widget _buildIndicator(BuildContext context, double performanceValue) { + return new Container( + constraints: new BoxConstraints.tightFor( + width: double.INFINITY, + height: _kLinearProgressIndicatorHeight + ), + child: new CustomPaint( + token: _getCustomPaintToken(performanceValue), + callback: (sky.Canvas canvas, Size size) { + _paint(context, performanceValue, canvas, size); + } + ) + ); + } +} + +class CircularProgressIndicator extends ProgressIndicator { + static const _kTwoPI = math.PI * 2.0; + static const _kEpsilon = .0000001; + // Canavs.drawArc(r, 0, 2*PI) doesn't draw anything, so just get close. + static const _kSweep = _kTwoPI - _kEpsilon; + static const _kStartAngle = -math.PI / 2.0; + + CircularProgressIndicator({ + Key key, + double value, + double bufferValue + }) : super(key: key, value: value, bufferValue: bufferValue); + + void _paint(BuildContext context, double performanceValue, sky.Canvas canvas, Size size) { + Paint paint = new Paint() + ..color = _getValueColor(context) + ..strokeWidth = _kCircularProgressIndicatorStrokeWidth + ..setStyle(sky.PaintingStyle.stroke); + + if (value != null) { + double angle = value.clamp(0.0, 1.0) * _kSweep; + sky.Path path = new sky.Path() + ..arcTo(Point.origin & size, _kStartAngle, angle, false); + canvas.drawPath(path, paint); + } else { + double startAngle = _kTwoPI * (1.75 * performanceValue - 0.75); + double endAngle = startAngle + _kTwoPI * 0.75; + double arcAngle = startAngle.clamp(0.0, _kTwoPI); + double arcSweep = endAngle.clamp(0.0, _kTwoPI) - arcAngle; + sky.Path path = new sky.Path() + ..arcTo(Point.origin & size, _kStartAngle + arcAngle, arcSweep, false); + canvas.drawPath(path, paint); + } + } + + Widget _buildIndicator(BuildContext context, double performanceValue) { + return new Container( + constraints: new BoxConstraints( + minWidth: _kMinCircularProgressIndicatorSize, + minHeight: _kMinCircularProgressIndicatorSize + ), + child: new CustomPaint( + token: _getCustomPaintToken(performanceValue), + callback: (sky.Canvas canvas, Size size) { + _paint(context, performanceValue, canvas, size); + } + ) + ); + } +} diff --git a/packages/flutter/lib/src/fn3/radio.dart b/packages/flutter/lib/src/fn3/radio.dart new file mode 100644 index 0000000000..2e5ed26f3d --- /dev/null +++ b/packages/flutter/lib/src/fn3/radio.dart @@ -0,0 +1,74 @@ +// 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:sky' as sky; + +import 'package:sky/src/fn3/basic.dart'; +import 'package:sky/src/fn3/framework.dart'; +import 'package:sky/src/fn3/gesture_detector.dart'; +import 'package:sky/src/fn3/theme.dart'; + +const sky.Color _kLightOffColor = const sky.Color(0x8A000000); +const sky.Color _kDarkOffColor = const sky.Color(0xB2FFFFFF); + +typedef RadioValueChanged(Object value); + +class Radio extends StatefulComponent { + Radio({ + Key key, + this.value, + this.groupValue, + this.onChanged + }) : super(key: key) { + assert(onChanged != null); + } + + final Object value; + final Object groupValue; + final RadioValueChanged onChanged; + + RadioState createState() => new RadioState(this); +} + +class RadioState extends ComponentState { + RadioState(Radio config) : super(config); + + Color _getColor(BuildContext context) { + ThemeData themeData = Theme.of(context); + if (config.value == config.groupValue) + return themeData.accentColor; + return themeData.brightness == ThemeBrightness.light ? _kLightOffColor : _kDarkOffColor; + } + + Widget build(BuildContext context) { + const double kDiameter = 16.0; + const double kOuterRadius = kDiameter / 2; + const double kInnerRadius = 5.0; + return new GestureDetector( + onTap: () => config.onChanged(config.value), + child: new Container( + margin: const EdgeDims.symmetric(horizontal: 5.0), + width: kDiameter, + height: kDiameter, + child: new CustomPaint( + callback: (sky.Canvas canvas, Size size) { + + Paint paint = new Paint()..color = _getColor(context); + + // Draw the outer circle + paint.setStyle(sky.PaintingStyle.stroke); + paint.strokeWidth = 2.0; + canvas.drawCircle(const Point(kOuterRadius, kOuterRadius), kOuterRadius, paint); + + // Draw the inner circle + if (config.value == config.groupValue) { + paint.setStyle(sky.PaintingStyle.fill); + canvas.drawCircle(const Point(kOuterRadius, kOuterRadius), kInnerRadius, paint); + } + } + ) + ) + ); + } +} diff --git a/packages/flutter/lib/src/fn3/switch.dart b/packages/flutter/lib/src/fn3/switch.dart new file mode 100644 index 0000000000..ef132ec790 --- /dev/null +++ b/packages/flutter/lib/src/fn3/switch.dart @@ -0,0 +1,135 @@ +// 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:sky' as sky; + +import 'package:sky/material.dart'; +import 'package:sky/painting.dart'; +import 'package:sky/rendering.dart'; +import 'package:sky/src/fn3/basic.dart'; +import 'package:sky/src/fn3/theme.dart'; +import 'package:sky/src/fn3/framework.dart'; + +export 'package:sky/rendering.dart' show ValueChanged; + +const sky.Color _kThumbOffColor = const sky.Color(0xFFFAFAFA); +const sky.Color _kTrackOffColor = const sky.Color(0x42000000); +const double _kSwitchWidth = 35.0; +const double _kThumbRadius = 10.0; +const double _kSwitchHeight = _kThumbRadius * 2.0; +const double _kTrackHeight = 14.0; +const double _kTrackRadius = _kTrackHeight / 2.0; +const double _kTrackWidth = + _kSwitchWidth - (_kThumbRadius - _kTrackRadius) * 2.0; +const Duration _kCheckDuration = const Duration(milliseconds: 200); +const Size _kSwitchSize = const Size(_kSwitchWidth + 2.0, _kSwitchHeight + 2.0); +const double _kReactionRadius = _kSwitchWidth / 2.0; + +class Switch extends LeafRenderObjectWidget { + Switch({ Key key, this.value, this.onChanged }) + : super(key: key); + + final bool value; + final ValueChanged onChanged; + + _RenderSwitch createRenderObject() => new _RenderSwitch( + value: value, + thumbColor: null, + onChanged: onChanged + ); + + void updateRenderObject(_RenderSwitch renderObject, Switch oldWidget) { + renderObject.value = value; + renderObject.onChanged = onChanged; + // TODO(abarth): How do we get the current theme here? + // renderObject.thumbColor = Theme.of(this).accentColor; + } +} + +class _RenderSwitch extends RenderToggleable { + _RenderSwitch({ + bool value, + Color thumbColor: _kThumbOffColor, + ValueChanged onChanged + }) : _thumbColor = thumbColor, + super(value: value, onChanged: onChanged, size: _kSwitchSize) {} + + Color _thumbColor; + Color get thumbColor => _thumbColor; + void set thumbColor(Color value) { + if (value == _thumbColor) return; + _thumbColor = value; + markNeedsPaint(); + } + + RadialReaction _radialReaction; + + void handleEvent(sky.Event event, BoxHitTestEntry entry) { + if (event is sky.PointerEvent) { + if (event.type == 'pointerdown') + _showRadialReaction(entry.localPosition); + else if (event.type == 'pointerup') + _hideRadialReaction(); + } + super.handleEvent(event, entry); + } + + void _showRadialReaction(Point startLocation) { + if (_radialReaction != null) + return; + _radialReaction = new RadialReaction( + center: new Point(_kSwitchSize.width / 2.0, _kSwitchSize.height / 2.0), + radius: _kReactionRadius, + startPosition: startLocation) + ..addListener(markNeedsPaint) + ..show(); + } + + Future _hideRadialReaction() async { + if (_radialReaction == null) + return; + await _radialReaction.hide(); + _radialReaction = null; + } + + void paint(PaintingContext context, Offset offset) { + final PaintingCanvas canvas = context.canvas; + sky.Color thumbColor = _kThumbOffColor; + sky.Color trackColor = _kTrackOffColor; + if (value) { + thumbColor = _thumbColor; + trackColor = new sky.Color(_thumbColor.value & 0x80FFFFFF); + } + + // Draw the track rrect + sky.Paint paint = new sky.Paint() + ..color = trackColor + ..style = sky.PaintingStyle.fill; + sky.Rect rect = new sky.Rect.fromLTWH(offset.dx, + offset.dy + _kSwitchHeight / 2.0 - _kTrackHeight / 2.0, _kTrackWidth, + _kTrackHeight); + sky.RRect rrect = new sky.RRect() + ..setRectXY(rect, _kTrackRadius, _kTrackRadius); + canvas.drawRRect(rrect, paint); + + if (_radialReaction != null) + _radialReaction.paint(canvas, offset); + + // Draw the raised thumb with a shadow + paint.color = thumbColor; + ShadowDrawLooperBuilder builder = new ShadowDrawLooperBuilder(); + for (BoxShadow boxShadow in shadows[1]) + builder.addShadow(boxShadow.offset, boxShadow.color, boxShadow.blur); + paint.drawLooper = builder.build(); + + // The thumb contracts slightly during the animation + double inset = 2.0 - (position.value - 0.5).abs() * 2.0; + Point thumbPos = new Point(offset.dx + + _kTrackRadius + + position.value * (_kTrackWidth - _kTrackRadius * 2), + offset.dy + _kSwitchHeight / 2.0); + canvas.drawCircle(thumbPos, _kThumbRadius - inset, paint); + } +}