Port Focus and Navigator-based widgets to fn3
This commit is contained in:
parent
93ae1f287e
commit
8bcad76384
257
packages/flutter/lib/src/fn3/dismissable.dart
Normal file
257
packages/flutter/lib/src/fn3/dismissable.dart
Normal file
@ -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<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
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
224
packages/flutter/lib/src/fn3/focus.dart
Normal file
224
packages/flutter/lib/src/fn3/focus.dart
Normal file
@ -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<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() { }
|
||||
|
219
packages/flutter/lib/src/fn3/navigator.dart
Normal file
219
packages/flutter/lib/src/fn3/navigator.dart
Normal file
@ -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<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()));
|
||||
}
|
||||
}
|
163
packages/flutter/lib/src/fn3/progress_indicator.dart
Normal file
163
packages/flutter/lib/src/fn3/progress_indicator.dart
Normal file
@ -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<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);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
74
packages/flutter/lib/src/fn3/radio.dart
Normal file
74
packages/flutter/lib/src/fn3/radio.dart
Normal file
@ -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<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);
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
135
packages/flutter/lib/src/fn3/switch.dart
Normal file
135
packages/flutter/lib/src/fn3/switch.dart
Normal file
@ -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);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user