浏览代码

Implement Dismissible.

/main
Yuncong Zhang 6 年前
当前提交
9b1bba59
共有 5 个文件被更改,包括 658 次插入1 次删除
  1. 2
      Runtime/rendering/proxy_box.cs
  2. 2
      Runtime/widgets/automatic_keep_alive.cs
  3. 2
      Runtime/widgets/ticker_provider.cs
  4. 650
      Runtime/widgets/dismissible.cs
  5. 3
      Runtime/widgets/dismissible.cs.meta

2
Runtime/rendering/proxy_box.cs


public abstract T getClip(Size size);
public Rect getApproximateClipRect(Size size) {
public virtual Rect getApproximateClipRect(Size size) {
return Offset.zero & size;
}

2
Runtime/widgets/automatic_keep_alive.cs


}
}
// There is a copy of the implementation of this mixin at widgets/dismissble.cs,
// in AutomaticKeepAliveClientWithTickerProviderStateMixin, remember to keep the copy up to date
public abstract class AutomaticKeepAliveClientMixin<T> : State<T> where T : StatefulWidget {
KeepAliveHandle _keepAliveHandle;

2
Runtime/widgets/ticker_provider.cs


}
}
// There is a copy of the implementation of this mixin at widgets/dismissble.cs,
// in AutomaticKeepAliveClientWithTickerProviderStateMixin, remember to keep the copy up to date
public abstract class TickerProviderStateMixin<T> : State<T>, TickerProvider where T : StatefulWidget {
HashSet<Ticker> _tickers;

650
Runtime/widgets/dismissible.cs


using System;
using System.Collections.Generic;
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 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,
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
) : 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);
}
public readonly Widget child;
public readonly Widget background;
public readonly Widget secondaryBackground;
public readonly VoidCallback onResize;
public readonly DismissDirectionCallback onDismissed;
public readonly DismissDirection direction;
public readonly TimeSpan resizeDuration;
public readonly Dictionary<DismissDirection?, float?> dismissThresholds;
public readonly TimeSpan movementDuration;
public readonly float crossAxisEndOffset;
public override State createState() {
return new _DismissibleState();
}
}
public abstract class AutomaticKeepAliveClientWithTickerProviderStateMixin<T> : State<T>, TickerProvider
where T : StatefulWidget {
HashSet<Ticker> _tickers;
public Ticker createTicker(TickerCallback onTick) {
this._tickers = this._tickers ?? new HashSet<Ticker>();
var result = new _AutomaticWidgetTicker<T>(onTick, this, debugLabel: "created by " + this);
this._tickers.Add(result);
return result;
}
internal void _removeTicker(_AutomaticWidgetTicker<T> ticker) {
D.assert(this._tickers != null);
D.assert(this._tickers.Contains(ticker));
this._tickers.Remove(ticker);
}
public override void dispose() {
D.assert(() => {
if (this._tickers != null) {
foreach (Ticker ticker in this._tickers) {
if (ticker.isActive) {
throw new UIWidgetsError(
this + " was disposed with an active Ticker.\n" +
this.GetType() +
" created a Ticker via its TickerProviderStateMixin, but at the time " +
"dispose() was called on the mixin, that Ticker was still active. All Tickers must " +
"be disposed before calling super.dispose(). Tickers used by AnimationControllers " +
"should be disposed by calling dispose() on the AnimationController itself. " +
"Otherwise, the ticker will leak.\n" +
"The offending ticker was: " + ticker.toString(debugIncludeStack: true)
);
}
}
}
return true;
});
base.dispose();
}
public override void didChangeDependencies() {
bool muted = !TickerMode.of(this.context);
if (this._tickers != null) {
foreach (Ticker ticker in this._tickers) {
ticker.muted = muted;
}
}
base.didChangeDependencies();
}
public override void debugFillProperties(DiagnosticPropertiesBuilder properties) {
base.debugFillProperties(properties);
properties.add(new DiagnosticsProperty<HashSet<Ticker>>(
"tickers",
this._tickers,
description: this._tickers != null ? "tracking " + this._tickers.Count + " tickers" : null,
defaultValue: Diagnostics.kNullDefaultValue
));
}
KeepAliveHandle _keepAliveHandle;
void _ensureKeepAlive() {
D.assert(this._keepAliveHandle == null);
this._keepAliveHandle = new KeepAliveHandle();
new KeepAliveNotification(this._keepAliveHandle).dispatch(this.context);
}
void _releaseKeepAlive() {
this._keepAliveHandle.release();
this._keepAliveHandle = null;
}
protected abstract bool wantKeepAlive { get; }
protected void updateKeepAlive() {
if (this.wantKeepAlive) {
if (this._keepAliveHandle == null) {
this._ensureKeepAlive();
}
}
else {
if (this._keepAliveHandle != null) {
this._releaseKeepAlive();
}
}
}
public override void initState() {
base.initState();
if (this.wantKeepAlive) {
this._ensureKeepAlive();
}
}
public override void deactivate() {
if (this._keepAliveHandle != null) {
this._releaseKeepAlive();
}
base.deactivate();
}
public override Widget build(BuildContext context) {
if (this.wantKeepAlive && this._keepAliveHandle == null) {
this._ensureKeepAlive();
}
return null;
}
}
class _AutomaticWidgetTicker<T> : Ticker where T : StatefulWidget {
internal _AutomaticWidgetTicker(
TickerCallback onTick,
AutomaticKeepAliveClientWithTickerProviderStateMixin<T> creator,
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;
if (this._moveController.isCompleted) {
this._startResizeAnimation();
return;
}
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[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.0);
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[this._dismissDirection] ?? _kDismissThreshold)) {
this._moveController.forward();
}
else {
this._moveController.reverse();
}
}
break;
}
}
void _handleDismissStatusChanged(AnimationStatus status) {
if (status == AnimationStatus.completed && !this._dragUnderway) {
this._startResizeAnimation();
}
this.updateKeepAlive();
}
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
);
}
}
}

3
Runtime/widgets/dismissible.cs.meta


fileFormatVersion: 2
guid: abd926e0982c44ca80edbb282a52b90b
timeCreated: 1552033946
正在加载...
取消
保存