fix: calculate overlap

This commit is contained in:
nathan 2022-09-25 16:27:59 +08:00 committed by appflowy
parent a4b08e4f46
commit ee463037e3
10 changed files with 120 additions and 106 deletions

View File

@ -8,25 +8,28 @@ class Log {
static void info(String? message) { static void info(String? message) {
if (enableLog) { if (enableLog) {
debugPrint('[Info]=> $message'); debugPrint('AppFlowyBoard: [Info]=> $message');
} }
} }
static void debug(String? message) { static void debug(String? message) {
if (enableLog) { if (enableLog) {
debugPrint('🐛[Debug] - ${DateTime.now().second}=> $message'); debugPrint(
'AppFlowyBoard: 🐛[Debug] - ${DateTime.now().second}=> $message');
} }
} }
static void warn(String? message) { static void warn(String? message) {
if (enableLog) { if (enableLog) {
debugPrint('🐛[Warn] - ${DateTime.now().second} => $message'); debugPrint(
'AppFlowyBoard: 🐛[Warn] - ${DateTime.now().second} => $message');
} }
} }
static void trace(String? message) { static void trace(String? message) {
if (enableLog) { if (enableLog) {
debugPrint('❗️[Trace] - ${DateTime.now().second}=> $message'); debugPrint(
'AppFlowyBoard: ❗️[Trace] - ${DateTime.now().second}=> $message');
} }
} }
} }

View File

@ -217,15 +217,15 @@ class AppFlowyBoardController extends ChangeNotifier
final fromGroupItem = fromGroupController.removeAt(fromGroupIndex); final fromGroupItem = fromGroupController.removeAt(fromGroupIndex);
if (toGroupController.items.length > toGroupIndex) { if (toGroupController.items.length > toGroupIndex) {
assert(toGroupController.items[toGroupIndex] is PhantomGroupItem); assert(toGroupController.items[toGroupIndex] is PhantomGroupItem);
}
toGroupController.replace(toGroupIndex, fromGroupItem); toGroupController.replace(toGroupIndex, fromGroupItem);
onMoveGroupItemToGroup?.call( onMoveGroupItemToGroup?.call(
fromGroupId, fromGroupId,
fromGroupIndex, fromGroupIndex,
toGroupId, toGroupId,
toGroupIndex, toGroupIndex,
); );
}
} }
@override @override

View File

@ -112,7 +112,7 @@ class AppFlowyBoardGroup extends StatefulWidget {
this.itemMargin = EdgeInsets.zero, this.itemMargin = EdgeInsets.zero,
this.cornerRadius = 0.0, this.cornerRadius = 0.0,
this.backgroundColor = Colors.transparent, this.backgroundColor = Colors.transparent,
}) : config = const ReorderFlexConfig(setStateWhenEndDrag: false), }) : config = const ReorderFlexConfig(),
super(key: key); super(key: key);
@override @override

View File

