浏览代码
Merge pull request #86 from UnityTech/yczhang
Merge pull request #86 from UnityTech/yczhang
Set antiAliasing, fix ShrineDemo, implement GridView ReorderableListView RefreshIndicator UserAccountsDrawerHeader./main
GitHub
6 年前
当前提交
0c130835
共有 23 个文件被更改,包括 2066 次插入 和 26 次删除
-
2Editor/UIWidgetsPanelEditor.cs
-
8Runtime/editor/editor_window.cs
-
1Runtime/editor/rasterizer.cs
-
27Runtime/editor/surface.cs
-
13Runtime/engine/UIWidgetsPanel.cs
-
1Runtime/flow/compositor_context.cs
-
11Runtime/flow/raster_cache.cs
-
2Runtime/gestures/multidrag.cs
-
12Runtime/ui/painting/canvas_impl.cs
-
8Runtime/ui/window.cs
-
2Runtime/widgets/navigator.cs
-
329Runtime/widgets/scroll_view.cs
-
2Samples/UIWidgetsGallery/demo/shrine/shrine_order.cs
-
6Tests/Editor/CanvasAndLayers.cs
-
387Runtime/material/refresh_indicator.cs
-
3Runtime/material/refresh_indicator.cs.meta
-
397Runtime/material/reorderable_list.cs
-
3Runtime/material/reorderable_list.cs.meta
-
348Runtime/material/user_accounts_drawer_header.cs
-
3Runtime/material/user_accounts_drawer_header.cs.meta
-
520Runtime/widgets/overscroll_indicator.cs
-
3Runtime/widgets/overscroll_indicator.cs.meta
-
4.DS_Store
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using RSG; |
|||
using Unity.UIWidgets.animation; |
|||
using Unity.UIWidgets.foundation; |
|||
using Unity.UIWidgets.painting; |
|||
using Unity.UIWidgets.ui; |
|||
using Unity.UIWidgets.widgets; |
|||
using UnityEngine; |
|||
using Color = Unity.UIWidgets.ui.Color; |
|||
|
|||
namespace Unity.UIWidgets.material { |
|||
class RefreshIndicatorUtils { |
|||
public const float _kDragContainerExtentPercentage = 0.25f; |
|||
|
|||
public const float _kDragSizeFactorLimit = 1.5f; |
|||
|
|||
public static readonly TimeSpan _kIndicatorSnapDuration = new TimeSpan(0, 0, 0, 0, 150); |
|||
|
|||
public static readonly TimeSpan _kIndicatorScaleDuration = new TimeSpan(0, 0, 0, 0, 200); |
|||
} |
|||
|
|||
public delegate Promise RefreshCallback(); |
|||
|
|||
enum _RefreshIndicatorMode { |
|||
drag, // Pointer is down.
|
|||
armed, // Dragged far enough that an up event will run the onRefresh callback.
|
|||
snap, // Animating to the indicator"s final "displacement".
|
|||
refresh, // Running the refresh callback.
|
|||
done, // Animating the indicator"s fade-out after refreshing.
|
|||
canceled, // Animating the indicator"s fade-out after not arming.
|
|||
} |
|||
|
|||
public class RefreshIndicator : StatefulWidget { |
|||
public RefreshIndicator( |
|||
Key key = null, |
|||
Widget child = null, |
|||
float displacement = 40.0f, |
|||
RefreshCallback onRefresh = null, |
|||
Color color = null, |
|||
Color backgroundColor = null, |
|||
ScrollNotificationPredicate notificationPredicate = null |
|||
) : base(key: key) { |
|||
D.assert(child != null); |
|||
D.assert(onRefresh != null); |
|||
this.child = child; |
|||
this.displacement = displacement; |
|||
this.onRefresh = onRefresh; |
|||
this.color = color; |
|||
this.backgroundColor = backgroundColor; |
|||
this.notificationPredicate = notificationPredicate ?? ScrollNotification.defaultScrollNotificationPredicate; |
|||
} |
|||
|
|||
public readonly Widget child; |
|||
|
|||
public readonly float displacement; |
|||
|
|||
public readonly RefreshCallback onRefresh; |
|||
|
|||
public readonly Color color; |
|||
|
|||
public readonly Color backgroundColor; |
|||
|
|||
public readonly ScrollNotificationPredicate notificationPredicate; |
|||
|
|||
public override State createState() { |
|||
return new RefreshIndicatorState(); |
|||
} |
|||
} |
|||
|
|||
public class RefreshIndicatorState : TickerProviderStateMixin<RefreshIndicator> { |
|||
AnimationController _positionController; |
|||
AnimationController _scaleController; |
|||
Animation<float> _positionFactor; |
|||
Animation<float> _scaleFactor; |
|||
Animation<float> _value; |
|||
Animation<Color> _valueColor; |
|||
|
|||
_RefreshIndicatorMode? _mode; |
|||
Promise _pendingRefreshFuture; |
|||
bool? _isIndicatorAtTop; |
|||
float? _dragOffset; |
|||
|
|||
static readonly Animatable<float> _threeQuarterTween = new FloatTween(begin: 0.0f, end: 0.75f); |
|||
|
|||
static readonly Animatable<float> _kDragSizeFactorLimitTween = |
|||
new FloatTween(begin: 0.0f, end: RefreshIndicatorUtils._kDragSizeFactorLimit); |
|||
|
|||
static readonly Animatable<float> _oneToZeroTween = new FloatTween(begin: 1.0f, end: 0.0f); |
|||
|
|||
public RefreshIndicatorState() { |
|||
} |
|||
|
|||
public override void initState() { |
|||
base.initState(); |
|||
this._positionController = new AnimationController(vsync: this); |
|||
this._positionFactor = this._positionController.drive(_kDragSizeFactorLimitTween); |
|||
this._value = |
|||
this._positionController |
|||
.drive(_threeQuarterTween); // The "value" of the circular progress indicator during a drag.
|
|||
|
|||
this._scaleController = new AnimationController(vsync: this); |
|||
this._scaleFactor = this._scaleController.drive(_oneToZeroTween); |
|||
} |
|||
|
|||
public override void didChangeDependencies() { |
|||
ThemeData theme = Theme.of(this.context); |
|||
this._valueColor = this._positionController.drive( |
|||
new ColorTween( |
|||
begin: (this.widget.color ?? theme.accentColor).withOpacity(0.0f), |
|||
end: (this.widget.color ?? theme.accentColor).withOpacity(1.0f) |
|||
).chain(new CurveTween( |
|||
curve: new Interval(0.0f, 1.0f / RefreshIndicatorUtils._kDragSizeFactorLimit) |
|||
)) |
|||
); |
|||
base.didChangeDependencies(); |
|||
} |
|||
|
|||
public override void dispose() { |
|||
this._positionController.dispose(); |
|||
this._scaleController.dispose(); |
|||
base.dispose(); |
|||
} |
|||
|
|||
bool _handleScrollNotification(ScrollNotification notification) { |
|||
if (!this.widget.notificationPredicate(notification)) { |
|||
return false; |
|||
} |
|||
|
|||
if (notification is ScrollStartNotification && notification.metrics.extentBefore() == 0.0f && |
|||
this._mode == null && this._start(notification.metrics.axisDirection)) { |
|||
this.setState(() => { this._mode = _RefreshIndicatorMode.drag; }); |
|||
return false; |
|||
} |
|||
|
|||
bool? indicatorAtTopNow = null; |
|||
switch (notification.metrics.axisDirection) { |
|||
case AxisDirection.down: |
|||
indicatorAtTopNow = true; |
|||
break; |
|||
case AxisDirection.up: |
|||
indicatorAtTopNow = false; |
|||
break; |
|||
case AxisDirection.left: |
|||
case AxisDirection.right: |
|||
indicatorAtTopNow = null; |
|||
break; |
|||
} |
|||
|
|||
if (indicatorAtTopNow != this._isIndicatorAtTop) { |
|||
if (this._mode == _RefreshIndicatorMode.drag || this._mode == _RefreshIndicatorMode.armed) { |
|||
this._dismiss(_RefreshIndicatorMode.canceled); |
|||
} |
|||
} |
|||
else if (notification is ScrollUpdateNotification) { |
|||
if (this._mode == _RefreshIndicatorMode.drag || this._mode == _RefreshIndicatorMode.armed) { |
|||
if (notification.metrics.extentBefore() > 0.0f) { |
|||
this._dismiss(_RefreshIndicatorMode.canceled); |
|||
} |
|||
else { |
|||
this._dragOffset -= (notification as ScrollUpdateNotification).scrollDelta; |
|||
this._checkDragOffset(notification.metrics.viewportDimension); |
|||
} |
|||
} |
|||
|
|||
if (this._mode == _RefreshIndicatorMode.armed && |
|||
(notification as ScrollUpdateNotification).dragDetails == null) { |
|||
this._show(); |
|||
} |
|||
} |
|||
else if (notification is OverscrollNotification) { |
|||
if (this._mode == _RefreshIndicatorMode.drag || this._mode == _RefreshIndicatorMode.armed) { |
|||
this._dragOffset -= (notification as OverscrollNotification).overscroll / 2.0f; |
|||
this._checkDragOffset(notification.metrics.viewportDimension); |
|||
} |
|||
} |
|||
else if (notification is ScrollEndNotification) { |
|||
switch (this._mode) { |
|||
case _RefreshIndicatorMode.armed: |
|||
this._show(); |
|||
break; |
|||
case _RefreshIndicatorMode.drag: |
|||
this._dismiss(_RefreshIndicatorMode.canceled); |
|||
break; |
|||
default: |
|||
break; |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
bool _handleGlowNotification(OverscrollIndicatorNotification notification) { |
|||
if (notification.depth != 0 || !notification.leading) { |
|||
return false; |
|||
} |
|||
|
|||
if (this._mode == _RefreshIndicatorMode.drag) { |
|||
notification.disallowGlow(); |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
bool _start(AxisDirection direction) { |
|||
D.assert(this._mode == null); |
|||
D.assert(this._isIndicatorAtTop == null); |
|||
D.assert(this._dragOffset == null); |
|||
switch (direction) { |
|||
case AxisDirection.down: |
|||
this._isIndicatorAtTop = true; |
|||
break; |
|||
case AxisDirection.up: |
|||
this._isIndicatorAtTop = false; |
|||
break; |
|||
case AxisDirection.left: |
|||
case AxisDirection.right: |
|||
this._isIndicatorAtTop = null; |
|||
return false; |
|||
} |
|||
|
|||
this._dragOffset = 0.0f; |
|||
this._scaleController.setValue(0.0f); |
|||
this._positionController.setValue(0.0f); |
|||
return true; |
|||
} |
|||
|
|||
void _checkDragOffset(float containerExtent) { |
|||
D.assert(this._mode == _RefreshIndicatorMode.drag || this._mode == _RefreshIndicatorMode.armed); |
|||
float? newValue = this._dragOffset / |
|||
(containerExtent * RefreshIndicatorUtils._kDragContainerExtentPercentage); |
|||
if (this._mode == _RefreshIndicatorMode.armed) { |
|||
newValue = Mathf.Max(newValue ?? 0.0f, 1.0f / RefreshIndicatorUtils._kDragSizeFactorLimit); |
|||
} |
|||
|
|||
this._positionController.setValue(newValue?.clamp(0.0f, 1.0f) ?? 0.0f); // this triggers various rebuilds
|
|||
if (this._mode == _RefreshIndicatorMode.drag && this._valueColor.value.alpha == 0xFF) { |
|||
this._mode = _RefreshIndicatorMode.armed; |
|||
} |
|||
} |
|||
|
|||
IPromise _dismiss(_RefreshIndicatorMode newMode) { |
|||
D.assert(newMode == _RefreshIndicatorMode.canceled || newMode == _RefreshIndicatorMode.done); |
|||
this.setState(() => { this._mode = newMode; }); |
|||
switch (this._mode) { |
|||
case _RefreshIndicatorMode.done: |
|||
return this._scaleController |
|||
.animateTo(1.0f, duration: RefreshIndicatorUtils._kIndicatorScaleDuration).Then(() => { |
|||
if (this.mounted && this._mode == newMode) { |
|||
this._dragOffset = null; |
|||
this._isIndicatorAtTop = null; |
|||
this.setState(() => { this._mode = null; }); |
|||
} |
|||
}); |
|||
case _RefreshIndicatorMode.canceled: |
|||
return this._positionController |
|||
.animateTo(0.0f, duration: RefreshIndicatorUtils._kIndicatorScaleDuration).Then(() => { |
|||
if (this.mounted && this._mode == newMode) { |
|||
this._dragOffset = null; |
|||
this._isIndicatorAtTop = null; |
|||
this.setState(() => { this._mode = null; }); |
|||
} |
|||
}); |
|||
default: |
|||
throw new Exception("Unknown refresh indicator mode: " + this._mode); |
|||
} |
|||
} |
|||
|
|||
void _show() { |
|||
D.assert(this._mode != _RefreshIndicatorMode.refresh); |
|||
D.assert(this._mode != _RefreshIndicatorMode.snap); |
|||
Promise completer = new Promise(); |
|||
this._pendingRefreshFuture = completer; |
|||
this._mode = _RefreshIndicatorMode.snap; |
|||
this._positionController |
|||
.animateTo(1.0f / RefreshIndicatorUtils._kDragSizeFactorLimit, |
|||
duration: RefreshIndicatorUtils._kIndicatorSnapDuration) |
|||
.Then(() => { |
|||
if (this.mounted && this._mode == _RefreshIndicatorMode.snap) { |
|||
D.assert(this.widget.onRefresh != null); |
|||
this.setState(() => { this._mode = _RefreshIndicatorMode.refresh; }); |
|||
|
|||
Promise refreshResult = this.widget.onRefresh(); |
|||
D.assert(() => { |
|||
if (refreshResult == null) { |
|||
UIWidgetsError.reportError(new UIWidgetsErrorDetails( |
|||
exception: new UIWidgetsError( |
|||
"The onRefresh callback returned null.\n" + |
|||
"The RefreshIndicator onRefresh callback must return a Promise." |
|||
), |
|||
context: "when calling onRefresh", |
|||
library: "material library" |
|||
)); |
|||
} |
|||
|
|||
return true; |
|||
}); |
|||
if (refreshResult == null) { |
|||
return; |
|||
} |
|||
|
|||
refreshResult.Then(() => { |
|||
if (this.mounted && this._mode == _RefreshIndicatorMode.refresh) { |
|||
completer.Resolve(); |
|||
this._dismiss(_RefreshIndicatorMode.done); |
|||
} |
|||
}); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
Promise show(bool atTop = true) { |
|||
if (this._mode != _RefreshIndicatorMode.refresh && this._mode != _RefreshIndicatorMode.snap) { |
|||
if (this._mode == null) { |
|||
this._start(atTop ? AxisDirection.down : AxisDirection.up); |
|||
} |
|||
|
|||
this._show(); |
|||
} |
|||
|
|||
return this._pendingRefreshFuture; |
|||
} |
|||
|
|||
GlobalKey _key = GlobalKey.key(); |
|||
|
|||
public override Widget build(BuildContext context) { |
|||
D.assert(MaterialD.debugCheckHasMaterialLocalizations(context)); |
|||
Widget child = new NotificationListener<ScrollNotification>( |
|||
key: this._key, |
|||
onNotification: this._handleScrollNotification, |
|||
child: new NotificationListener<OverscrollIndicatorNotification>( |
|||
onNotification: this._handleGlowNotification, |
|||
child: this.widget.child |
|||
) |
|||
); |
|||
if (this._mode == null) { |
|||
D.assert(this._dragOffset == null); |
|||
D.assert(this._isIndicatorAtTop == null); |
|||
return child; |
|||
} |
|||
|
|||
D.assert(this._dragOffset != null); |
|||
D.assert(this._isIndicatorAtTop != null); |
|||
|
|||
bool showIndeterminateIndicator = |
|||
this._mode == _RefreshIndicatorMode.refresh || this._mode == _RefreshIndicatorMode.done; |
|||
|
|||
return new Stack( |
|||
children: new List<Widget> { |
|||
child, |
|||
new Positioned( |
|||
top: this._isIndicatorAtTop == true ? 0.0f : (float?) null, |
|||
bottom: this._isIndicatorAtTop != true ? 0.0f : (float?) null, |
|||
left: 0.0f, |
|||
right: 0.0f, |
|||
child: new SizeTransition( |
|||
axisAlignment: this._isIndicatorAtTop == true ? 1.0f : -1.0f, |
|||
sizeFactor: this._positionFactor, // this is what brings it down
|
|||
child: new Container( |
|||
padding: this._isIndicatorAtTop == true |
|||
? EdgeInsets.only(top: this.widget.displacement) |
|||
: EdgeInsets.only(bottom: this.widget.displacement), |
|||
alignment: this._isIndicatorAtTop == true |
|||
? Alignment.topCenter |
|||
: Alignment.bottomCenter, |
|||
child: new ScaleTransition( |
|||
scale: this._scaleFactor, |
|||
child: new AnimatedBuilder( |
|||
animation: this._positionController, |
|||
builder: (BuildContext _context, Widget _child) => { |
|||
return new RefreshProgressIndicator( |
|||
value: showIndeterminateIndicator ? (float?) null : this._value.value, |
|||
valueColor: this._valueColor, |
|||
backgroundColor: this.widget.backgroundColor |
|||
); |
|||
} |
|||
) |
|||
) |
|||
) |
|||
) |
|||
) |
|||
} |
|||
); |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 74dd879101fe4541a5b42619cf4f309c |
|||
timeCreated: 1554344854 |
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Unity.UIWidgets.animation; |
|||
using Unity.UIWidgets.foundation; |
|||
using Unity.UIWidgets.gestures; |
|||
using Unity.UIWidgets.painting; |
|||
using Unity.UIWidgets.rendering; |
|||
using Unity.UIWidgets.ui; |
|||
using Unity.UIWidgets.widgets; |
|||
using UnityEngine; |
|||
|
|||
namespace Unity.UIWidgets.material { |
|||
public delegate void ReorderCallback(int oldIndex, int newIndex); |
|||
|
|||
public class ReorderableListView : StatefulWidget { |
|||
public ReorderableListView( |
|||
Widget header = null, |
|||
List<Widget> children = null, |
|||
ReorderCallback onReorder = null, |
|||
Axis scrollDirection = Axis.vertical, |
|||
EdgeInsets padding = null |
|||
) { |
|||
D.assert(onReorder != null); |
|||
D.assert(children != null); |
|||
D.assert( |
|||
children.All((Widget w) => w.key != null), |
|||
"All children of this widget must have a key." |
|||
); |
|||
this.header = header; |
|||
this.children = children; |
|||
this.scrollDirection = scrollDirection; |
|||
this.padding = padding; |
|||
this.onReorder = onReorder; |
|||
} |
|||
|
|||
public readonly Widget header; |
|||
|
|||
public readonly List<Widget> children; |
|||
|
|||
public readonly Axis scrollDirection; |
|||
|
|||
public readonly EdgeInsets padding; |
|||
|
|||
public readonly ReorderCallback onReorder; |
|||
|
|||
public override State createState() { |
|||
return new _ReorderableListViewState(); |
|||
} |
|||
} |
|||
|
|||
class _ReorderableListViewState : State<ReorderableListView> { |
|||
GlobalKey _overlayKey = GlobalKey.key(debugLabel: "$ReorderableListView overlay key"); |
|||
|
|||
OverlayEntry _listOverlayEntry; |
|||
|
|||
public override void initState() { |
|||
base.initState(); |
|||
this._listOverlayEntry = new OverlayEntry( |
|||
opaque: true, |
|||
builder: (BuildContext context) => { |
|||
return new _ReorderableListContent( |
|||
header: this.widget.header, |
|||
children: this.widget.children, |
|||
scrollDirection: this.widget.scrollDirection, |
|||
onReorder: this.widget.onReorder, |
|||
padding: this.widget.padding |
|||
); |
|||
} |
|||
); |
|||
} |
|||
|
|||
public override Widget build(BuildContext context) { |
|||
return new Overlay( |
|||
key: this._overlayKey, |
|||
initialEntries: new List<OverlayEntry> { |
|||
this._listOverlayEntry |
|||
}); |
|||
} |
|||
} |
|||
|
|||
class _ReorderableListContent : StatefulWidget { |
|||
public _ReorderableListContent( |
|||
Widget header, |
|||
List<Widget> children, |
|||
Axis scrollDirection, |
|||
EdgeInsets padding, |
|||
ReorderCallback onReorder |
|||
) { |
|||
this.header = header; |
|||
this.children = children; |
|||
this.scrollDirection = scrollDirection; |
|||
this.padding = padding; |
|||
this.onReorder = onReorder; |
|||
} |
|||
|
|||
public readonly Widget header; |
|||
public readonly List<Widget> children; |
|||
public readonly Axis scrollDirection; |
|||
public readonly EdgeInsets padding; |
|||
public readonly ReorderCallback onReorder; |
|||
|
|||
public override State createState() { |
|||
return new _ReorderableListContentState(); |
|||
} |
|||
} |
|||
|
|||
class _ReorderableListContentState : TickerProviderStateMixin<_ReorderableListContent> { |
|||
const float _defaultDropAreaExtent = 100.0f; |
|||
|
|||
const float _dropAreaMargin = 8.0f; |
|||
|
|||
readonly TimeSpan _reorderAnimationDuration = new TimeSpan(0, 0, 0, 0, 200); |
|||
|
|||
readonly TimeSpan _scrollAnimationDuration = new TimeSpan(0, 0, 0, 0, 200); |
|||
|
|||
ScrollController _scrollController; |
|||
|
|||
AnimationController _entranceController; |
|||
|
|||
AnimationController _ghostController; |
|||
|
|||
Key _dragging; |
|||
|
|||
Size _draggingFeedbackSize; |
|||
|
|||
int _dragStartIndex = 0; |
|||
|
|||
int _ghostIndex = 0; |
|||
|
|||
int _currentIndex = 0; |
|||
|
|||
int _nextIndex = 0; |
|||
|
|||
bool _scrolling = false; |
|||
|
|||
float _dropAreaExtent { |
|||
get { |
|||
if (this._draggingFeedbackSize == null) { |
|||
return _defaultDropAreaExtent; |
|||
} |
|||
|
|||
float dropAreaWithoutMargin; |
|||
switch (this.widget.scrollDirection) { |
|||
case Axis.horizontal: |
|||
dropAreaWithoutMargin = this._draggingFeedbackSize.width; |
|||
break; |
|||
case Axis.vertical: |
|||
default: |
|||
dropAreaWithoutMargin = this._draggingFeedbackSize.height; |
|||
break; |
|||
} |
|||
|
|||
return dropAreaWithoutMargin + _dropAreaMargin; |
|||
} |
|||
} |
|||
|
|||
public override void initState() { |
|||
base.initState(); |
|||
this._entranceController = new AnimationController(vsync: this, duration: this._reorderAnimationDuration); |
|||
this._ghostController = new AnimationController(vsync: this, duration: this._reorderAnimationDuration); |
|||
this._entranceController.addStatusListener(this._onEntranceStatusChanged); |
|||
} |
|||
|
|||
public override void didChangeDependencies() { |
|||
this._scrollController = PrimaryScrollController.of(this.context) ?? new ScrollController(); |
|||
base.didChangeDependencies(); |
|||
} |
|||
|
|||
public override void dispose() { |
|||
this._entranceController.dispose(); |
|||
this._ghostController.dispose(); |
|||
base.dispose(); |
|||
} |
|||
|
|||
void _requestAnimationToNextIndex() { |
|||
if (this._entranceController.isCompleted) { |
|||
this._ghostIndex = this._currentIndex; |
|||
if (this._nextIndex == this._currentIndex) { |
|||
return; |
|||
} |
|||
|
|||
this._currentIndex = this._nextIndex; |
|||
this._ghostController.reverse(from: 1.0f); |
|||
this._entranceController.forward(from: 0.0f); |
|||
} |
|||
} |
|||
|
|||
void _onEntranceStatusChanged(AnimationStatus status) { |
|||
if (status == AnimationStatus.completed) { |
|||
this.setState(() => { this._requestAnimationToNextIndex(); }); |
|||
} |
|||
} |
|||
|
|||
void _scrollTo(BuildContext context) { |
|||
if (this._scrolling) { |
|||
return; |
|||
} |
|||
|
|||
RenderObject contextObject = context.findRenderObject(); |
|||
RenderAbstractViewport viewport = RenderViewportUtils.of(contextObject); |
|||
D.assert(viewport != null); |
|||
float margin = this._dropAreaExtent; |
|||
float scrollOffset = this._scrollController.offset; |
|||
float topOffset = Mathf.Max(this._scrollController.position.minScrollExtent, |
|||
viewport.getOffsetToReveal(contextObject, 0.0f).offset - margin |
|||
); |
|||
float bottomOffset = Mathf.Min(this._scrollController.position.maxScrollExtent, |
|||
viewport.getOffsetToReveal(contextObject, 1.0f).offset + margin |
|||
); |
|||
bool onScreen = scrollOffset <= topOffset && scrollOffset >= bottomOffset; |
|||
|
|||
if (!onScreen) { |
|||
this._scrolling = true; |
|||
this._scrollController.position.animateTo( |
|||
scrollOffset < bottomOffset ? bottomOffset : topOffset, |
|||
duration: this._scrollAnimationDuration, |
|||
curve: Curves.easeInOut |
|||
).Then(() => { this.setState(() => { this._scrolling = false; }); }); |
|||
} |
|||
} |
|||
|
|||
Widget _buildContainerForScrollDirection(List<Widget> children = null) { |
|||
switch (this.widget.scrollDirection) { |
|||
case Axis.horizontal: |
|||
return new Row(children: children); |
|||
case Axis.vertical: |
|||
default: |
|||
return new Column(children: children); |
|||
} |
|||
} |
|||
|
|||
Widget _wrap(Widget toWrap, int index, BoxConstraints constraints) { |
|||
D.assert(toWrap.key != null); |
|||
GlobalObjectKey<State<_ReorderableListContent>> keyIndexGlobalKey = new GlobalObjectKey<State<_ReorderableListContent>>(toWrap.key); |
|||
|
|||
void onDragStarted() { |
|||
this.setState(() => { |
|||
this._dragging = toWrap.key; |
|||
this._dragStartIndex = index; |
|||
this._ghostIndex = index; |
|||
this._currentIndex = index; |
|||
this._entranceController.setValue(1.0f); |
|||
this._draggingFeedbackSize = keyIndexGlobalKey.currentContext.size; |
|||
}); |
|||
} |
|||
|
|||
void reorder(int startIndex, int endIndex) { |
|||
this.setState(() => { |
|||
if (startIndex != endIndex) { |
|||
this.widget.onReorder(startIndex, endIndex); |
|||
} |
|||
|
|||
this._ghostController.reverse(from: 0.1f); |
|||
this._entranceController.reverse(from: 0.1f); |
|||
this._dragging = null; |
|||
}); |
|||
} |
|||
|
|||
void onDragEnded() { |
|||
reorder(this._dragStartIndex, this._currentIndex); |
|||
} |
|||
|
|||
|
|||
Widget wrapWithKeyedSubtree() { |
|||
return new KeyedSubtree( |
|||
key: keyIndexGlobalKey, |
|||
child: toWrap |
|||
); |
|||
} |
|||
|
|||
Widget buildDragTarget(BuildContext context, List<Key> acceptedCandidates, List<Key> rejectedCandidates) { |
|||
Widget toWrapWithKeyedSubtree = wrapWithKeyedSubtree(); |
|||
Widget child = new LongPressDraggable<Key>( |
|||
maxSimultaneousDrags: 1, |
|||
axis: this.widget.scrollDirection, |
|||
data: toWrap.key, |
|||
feedback: new Container( |
|||
alignment: Alignment.topLeft, |
|||
// These constraints will limit the cross axis of the drawn widget.
|
|||
constraints: constraints, |
|||
child: new Material( |
|||
elevation: 6.0f, |
|||
child: toWrapWithKeyedSubtree |
|||
) |
|||
), |
|||
child: this._dragging == toWrap.key ? new SizedBox() : toWrapWithKeyedSubtree, |
|||
childWhenDragging: new SizedBox(), |
|||
dragAnchor: DragAnchor.child, |
|||
onDragStarted: onDragStarted, |
|||
onDragCompleted: onDragEnded, |
|||
onDraggableCanceled: (Velocity velocity, Offset offset) => { onDragEnded(); } |
|||
); |
|||
|
|||
if (index >= this.widget.children.Count) { |
|||
child = toWrap; |
|||
} |
|||
|
|||
Widget spacing; |
|||
switch (this.widget.scrollDirection) { |
|||
case Axis.horizontal: |
|||
spacing = new SizedBox(width: this._dropAreaExtent); |
|||
break; |
|||
case Axis.vertical: |
|||
default: |
|||
spacing = new SizedBox(height: this._dropAreaExtent); |
|||
break; |
|||
} |
|||
|
|||
if (this._currentIndex == index) { |
|||
return this._buildContainerForScrollDirection(children: new List<Widget> { |
|||
new SizeTransition( |
|||
sizeFactor: this._entranceController, |
|||
axis: this.widget.scrollDirection, |
|||
child: spacing |
|||
), |
|||
child |
|||
}); |
|||
} |
|||
|
|||
if (this._ghostIndex == index) { |
|||
return this._buildContainerForScrollDirection(children: new List<Widget> { |
|||
new SizeTransition( |
|||
sizeFactor: this._ghostController, |
|||
axis: this.widget.scrollDirection, |
|||
child: spacing |
|||
), |
|||
child |
|||
}); |
|||
} |
|||
|
|||
return child; |
|||
} |
|||
|
|||
return new Builder(builder: (BuildContext context) => { |
|||
return new DragTarget<Key>( |
|||
builder: buildDragTarget, |
|||
onWillAccept: (Key toAccept) => { |
|||
this.setState(() => { |
|||
this._nextIndex = index; |
|||
this._requestAnimationToNextIndex(); |
|||
}); |
|||
this._scrollTo(context); |
|||
return this._dragging == toAccept && toAccept != toWrap.key; |
|||
}, |
|||
onAccept: (Key accepted) => { }, |
|||
onLeave: (Key leaving) => { } |
|||
); |
|||
}); |
|||
} |
|||
|
|||
public override Widget build(BuildContext context) { |
|||
D.assert(MaterialD.debugCheckHasMaterialLocalizations(context)); |
|||
return new LayoutBuilder(builder: (BuildContext _, BoxConstraints constraints) => { |
|||
List<Widget> wrappedChildren = new List<Widget> { }; |
|||
if (this.widget.header != null) { |
|||
wrappedChildren.Add(this.widget.header); |
|||
} |
|||
|
|||
for (int i = 0; i < this.widget.children.Count; i += 1) { |
|||
wrappedChildren.Add(this._wrap(this.widget.children[i], i, constraints)); |
|||
} |
|||
|
|||
Key endWidgetKey = Key.key("DraggableList - End Widget"); |
|||
Widget finalDropArea; |
|||
switch (this.widget.scrollDirection) { |
|||
case Axis.horizontal: |
|||
finalDropArea = new SizedBox( |
|||
key: endWidgetKey, |
|||
width: _defaultDropAreaExtent, |
|||
height: constraints.maxHeight |
|||
); |
|||
break; |
|||
case Axis.vertical: |
|||
default: |
|||
finalDropArea = new SizedBox( |
|||
key: endWidgetKey, |
|||
height: _defaultDropAreaExtent, |
|||
width: constraints.maxWidth |
|||
); |
|||
break; |
|||
} |
|||
|
|||
wrappedChildren.Add(this._wrap( |
|||
finalDropArea, this.widget.children.Count, |
|||
constraints) |
|||
); |
|||
return new SingleChildScrollView( |
|||
scrollDirection: this.widget.scrollDirection, |
|||
child: this._buildContainerForScrollDirection(children: wrappedChildren), |
|||
padding: this.widget.padding, |
|||
controller: this._scrollController |
|||
); |
|||
}); |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 730da7185538491389639e2cc7099a21 |
|||
timeCreated: 1554301017 |
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Unity.UIWidgets.animation; |
|||
using Unity.UIWidgets.foundation; |
|||
using Unity.UIWidgets.gestures; |
|||
using Unity.UIWidgets.painting; |
|||
using Unity.UIWidgets.rendering; |
|||
using Unity.UIWidgets.ui; |
|||
using Unity.UIWidgets.widgets; |
|||
using UnityEngine; |
|||
using Transform = Unity.UIWidgets.widgets.Transform; |
|||
|
|||
namespace Unity.UIWidgets.material { |
|||
class UserAccountsDrawerHeaderUtils { |
|||
public const float _kAccountDetailsHeight = 56.0f; |
|||
} |
|||
|
|||
class _AccountPictures : StatelessWidget { |
|||
public _AccountPictures( |
|||
Key key = null, |
|||
Widget currentAccountPicture = null, |
|||
List<Widget> otherAccountsPictures = null |
|||
) : base(key: key) { |
|||
this.currentAccountPicture = currentAccountPicture; |
|||
this.otherAccountsPictures = otherAccountsPictures; |
|||
} |
|||
|
|||
public readonly Widget currentAccountPicture; |
|||
public readonly List<Widget> otherAccountsPictures; |
|||
|
|||
public override Widget build(BuildContext context) { |
|||
return new Stack( |
|||
children: new List<Widget> { |
|||
new Positioned( |
|||
top: 0.0f, |
|||
right: 0.0f, |
|||
child: new Row( |
|||
children: (this.otherAccountsPictures ?? new List<Widget> { }) |
|||
.GetRange(0, Math.Min(3, this.otherAccountsPictures?.Count ?? 0)) |
|||
.Select<Widget, Widget>( |
|||
(Widget picture) => { |
|||
return new Padding( |
|||
padding: EdgeInsets.only(left: 8.0f), |
|||
child: new Container( |
|||
padding: EdgeInsets.only(left: 8.0f, bottom: 8.0f), |
|||
width: 48.0f, |
|||
height: 48.0f, |
|||
child: picture |
|||
) |
|||
); |
|||
}).ToList() |
|||
) |
|||
), |
|||
new Positioned( |
|||
top: 0.0f, |
|||
child: new SizedBox( |
|||
width: 72.0f, |
|||
height: 72.0f, |
|||
child: this.currentAccountPicture |
|||
) |
|||
) |
|||
} |
|||
); |
|||
} |
|||
} |
|||
|
|||
class _AccountDetails : StatefulWidget { |
|||
public _AccountDetails( |
|||
Key key = null, |
|||
Widget accountName = null, |
|||
Widget accountEmail = null, |
|||
VoidCallback onTap = null, |
|||
bool? isOpen = null |
|||
) : base(key: key) { |
|||
D.assert(accountName != null); |
|||
D.assert(accountEmail != null); |
|||
this.accountName = accountName; |
|||
this.accountEmail = accountEmail; |
|||
this.onTap = onTap; |
|||
this.isOpen = isOpen; |
|||
} |
|||
|
|||
public readonly Widget accountName; |
|||
public readonly Widget accountEmail; |
|||
public readonly VoidCallback onTap; |
|||
public readonly bool? isOpen; |
|||
|
|||
public override State createState() { |
|||
return new _AccountDetailsState(); |
|||
} |
|||
} |
|||
|
|||
class _AccountDetailsState : SingleTickerProviderStateMixin<_AccountDetails> { |
|||
Animation<float> _animation; |
|||
AnimationController _controller; |
|||
|
|||
public override void initState() { |
|||
base.initState(); |
|||
this._controller = new AnimationController( |
|||
value: this.widget.isOpen == true ? 1.0f : 0.0f, |
|||
duration: new TimeSpan(0, 0, 0, 0, 200), |
|||
vsync: this |
|||
); |
|||
this._animation = new CurvedAnimation( |
|||
parent: this._controller, |
|||
curve: Curves.fastOutSlowIn, |
|||
reverseCurve: Curves.fastOutSlowIn.flipped |
|||
); |
|||
this._animation.addListener(() => this.setState(() => { })); |
|||
} |
|||
|
|||
public override void dispose() { |
|||
this._controller.dispose(); |
|||
base.dispose(); |
|||
} |
|||
|
|||
public override void didUpdateWidget(StatefulWidget _oldWidget) { |
|||
base.didUpdateWidget(_oldWidget); |
|||
_AccountDetails oldWidget = _oldWidget as _AccountDetails; |
|||
if (this._animation.status == AnimationStatus.dismissed || |
|||
this._animation.status == AnimationStatus.reverse) { |
|||
this._controller.forward(); |
|||
} |
|||
else { |
|||
this._controller.reverse(); |
|||
} |
|||
} |
|||
|
|||
public override Widget build(BuildContext context) { |
|||
D.assert(WidgetsD.debugCheckHasDirectionality(context)); |
|||
D.assert(MaterialD.debugCheckHasMaterialLocalizations(context)); |
|||
D.assert(MaterialD.debugCheckHasMaterialLocalizations(context)); |
|||
|
|||
ThemeData theme = Theme.of(context); |
|||
List<Widget> children = new List<Widget> { }; |
|||
|
|||
if (this.widget.accountName != null) { |
|||
Widget accountNameLine = new LayoutId( |
|||
id: _AccountDetailsLayout.accountName, |
|||
child: new Padding( |
|||
padding: EdgeInsets.symmetric(vertical: 2.0f), |
|||
child: new DefaultTextStyle( |
|||
style: theme.primaryTextTheme.body2, |
|||
overflow: TextOverflow.ellipsis, |
|||
child: this.widget.accountName |
|||
) |
|||
) |
|||
); |
|||
children.Add(accountNameLine); |
|||
} |
|||
|
|||
if (this.widget.accountEmail != null) { |
|||
Widget accountEmailLine = new LayoutId( |
|||
id: _AccountDetailsLayout.accountEmail, |
|||
child: new Padding( |
|||
padding: EdgeInsets.symmetric(vertical: 2.0f), |
|||
child: new DefaultTextStyle( |
|||
style: theme.primaryTextTheme.body1, |
|||
overflow: TextOverflow.ellipsis, |
|||
child: this.widget.accountEmail |
|||
) |
|||
) |
|||
); |
|||
children.Add(accountEmailLine); |
|||
} |
|||
|
|||
if (this.widget.onTap != null) { |
|||
MaterialLocalizations localizations = MaterialLocalizations.of(context); |
|||
Widget dropDownIcon = new LayoutId( |
|||
id: _AccountDetailsLayout.dropdownIcon, |
|||
child: new SizedBox( |
|||
height: UserAccountsDrawerHeaderUtils._kAccountDetailsHeight, |
|||
width: UserAccountsDrawerHeaderUtils._kAccountDetailsHeight, |
|||
child: new Center( |
|||
child: Transform.rotate( |
|||
degree: this._animation.value * Mathf.PI, |
|||
child: new Icon( |
|||
Icons.arrow_drop_down, |
|||
color: Colors.white |
|||
) |
|||
) |
|||
) |
|||
) |
|||
); |
|||
children.Add(dropDownIcon); |
|||
} |
|||
|
|||
Widget accountDetails = new CustomMultiChildLayout( |
|||
layoutDelegate: new _AccountDetailsLayout(), |
|||
children: children |
|||
); |
|||
|
|||
if (this.widget.onTap != null) { |
|||
accountDetails = new InkWell( |
|||
onTap: this.widget.onTap == null ? (GestureTapCallback) null : () => { this.widget.onTap(); }, |
|||
child: accountDetails |
|||
); |
|||
} |
|||
|
|||
return new SizedBox( |
|||
height: UserAccountsDrawerHeaderUtils._kAccountDetailsHeight, |
|||
child: accountDetails |
|||
); |
|||
} |
|||
} |
|||
|
|||
|
|||
class _AccountDetailsLayout : MultiChildLayoutDelegate { |
|||
public _AccountDetailsLayout() { |
|||
} |
|||
|
|||
public const string accountName = "accountName"; |
|||
public const string accountEmail = "accountEmail"; |
|||
public const string dropdownIcon = "dropdownIcon"; |
|||
|
|||
public override void performLayout(Size size) { |
|||
Size iconSize = null; |
|||
if (this.hasChild(dropdownIcon)) { |
|||
iconSize = this.layoutChild(dropdownIcon, BoxConstraints.loose(size)); |
|||
this.positionChild(dropdownIcon, this._offsetForIcon(size, iconSize)); |
|||
} |
|||
|
|||
string bottomLine = this.hasChild(accountEmail) |
|||
? accountEmail |
|||
: (this.hasChild(accountName) ? accountName : null); |
|||
|
|||
if (bottomLine != null) { |
|||
Size constraintSize = iconSize == null ? size : size - new Offset(iconSize.width, 0.0f); |
|||
iconSize = iconSize ?? new Size(UserAccountsDrawerHeaderUtils._kAccountDetailsHeight, |
|||
UserAccountsDrawerHeaderUtils._kAccountDetailsHeight); |
|||
|
|||
Size bottomLineSize = this.layoutChild(bottomLine, BoxConstraints.loose(constraintSize)); |
|||
Offset bottomLineOffset = this._offsetForBottomLine(size, iconSize, bottomLineSize); |
|||
this.positionChild(bottomLine, bottomLineOffset); |
|||
|
|||
if (bottomLine == accountEmail && this.hasChild(accountName)) { |
|||
Size nameSize = this.layoutChild(accountName, BoxConstraints.loose(constraintSize)); |
|||
this.positionChild(accountName, this._offsetForName(size, nameSize, bottomLineOffset)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public override bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) { |
|||
return true; |
|||
} |
|||
|
|||
Offset _offsetForIcon(Size size, Size iconSize) { |
|||
return new Offset(size.width - iconSize.width, size.height - iconSize.height); |
|||
} |
|||
|
|||
Offset _offsetForBottomLine(Size size, Size iconSize, Size bottomLineSize) { |
|||
float y = size.height - 0.5f * iconSize.height - 0.5f * bottomLineSize.height; |
|||
return new Offset(0.0f, y); |
|||
} |
|||
|
|||
Offset _offsetForName(Size size, Size nameSize, Offset bottomLineOffset) { |
|||
float y = bottomLineOffset.dy - nameSize.height; |
|||
return new Offset(0.0f, y); |
|||
} |
|||
} |
|||
|
|||
public class UserAccountsDrawerHeader : StatefulWidget { |
|||
public UserAccountsDrawerHeader( |
|||
Key key = null, |
|||
Decoration decoration = null, |
|||
EdgeInsets margin = null, |
|||
Widget currentAccountPicture = null, |
|||
List<Widget> otherAccountsPictures = null, |
|||
Widget accountName = null, |
|||
Widget accountEmail = null, |
|||
VoidCallback onDetailsPressed = null |
|||
) : base(key: key) { |
|||
D.assert(accountName != null); |
|||
D.assert(accountEmail != null); |
|||
this.decoration = decoration; |
|||
this.margin = margin ?? EdgeInsets.only(bottom: 8.0f); |
|||
this.currentAccountPicture = currentAccountPicture; |
|||
this.otherAccountsPictures = otherAccountsPictures; |
|||
this.accountName = accountName; |
|||
this.accountEmail = accountEmail; |
|||
this.onDetailsPressed = onDetailsPressed; |
|||
} |
|||
|
|||
public readonly Decoration decoration; |
|||
|
|||
public readonly EdgeInsets margin; |
|||
|
|||
public readonly Widget currentAccountPicture; |
|||
|
|||
public readonly List<Widget> otherAccountsPictures; |
|||
|
|||
public readonly Widget accountName; |
|||
|
|||
public readonly Widget accountEmail; |
|||
|
|||
public readonly VoidCallback onDetailsPressed; |
|||
|
|||
public override State createState() { |
|||
return new _UserAccountsDrawerHeaderState(); |
|||
} |
|||
} |
|||
|
|||
class _UserAccountsDrawerHeaderState : State<UserAccountsDrawerHeader> { |
|||
bool _isOpen = false; |
|||
|
|||
void _handleDetailsPressed() { |
|||
this.setState(() => { this._isOpen = !this._isOpen; }); |
|||
this.widget.onDetailsPressed(); |
|||
} |
|||
|
|||
public override Widget build(BuildContext context) { |
|||
D.assert(MaterialD.debugCheckHasMaterial(context)); |
|||
D.assert(MaterialD.debugCheckHasMaterialLocalizations(context)); |
|||
return new DrawerHeader( |
|||
decoration: this.widget.decoration ?? new BoxDecoration( |
|||
color: Theme.of(context).primaryColor |
|||
), |
|||
margin: this.widget.margin, |
|||
padding: EdgeInsets.only(top: 16.0f, left: 16.0f), |
|||
child: new SafeArea( |
|||
bottom: false, |
|||
child: new Column( |
|||
crossAxisAlignment: CrossAxisAlignment.stretch, |
|||
children: new List<Widget> { |
|||
new Expanded( |
|||
child: new Padding( |
|||
padding: EdgeInsets.only(right: 16.0f), |
|||
child: new _AccountPictures( |
|||
currentAccountPicture: this.widget.currentAccountPicture, |
|||
otherAccountsPictures: this.widget.otherAccountsPictures |
|||
) |
|||
) |
|||
), |
|||
new _AccountDetails( |
|||
accountName: this.widget.accountName, |
|||
accountEmail: this.widget.accountEmail, |
|||
isOpen: this._isOpen, |
|||
onTap: this.widget.onDetailsPressed == null |
|||
? (VoidCallback) null |
|||
: () => { this._handleDetailsPressed(); }) |
|||
} |
|||
) |
|||
) |
|||
); |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: fb34d1a7ff484d05b8074a10730d51a5 |
|||
timeCreated: 1554352270 |
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Unity.UIWidgets.animation; |
|||
using Unity.UIWidgets.async; |
|||
using Unity.UIWidgets.foundation; |
|||
using Unity.UIWidgets.painting; |
|||
using Unity.UIWidgets.physics; |
|||
using Unity.UIWidgets.rendering; |
|||
using Unity.UIWidgets.scheduler; |
|||
using Unity.UIWidgets.ui; |
|||
using UnityEngine; |
|||
using Canvas = Unity.UIWidgets.ui.Canvas; |
|||
using Color = Unity.UIWidgets.ui.Color; |
|||
using Rect = Unity.UIWidgets.ui.Rect; |
|||
|
|||
namespace Unity.UIWidgets.widgets { |
|||
public class GlowingOverscrollIndicator : StatefulWidget { |
|||
public GlowingOverscrollIndicator( |
|||
Key key = null, |
|||
bool showLeading = true, |
|||
bool showTrailing = true, |
|||
AxisDirection axisDirection = AxisDirection.up, |
|||
Color color = null, |
|||
ScrollNotificationPredicate notificationPredicate = null, |
|||
Widget child = null |
|||
) : base(key: key) { |
|||
D.assert(color != null); |
|||
this.showLeading = showLeading; |
|||
this.showTrailing = showTrailing; |
|||
this.axisDirection = axisDirection; |
|||
this.child = child; |
|||
this.color = color; |
|||
this.notificationPredicate = notificationPredicate ?? ScrollNotification.defaultScrollNotificationPredicate; |
|||
} |
|||
|
|||
public readonly bool showLeading; |
|||
|
|||
public readonly bool showTrailing; |
|||
|
|||
public readonly AxisDirection axisDirection; |
|||
|
|||
public Axis axis { |
|||
get { return AxisUtils.axisDirectionToAxis(this.axisDirection); } |
|||
} |
|||
|
|||
public readonly Color color; |
|||
|
|||
public readonly ScrollNotificationPredicate notificationPredicate; |
|||
|
|||
public readonly Widget child; |
|||
|
|||
public override State createState() { |
|||
return new _GlowingOverscrollIndicatorState(); |
|||
} |
|||
|
|||
public override void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
|||
base.debugFillProperties(properties); |
|||
properties.add(new EnumProperty<AxisDirection>("axisDirection", this.axisDirection)); |
|||
string showDescription; |
|||
if (this.showLeading && this.showTrailing) { |
|||
showDescription = "both sides"; |
|||
} |
|||
else if (this.showLeading) { |
|||
showDescription = "leading side only"; |
|||
} |
|||
else if (this.showTrailing) { |
|||
showDescription = "trailing side only"; |
|||
} |
|||
else { |
|||
showDescription = "neither side (!)"; |
|||
} |
|||
|
|||
properties.add(new MessageProperty("show", showDescription)); |
|||
properties.add(new DiagnosticsProperty<Color>("color", this.color, showName: false)); |
|||
} |
|||
} |
|||
|
|||
class _GlowingOverscrollIndicatorState : TickerProviderStateMixin<GlowingOverscrollIndicator> { |
|||
_GlowController _leadingController; |
|||
_GlowController _trailingController; |
|||
Listenable _leadingAndTrailingListener; |
|||
|
|||
public override void initState() { |
|||
base.initState(); |
|||
this._leadingController = |
|||
new _GlowController(vsync: this, color: this.widget.color, axis: this.widget.axis); |
|||
this._trailingController = |
|||
new _GlowController(vsync: this, color: this.widget.color, axis: this.widget.axis); |
|||
this._leadingAndTrailingListener = ListenableUtils.merge(new List<Listenable> |
|||
{this._leadingController, this._trailingController}); |
|||
} |
|||
|
|||
public override void didUpdateWidget(StatefulWidget _oldWidget) { |
|||
base.didUpdateWidget(_oldWidget); |
|||
GlowingOverscrollIndicator oldWidget = _oldWidget as GlowingOverscrollIndicator; |
|||
if (oldWidget.color != this.widget.color || oldWidget.axis != this.widget.axis) { |
|||
this._leadingController.color = this.widget.color; |
|||
this._leadingController.axis = this.widget.axis; |
|||
this._trailingController.color = this.widget.color; |
|||
this._trailingController.axis = this.widget.axis; |
|||
} |
|||
} |
|||
|
|||
Type _lastNotificationType; |
|||
Dictionary<bool, bool> _accepted = new Dictionary<bool, bool> {{false, true}, {true, true}}; |
|||
|
|||
bool _handleScrollNotification(ScrollNotification notification) { |
|||
if (!this.widget.notificationPredicate(notification)) { |
|||
return false; |
|||
} |
|||
|
|||
if (notification is OverscrollNotification) { |
|||
_GlowController controller; |
|||
OverscrollNotification _notification = notification as OverscrollNotification; |
|||
if (_notification.overscroll < 0.0f) { |
|||
controller = this._leadingController; |
|||
} |
|||
else if (_notification.overscroll > 0.0f) { |
|||
controller = this._trailingController; |
|||
} |
|||
else { |
|||
throw new Exception("overscroll is 0.0f!"); |
|||
} |
|||
|
|||
bool isLeading = controller == this._leadingController; |
|||
if (this._lastNotificationType != typeof(OverscrollNotification)) { |
|||
OverscrollIndicatorNotification confirmationNotification = |
|||
new OverscrollIndicatorNotification(leading: isLeading); |
|||
confirmationNotification.dispatch(this.context); |
|||
this._accepted[isLeading] = confirmationNotification._accepted; |
|||
} |
|||
|
|||
D.assert(controller != null); |
|||
D.assert(_notification.metrics.axis() == this.widget.axis); |
|||
if (this._accepted[isLeading]) { |
|||
if (_notification.velocity != 0.0f) { |
|||
D.assert(_notification.dragDetails == null); |
|||
controller.absorbImpact(_notification.velocity.abs()); |
|||
} |
|||
else { |
|||
D.assert(_notification.overscroll != 0.0f); |
|||
if (_notification.dragDetails != null) { |
|||
D.assert(_notification.dragDetails.globalPosition != null); |
|||
RenderBox renderer = (RenderBox) _notification.context.findRenderObject(); |
|||
D.assert(renderer != null); |
|||
D.assert(renderer.hasSize); |
|||
Size size = renderer.size; |
|||
Offset position = renderer.globalToLocal(_notification.dragDetails.globalPosition); |
|||
switch (_notification.metrics.axis()) { |
|||
case Axis.horizontal: |
|||
controller.pull(_notification.overscroll.abs(), size.width, |
|||
position.dy.clamp(0.0f, size.height), size.height); |
|||
break; |
|||
case Axis.vertical: |
|||
controller.pull(_notification.overscroll.abs(), size.height, |
|||
position.dx.clamp(0.0f, size.width), size.width); |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
else if (notification is ScrollEndNotification || notification is ScrollUpdateNotification) { |
|||
if ((notification as ScrollEndNotification).dragDetails != null) { |
|||
this._leadingController.scrollEnd(); |
|||
this._trailingController.scrollEnd(); |
|||
} |
|||
} |
|||
|
|||
this._lastNotificationType = notification.GetType(); |
|||
return false; |
|||
} |
|||
|
|||
public override void dispose() { |
|||
this._leadingController.dispose(); |
|||
this._trailingController.dispose(); |
|||
base.dispose(); |
|||
} |
|||
|
|||
public override Widget build(BuildContext context) { |
|||
return new NotificationListener<ScrollNotification>( |
|||
onNotification: this._handleScrollNotification, |
|||
child: new RepaintBoundary( |
|||
child: new CustomPaint( |
|||
foregroundPainter: new _GlowingOverscrollIndicatorPainter( |
|||
leadingController: this.widget.showLeading ? this._leadingController : null, |
|||
trailingController: this.widget.showTrailing ? this._trailingController : null, |
|||
axisDirection: this.widget.axisDirection, |
|||
repaint: this._leadingAndTrailingListener |
|||
), |
|||
child: new RepaintBoundary( |
|||
child: this.widget.child |
|||
) |
|||
) |
|||
) |
|||
); |
|||
} |
|||
} |
|||
|
|||
|
|||
enum _GlowState { |
|||
idle, |
|||
absorb, |
|||
pull, |
|||
recede |
|||
} |
|||
|
|||
class _GlowController : ChangeNotifier { |
|||
public _GlowController( |
|||
TickerProvider vsync, |
|||
Color color, |
|||
Axis axis |
|||
) { |
|||
D.assert(vsync != null); |
|||
D.assert(color != null); |
|||
this._color = color; |
|||
this._axis = axis; |
|||
this._glowController = new AnimationController(vsync: vsync); |
|||
this._glowController.addStatusListener(this._changePhase); |
|||
Animation<float> decelerator = new CurvedAnimation( |
|||
parent: this._glowController, |
|||
curve: Curves.decelerate |
|||
); |
|||
decelerator.addListener(this.notifyListeners); |
|||
this._glowOpacity = decelerator.drive(this._glowOpacityTween); |
|||
this._glowSize = decelerator.drive(this._glowSizeTween); |
|||
this._displacementTicker = vsync.createTicker(this._tickDisplacement); |
|||
} |
|||
|
|||
_GlowState _state = _GlowState.idle; |
|||
AnimationController _glowController; |
|||
Timer _pullRecedeTimer; |
|||
|
|||
FloatTween _glowOpacityTween = new FloatTween(begin: 0.0f, end: 0.0f); |
|||
Animation<float> _glowOpacity; |
|||
FloatTween _glowSizeTween = new FloatTween(begin: 0.0f, end: 0.0f); |
|||
Animation<float> _glowSize; |
|||
|
|||
Ticker _displacementTicker; |
|||
TimeSpan? _displacementTickerLastElapsed; |
|||
float _displacementTarget = 0.5f; |
|||
float _displacement = 0.5f; |
|||
|
|||
float _pullDistance = 0.0f; |
|||
|
|||
public Color color { |
|||
get { return this._color; } |
|||
set { |
|||
D.assert(this.color != null); |
|||
if (this.color == value) { |
|||
return; |
|||
} |
|||
|
|||
this._color = value; |
|||
this.notifyListeners(); |
|||
} |
|||
} |
|||
|
|||
Color _color; |
|||
|
|||
public Axis axis { |
|||
get { return this._axis; } |
|||
set { |
|||
if (this.axis == value) { |
|||
return; |
|||
} |
|||
|
|||
this._axis = value; |
|||
this.notifyListeners(); |
|||
} |
|||
} |
|||
|
|||
Axis _axis; |
|||
|
|||
readonly TimeSpan _recedeTime = new TimeSpan(0, 0, 0, 0, 600); |
|||
readonly TimeSpan _pullTime = new TimeSpan(0, 0, 0, 0, 167); |
|||
readonly TimeSpan _pullHoldTime = new TimeSpan(0, 0, 0, 0, 167); |
|||
readonly TimeSpan _pullDecayTime = new TimeSpan(0, 0, 0, 0, 2000); |
|||
static readonly TimeSpan _crossAxisHalfTime = new TimeSpan(0, 0, 0, 0, (1000.0f / 60.0f).round()); |
|||
|
|||
const float _maxOpacity = 0.5f; |
|||
const float _pullOpacityGlowFactor = 0.8f; |
|||
const float _velocityGlowFactor = 0.00006f; |
|||
const float _sqrt3 = 1.73205080757f; // Mathf.Sqrt(3)
|
|||
const float _widthToHeightFactor = (3.0f / 4.0f) * (2.0f - _sqrt3); |
|||
|
|||
const float _minVelocity = 100.0f; // logical pixels per second
|
|||
const float _maxVelocity = 10000.0f; // logical pixels per second
|
|||
|
|||
public override void dispose() { |
|||
this._glowController.dispose(); |
|||
this._displacementTicker.dispose(); |
|||
this._pullRecedeTimer?.cancel(); |
|||
base.dispose(); |
|||
} |
|||
|
|||
public void absorbImpact(float velocity) { |
|||
D.assert(velocity >= 0.0f); |
|||
this._pullRecedeTimer?.cancel(); |
|||
this._pullRecedeTimer = null; |
|||
velocity = velocity.clamp(_minVelocity, _maxVelocity); |
|||
this._glowOpacityTween.begin = this._state == _GlowState.idle ? 0.3f : this._glowOpacity.value; |
|||
this._glowOpacityTween.end = |
|||
(velocity * _velocityGlowFactor).clamp(this._glowOpacityTween.begin, _maxOpacity); |
|||
this._glowSizeTween.begin = this._glowSize.value; |
|||
this._glowSizeTween.end = Mathf.Min(0.025f + 7.5e-7f * velocity * velocity, 1.0f); |
|||
this._glowController.duration = new TimeSpan(0, 0, 0, 0, (0.15f + velocity * 0.02f).round()); |
|||
this._glowController.forward(from: 0.0f); |
|||
this._displacement = 0.5f; |
|||
this._state = _GlowState.absorb; |
|||
} |
|||
|
|||
public void pull(float overscroll, float extent, float crossAxisOffset, float crossExtent) { |
|||
this._pullRecedeTimer?.cancel(); |
|||
this._pullDistance += |
|||
overscroll / 200.0f; // This factor is magic. Not clear why we need it to match Android.
|
|||
this._glowOpacityTween.begin = this._glowOpacity.value; |
|||
this._glowOpacityTween.end = |
|||
Mathf.Min(this._glowOpacity.value + overscroll / extent * _pullOpacityGlowFactor, _maxOpacity); |
|||
float height = Mathf.Min(extent, crossExtent * _widthToHeightFactor); |
|||
this._glowSizeTween.begin = this._glowSize.value; |
|||
this._glowSizeTween.end = Mathf.Max(1.0f - 1.0f / (0.7f * Mathf.Sqrt(this._pullDistance * height)), |
|||
this._glowSize.value); |
|||
this._displacementTarget = crossAxisOffset / crossExtent; |
|||
if (this._displacementTarget != this._displacement) { |
|||
if (!this._displacementTicker.isTicking) { |
|||
D.assert(this._displacementTickerLastElapsed == null); |
|||
this._displacementTicker.start(); |
|||
} |
|||
} |
|||
else { |
|||
this._displacementTicker.stop(); |
|||
this._displacementTickerLastElapsed = null; |
|||
} |
|||
|
|||
this._glowController.duration = this._pullTime; |
|||
if (this._state != _GlowState.pull) { |
|||
this._glowController.forward(from: 0.0f); |
|||
this._state = _GlowState.pull; |
|||
} |
|||
else { |
|||
if (!this._glowController.isAnimating) { |
|||
D.assert(this._glowController.value == 1.0f); |
|||
this.notifyListeners(); |
|||
} |
|||
} |
|||
|
|||
this._pullRecedeTimer = |
|||
Window.instance.run(this._pullHoldTime, () => this._recede(this._pullDecayTime)); |
|||
} |
|||
|
|||
public void scrollEnd() { |
|||
if (this._state == _GlowState.pull) { |
|||
this._recede(this._recedeTime); |
|||
} |
|||
} |
|||
|
|||
void _changePhase(AnimationStatus status) { |
|||
if (status != AnimationStatus.completed) { |
|||
return; |
|||
} |
|||
|
|||
switch (this._state) { |
|||
case _GlowState.absorb: |
|||
this._recede(this._recedeTime); |
|||
break; |
|||
case _GlowState.recede: |
|||
this._state = _GlowState.idle; |
|||
this._pullDistance = 0.0f; |
|||
break; |
|||
case _GlowState.pull: |
|||
case _GlowState.idle: |
|||
break; |
|||
} |
|||
} |
|||
|
|||
void _recede(TimeSpan duration) { |
|||
if (this._state == _GlowState.recede || this._state == _GlowState.idle) { |
|||
return; |
|||
} |
|||
|
|||
this._pullRecedeTimer?.cancel(); |
|||
this._pullRecedeTimer = null; |
|||
this._glowOpacityTween.begin = this._glowOpacity.value; |
|||
this._glowOpacityTween.end = 0.0f; |
|||
this._glowSizeTween.begin = this._glowSize.value; |
|||
this._glowSizeTween.end = 0.0f; |
|||
this._glowController.duration = duration; |
|||
this._glowController.forward(from: 0.0f); |
|||
this._state = _GlowState.recede; |
|||
} |
|||
|
|||
void _tickDisplacement(TimeSpan elapsed) { |
|||
if (this._displacementTickerLastElapsed != null) { |
|||
float? t = elapsed.Milliseconds - this._displacementTickerLastElapsed?.Milliseconds; |
|||
this._displacement = this._displacementTarget - (this._displacementTarget - this._displacement) * |
|||
Mathf.Pow(2.0f, (-t ?? 0.0f) / _crossAxisHalfTime.Milliseconds); |
|||
this.notifyListeners(); |
|||
} |
|||
|
|||
if (PhysicsUtils.nearEqual(this._displacementTarget, this._displacement, |
|||
Tolerance.defaultTolerance.distance)) { |
|||
this._displacementTicker.stop(); |
|||
this._displacementTickerLastElapsed = null; |
|||
} |
|||
else { |
|||
this._displacementTickerLastElapsed = elapsed; |
|||
} |
|||
} |
|||
|
|||
public void paint(Canvas canvas, Size size) { |
|||
if (this._glowOpacity.value == 0.0f) { |
|||
return; |
|||
} |
|||
|
|||
float baseGlowScale = size.width > size.height ? size.height / size.width : 1.0f; |
|||
float radius = size.width * 3.0f / 2.0f; |
|||
float height = Mathf.Min(size.height, size.width * _widthToHeightFactor); |
|||
float scaleY = this._glowSize.value * baseGlowScale; |
|||
Rect rect = Rect.fromLTWH(0.0f, 0.0f, size.width, height); |
|||
Offset center = new Offset((size.width / 2.0f) * (0.5f + this._displacement), height - radius); |
|||
Paint paint = new Paint(); |
|||
paint.color = this.color.withOpacity(this._glowOpacity.value); |
|||
canvas.save(); |
|||
canvas.scale(1.0f, scaleY); |
|||
canvas.clipRect(rect); |
|||
canvas.drawCircle(center, radius, paint); |
|||
canvas.restore(); |
|||
} |
|||
} |
|||
|
|||
class _GlowingOverscrollIndicatorPainter : AbstractCustomPainter { |
|||
public _GlowingOverscrollIndicatorPainter( |
|||
_GlowController leadingController, |
|||
_GlowController trailingController, |
|||
AxisDirection axisDirection, |
|||
Listenable repaint |
|||
) : base( |
|||
repaint: repaint |
|||
) { |
|||
this.leadingController = leadingController; |
|||
this.trailingController = trailingController; |
|||
this.axisDirection = axisDirection; |
|||
} |
|||
|
|||
public readonly _GlowController leadingController; |
|||
|
|||
public readonly _GlowController trailingController; |
|||
|
|||
public readonly AxisDirection axisDirection; |
|||
|
|||
const float piOver2 = Mathf.PI / 2.0f; |
|||
|
|||
void _paintSide(Canvas canvas, Size size, _GlowController controller, AxisDirection axisDirection, |
|||
GrowthDirection growthDirection) { |
|||
if (controller == null) { |
|||
return; |
|||
} |
|||
|
|||
switch (GrowthDirectionUtils.applyGrowthDirectionToAxisDirection(axisDirection, growthDirection)) { |
|||
case AxisDirection.up: |
|||
controller.paint(canvas, size); |
|||
break; |
|||
case AxisDirection.down: |
|||
canvas.save(); |
|||
canvas.translate(0.0f, size.height); |
|||
canvas.scale(1.0f, -1.0f); |
|||
controller.paint(canvas, size); |
|||
canvas.restore(); |
|||
break; |
|||
case AxisDirection.left: |
|||
canvas.save(); |
|||
canvas.rotate(piOver2); |
|||
canvas.scale(1.0f, -1.0f); |
|||
controller.paint(canvas, new Size(size.height, size.width)); |
|||
canvas.restore(); |
|||
break; |
|||
case AxisDirection.right: |
|||
canvas.save(); |
|||
canvas.translate(size.width, 0.0f); |
|||
canvas.rotate(piOver2); |
|||
controller.paint(canvas, new Size(size.height, size.width)); |
|||
canvas.restore(); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
public override void paint(Canvas canvas, Size size) { |
|||
this._paintSide(canvas, size, this.leadingController, this.axisDirection, GrowthDirection.reverse); |
|||
this._paintSide(canvas, size, this.trailingController, this.axisDirection, GrowthDirection.forward); |
|||
} |
|||
|
|||
public override bool shouldRepaint(CustomPainter _oldDelegate) { |
|||
_GlowingOverscrollIndicatorPainter oldDelegate = _oldDelegate as _GlowingOverscrollIndicatorPainter; |
|||
return oldDelegate.leadingController != this.leadingController |
|||
|| oldDelegate.trailingController != this.trailingController; |
|||
} |
|||
} |
|||
|
|||
public class OverscrollIndicatorNotification : ViewportNotificationMixinNotification { |
|||
public OverscrollIndicatorNotification( |
|||
bool leading |
|||
) { |
|||
this.leading = leading; |
|||
} |
|||
|
|||
public readonly bool leading; |
|||
|
|||
internal bool _accepted = true; |
|||
|
|||
public void disallowGlow() { |
|||
this._accepted = false; |
|||
} |
|||
|
|||
protected override void debugFillDescription(List<string> description) { |
|||
base.debugFillDescription(description); |
|||
description.Add($"side: {(this.leading ? "leading edge" : "trailing edge")}"); |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: b87c58140c4b442e82a27b804f8502c7 |
|||
timeCreated: 1554348404 |
|
|||
|