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.
|
/// additional state when the config field's value is changed.
|
||||||
void didUpdateConfig(T oldConfig) { }
|
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
|
/// Called when this object is removed from the tree. Override this to clean
|
||||||
/// up any resources allocated by this object.
|
/// up any resources allocated by this object.
|
||||||
void dispose() { }
|
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