@ -124,6 +124,8 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
Log.debug('[$AppFlowyGroupController] $groupData add $newItem'); Log.debug('[$AppFlowyGroupController] $groupData add $newItem');
} else { } else {
if (index >= groupData._items.length) { if (index >= groupData._items.length) {
Log.warn(
'[$AppFlowyGroupController] unexpected items length, index should less than the count of the items. Index: $index, items count: ${items.length}');
return; return;
} }

View File

@ -22,6 +22,8 @@ class FlexDragTargetData extends DragTargetData {
Size? get feedbackSize => _state.feedbackSize; Size? get feedbackSize => _state.feedbackSize;
bool get isDragging => _state.isDragging();
final String dragTargetId; final String dragTargetId;
Offset dragTargetOffset = Offset.zero; Offset dragTargetOffset = Offset.zero;
@ -48,39 +50,20 @@ class FlexDragTargetData extends DragTargetData {
bool isOverlapWithWidgets(List<GlobalObjectKey> widgetKeys) { bool isOverlapWithWidgets(List<GlobalObjectKey> widgetKeys) {
final renderBox = dragTargetIndexKey.currentContext?.findRenderObject(); final renderBox = dragTargetIndexKey.currentContext?.findRenderObject();
if (renderBox == null) return false; if (renderBox == null) return false;
if (renderBox is! RenderBox) return false; if (renderBox is! RenderBox) return false;
final size = feedbackSize ?? Size.zero; final size = feedbackSize ?? Size.zero;
final Rect rect = dragTargetOffset & size;
final Rect dragTargetRect = renderBox.localToGlobal(Offset.zero) & size;
for (final widgetKey in widgetKeys) { for (final widgetKey in widgetKeys) {
final renderObject = widgetKey.currentContext?.findRenderObject(); final renderObject = widgetKey.currentContext?.findRenderObject();
if (renderObject != null && renderObject is RenderBox) { if (renderObject != null && renderObject is RenderBox) {
Rect widgetRect = Rect widgetRect =
renderObject.localToGlobal(Offset.zero) & renderObject.size; renderObject.localToGlobal(Offset.zero) & renderObject.size;
// return rect.overlaps(widgetRect); return dragTargetRect.overlaps(widgetRect);
if (rect.right <= widgetRect.left || widgetRect.right <= rect.left) {
return false;
}
if (rect.bottom <= widgetRect.top || widgetRect.bottom <= rect.top) {
return false;
}
return true;
} }
} }
// final HitTestResult result = HitTestResult();
// WidgetsBinding.instance.hitTest(result, position);
// for (final HitTestEntry entry in result.path) {
// final HitTestTarget target = entry.target;
// if (target is RenderMetaData) {
// print(target.metaData);
// }
// print(target);
// }
return false; return false;
} }
} }
@ -113,7 +96,7 @@ class DraggingState {
int currentIndex = -1; int currentIndex = -1;
/// The widget to move the dragging widget too after the current index. /// The widget to move the dragging widget too after the current index.
int nextIndex = 0; int nextIndex = -1;
/// Whether or not we are currently scrolling this view to show a widget. /// Whether or not we are currently scrolling this view to show a widget.
bool scrolling = false; bool scrolling = false;
@ -149,6 +132,7 @@ class DraggingState {
dragStartIndex = -1; dragStartIndex = -1;
phantomIndex = -1; phantomIndex = -1;
currentIndex = -1; currentIndex = -1;
nextIndex = -1;
_draggingWidget = null; _draggingWidget = null;
} }

View File

@ -1,3 +1,4 @@
import 'package:appflowy_board/src/utils/log.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -184,8 +185,9 @@ class _ReorderDragTargetState<T extends DragTargetData>
/// When the drag does not end inside a DragTarget widget, the /// When the drag does not end inside a DragTarget widget, the
/// drag fails, but we still reorder the widget to the last position it /// drag fails, but we still reorder the widget to the last position it
/// had been dragged to. /// had been dragged to.
onDraggableCanceled: (Velocity velocity, Offset offset) => onDraggableCanceled: (Velocity velocity, Offset offset) {
widget.onDragEnded(widget.dragTargetData), widget.onDragEnded(widget.dragTargetData);
},
child: widget.child, child: widget.child,
); );
@ -221,8 +223,11 @@ class DragTargetAnimation {
// where the widget used to be. // where the widget used to be.
late AnimationController phantomController; late AnimationController phantomController;
// Uses to simulate the insert animation when card was moved from on group to
// another group. Check out the [FakeDragTarget].
late AnimationController insertController; late AnimationController insertController;
// Used to remove the phantom
late AnimationController deleteController; late AnimationController deleteController;
DragTargetAnimation({ DragTargetAnimation({
@ -238,7 +243,7 @@ class DragTargetAnimation {
value: 0, vsync: vsync, duration: reorderAnimationDuration); value: 0, vsync: vsync, duration: reorderAnimationDuration);
insertController = AnimationController( insertController = AnimationController(
value: 0.0, vsync: vsync, duration: const Duration(milliseconds: 200)); value: 0.0, vsync: vsync, duration: const Duration(milliseconds: 100));
deleteController = AnimationController( deleteController = AnimationController(
value: 0.0, vsync: vsync, duration: const Duration(milliseconds: 1)); value: 0.0, vsync: vsync, duration: const Duration(milliseconds: 1));
@ -423,7 +428,6 @@ abstract class FakeDragTargetEventData {
} }
class FakeDragTarget<T extends DragTargetData> extends StatefulWidget { class FakeDragTarget<T extends DragTargetData> extends StatefulWidget {
final Duration animationDuration;
final FakeDragTargetEventTrigger eventTrigger; final FakeDragTargetEventTrigger eventTrigger;
final FakeDragTargetEventData eventData; final FakeDragTargetEventData eventData;
final DragTargetOnStarted onDragStarted; final DragTargetOnStarted onDragStarted;
@ -442,7 +446,6 @@ class FakeDragTarget<T extends DragTargetData> extends StatefulWidget {
required this.insertAnimationController, required this.insertAnimationController,
required this.deleteAnimationController, required this.deleteAnimationController,
required this.child, required this.child,
this.animationDuration = const Duration(milliseconds: 250),
}) : super(key: key); }) : super(key: key);
@override @override
@ -468,6 +471,7 @@ class _FakeDragTargetState<T extends DragTargetData>
// }); // });
widget.eventTrigger.fakeOnDragEnded(() { widget.eventTrigger.fakeOnDragEnded(() {
Log.trace("[$FakeDragTarget] on drag end");
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onDragEnded(widget.eventData.dragTargetData as T); widget.onDragEnded(widget.eventData.dragTargetData as T);
}); });
@ -476,6 +480,13 @@ class _FakeDragTargetState<T extends DragTargetData>
super.initState(); super.initState();
} }
@override
void dispose() {
widget.insertAnimationController
.removeStatusListener(_onInsertedAnimationStatusChanged);
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (simulateDragging) { if (simulateDragging) {
@ -503,14 +514,18 @@ class _FakeDragTargetState<T extends DragTargetData>
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
simulateDragging = true; if (widget.onWillAccept(widget.eventData.dragTargetData as T)) {
widget.deleteAnimationController.reverse(from: 1.0); Log.trace("[$FakeDragTarget] on drag start");
widget.onWillAccept(widget.eventData.dragTargetData as T); simulateDragging = true;
widget.onDragStarted( widget.deleteAnimationController.reverse(from: 1.0);
widget.child, widget.onDragStarted(
widget.eventData.index, widget.child,
widget.eventData.feedbackSize, widget.eventData.index,
); widget.eventData.feedbackSize,
);
} else {
Log.trace("[$FakeDragTarget] cancel start drag");
}
}); });
}); });
} }

