Commit 8bcad763 authored by Adam Barth's avatar Adam Barth

Port Focus and Navigator-based widgets to fn3

parent 93ae1f28
// 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<Dismissable> {
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<double> squashAxisExtent = new AnimatedValue<double>(
_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<double>(1.0, end: 0.0),
child: new SlideTransition(
performance: _fadePerformance.view,
position: new AnimatedValue<Point>(Point.origin, end: _activeCardDragEndPoint),
child: config.child
)
)
)
);
}
}
// 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<Focus> {
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);
}
}
......@@ -335,6 +335,11 @@ abstract class ComponentState<T extends StatefulComponent> {
/// 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() { }
......
// 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<Point>(_kTransitionStartPoint, end: Point.origin, curve: easeOut),
child: new FadeTransition(
performance: performance,
opacity: new AnimatedValue<double>(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<Route> routes) {
for (Route route in routes) {
if (route.name != null)
namedRoutes[route.name] = route;
}
recents.add(routes[0]);
}
List<RouteBase> recents = new List<RouteBase>();
int index = 0;
Map<String, RouteBase> namedRoutes = new Map<String, RouteBase>();
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<Navigator> {
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<Widget> visibleRoutes = new List<Widget>();
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()));
}
}
// 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<ProgressIndicator> {
ProgressIndicatorState(ProgressIndicator config) : super(config) {
_performance = new AnimationPerformance()
..duration = const Duration(milliseconds: 1500)
..variable = new AnimatedValue<double>(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<double>).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);
}
)
);
}
}
// 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<Radio> {
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);
}
}
)
)
);
}
}
// 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);
}
}
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