您最多选择25个主题
主题必须以中文或者字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
575 行
22 KiB
575 行
22 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using RSG;
|
|
using Unity.UIWidgets.animation;
|
|
using Unity.UIWidgets.foundation;
|
|
using Unity.UIWidgets.gestures;
|
|
using Unity.UIWidgets.painting;
|
|
using Unity.UIWidgets.rendering;
|
|
using Unity.UIWidgets.scheduler;
|
|
using Unity.UIWidgets.ui;
|
|
|
|
namespace Unity.UIWidgets.widgets {
|
|
public delegate void DismissDirectionCallback(DismissDirection? direction);
|
|
|
|
public delegate Promise<bool> ConfirmDismissCallback(DismissDirection? direction);
|
|
|
|
public enum DismissDirection {
|
|
vertical,
|
|
|
|
horizontal,
|
|
|
|
endToStart,
|
|
|
|
startToEnd,
|
|
|
|
up,
|
|
|
|
down
|
|
}
|
|
|
|
public class Dismissible : StatefulWidget {
|
|
public Dismissible(
|
|
Key key = null,
|
|
Widget child = null,
|
|
Widget background = null,
|
|
Widget secondaryBackground = null,
|
|
ConfirmDismissCallback confirmDismiss = null,
|
|
VoidCallback onResize = null,
|
|
DismissDirectionCallback onDismissed = null,
|
|
DismissDirection direction = DismissDirection.horizontal,
|
|
TimeSpan? resizeDuration = null,
|
|
Dictionary<DismissDirection?, float?> dismissThresholds = null,
|
|
TimeSpan? movementDuration = null,
|
|
float crossAxisEndOffset = 0.0f,
|
|
DragStartBehavior dragStartBehavior = DragStartBehavior.start
|
|
) : base(key: key) {
|
|
D.assert(key != null);
|
|
D.assert(secondaryBackground != null ? background != null : true);
|
|
this.resizeDuration = resizeDuration ?? new TimeSpan(0, 0, 0, 0, 300);
|
|
this.dismissThresholds = dismissThresholds ?? new Dictionary<DismissDirection?, float?>();
|
|
this.movementDuration = movementDuration ?? new TimeSpan(0, 0, 0, 0, 200);
|
|
this.child = child;
|
|
this.background = background;
|
|
this.secondaryBackground = secondaryBackground;
|
|
this.confirmDismiss = confirmDismiss;
|
|
this.onResize = onResize;
|
|
this.onDismissed = onDismissed;
|
|
this.direction = direction;
|
|
this.crossAxisEndOffset = crossAxisEndOffset;
|
|
this.dragStartBehavior = dragStartBehavior;
|
|
}
|
|
|
|
public readonly Widget child;
|
|
|
|
public readonly Widget background;
|
|
|
|
public readonly Widget secondaryBackground;
|
|
|
|
public readonly VoidCallback onResize;
|
|
|
|
public readonly DismissDirectionCallback onDismissed;
|
|
|
|
public readonly ConfirmDismissCallback confirmDismiss;
|
|
|
|
public readonly DismissDirection direction;
|
|
|
|
public readonly TimeSpan? resizeDuration;
|
|
|
|
public readonly Dictionary<DismissDirection?, float?> dismissThresholds;
|
|
|
|
public readonly TimeSpan? movementDuration;
|
|
|
|
public readonly float crossAxisEndOffset;
|
|
|
|
public readonly DragStartBehavior dragStartBehavior;
|
|
|
|
public override State createState() {
|
|
return new _DismissibleState();
|
|
}
|
|
}
|
|
|
|
class _AutomaticWidgetTicker<T> : Ticker where T : StatefulWidget {
|
|
internal _AutomaticWidgetTicker(
|
|
TickerCallback onTick,
|
|
AutomaticKeepAliveClientWithTickerProviderStateMixin<T> creator,
|
|
Func<string> debugLabel = null) :
|
|
base(onTick: onTick, debugLabel: debugLabel) {
|
|
this._creator = creator;
|
|
}
|
|
|
|
readonly AutomaticKeepAliveClientWithTickerProviderStateMixin<T> _creator;
|
|
|
|
public override void dispose() {
|
|
this._creator._removeTicker(this);
|
|
base.dispose();
|
|
}
|
|
}
|
|
|
|
class _DismissibleClipper : CustomClipper<Rect> {
|
|
public _DismissibleClipper(
|
|
Axis axis,
|
|
Animation<Offset> moveAnimation
|
|
) : base(reclip: moveAnimation) {
|
|
D.assert(moveAnimation != null);
|
|
this.axis = axis;
|
|
this.moveAnimation = moveAnimation;
|
|
}
|
|
|
|
public readonly Axis axis;
|
|
public readonly Animation<Offset> moveAnimation;
|
|
|
|
public override Rect getClip(Size size) {
|
|
switch (this.axis) {
|
|
case Axis.horizontal:
|
|
float offset1 = this.moveAnimation.value.dx * size.width;
|
|
if (offset1 < 0) {
|
|
return Rect.fromLTRB(size.width + offset1, 0.0f, size.width, size.height);
|
|
}
|
|
|
|
return Rect.fromLTRB(0.0f, 0.0f, offset1, size.height);
|
|
case Axis.vertical:
|
|
float offset = this.moveAnimation.value.dy * size.height;
|
|
if (offset < 0) {
|
|
return Rect.fromLTRB(0.0f, size.height + offset, size.width, size.height);
|
|
}
|
|
|
|
return Rect.fromLTRB(0.0f, 0.0f, size.width, offset);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public override Rect getApproximateClipRect(Size size) {
|
|
return this.getClip(size);
|
|
}
|
|
|
|
public override bool shouldReclip(CustomClipper<Rect> oldClipper) {
|
|
D.assert(oldClipper is _DismissibleClipper);
|
|
_DismissibleClipper clipper = oldClipper as _DismissibleClipper;
|
|
return clipper.axis != this.axis
|
|
|| clipper.moveAnimation.value != this.moveAnimation.value;
|
|
}
|
|
}
|
|
|
|
enum _FlingGestureKind {
|
|
none,
|
|
forward,
|
|
reverse
|
|
}
|
|
|
|
public class _DismissibleState : AutomaticKeepAliveClientWithTickerProviderStateMixin<Dismissible> {
|
|
static readonly Curve _kResizeTimeCurve = new Interval(0.4f, 1.0f, curve: Curves.ease);
|
|
const float _kMinFlingVelocity = 700.0f;
|
|
const float _kMinFlingVelocityDelta = 400.0f;
|
|
const float _kFlingVelocityScale = 1.0f / 300.0f;
|
|
const float _kDismissThreshold = 0.4f;
|
|
|
|
public override void initState() {
|
|
base.initState();
|
|
this._moveController = new AnimationController(duration: this.widget.movementDuration, vsync: this);
|
|
this._moveController.addStatusListener(this._handleDismissStatusChanged);
|
|
this._updateMoveAnimation();
|
|
}
|
|
|
|
AnimationController _moveController;
|
|
Animation<Offset> _moveAnimation;
|
|
|
|
AnimationController _resizeController;
|
|
Animation<float> _resizeAnimation;
|
|
|
|
float _dragExtent = 0.0f;
|
|
bool _dragUnderway = false;
|
|
Size _sizePriorToCollapse;
|
|
|
|
protected override bool wantKeepAlive {
|
|
get { return this._moveController?.isAnimating == true || this._resizeController?.isAnimating == true; }
|
|
}
|
|
|
|
public override void dispose() {
|
|
this._moveController.dispose();
|
|
this._resizeController?.dispose();
|
|
base.dispose();
|
|
}
|
|
|
|
bool _directionIsXAxis {
|
|
get {
|
|
return this.widget.direction == DismissDirection.horizontal
|
|
|| this.widget.direction == DismissDirection.endToStart
|
|
|| this.widget.direction == DismissDirection.startToEnd;
|
|
}
|
|
}
|
|
|
|
DismissDirection? _extentToDirection(float? extent) {
|
|
if (extent == 0.0) {
|
|
return null;
|
|
}
|
|
|
|
if (this._directionIsXAxis) {
|
|
switch (Directionality.of(this.context)) {
|
|
case TextDirection.rtl:
|
|
return extent < 0 ? DismissDirection.startToEnd : DismissDirection.endToStart;
|
|
case TextDirection.ltr:
|
|
return extent > 0 ? DismissDirection.startToEnd : DismissDirection.endToStart;
|
|
}
|
|
|
|
D.assert(false);
|
|
return null;
|
|
}
|
|
|
|
return extent > 0 ? DismissDirection.down : DismissDirection.up;
|
|
}
|
|
|
|
DismissDirection? _dismissDirection {
|
|
get { return this._extentToDirection(this._dragExtent); }
|
|
}
|
|
|
|
bool _isActive {
|
|
get { return this._dragUnderway || this._moveController.isAnimating; }
|
|
}
|
|
|
|
float _overallDragAxisExtent {
|
|
get {
|
|
Size size = this.context.size;
|
|
return this._directionIsXAxis ? size.width : size.height;
|
|
}
|
|
}
|
|
|
|
void _handleDragStart(DragStartDetails details) {
|
|
this._dragUnderway = true;
|
|
if (this._moveController.isAnimating) {
|
|
this._dragExtent = this._moveController.value * this._overallDragAxisExtent * this._dragExtent.sign();
|
|
this._moveController.stop();
|
|
}
|
|
else {
|
|
this._dragExtent = 0.0f;
|
|
this._moveController.setValue(0.0f);
|
|
}
|
|
|
|
this.setState(() => { this._updateMoveAnimation(); });
|
|
}
|
|
|
|
void _handleDragUpdate(DragUpdateDetails details) {
|
|
if (!this._isActive || this._moveController.isAnimating) {
|
|
return;
|
|
}
|
|
|
|
float delta = details.primaryDelta ?? 0.0f;
|
|
float oldDragExtent = this._dragExtent;
|
|
switch (this.widget.direction) {
|
|
case DismissDirection.horizontal:
|
|
case DismissDirection.vertical:
|
|
this._dragExtent += delta;
|
|
break;
|
|
|
|
case DismissDirection.up:
|
|
if (this._dragExtent + delta < 0) {
|
|
this._dragExtent += delta;
|
|
}
|
|
|
|
break;
|
|
|
|
case DismissDirection.down:
|
|
if (this._dragExtent + delta > 0) {
|
|
this._dragExtent += delta;
|
|
}
|
|
|
|
break;
|
|
|
|
case DismissDirection.endToStart:
|
|
switch (Directionality.of(this.context)) {
|
|
case TextDirection.rtl:
|
|
if (this._dragExtent + delta > 0) {
|
|
this._dragExtent += delta;
|
|
}
|
|
|
|
break;
|
|
case TextDirection.ltr:
|
|
if (this._dragExtent + delta < 0) {
|
|
this._dragExtent += delta;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
break;
|
|
|
|
case DismissDirection.startToEnd:
|
|
switch (Directionality.of(this.context)) {
|
|
case TextDirection.rtl:
|
|
if (this._dragExtent + delta < 0) {
|
|
this._dragExtent += delta;
|
|
}
|
|
|
|
break;
|
|
case TextDirection.ltr:
|
|
if (this._dragExtent + delta > 0) {
|
|
this._dragExtent += delta;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
if (oldDragExtent.sign() != this._dragExtent.sign()) {
|
|
this.setState(() => { this._updateMoveAnimation(); });
|
|
}
|
|
|
|
if (!this._moveController.isAnimating) {
|
|
this._moveController.setValue(this._dragExtent.abs() / this._overallDragAxisExtent);
|
|
}
|
|
}
|
|
|
|
void _updateMoveAnimation() {
|
|
float end = this._dragExtent.sign();
|
|
this._moveAnimation = this._moveController.drive(
|
|
new OffsetTween(
|
|
begin: Offset.zero,
|
|
end: this._directionIsXAxis
|
|
? new Offset(end, this.widget.crossAxisEndOffset)
|
|
: new Offset(this.widget.crossAxisEndOffset, end)
|
|
)
|
|
);
|
|
}
|
|
|
|
_FlingGestureKind _describeFlingGesture(Velocity velocity) {
|
|
if (this._dragExtent == 0.0f) {
|
|
return _FlingGestureKind.none;
|
|
}
|
|
|
|
float vx = velocity.pixelsPerSecond.dx;
|
|
float vy = velocity.pixelsPerSecond.dy;
|
|
DismissDirection? flingDirection;
|
|
if (this._directionIsXAxis) {
|
|
if (vx.abs() - vy.abs() < _kMinFlingVelocityDelta || vx.abs() < _kMinFlingVelocity) {
|
|
return _FlingGestureKind.none;
|
|
}
|
|
|
|
D.assert(vx != 0.0f);
|
|
flingDirection = this._extentToDirection(vx);
|
|
}
|
|
else {
|
|
if (vy.abs() - vx.abs() < _kMinFlingVelocityDelta || vy.abs() < _kMinFlingVelocity) {
|
|
return _FlingGestureKind.none;
|
|
}
|
|
|
|
D.assert(vy != 0.0);
|
|
flingDirection = this._extentToDirection(vy);
|
|
}
|
|
|
|
D.assert(this._dismissDirection != null);
|
|
if (flingDirection == this._dismissDirection) {
|
|
return _FlingGestureKind.forward;
|
|
}
|
|
|
|
return _FlingGestureKind.reverse;
|
|
}
|
|
|
|
void _handleDragEnd(DragEndDetails details) {
|
|
if (!this._isActive || this._moveController.isAnimating) {
|
|
return;
|
|
}
|
|
|
|
this._dragUnderway = false;
|
|
this._confirmStartResizeAnimation().Then((value) => {
|
|
if (this._moveController.isCompleted && value) {
|
|
this._startResizeAnimation();
|
|
}
|
|
else {
|
|
float flingVelocity = this._directionIsXAxis
|
|
? details.velocity.pixelsPerSecond.dx
|
|
: details.velocity.pixelsPerSecond.dy;
|
|
switch (this._describeFlingGesture(details.velocity)) {
|
|
case _FlingGestureKind.forward:
|
|
D.assert(this._dragExtent != 0.0f);
|
|
D.assert(!this._moveController.isDismissed);
|
|
if ((this.widget.dismissThresholds.getOrDefault(this._dismissDirection) ??
|
|
_kDismissThreshold) >= 1.0) {
|
|
this._moveController.reverse();
|
|
break;
|
|
}
|
|
|
|
this._dragExtent = flingVelocity.sign();
|
|
this._moveController.fling(velocity: flingVelocity.abs() * _kFlingVelocityScale);
|
|
break;
|
|
case _FlingGestureKind.reverse:
|
|
D.assert(this._dragExtent != 0.0f);
|
|
D.assert(!this._moveController.isDismissed);
|
|
this._dragExtent = flingVelocity.sign();
|
|
this._moveController.fling(velocity: -flingVelocity.abs() * _kFlingVelocityScale);
|
|
break;
|
|
case _FlingGestureKind.none:
|
|
if (!this._moveController.isDismissed) {
|
|
// we already know it's not completed, we check that above
|
|
if (this._moveController.value >
|
|
(this.widget.dismissThresholds.getOrDefault(this._dismissDirection) ??
|
|
_kDismissThreshold)) {
|
|
this._moveController.forward();
|
|
}
|
|
else {
|
|
this._moveController.reverse();
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
void _handleDismissStatusChanged(AnimationStatus status) {
|
|
if (status == AnimationStatus.completed && !this._dragUnderway) {
|
|
this._confirmStartResizeAnimation().Then((value) => {
|
|
if (value) {
|
|
this._startResizeAnimation();
|
|
}
|
|
else {
|
|
this._moveController.reverse();
|
|
}
|
|
|
|
this.updateKeepAlive();
|
|
});
|
|
}
|
|
}
|
|
|
|
IPromise<bool> _confirmStartResizeAnimation() {
|
|
if (this.widget.confirmDismiss != null) {
|
|
DismissDirection? direction = this._dismissDirection;
|
|
D.assert(direction != null);
|
|
return this.widget.confirmDismiss(direction);
|
|
}
|
|
|
|
return Promise<bool>.Resolved(true);
|
|
}
|
|
|
|
void _startResizeAnimation() {
|
|
D.assert(this._moveController != null);
|
|
D.assert(this._moveController.isCompleted);
|
|
D.assert(this._resizeController == null);
|
|
D.assert(this._sizePriorToCollapse == null);
|
|
if (this.widget.resizeDuration == null) {
|
|
if (this.widget.onDismissed != null) {
|
|
DismissDirection? direction = this._dismissDirection;
|
|
D.assert(direction != null);
|
|
this.widget.onDismissed(direction);
|
|
}
|
|
}
|
|
else {
|
|
this._resizeController = new AnimationController(duration: this.widget.resizeDuration, vsync: this);
|
|
this._resizeController.addListener(this._handleResizeProgressChanged);
|
|
this._resizeController.addStatusListener((AnimationStatus status) => this.updateKeepAlive());
|
|
this._resizeController.forward();
|
|
this.setState(() => {
|
|
this._sizePriorToCollapse = this.context.size;
|
|
this._resizeAnimation = this._resizeController.drive(
|
|
new CurveTween(
|
|
curve: _kResizeTimeCurve
|
|
)
|
|
).drive(
|
|
new FloatTween(
|
|
begin: 1.0f,
|
|
end: 0.0f
|
|
)
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
void _handleResizeProgressChanged() {
|
|
if (this._resizeController.isCompleted) {
|
|
if (this.widget.onDismissed != null) {
|
|
DismissDirection? direction = this._dismissDirection;
|
|
D.assert(direction != null);
|
|
this.widget.onDismissed(direction);
|
|
}
|
|
}
|
|
else {
|
|
if (this.widget.onResize != null) {
|
|
this.widget.onResize();
|
|
}
|
|
}
|
|
}
|
|
|
|
public override Widget build(BuildContext context) {
|
|
base.build(context); // See AutomaticKeepAliveClientMixin.
|
|
|
|
D.assert(!this._directionIsXAxis || WidgetsD.debugCheckHasDirectionality(context));
|
|
|
|
Widget background = this.widget.background;
|
|
if (this.widget.secondaryBackground != null) {
|
|
DismissDirection? direction = this._dismissDirection;
|
|
if (direction == DismissDirection.endToStart || direction == DismissDirection.up) {
|
|
background = this.widget.secondaryBackground;
|
|
}
|
|
}
|
|
|
|
if (this._resizeAnimation != null) {
|
|
// we've been dragged aside, and are now resizing.
|
|
D.assert(() => {
|
|
if (this._resizeAnimation.status != AnimationStatus.forward) {
|
|
D.assert(this._resizeAnimation.status == AnimationStatus.completed);
|
|
throw new UIWidgetsError(
|
|
"A dismissed Dismissible widget is still part of the tree.\n" +
|
|
"Make sure to implement the onDismissed handler and to immediately remove the Dismissible\n" +
|
|
"widget from the application once that handler has fired."
|
|
);
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
return new SizeTransition(
|
|
sizeFactor: this._resizeAnimation,
|
|
axis: this._directionIsXAxis ? Axis.vertical : Axis.horizontal,
|
|
child: new SizedBox(
|
|
width: this._sizePriorToCollapse.width,
|
|
height: this._sizePriorToCollapse.height,
|
|
child: background
|
|
)
|
|
);
|
|
}
|
|
|
|
Widget content = new SlideTransition(
|
|
position: this._moveAnimation,
|
|
child: this.widget.child
|
|
);
|
|
|
|
if (background != null) {
|
|
List<Widget> children = new List<Widget> { };
|
|
|
|
if (!this._moveAnimation.isDismissed) {
|
|
children.Add(Positioned.fill(
|
|
child: new ClipRect(
|
|
clipper: new _DismissibleClipper(
|
|
axis: this._directionIsXAxis ? Axis.horizontal : Axis.vertical,
|
|
moveAnimation: this._moveAnimation
|
|
),
|
|
child: background
|
|
)
|
|
));
|
|
}
|
|
|
|
children.Add(content);
|
|
content = new Stack(children: children);
|
|
}
|
|
|
|
return new GestureDetector(
|
|
onHorizontalDragStart: this._directionIsXAxis ? (GestureDragStartCallback) this._handleDragStart : null,
|
|
onHorizontalDragUpdate: this._directionIsXAxis
|
|
? (GestureDragUpdateCallback) this._handleDragUpdate
|
|
: null,
|
|
onHorizontalDragEnd: this._directionIsXAxis ? (GestureDragEndCallback) this._handleDragEnd : null,
|
|
onVerticalDragStart: this._directionIsXAxis ? null : (GestureDragStartCallback) this._handleDragStart,
|
|
onVerticalDragUpdate: this._directionIsXAxis
|
|
? null
|
|
: (GestureDragUpdateCallback) this._handleDragUpdate,
|
|
onVerticalDragEnd: this._directionIsXAxis ? null : (GestureDragEndCallback) this._handleDragEnd,
|
|
behavior: HitTestBehavior.opaque,
|
|
child: content,
|
|
dragStartBehavior: this.widget.dragStartBehavior
|
|
);
|
|
}
|
|
}
|
|
}
|