View File

@ -35,7 +35,7 @@ abstract class DragTargetInterceptor {
abstract class OverlapDragTargetDelegate { abstract class OverlapDragTargetDelegate {
void cancel(); void cancel();
void moveTo( void dragTargetDidMoveToReorderFlex(
String reorderFlexId, String reorderFlexId,
FlexDragTargetData dragTargetData, FlexDragTargetData dragTargetData,
int dragTargetIndex, int dragTargetIndex,
@ -99,7 +99,8 @@ class OverlappingDragTargetInterceptor extends DragTargetInterceptor {
if (index != -1) { if (index != -1) {
Log.trace( Log.trace(
'[$OverlappingDragTargetInterceptor] move to $dragTargetId at $index'); '[$OverlappingDragTargetInterceptor] move to $dragTargetId at $index');
delegate.moveTo(dragTargetId, dragTargetData, index); delegate.dragTargetDidMoveToReorderFlex(
dragTargetId, dragTargetData, index);
columnsState columnsState
.getReorderFlexState(groupId: dragTargetId) .getReorderFlexState(groupId: dragTargetId)
@ -153,7 +154,7 @@ class CrossReorderFlexDragTargetInterceptor extends DragTargetInterceptor {
/// it means the dragTarget is dragging on the top of its own list. /// it means the dragTarget is dragging on the top of its own list.
/// Otherwise, it means the dargTarget was moved to another list. /// Otherwise, it means the dargTarget was moved to another list.
Log.trace( Log.trace(
"[$CrossReorderFlexDragTargetInterceptor] $reorderFlexId accept ${dragTargetData.reorderFlexId} ${reorderFlexId != dragTargetData.reorderFlexId}"); "[$CrossReorderFlexDragTargetInterceptor] $reorderFlexId should accept ${dragTargetData.reorderFlexId} : ${reorderFlexId != dragTargetData.reorderFlexId}");
return reorderFlexId != dragTargetData.reorderFlexId; return reorderFlexId != dragTargetData.reorderFlexId;
} else { } else {
Log.trace( Log.trace(

View File

@ -38,20 +38,13 @@ abstract class ReorderDragTargetIndexKeyStorage {
class ReorderFlexConfig { class ReorderFlexConfig {
/// The opacity of the dragging widget /// The opacity of the dragging widget
final double draggingWidgetOpacity = 0.3; final double draggingWidgetOpacity = 0.4;
// How long an animation to reorder an element // How long an animation to reorder an element
final Duration reorderAnimationDuration = const Duration(milliseconds: 300); final Duration reorderAnimationDuration = const Duration(milliseconds: 200);
// How long an animation to scroll to an off-screen element // How long an animation to scroll to an off-screen element
final Duration scrollAnimationDuration = const Duration(milliseconds: 300); final Duration scrollAnimationDuration = const Duration(milliseconds: 200);
/// Determines if setSatte method needs to be called when the drag is complete.
/// Default value is [true].
///
/// If the [ReorderFlex] needs to be rebuild after the [ReorderFlex] end dragging,
/// the [setStateWhenEndDrag] should set to [true].
final bool setStateWhenEndDrag;
final bool useMoveAnimation; final bool useMoveAnimation;
@ -59,7 +52,6 @@ class ReorderFlexConfig {
const ReorderFlexConfig({ const ReorderFlexConfig({
this.useMoveAnimation = true, this.useMoveAnimation = true,
this.setStateWhenEndDrag = true,
}) : useMovePlaceholder = !useMoveAnimation; }) : useMovePlaceholder = !useMoveAnimation;
} }
@ -149,6 +141,7 @@ class ReorderFlexState extends State<ReorderFlex>
reorderAnimationDuration: widget.config.reorderAnimationDuration, reorderAnimationDuration: widget.config.reorderAnimationDuration,
entranceAnimateStatusChanged: (status) { entranceAnimateStatusChanged: (status) {
if (status == AnimationStatus.completed) { if (status == AnimationStatus.completed) {
if (dragState.nextIndex == -1) return;
setState(() => _requestAnimationToNextIndex()); setState(() => _requestAnimationToNextIndex());
} }
}, },
@ -377,12 +370,18 @@ class ReorderFlexState extends State<ReorderFlex>
dragTargetData.dragTargetOffset = offset; dragTargetData.dragTargetOffset = offset;
}, },
onDragEnded: (dragTargetData) { onDragEnded: (dragTargetData) {
if (!mounted) return; if (!mounted) {
Log.warn(
"[DragTarget]: Group:[${widget.dataSource.identifier}] end dragging but current widget was unmounted");
return;
}
Log.debug( Log.debug(
"[DragTarget]: Group:[${widget.dataSource.identifier}] end dragging"); "[DragTarget]: Group:[${widget.dataSource.identifier}] end dragging");
_notifier.updateDragTargetIndex(-1);
onDragEnded() { _notifier.updateDragTargetIndex(-1);
_animation.insertController.stop();
setState(() {
if (dragTargetData.reorderFlexId == widget.reorderFlexId) { if (dragTargetData.reorderFlexId == widget.reorderFlexId) {
_onReordered( _onReordered(
dragState.dragStartIndex, dragState.dragStartIndex,
@ -391,13 +390,7 @@ class ReorderFlexState extends State<ReorderFlex>
} }
dragState.endDragging(); dragState.endDragging();
widget.onDragEnded?.call(); widget.onDragEnded?.call();
} });
if (widget.config.setStateWhenEndDrag) {
setState(() => onDragEnded());
} else {
onDragEnded();
}
}, },
onWillAccept: (FlexDragTargetData dragTargetData) { onWillAccept: (FlexDragTargetData dragTargetData) {
// Do not receive any events if the Insert item is animating. // Do not receive any events if the Insert item is animating.
@ -405,19 +398,23 @@ class ReorderFlexState extends State<ReorderFlex>
return false; return false;
} }
assert(widget.dataSource.items.length > dragTargetIndex); if (dragTargetData.isDragging) {
if (_interceptDragTarget(dragTargetData, (interceptor) { assert(widget.dataSource.items.length > dragTargetIndex);
interceptor.onWillAccept( if (_interceptDragTarget(dragTargetData, (interceptor) {
context: builderContext, interceptor.onWillAccept(
reorderFlexState: this, context: builderContext,
dragTargetData: dragTargetData, reorderFlexState: this,
dragTargetId: reorderFlexItem.id, dragTargetData: dragTargetData,
dragTargetIndex: dragTargetIndex, dragTargetId: reorderFlexItem.id,
); dragTargetIndex: dragTargetIndex,
})) { );
return true; })) {
return true;
} else {
return handleOnWillAccept(builderContext, dragTargetIndex);
}
} else { } else {
return handleOnWillAccept(builderContext, dragTargetIndex); return false;
} }
}, },
onAccept: (dragTargetData) { onAccept: (dragTargetData) {
@ -485,6 +482,10 @@ class ReorderFlexState extends State<ReorderFlex>
} }
void resetDragTargetIndex(int dragTargetIndex) { void resetDragTargetIndex(int dragTargetIndex) {
if (dragTargetIndex > widget.dataSource.items.length) {
return;
}
dragState.setStartDraggingIndex(dragTargetIndex); dragState.setStartDraggingIndex(dragTargetIndex);
widget.dragStateStorage?.write( widget.dragStateStorage?.write(
widget.reorderFlexId, widget.reorderFlexId,
@ -521,6 +522,9 @@ class ReorderFlexState extends State<ReorderFlex>
} }
void _onReordered(int fromIndex, int toIndex) { void _onReordered(int fromIndex, int toIndex) {
if (toIndex == -1) return;
if (fromIndex == -1) return;
if (fromIndex != toIndex) { if (fromIndex != toIndex) {
widget.onReorder.call(fromIndex, toIndex); widget.onReorder.call(fromIndex, toIndex);
} }

View File

@ -94,7 +94,8 @@ class BoardPhantomController extends OverlapDragTargetDelegate
/// Remove the phantom in the group if it contains phantom /// Remove the phantom in the group if it contains phantom
void _removePhantom(String groupId) { void _removePhantom(String groupId) {
if (delegate.removePhantom(groupId)) { final didRemove = delegate.removePhantom(groupId);
if (didRemove) {
phantomState.notifyDidRemovePhantom(groupId); phantomState.notifyDidRemovePhantom(groupId);
phantomState.removeGroupListener(groupId); phantomState.removeGroupListener(groupId);
} }
@ -195,7 +196,7 @@ class BoardPhantomController extends OverlapDragTargetDelegate
} }
@override @override
void moveTo( void dragTargetDidMoveToReorderFlex(
String reorderFlexId, String reorderFlexId,
FlexDragTargetData dragTargetData, FlexDragTargetData dragTargetData,
int dragTargetIndex, int dragTargetIndex,

View File

@ -1,50 +1,54 @@
import 'package:appflowy_board/src/utils/log.dart';
import 'phantom_controller.dart'; import 'phantom_controller.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class GroupPhantomState { class GroupPhantomState {
final _states = <String, GroupState>{}; final _groupStates = <String, GroupState>{};
final _groupIsDragging = <String, bool>{};
void setGroupIsDragging(String groupId, bool isDragging) { void setGroupIsDragging(String groupId, bool isDragging) {
_stateWithId(groupId).isDragging = isDragging; _groupIsDragging[groupId] = isDragging;
} }
bool isDragging(String groupId) { bool isDragging(String groupId) {
return _stateWithId(groupId).isDragging; return _groupIsDragging[groupId] ?? false;
} }
void addGroupListener(String groupId, PassthroughPhantomListener listener) { void addGroupListener(String groupId, PassthroughPhantomListener listener) {
_stateWithId(groupId).notifier.addListener( if (_groupStates[groupId] == null) {
onInserted: (index) => listener.onInserted?.call(index), Log.debug("[$GroupPhantomState] add group listener: $groupId");
onDeleted: () => listener.onDragEnded?.call(), _groupStates[groupId] = GroupState();
); _groupStates[groupId]?.notifier.addListener(
onInserted: (index) => listener.onInserted?.call(index),
onDeleted: () => listener.onDragEnded?.call(),
);
}
} }
void removeGroupListener(String groupId) { void removeGroupListener(String groupId) {
_stateWithId(groupId).notifier.dispose(); Log.debug("[$GroupPhantomState] remove group listener: $groupId");
_states.remove(groupId); final groupState = _groupStates.remove(groupId);
groupState?.dispose();
} }
void notifyDidInsertPhantom(String groupId, int index) { void notifyDidInsertPhantom(String groupId, int index) {
_stateWithId(groupId).notifier.insert(index); _groupStates[groupId]?.notifier.insert(index);
} }
void notifyDidRemovePhantom(String groupId) { void notifyDidRemovePhantom(String groupId) {
_stateWithId(groupId).notifier.remove(); Log.debug("[$GroupPhantomState] $groupId remove phantom");
} _groupStates[groupId]?.notifier.remove();
GroupState _stateWithId(String groupId) {
var state = _states[groupId];
if (state == null) {
state = GroupState();
_states[groupId] = state;
}
return state;
} }
} }
class GroupState { class GroupState {
bool isDragging = false; bool isDragging = false;
final notifier = PassthroughPhantomNotifier(); final notifier = PassthroughPhantomNotifier();
void dispose() {
notifier.dispose();
}
} }
abstract class PassthroughPhantomListener { abstract class PassthroughPhantomListener {