您最多选择25个主题
主题必须以中文或者字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
397 行
13 KiB
397 行
13 KiB
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.physics;
|
|
using Unity.UIWidgets.rendering;
|
|
using Unity.UIWidgets.ui;
|
|
using Unity.UIWidgets.widgets;
|
|
|
|
namespace Unity.UIWidget.material {
|
|
public delegate Widget ScrollableWidgetBuilder(
|
|
BuildContext context,
|
|
ScrollController scrollController
|
|
);
|
|
|
|
public class DraggableScrollableSheet : StatefulWidget {
|
|
public DraggableScrollableSheet(
|
|
Key key = null,
|
|
float initialChildSize = 0.5f,
|
|
float minChildSize = 0.25f,
|
|
bool expand = true,
|
|
ScrollableWidgetBuilder builder = null
|
|
) : base(key: key) {
|
|
D.assert(builder != null);
|
|
this.initialChildSize = initialChildSize;
|
|
this.minChildSize = minChildSize;
|
|
this.expand = expand;
|
|
this.builder = builder;
|
|
}
|
|
|
|
public readonly float initialChildSize;
|
|
|
|
public readonly float minChildSize;
|
|
|
|
public readonly float maxChildSize;
|
|
|
|
public readonly bool expand;
|
|
|
|
public readonly ScrollableWidgetBuilder builder;
|
|
|
|
public override State createState() {
|
|
return new _DraggableScrollableSheetState();
|
|
}
|
|
}
|
|
|
|
public class DraggableScrollableNotification : ViewportNotificationMixinNotification {
|
|
public DraggableScrollableNotification(
|
|
float extent,
|
|
float minExtent,
|
|
float maxExtent,
|
|
float initialExtent,
|
|
BuildContext context
|
|
) {
|
|
D.assert(0.0f <= minExtent);
|
|
D.assert(maxExtent <= 1.0f);
|
|
D.assert(minExtent <= extent);
|
|
D.assert(minExtent <= initialExtent);
|
|
D.assert(extent <= maxExtent);
|
|
D.assert(initialExtent <= maxExtent);
|
|
D.assert(context != null);
|
|
|
|
this.extent = extent;
|
|
this.minExtent = minExtent;
|
|
this.maxExtent = maxExtent;
|
|
this.initialExtent = initialExtent;
|
|
this.context = context;
|
|
}
|
|
|
|
public readonly float extent;
|
|
|
|
public readonly float minExtent;
|
|
|
|
public readonly float maxExtent;
|
|
|
|
public readonly float initialExtent;
|
|
|
|
public readonly BuildContext context;
|
|
|
|
protected override void debugFillDescription(List<string> description) {
|
|
base.debugFillDescription(description);
|
|
description.Add(
|
|
$"minExtent: {minExtent}, extent: {extent}, maxExtent: {maxExtent}, initialExtent: {initialExtent}");
|
|
}
|
|
}
|
|
|
|
class _DraggableSheetExtent {
|
|
public _DraggableSheetExtent(
|
|
float minExtent,
|
|
float maxExtent,
|
|
float initialExtent,
|
|
VoidCallback listener
|
|
) {
|
|
D.assert(minExtent >= 0);
|
|
D.assert(maxExtent <= 1);
|
|
D.assert(minExtent <= initialExtent);
|
|
D.assert(initialExtent <= maxExtent);
|
|
|
|
this.minExtent = minExtent;
|
|
this.maxExtent = maxExtent;
|
|
this.initialExtent = initialExtent;
|
|
_currentExtent = new ValueNotifier<float>(initialExtent);
|
|
_currentExtent.addListener(listener);
|
|
availablePixels = float.PositiveInfinity;
|
|
}
|
|
|
|
float minExtent;
|
|
float maxExtent;
|
|
internal float initialExtent;
|
|
internal ValueNotifier<float> _currentExtent;
|
|
internal float availablePixels;
|
|
|
|
public bool isAtMin {
|
|
get { return minExtent >= _currentExtent.value; }
|
|
}
|
|
|
|
public bool isAtMax {
|
|
get { return maxExtent <= _currentExtent.value; }
|
|
}
|
|
|
|
public float currentExtent {
|
|
get { return _currentExtent.value; }
|
|
set { _currentExtent.value = value.clamp(minExtent, maxExtent); }
|
|
}
|
|
|
|
public float additionalMinExtent {
|
|
get { return isAtMin ? 0.0f : 1.0f; }
|
|
}
|
|
|
|
public float additionalMaxExtent {
|
|
get { return isAtMax ? 0.0f : 1.0f; }
|
|
}
|
|
|
|
public void addPixelDelta(float delta, BuildContext context) {
|
|
if (availablePixels == 0) {
|
|
return;
|
|
}
|
|
|
|
currentExtent += delta / availablePixels * maxExtent;
|
|
new DraggableScrollableNotification(
|
|
minExtent: minExtent,
|
|
maxExtent: maxExtent,
|
|
extent: currentExtent,
|
|
initialExtent: initialExtent,
|
|
context: context
|
|
).dispatch(context);
|
|
}
|
|
}
|
|
|
|
class _DraggableScrollableSheetState : State<DraggableScrollableSheet> {
|
|
_DraggableScrollableSheetScrollController _scrollController;
|
|
_DraggableSheetExtent _extent;
|
|
|
|
public override void initState() {
|
|
base.initState();
|
|
_extent = new _DraggableSheetExtent(
|
|
minExtent: widget.minChildSize,
|
|
maxExtent: widget.maxChildSize,
|
|
initialExtent: widget.initialChildSize,
|
|
listener: _setExtent
|
|
);
|
|
_scrollController = new _DraggableScrollableSheetScrollController(extent: _extent);
|
|
}
|
|
|
|
public override void didChangeDependencies() {
|
|
base.didChangeDependencies();
|
|
if (_InheritedResetNotifier.shouldReset(context)) {
|
|
if (_scrollController.offset != 0.0f) {
|
|
_scrollController.animateTo(
|
|
0.0f,
|
|
duration: new TimeSpan(0, 0, 0, 0, 1),
|
|
curve: Curves.linear
|
|
);
|
|
}
|
|
|
|
_extent._currentExtent.value = _extent.initialExtent;
|
|
}
|
|
}
|
|
|
|
void _setExtent() {
|
|
setState(() => { });
|
|
}
|
|
|
|
public override Widget build(BuildContext context) {
|
|
return new LayoutBuilder(
|
|
builder: (BuildContext subContext, BoxConstraints subConstraints) => {
|
|
_extent.availablePixels = widget.maxChildSize * subConstraints.biggest.height;
|
|
Widget sheet = new FractionallySizedBox(
|
|
heightFactor: _extent.currentExtent,
|
|
child: widget.builder(subContext, _scrollController),
|
|
alignment: Alignment.bottomCenter
|
|
);
|
|
return widget.expand ? SizedBox.expand(child: sheet) : sheet;
|
|
}
|
|
);
|
|
}
|
|
|
|
public override void dispose() {
|
|
_scrollController.dispose();
|
|
base.dispose();
|
|
}
|
|
}
|
|
|
|
class _DraggableScrollableSheetScrollController : ScrollController {
|
|
public _DraggableScrollableSheetScrollController(
|
|
float initialScrollOffset = 0.0f,
|
|
string debugLabel = null,
|
|
_DraggableSheetExtent extent = null
|
|
) : base(debugLabel: debugLabel, initialScrollOffset: initialScrollOffset) {
|
|
D.assert(extent != null);
|
|
this.extent = extent;
|
|
}
|
|
|
|
public readonly _DraggableSheetExtent extent;
|
|
|
|
public override ScrollPosition createScrollPosition(
|
|
ScrollPhysics physics,
|
|
ScrollContext context,
|
|
ScrollPosition oldPosition
|
|
) {
|
|
return new _DraggableScrollableSheetScrollPosition(
|
|
physics: physics,
|
|
context: context,
|
|
oldPosition: oldPosition,
|
|
extent: extent
|
|
);
|
|
}
|
|
|
|
protected override void debugFillDescription(List<string> description) {
|
|
base.debugFillDescription(description);
|
|
description.Add($"extent: {extent}");
|
|
}
|
|
}
|
|
|
|
class _DraggableScrollableSheetScrollPosition : ScrollPositionWithSingleContext {
|
|
public _DraggableScrollableSheetScrollPosition(
|
|
ScrollPhysics physics = null,
|
|
ScrollContext context = null,
|
|
float initialPixels = 0.0f,
|
|
bool keepScrollOffset = true,
|
|
ScrollPosition oldPosition = null,
|
|
string debugLabel = null,
|
|
_DraggableSheetExtent extent = null
|
|
) : base(physics: physics,
|
|
context: context,
|
|
initialPixels: initialPixels,
|
|
keepScrollOffset: keepScrollOffset,
|
|
oldPosition: oldPosition,
|
|
debugLabel: debugLabel) {
|
|
D.assert(extent != null);
|
|
this.extent = extent;
|
|
}
|
|
|
|
VoidCallback _dragCancelCallback;
|
|
public readonly _DraggableSheetExtent extent;
|
|
|
|
bool listShouldScroll {
|
|
get { return pixels > 0.0f; }
|
|
}
|
|
|
|
public override bool applyContentDimensions(float minScrollExtent, float maxScrollExtent) {
|
|
return base.applyContentDimensions(
|
|
minScrollExtent - extent.additionalMinExtent,
|
|
maxScrollExtent + extent.additionalMaxExtent
|
|
);
|
|
}
|
|
|
|
public override void applyUserOffset(float delta) {
|
|
if (!listShouldScroll &&
|
|
(!(extent.isAtMin || extent.isAtMax) ||
|
|
(extent.isAtMin && delta < 0) ||
|
|
(extent.isAtMax && delta > 0))) {
|
|
extent.addPixelDelta(-delta, context.notificationContext);
|
|
}
|
|
else {
|
|
base.applyUserOffset(delta);
|
|
}
|
|
}
|
|
|
|
public override void goBallistic(float velocity) {
|
|
if (velocity == 0.0f ||
|
|
(velocity < 0.0f && listShouldScroll) ||
|
|
(velocity > 0.0f && extent.isAtMax)) {
|
|
base.goBallistic(velocity);
|
|
return;
|
|
}
|
|
|
|
_dragCancelCallback?.Invoke();
|
|
_dragCancelCallback = null;
|
|
|
|
Simulation simulation = new ClampingScrollSimulation(
|
|
position: extent.currentExtent,
|
|
velocity: velocity,
|
|
tolerance: physics.tolerance
|
|
);
|
|
|
|
AnimationController ballisticController = AnimationController.unbounded(
|
|
debugLabel: $"{GetType()}",
|
|
vsync: context.vsync
|
|
);
|
|
|
|
float lastDelta = 0;
|
|
|
|
void _tick() {
|
|
float delta = ballisticController.value - lastDelta;
|
|
lastDelta = ballisticController.value;
|
|
extent.addPixelDelta(delta, context.notificationContext);
|
|
if ((velocity > 0 && extent.isAtMax) || (velocity < 0 && extent.isAtMin)) {
|
|
velocity = ballisticController.velocity +
|
|
(physics.tolerance.velocity * ballisticController.velocity.sign());
|
|
base.goBallistic(velocity);
|
|
ballisticController.stop();
|
|
}
|
|
else if (ballisticController.isCompleted) {
|
|
base.goBallistic(0);
|
|
}
|
|
}
|
|
|
|
ballisticController.addListener(_tick);
|
|
ballisticController.animateWith(simulation).whenCompleteOrCancel(
|
|
ballisticController.dispose
|
|
);
|
|
}
|
|
|
|
public override Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) {
|
|
_dragCancelCallback = dragCancelCallback;
|
|
return base.drag(details, dragCancelCallback);
|
|
}
|
|
}
|
|
|
|
public class DraggableScrollableActuator : StatelessWidget {
|
|
public DraggableScrollableActuator(
|
|
Key key = null,
|
|
Widget child = null
|
|
) : base(key: key) {
|
|
D.assert(child != null);
|
|
this.child = child;
|
|
}
|
|
|
|
public readonly Widget child;
|
|
|
|
readonly _ResetNotifier _notifier = new _ResetNotifier();
|
|
|
|
public static bool reset(BuildContext context) {
|
|
_InheritedResetNotifier notifier = context.dependOnInheritedWidgetOfExactType<_InheritedResetNotifier>();
|
|
if (notifier == null) {
|
|
return false;
|
|
}
|
|
|
|
return notifier._sendReset();
|
|
}
|
|
|
|
public override Widget build(BuildContext context) {
|
|
return new _InheritedResetNotifier(child: child, notifier: _notifier);
|
|
}
|
|
}
|
|
|
|
class _ResetNotifier : ChangeNotifier {
|
|
internal bool _wasCalled = false;
|
|
|
|
internal bool sendReset() {
|
|
if (!hasListeners) {
|
|
return false;
|
|
}
|
|
|
|
_wasCalled = true;
|
|
notifyListeners();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
class _InheritedResetNotifier : InheritedNotifier<_ResetNotifier> {
|
|
public _InheritedResetNotifier(
|
|
Key key = null,
|
|
Widget child = null,
|
|
_ResetNotifier notifier = null
|
|
) : base(key: key, child: child, notifier: notifier) {
|
|
}
|
|
|
|
internal bool _sendReset() {
|
|
return notifier.sendReset();
|
|
}
|
|
|
|
public static bool shouldReset(BuildContext context) {
|
|
InheritedWidget widget = context.dependOnInheritedWidgetOfExactType<_InheritedResetNotifier>();
|
|
if (widget == null) {
|
|
return false;
|
|
}
|
|
|
|
_InheritedResetNotifier inheritedNotifier = widget as _InheritedResetNotifier;
|
|
bool wasCalled = inheritedNotifier.notifier._wasCalled;
|
|
inheritedNotifier.notifier._wasCalled = false;
|
|
return wasCalled;
|
|
}
|
|
}
|
|
}
|