您最多选择25个主题
主题必须以中文或者字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
1039 行
40 KiB
1039 行
40 KiB
using System.Collections.Generic;
|
|
using Unity.UIWidgets.foundation;
|
|
using Unity.UIWidgets.animation;
|
|
using Unity.UIWidgets.rendering;
|
|
using Unity.UIWidgets.ui;
|
|
using Unity.UIWidgets.widgets;
|
|
using Unity.UIWidgets.scheduler2;
|
|
using System;
|
|
using Unity.UIWidgets.gestures;
|
|
using Unity.UIWidgets.painting;
|
|
using UnityEngine;
|
|
using Color = Unity.UIWidgets.ui.Color;
|
|
using Rect = Unity.UIWidgets.ui.Rect;
|
|
using Transform = Unity.UIWidgets.widgets.Transform;
|
|
|
|
namespace Unity.UIWidgets.cupertino {
|
|
|
|
class CupertinoContextMenuUtils {
|
|
|
|
public static float _kOpenScale = 1.1f;
|
|
public static float kMinFlingVelocity = 50.0f;
|
|
|
|
public static Rect _getRect(GlobalKey globalKey) {
|
|
D.assert(globalKey.currentContext != null);
|
|
RenderBox renderBoxContainer = globalKey.currentContext.findRenderObject() as RenderBox;
|
|
Offset containerOffset = renderBoxContainer.localToGlobal(
|
|
renderBoxContainer.paintBounds.topLeft
|
|
);
|
|
return containerOffset & renderBoxContainer.paintBounds.size;
|
|
}
|
|
|
|
public static Rect fromCenter(Offset center, float width, float height) {
|
|
return Rect.fromLTRB(
|
|
center.dx - width / 2,
|
|
center.dy - height / 2,
|
|
center.dx + width / 2,
|
|
center.dy + height / 2
|
|
);
|
|
}
|
|
|
|
}
|
|
public delegate void _DismissCallback(
|
|
BuildContext context,
|
|
float scale,
|
|
float opacity
|
|
);
|
|
|
|
public delegate Widget ContextMenuPreviewBuilder(
|
|
BuildContext context,
|
|
Animation<float> animation,
|
|
Widget child
|
|
);
|
|
|
|
public delegate Widget _ContextMenuPreviewBuilderChildless (
|
|
BuildContext context,
|
|
Animation<float> animation
|
|
);
|
|
|
|
public enum _ContextMenuLocation {
|
|
center,
|
|
left,
|
|
right,
|
|
}
|
|
|
|
public class CupertinoContextMenu : StatefulWidget {
|
|
public CupertinoContextMenu(
|
|
Key key = null,
|
|
List<Widget> actions = null,
|
|
Widget child = null,
|
|
ContextMenuPreviewBuilder previewBuilder = null
|
|
) : base(key: key) {
|
|
D.assert(actions != null && actions.isNotEmpty());
|
|
D.assert(child != null);
|
|
this.actions = actions;
|
|
this.child = child;
|
|
this.previewBuilder = previewBuilder;
|
|
}
|
|
|
|
public readonly Widget child;
|
|
|
|
public readonly List<Widget> actions;
|
|
|
|
public readonly ContextMenuPreviewBuilder previewBuilder;
|
|
|
|
public override State createState() {
|
|
return new _CupertinoContextMenuState();
|
|
}
|
|
|
|
}
|
|
|
|
public class _CupertinoContextMenuState : TickerProviderStateMixin<CupertinoContextMenu> {
|
|
|
|
public readonly GlobalKey _childGlobalKey = GlobalKey<State<StatefulWidget>>.key();
|
|
static readonly TimeSpan kLongPressTimeout = TimeSpan.FromMilliseconds(500);
|
|
public bool _childHidden = false;
|
|
public AnimationController _openController;
|
|
public Rect _decoyChildEndRect;
|
|
public OverlayEntry _lastOverlayEntry;
|
|
public _ContextMenuRoute _route;
|
|
|
|
public override void initState() {
|
|
base.initState();
|
|
_openController = new AnimationController(
|
|
duration: kLongPressTimeout,
|
|
vsync: this
|
|
);
|
|
_openController.addStatusListener(_onDecoyAnimationStatusChange);
|
|
}
|
|
|
|
public _ContextMenuLocation _contextMenuLocation {
|
|
get {
|
|
Rect childRect = CupertinoContextMenuUtils._getRect(_childGlobalKey);
|
|
float screenWidth = MediaQuery.of(context).size.width;
|
|
float center = screenWidth / 2;
|
|
bool centerDividesChild = childRect.left < center
|
|
&& childRect.right > center;
|
|
float distanceFromCenter = (center - childRect.center.dx).abs();
|
|
if (centerDividesChild && distanceFromCenter <= childRect.width / 4) {
|
|
return _ContextMenuLocation.center;
|
|
}
|
|
|
|
if (childRect.center.dx > center) {
|
|
return _ContextMenuLocation.right;
|
|
}
|
|
|
|
return _ContextMenuLocation.left;
|
|
}
|
|
}
|
|
public void _openContextMenu() {
|
|
setState(()=>{
|
|
_childHidden = true;
|
|
});
|
|
|
|
_route = new _ContextMenuRoute(
|
|
actions: widget.actions,
|
|
barrierLabel: "Dismiss",
|
|
filter: ImageFilter.blur(
|
|
sigmaX: 5.0f,
|
|
sigmaY: 5.0f
|
|
),
|
|
contextMenuLocation: _contextMenuLocation,
|
|
previousChildRect: _decoyChildEndRect,
|
|
builder: (BuildContext _context, Animation<float> animation)=>{
|
|
if (widget.previewBuilder == null) {
|
|
return widget.child;
|
|
}
|
|
return widget.previewBuilder(_context, animation, widget.child);
|
|
}
|
|
);
|
|
Navigator.of(context, rootNavigator: true).push(_route);
|
|
_route.animation.addStatusListener(_routeAnimationStatusListener);
|
|
}
|
|
|
|
public void _onDecoyAnimationStatusChange(AnimationStatus animationStatus) {
|
|
switch (animationStatus) {
|
|
case AnimationStatus.dismissed:
|
|
if (_route == null) {
|
|
setState(()=> {
|
|
_childHidden = false;
|
|
});
|
|
}
|
|
_lastOverlayEntry?.remove();
|
|
_lastOverlayEntry = null;
|
|
break;
|
|
case AnimationStatus.completed:
|
|
setState(()=>{
|
|
_childHidden = true;
|
|
});
|
|
_openContextMenu();
|
|
SchedulerBinding.instance.addPostFrameCallback((TimeSpan timestamp) =>{
|
|
_lastOverlayEntry?.remove();
|
|
_lastOverlayEntry = null;
|
|
_openController.reset();
|
|
});
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
public void _routeAnimationStatusListener(AnimationStatus status) {
|
|
if (status != AnimationStatus.dismissed) {
|
|
return;
|
|
}
|
|
setState(()=>{
|
|
_childHidden = false;
|
|
});
|
|
_route.animation.removeStatusListener(_routeAnimationStatusListener);
|
|
_route = null;
|
|
}
|
|
|
|
public void _onTap() {
|
|
if (_openController.isAnimating && _openController.value < 0.5f) {
|
|
_openController.reverse();
|
|
}
|
|
}
|
|
|
|
public void _onTapCancel() {
|
|
if (_openController.isAnimating && _openController.value < 0.5f) {
|
|
_openController.reverse();
|
|
}
|
|
}
|
|
|
|
public void _onTapUp(TapUpDetails details) {
|
|
if(_openController.isAnimating && _openController.value < 0.5f) {
|
|
_openController.reverse();
|
|
}
|
|
}
|
|
|
|
public void _onTapDown(TapDownDetails details) {
|
|
setState(()=>{
|
|
_childHidden = true;
|
|
});
|
|
Rect childRect = CupertinoContextMenuUtils._getRect(_childGlobalKey);
|
|
_decoyChildEndRect = CupertinoContextMenuUtils.fromCenter(
|
|
center: childRect.center,
|
|
width: childRect.width * CupertinoContextMenuUtils._kOpenScale,
|
|
height: childRect.height * CupertinoContextMenuUtils._kOpenScale
|
|
);
|
|
_lastOverlayEntry = new OverlayEntry(
|
|
opaque: false,
|
|
builder: (BuildContext context) => {
|
|
return new _DecoyChild(
|
|
beginRect: childRect,
|
|
child: widget.child,
|
|
controller: _openController,
|
|
endRect: _decoyChildEndRect
|
|
);
|
|
}
|
|
);
|
|
Overlay.of(context).insert(_lastOverlayEntry);
|
|
_openController.forward();
|
|
}
|
|
|
|
public override Widget build(BuildContext context) {
|
|
return new GestureDetector(
|
|
onTapCancel: _onTapCancel,
|
|
onTapDown: _onTapDown,
|
|
onTapUp: _onTapUp,
|
|
onTap: _onTap,
|
|
child: new TickerMode(
|
|
enabled: !_childHidden,
|
|
child: new Opacity(
|
|
key: _childGlobalKey,
|
|
opacity: _childHidden ? 0.0f : 1.0f,
|
|
child: widget.child
|
|
)
|
|
)
|
|
);
|
|
}
|
|
public override void dispose() {
|
|
_openController.dispose();
|
|
base.dispose();
|
|
}
|
|
}
|
|
public class _DecoyChild : StatefulWidget {
|
|
public _DecoyChild(
|
|
Key key = null,
|
|
Rect beginRect = null,
|
|
AnimationController controller = null,
|
|
Rect endRect = null,
|
|
Widget child = null
|
|
) : base(key: key) {
|
|
this.beginRect = beginRect;
|
|
this.controller = controller;
|
|
this.endRect = endRect;
|
|
this.child = child;
|
|
}
|
|
|
|
public readonly Rect beginRect;
|
|
public readonly AnimationController controller;
|
|
public readonly Rect endRect;
|
|
public readonly Widget child;
|
|
|
|
public override State createState() {
|
|
return new _DecoyChildState();
|
|
}
|
|
}
|
|
public class _DecoyChildState : TickerProviderStateMixin<_DecoyChild> {
|
|
|
|
public static readonly Color _lightModeMaskColor = new Color(0xFF888888);
|
|
public static readonly Color _masklessColor = new Color(0xFFFFFFFF);
|
|
public readonly GlobalKey _childGlobalKey = GlobalKey<State<StatefulWidget>>.key();
|
|
public Animation<Color> _mask;
|
|
public Animation<Rect> _rect;
|
|
|
|
public override void initState() {
|
|
base.initState();
|
|
_mask = new _OnOffAnimationColor(
|
|
controller: widget.controller,
|
|
onValue: _lightModeMaskColor,
|
|
offValue: _masklessColor,
|
|
intervalOn: 0.0f,
|
|
intervalOff: 0.5f
|
|
);
|
|
Rect midRect = widget.beginRect.deflate(
|
|
widget.beginRect.width * (CupertinoContextMenuUtils._kOpenScale - 1.0f) / 2f
|
|
);
|
|
List<TweenSequenceItem<Rect>> tweenSequenceItems = new List<TweenSequenceItem<Rect>>();
|
|
tweenSequenceItems.Add(
|
|
new TweenSequenceItem<Rect>(
|
|
tween: new RectTween(
|
|
begin: widget.beginRect,
|
|
end: midRect
|
|
).chain(new CurveTween(curve: Curves.easeInOutCubic)),
|
|
weight: 1.0f
|
|
));
|
|
|
|
tweenSequenceItems.Add(
|
|
new TweenSequenceItem<Rect>(
|
|
tween: new RectTween(
|
|
begin: midRect,
|
|
end: widget.endRect
|
|
).chain(new CurveTween(curve: Curves.easeOutCubic)),
|
|
weight: 1.0f
|
|
));
|
|
|
|
_rect = new TweenSequence<Rect>(tweenSequenceItems).animate(widget.controller);
|
|
_rect.addListener(_rectListener);
|
|
|
|
}
|
|
public void _rectListener() {
|
|
if (widget.controller.value < 0.5f) {
|
|
return;
|
|
}
|
|
//HapticFeedback.selectionClick();????
|
|
/// tbc ???
|
|
_rect.removeListener(_rectListener);
|
|
}
|
|
public override void dispose() {
|
|
_rect.removeListener(_rectListener);
|
|
base.dispose();
|
|
}
|
|
|
|
public Widget _buildAnimation(BuildContext context, Widget child) {
|
|
Color color = widget.controller.status == AnimationStatus.reverse ? _masklessColor : _mask.value;
|
|
List<Color> colors = new List<Color>();
|
|
colors.Add(color);
|
|
colors.Add(color);
|
|
return Positioned.fromRect(
|
|
rect: _rect.value,
|
|
child: foundation_.kIsWeb
|
|
? (Widget) new Container(key: _childGlobalKey, child: widget.child)
|
|
: new ShaderMask(
|
|
key: _childGlobalKey,
|
|
shaderCallback: (Rect bounds) => {
|
|
return new LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: colors).createShader(bounds);
|
|
},
|
|
child: widget.child
|
|
)
|
|
);
|
|
}
|
|
public override Widget build(BuildContext context) {
|
|
List<Widget> widgets = new List<Widget>();
|
|
widgets.Add(new AnimatedBuilder(
|
|
builder: _buildAnimation,
|
|
animation: widget.controller)
|
|
);
|
|
return new Stack(
|
|
children: widgets
|
|
);
|
|
}
|
|
}
|
|
public class _ContextMenuRoute : PopupRoute {
|
|
public _ContextMenuRoute(
|
|
List<Widget> actions = null,
|
|
_ContextMenuLocation contextMenuLocation = default,
|
|
string barrierLabel = null,
|
|
_ContextMenuPreviewBuilderChildless builder = null,
|
|
ImageFilter filter = null,
|
|
Rect previousChildRect = null,
|
|
RouteSettings settings = null
|
|
) : base(filter: filter, settings: settings){
|
|
|
|
D.assert(actions != null && actions.isNotEmpty());
|
|
D.assert(contextMenuLocation != null);
|
|
this.barrierLabel = barrierLabel;
|
|
_actions = actions;
|
|
_builder = builder;
|
|
_contextMenuLocation = contextMenuLocation;
|
|
_previousChildRect = previousChildRect;
|
|
}
|
|
public readonly static Color _kModalBarrierColor = new Color(0x6604040F);
|
|
public readonly TimeSpan _kModalPopupTransitionDuration =new TimeSpan(0, 0, 0, 0, 335);
|
|
public readonly List<Widget> _actions;
|
|
public readonly _ContextMenuPreviewBuilderChildless _builder;
|
|
public readonly GlobalKey _childGlobalKey = new LabeledGlobalKey<State<StatefulWidget>>();
|
|
public readonly _ContextMenuLocation _contextMenuLocation;
|
|
public bool _externalOffstage = false;
|
|
public bool _internalOffstage = false;
|
|
public Orientation _lastOrientation;
|
|
public readonly Rect _previousChildRect;
|
|
public float _scale = 1.0f;
|
|
public readonly GlobalKey _sheetGlobalKey = new LabeledGlobalKey<State<StatefulWidget>>();
|
|
public readonly static CurveTween _curve = new CurveTween(
|
|
curve: Curves.easeOutBack
|
|
);
|
|
public readonly static CurveTween _curveReverse = new CurveTween(
|
|
curve: Curves.easeInBack
|
|
);
|
|
public readonly static RectTween _rectTween = new RectTween();
|
|
public readonly static Animatable<Rect> _rectAnimatable = _rectTween.chain(_curve);
|
|
public readonly static RectTween _rectTweenReverse = new RectTween();
|
|
public readonly static Animatable<Rect> _rectAnimatableReverse = _rectTweenReverse.chain(_curveReverse);
|
|
|
|
public readonly static RectTween _sheetRectTween = new RectTween();
|
|
public readonly Animatable<Rect> _sheetRectAnimatable = _sheetRectTween.chain(_curve);
|
|
public readonly Animatable<Rect> _sheetRectAnimatableReverse = _sheetRectTween.chain(_curveReverse);
|
|
public readonly static Tween< float> _sheetScaleTween = new FloatTween(0.0f,0.0f);
|
|
public readonly static Animatable< float> _sheetScaleAnimatable = _sheetScaleTween.chain(_curve);
|
|
public readonly static Animatable< float> _sheetScaleAnimatableReverse = _sheetScaleTween.chain(_curveReverse);
|
|
public readonly Tween< float> _opacityTween = new FloatTween(begin: 0.0f, end: 1.0f);
|
|
public Animation< float> _sheetOpacity;
|
|
|
|
//public readonly string barrierLabel;
|
|
public override string barrierLabel { get; }
|
|
|
|
public override Color barrierColor {
|
|
get { return _kModalBarrierColor; }
|
|
}
|
|
public override bool barrierDismissible{
|
|
get { return false; }
|
|
}
|
|
|
|
public override bool semanticsDismissible {
|
|
get { return true; }
|
|
}
|
|
|
|
public override TimeSpan transitionDuration {
|
|
get {
|
|
return _kModalPopupTransitionDuration;
|
|
}
|
|
}
|
|
public static AlignmentDirectional getSheetAlignment(_ContextMenuLocation contextMenuLocation) {
|
|
switch (contextMenuLocation) {
|
|
case _ContextMenuLocation.center:
|
|
return AlignmentDirectional.topCenter;
|
|
case _ContextMenuLocation.right:
|
|
return AlignmentDirectional.topEnd;
|
|
default:
|
|
return AlignmentDirectional.topStart;
|
|
}
|
|
}
|
|
public static Rect _getScaledRect(GlobalKey globalKey, float scale) {
|
|
Rect childRect = CupertinoContextMenuUtils._getRect(globalKey);
|
|
Size sizeScaled = childRect.size * scale;
|
|
Offset offsetScaled = new Offset(
|
|
childRect.left + (childRect.size.width - sizeScaled.width) / 2,
|
|
childRect.top + (childRect.size.height - sizeScaled.height) / 2
|
|
);
|
|
return offsetScaled & sizeScaled;
|
|
}
|
|
public static Rect _getSheetRectBegin(Orientation orientation, _ContextMenuLocation contextMenuLocation, Rect childRect, Rect sheetRect) {
|
|
switch (contextMenuLocation) {
|
|
case _ContextMenuLocation.center:
|
|
Offset target1 = (orientation == Orientation.portrait)
|
|
? childRect.bottomCenter
|
|
: childRect.topCenter;
|
|
Offset centered = target1 - new Offset(sheetRect.width / 2, 0.0f);
|
|
return centered & sheetRect.size;
|
|
case _ContextMenuLocation.right:
|
|
Offset target2 = ((orientation == Orientation.portrait)
|
|
? childRect.bottomRight
|
|
: childRect.topRight);
|
|
return (target2 - new Offset(sheetRect.width, 0.0f)) & sheetRect.size;
|
|
default:
|
|
Offset target3 = orientation == Orientation.portrait
|
|
? childRect.bottomLeft
|
|
: childRect.topLeft;
|
|
return target3 & sheetRect.size;
|
|
}
|
|
}
|
|
public void _onDismiss(BuildContext context, float scale, float opacity) {
|
|
_scale = scale;
|
|
_opacityTween.end = opacity;
|
|
_sheetOpacity = _opacityTween.animate(
|
|
new CurvedAnimation(
|
|
parent: animation,
|
|
curve: new Interval(0.9f, 1.0f)
|
|
));
|
|
Navigator.of(context).pop<object>();
|
|
}
|
|
public void _updateTweenRects() {
|
|
Rect childRect = _scale == null
|
|
? CupertinoContextMenuUtils._getRect(_childGlobalKey)
|
|
: _getScaledRect(_childGlobalKey, _scale);
|
|
_rectTween.begin = _previousChildRect;
|
|
_rectTween.end = childRect;
|
|
Rect childRectOriginal = CupertinoContextMenuUtils.fromCenter(
|
|
center: _previousChildRect.center,
|
|
width: _previousChildRect.width / CupertinoContextMenuUtils._kOpenScale,
|
|
height: _previousChildRect.height / CupertinoContextMenuUtils._kOpenScale
|
|
);
|
|
Rect sheetRect = CupertinoContextMenuUtils._getRect(_sheetGlobalKey);
|
|
Rect sheetRectBegin = _getSheetRectBegin(
|
|
_lastOrientation,
|
|
_contextMenuLocation,
|
|
childRectOriginal,
|
|
sheetRect
|
|
);
|
|
_sheetRectTween.begin = sheetRectBegin;
|
|
_sheetRectTween.end = sheetRect;
|
|
_sheetScaleTween.begin = 0.0f;
|
|
_sheetScaleTween.end = _scale;
|
|
_rectTweenReverse.begin = childRectOriginal;
|
|
_rectTweenReverse.end = childRect;
|
|
}
|
|
public void _setOffstageInternally() {
|
|
base.offstage = _externalOffstage || _internalOffstage;
|
|
changedInternalState();
|
|
}
|
|
|
|
protected internal override bool didPop(object result) {
|
|
_updateTweenRects();
|
|
return base.didPop(result);
|
|
}
|
|
|
|
public override bool offstage{
|
|
set{
|
|
_externalOffstage = value;
|
|
_setOffstageInternally();
|
|
}
|
|
}
|
|
|
|
protected internal override TickerFuture didPush() {
|
|
_internalOffstage = true;
|
|
_setOffstageInternally();
|
|
SchedulerBinding.instance.addPostFrameCallback((TimeSpan timeSpan)=>{
|
|
_updateTweenRects();
|
|
_internalOffstage = false;
|
|
_setOffstageInternally();
|
|
});
|
|
return base.didPush();
|
|
}
|
|
public override Animation<float> createAnimation() {
|
|
Animation< float> animation = base.createAnimation();
|
|
_sheetOpacity = _opacityTween.animate(new CurvedAnimation(
|
|
parent: animation,
|
|
curve: Curves.linear
|
|
));
|
|
return animation;
|
|
}
|
|
public override Widget buildPage(BuildContext context, Animation< float> animation, Animation< float> secondaryAnimation) {
|
|
return null;
|
|
}
|
|
public override Widget buildTransitions(BuildContext context1, Animation< float> animation, Animation< float> secondaryAnimation, Widget child) {
|
|
return new OrientationBuilder(
|
|
builder: (BuildContext context2, Orientation orientation)=>{
|
|
_lastOrientation = orientation;
|
|
if (!animation.isCompleted) {
|
|
bool reverse = animation.status == AnimationStatus.reverse;
|
|
Rect rect = reverse
|
|
? _rectAnimatableReverse.evaluate(animation)
|
|
: _rectAnimatable.evaluate(animation);
|
|
Rect sheetRect = reverse
|
|
? _sheetRectAnimatableReverse.evaluate(animation)
|
|
: _sheetRectAnimatable.evaluate(animation);
|
|
float sheetScale = reverse
|
|
? _sheetScaleAnimatableReverse.evaluate(animation)
|
|
: _sheetScaleAnimatable.evaluate(animation);
|
|
List<Widget> widgets = new List<Widget>();
|
|
widgets.Add(
|
|
Positioned.fromRect(
|
|
rect: sheetRect,
|
|
child: new Opacity(
|
|
opacity: _sheetOpacity.value,
|
|
child: Transform.scale(
|
|
alignment: getSheetAlignment(_contextMenuLocation),
|
|
scale: sheetScale,
|
|
child: new _ContextMenuSheet(
|
|
key: _sheetGlobalKey,
|
|
actions: _actions,
|
|
contextMenuLocation: _contextMenuLocation,
|
|
orientation: orientation
|
|
)
|
|
)
|
|
)
|
|
)
|
|
);
|
|
widgets.Add(
|
|
Positioned.fromRect(
|
|
key: _childGlobalKey,
|
|
rect: rect,
|
|
child: _builder(context2, animation)
|
|
));
|
|
return new Stack(
|
|
children: widgets
|
|
);
|
|
}
|
|
return new _ContextMenuRouteStatic(
|
|
actions: _actions,
|
|
child: _builder(context1, animation),
|
|
childGlobalKey: _childGlobalKey,
|
|
contextMenuLocation: _contextMenuLocation,
|
|
onDismiss: _onDismiss,
|
|
orientation: orientation,
|
|
sheetGlobalKey: _sheetGlobalKey
|
|
);
|
|
}
|
|
);
|
|
}
|
|
}
|
|
public class _ContextMenuRouteStatic : StatefulWidget {
|
|
public _ContextMenuRouteStatic(
|
|
Key key = null,
|
|
List<Widget> actions = null,
|
|
Widget child = null,
|
|
GlobalKey childGlobalKey = null,
|
|
_ContextMenuLocation contextMenuLocation = default,
|
|
_DismissCallback onDismiss =default,
|
|
Orientation orientation = default,
|
|
GlobalKey sheetGlobalKey = null
|
|
) : base(key: key) {
|
|
|
|
D.assert(contextMenuLocation != default);
|
|
D.assert(orientation != default);
|
|
this.actions = actions;
|
|
this.child = child;
|
|
this.childGlobalKey = childGlobalKey;
|
|
this.contextMenuLocation = contextMenuLocation;
|
|
this.onDismiss = onDismiss;
|
|
this.orientation = orientation;
|
|
this.sheetGlobalKey = sheetGlobalKey;
|
|
}
|
|
public readonly List<Widget> actions;
|
|
public readonly Widget child;
|
|
public readonly GlobalKey childGlobalKey;
|
|
public readonly _ContextMenuLocation contextMenuLocation;
|
|
public readonly _DismissCallback onDismiss;
|
|
public readonly Orientation orientation;
|
|
public readonly GlobalKey sheetGlobalKey;
|
|
|
|
public override State createState() {
|
|
return new _ContextMenuRouteStaticState();
|
|
}
|
|
}
|
|
|
|
public class _ContextMenuRouteStaticState : TickerProviderStateMixin<_ContextMenuRouteStatic> {
|
|
public readonly static float _kMinScale = 0.8f;
|
|
public readonly static float _kSheetScaleThreshold = 0.9f;
|
|
public readonly static float _kPadding = 20.0f;
|
|
public readonly static float _kDamping = 400.0f;
|
|
public readonly static TimeSpan _kMoveControllerDuration = TimeSpan.FromMilliseconds(600);
|
|
Offset _dragOffset;
|
|
public float _lastScale = 1.0f;
|
|
public AnimationController _moveController;
|
|
public AnimationController _sheetController;
|
|
public Animation<Offset> _moveAnimation;
|
|
public Animation< float> _sheetScaleAnimation;
|
|
public Animation< float> _sheetOpacityAnimation;
|
|
public static float _getScale(Orientation orientation, float maxDragDistance, float dy) {
|
|
float dyDirectional = dy <= 0.0f ? dy : -dy;
|
|
return Mathf.Max(
|
|
_kMinScale,
|
|
(maxDragDistance + dyDirectional) / maxDragDistance
|
|
);
|
|
}
|
|
void _onPanStart(DragStartDetails details) {
|
|
_moveController.setValue(1.0f) ;
|
|
_setDragOffset(Offset.zero);
|
|
}
|
|
void _onPanUpdate(DragUpdateDetails details) {
|
|
_setDragOffset(_dragOffset + details.delta);
|
|
}
|
|
void _onPanEnd(DragEndDetails details) {
|
|
if (details.velocity.pixelsPerSecond.dy.abs() >= CupertinoContextMenuUtils.kMinFlingVelocity) {
|
|
bool flingIsAway = details.velocity.pixelsPerSecond.dy > 0;
|
|
float finalPosition = flingIsAway
|
|
? _moveAnimation.value.dy + 100.0f
|
|
: 0.0f;
|
|
if (flingIsAway && _sheetController.status != AnimationStatus.forward) {
|
|
_sheetController.forward();
|
|
} else if (!flingIsAway && _sheetController.status != AnimationStatus.reverse) {
|
|
_sheetController.reverse();
|
|
}
|
|
|
|
_moveAnimation = new OffsetTween(
|
|
begin: new Offset(0.0f, _moveAnimation.value.dy),
|
|
end:new Offset(0.0f, finalPosition)
|
|
).animate(_moveController);
|
|
_moveController.reset();
|
|
_moveController.duration = TimeSpan.FromMilliseconds(64);
|
|
_moveController.forward();
|
|
_moveController.addStatusListener(_flingStatusListener);
|
|
return;
|
|
}
|
|
if (_lastScale == _kMinScale) {
|
|
widget.onDismiss(context, _lastScale, _sheetOpacityAnimation.value);
|
|
return;
|
|
}
|
|
_moveController.addListener(_moveListener);
|
|
_moveController.reverse();
|
|
}
|
|
|
|
void _moveListener() {
|
|
if (_lastScale > _kSheetScaleThreshold) {
|
|
_moveController.removeListener(_moveListener);
|
|
if (_sheetController.status != AnimationStatus.dismissed) {
|
|
_sheetController.reverse();
|
|
}
|
|
}
|
|
}
|
|
|
|
void _flingStatusListener(AnimationStatus status) {
|
|
if (status != AnimationStatus.completed) {
|
|
return;
|
|
}
|
|
_moveController.duration = _kMoveControllerDuration;
|
|
_moveController.removeStatusListener(_flingStatusListener);
|
|
if (_moveAnimation.value.dy == 0.0) {
|
|
return;
|
|
}
|
|
widget.onDismiss(context, _lastScale, _sheetOpacityAnimation.value);
|
|
}
|
|
Alignment _getChildAlignment(Orientation orientation, _ContextMenuLocation contextMenuLocation) {
|
|
switch (contextMenuLocation) {
|
|
case _ContextMenuLocation.center:
|
|
return orientation == Orientation.portrait
|
|
? Alignment.bottomCenter
|
|
: Alignment.topRight;
|
|
case _ContextMenuLocation.right:
|
|
return orientation == Orientation.portrait
|
|
? Alignment.bottomCenter
|
|
: Alignment.topLeft;
|
|
default:
|
|
return orientation == Orientation.portrait
|
|
? Alignment.bottomCenter
|
|
: Alignment.topRight;
|
|
}
|
|
}
|
|
|
|
void _setDragOffset(Offset dragOffset) {
|
|
float endX = _kPadding * dragOffset.dx / _kDamping;
|
|
float endY = dragOffset.dy >= 0.0
|
|
? dragOffset.dy
|
|
: _kPadding * dragOffset.dy / _kDamping;
|
|
setState(() =>{
|
|
_dragOffset = dragOffset;
|
|
_moveAnimation = new OffsetTween(
|
|
begin: Offset.zero,
|
|
end: new Offset(
|
|
endX.clamp(-_kPadding, _kPadding) ,
|
|
endY
|
|
)).animate(
|
|
new CurvedAnimation(
|
|
parent: _moveController,
|
|
curve: Curves.elasticIn
|
|
)
|
|
);
|
|
if (_lastScale <= _kSheetScaleThreshold && _sheetController.status != AnimationStatus.forward
|
|
&& _sheetScaleAnimation.value != 0.0f) {
|
|
_sheetController.forward();
|
|
} else if (_lastScale > _kSheetScaleThreshold && _sheetController.status != AnimationStatus.reverse
|
|
&& _sheetScaleAnimation.value != 1.0f) {
|
|
_sheetController.reverse();
|
|
}
|
|
});
|
|
}
|
|
List<Widget> _getChildren(Orientation orientation, _ContextMenuLocation contextMenuLocation) {
|
|
Expanded child = new Expanded(
|
|
child: new Align(
|
|
alignment: _getChildAlignment(
|
|
widget.orientation,
|
|
widget.contextMenuLocation
|
|
),
|
|
child: new AnimatedBuilder(
|
|
animation: _moveController,
|
|
builder: _buildChildAnimation,
|
|
child: widget.child
|
|
)
|
|
)
|
|
);
|
|
Container spacer = new Container(
|
|
width: _kPadding,
|
|
height: _kPadding
|
|
);
|
|
Expanded sheet = new Expanded(
|
|
child: new AnimatedBuilder(
|
|
animation: _sheetController,
|
|
builder: _buildSheetAnimation,
|
|
child: new _ContextMenuSheet(
|
|
key: widget.sheetGlobalKey,
|
|
actions: widget.actions,
|
|
contextMenuLocation: widget.contextMenuLocation,
|
|
orientation: widget.orientation
|
|
)
|
|
)
|
|
);
|
|
List<Widget> centerWidgets = new List<Widget>();
|
|
centerWidgets.Add(child);
|
|
centerWidgets.Add(spacer);
|
|
centerWidgets.Add(sheet);
|
|
List<Widget> rightWidgets = new List<Widget>();
|
|
rightWidgets.Add(sheet);
|
|
rightWidgets.Add(spacer);
|
|
rightWidgets.Add(child);
|
|
switch (contextMenuLocation) {
|
|
case _ContextMenuLocation.center:
|
|
return new List<Widget>{child, spacer, sheet};
|
|
case _ContextMenuLocation.right:
|
|
return orientation == Orientation.portrait
|
|
? new List<Widget>{child, spacer, sheet}
|
|
: new List<Widget>{sheet, spacer, child};
|
|
default:
|
|
return new List<Widget>{child, spacer, sheet};
|
|
}
|
|
}
|
|
|
|
// Build the animation for the _ContextMenuSheet.
|
|
Widget _buildSheetAnimation(BuildContext context, Widget child) {
|
|
return Transform.scale(
|
|
alignment: _ContextMenuRoute.getSheetAlignment(widget.contextMenuLocation),
|
|
scale: _sheetScaleAnimation.value,
|
|
child: new Opacity(
|
|
opacity: _sheetOpacityAnimation.value,
|
|
child: child
|
|
)
|
|
);
|
|
}
|
|
|
|
// Build the animation for the child.
|
|
Widget _buildChildAnimation(BuildContext context, Widget child) {
|
|
_lastScale = _getScale(
|
|
widget.orientation,
|
|
MediaQuery.of(context).size.height,
|
|
_moveAnimation.value.dy
|
|
);
|
|
return Transform.scale(
|
|
key: widget.childGlobalKey,
|
|
scale: _lastScale,
|
|
child: child
|
|
);
|
|
}
|
|
|
|
// Build the animation for the overall draggable dismissable content.
|
|
Widget _buildAnimation(BuildContext context, Widget child) {
|
|
return Transform.translate(
|
|
offset: _moveAnimation.value,
|
|
child: child
|
|
);
|
|
}
|
|
|
|
|
|
public override void initState() {
|
|
base.initState();
|
|
_moveController = new AnimationController(
|
|
duration: _kMoveControllerDuration,
|
|
value: 1.0f,
|
|
vsync: this
|
|
);
|
|
_sheetController = new AnimationController(
|
|
duration: TimeSpan.FromMilliseconds(100),
|
|
reverseDuration: TimeSpan.FromMilliseconds(300),
|
|
vsync: this
|
|
);
|
|
_sheetScaleAnimation = new FloatTween(
|
|
begin: 1.0f,
|
|
end: 0.0f
|
|
).animate(
|
|
new CurvedAnimation(
|
|
parent: _sheetController,
|
|
curve: Curves.linear,
|
|
reverseCurve: Curves.easeInBack
|
|
)
|
|
);
|
|
_sheetOpacityAnimation = new FloatTween(
|
|
begin: 1.0f,
|
|
end: 0.0f
|
|
).animate(_sheetController);
|
|
_setDragOffset(Offset.zero);
|
|
}
|
|
|
|
public override void dispose() {
|
|
_moveController.dispose();
|
|
_sheetController.dispose();
|
|
base.dispose();
|
|
}
|
|
|
|
|
|
public override Widget build(BuildContext context) {
|
|
List<Widget> children = _getChildren(
|
|
widget.orientation,
|
|
widget.contextMenuLocation
|
|
);
|
|
|
|
return new SafeArea(
|
|
child: new Padding(
|
|
padding: EdgeInsets.all(_kPadding),
|
|
child: new Align(
|
|
alignment: Alignment.topLeft,
|
|
child: new GestureDetector(
|
|
onPanEnd: _onPanEnd,
|
|
onPanStart: _onPanStart,
|
|
onPanUpdate: _onPanUpdate,
|
|
child: new AnimatedBuilder(
|
|
animation: _moveController,
|
|
builder: _buildAnimation,
|
|
child: widget.orientation == Orientation.portrait ?
|
|
(Widget) new Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: children
|
|
)
|
|
: new Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: children
|
|
)
|
|
)
|
|
)
|
|
)
|
|
)
|
|
);
|
|
}
|
|
}
|
|
public class _ContextMenuSheet : StatelessWidget {
|
|
public _ContextMenuSheet(
|
|
Key key = null,
|
|
List<Widget> actions = null,
|
|
_ContextMenuLocation contextMenuLocation = default,
|
|
Orientation orientation = default
|
|
) : base(key: key) {
|
|
D.assert(actions != null && actions.isNotEmpty());
|
|
D.assert(contextMenuLocation != null);
|
|
D.assert(orientation != null);
|
|
_contextMenuLocation = contextMenuLocation;
|
|
_orientation = orientation;
|
|
this.actions = actions;
|
|
}
|
|
public readonly List<Widget> actions;
|
|
public readonly _ContextMenuLocation _contextMenuLocation;
|
|
public readonly Orientation _orientation;
|
|
public List<Widget> children {
|
|
get{
|
|
Flexible menu = new Flexible(
|
|
fit: FlexFit.tight,
|
|
flex: 2,
|
|
child: new IntrinsicHeight(
|
|
child: new ClipRRect(
|
|
borderRadius: BorderRadius.circular(13.0f),
|
|
child: new Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: actions
|
|
)
|
|
)
|
|
)
|
|
);
|
|
List<Widget> spacers1 = new List<Widget>();
|
|
List<Widget> spacers2 = new List<Widget>();
|
|
spacers1.Add(new Spacer(
|
|
flex: 1
|
|
));
|
|
spacers1.Add(menu);
|
|
spacers1.Add(new Spacer(
|
|
flex: 1
|
|
));
|
|
spacers2.Add(menu);
|
|
spacers2.Add(new Spacer(
|
|
flex: 1
|
|
));
|
|
List<Widget> spacers3 = new List<Widget>();
|
|
List<Widget> spacers4 = new List<Widget>();
|
|
spacers3.Add(new Spacer(
|
|
flex: 1
|
|
));
|
|
spacers3.Add(menu);
|
|
spacers4.Add(menu);
|
|
spacers4.Add(new Spacer(
|
|
flex: 1
|
|
));
|
|
switch (_contextMenuLocation) {
|
|
case _ContextMenuLocation.center:
|
|
return _orientation == Orientation.portrait
|
|
? spacers1
|
|
: spacers2;
|
|
case _ContextMenuLocation.right:
|
|
return spacers3;
|
|
default:
|
|
return spacers4;
|
|
}
|
|
|
|
}
|
|
}
|
|
public override Widget build(BuildContext context) {
|
|
return new Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: children
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* We cannot use _OnOffAnimation<T> here directly since Tween<T> is an abstract class in UIWidgets (while it is not in flutter)
|
|
* Refer to "Twee<T>" for the detailed reasons why we do so
|
|
*
|
|
* Instead, we should explicitly define classes for each generic type here
|
|
*
|
|
*/
|
|
public class _OnOffAnimationColor : CompoundAnimation<Color> {
|
|
public _OnOffAnimationColor(
|
|
AnimationController controller = null,
|
|
Color onValue = default,
|
|
Color offValue = default,
|
|
float? intervalOn = null,
|
|
float? intervalOff = null
|
|
) : base(
|
|
first: new ColorTween(begin: offValue, end: onValue).animate(
|
|
new CurvedAnimation(
|
|
parent: controller,
|
|
curve: new Interval(intervalOn == null ? 0.0f : (float)intervalOn, intervalOn == null ? 0.0f : (float)intervalOn)
|
|
)
|
|
),
|
|
next: new ColorTween(begin: onValue, end: offValue).animate(
|
|
new CurvedAnimation(
|
|
parent: controller,
|
|
curve: new Interval(intervalOff == null ? 0.0f : (float)intervalOff, intervalOff == null ? 0.0f : (float)intervalOff)
|
|
)
|
|
)
|
|
)
|
|
{
|
|
D.assert(intervalOn != null && intervalOn >= 0.0 && intervalOn <= 1.0);
|
|
D.assert(intervalOff !=null && intervalOff >= 0.0 && intervalOff <= 1.0);
|
|
D.assert(intervalOn <= intervalOff);
|
|
}
|
|
public readonly Color _offValue;
|
|
public override Color value {
|
|
get {
|
|
return next.value.Equals( _offValue) ? next.value : first.value;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|