Merge pull request #895 from AppFlowy-IO/feat/board_move_card_animation

Feat/board move card animation
This commit is contained in:
Nathan.fooo 2022-08-23 20:34:40 +08:00 committed by GitHub
commit c7b671ca07
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 195 additions and 82 deletions

View File

@ -1,16 +1,21 @@
# 0.0.5
* Optimize insert card animation
* Enable insert card at the end of the column
* Fix some bugs
# 0.0.4 # 0.0.4
* fix some bugs * Fix some bugs
# 0.0.3 # 0.0.3
* Support customize UI * Support customize UI
* Update example * Update example
* Add AppFlowy style widget * Add AppFlowy style widget
## 0.0.2 # 0.0.2
* Update documentation * Update documentation
## 0.0.1 # 0.0.1
* Support drag and drop column * Support drag and drop column
* Support drag and drop column items from one to another * Support drag and drop column items from one to another

View File

@ -26,13 +26,18 @@ class _MultiBoardListExampleState extends State<MultiBoardListExample> {
List<AFColumnItem> a = [ List<AFColumnItem> a = [
TextItem("Card 1"), TextItem("Card 1"),
TextItem("Card 2"), TextItem("Card 2"),
// RichTextItem(title: "Card 3", subtitle: 'Aug 1, 2020 4:05 PM'), RichTextItem(title: "Card 3", subtitle: 'Aug 1, 2020 4:05 PM'),
TextItem("Card 4"), TextItem("Card 4"),
TextItem("Card 5"),
TextItem("Card 6"),
RichTextItem(title: "Card 7", subtitle: 'Aug 1, 2020 4:05 PM'),
RichTextItem(title: "Card 8", subtitle: 'Aug 1, 2020 4:05 PM'),
TextItem("Card 9"),
]; ];
final column1 = AFBoardColumnData(id: "To Do", items: a); final column1 = AFBoardColumnData(id: "To Do", items: a);
final column2 = AFBoardColumnData(id: "In Progress", items: <AFColumnItem>[ final column2 = AFBoardColumnData(id: "In Progress", items: <AFColumnItem>[
// RichTextItem(title: "Card 5", subtitle: 'Aug 1, 2020 4:05 PM'), RichTextItem(title: "Card 10", subtitle: 'Aug 1, 2020 4:05 PM'),
// TextItem("Card 6"), TextItem("Card 11"),
]); ]);
final column3 = AFBoardColumnData(id: "Done", items: <AFColumnItem>[]); final column3 = AFBoardColumnData(id: "Done", items: <AFColumnItem>[]);
@ -93,7 +98,7 @@ class _MultiBoardListExampleState extends State<MultiBoardListExample> {
return Align( return Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 40),
child: Text(item.s), child: Text(item.s),
), ),
); );

View File

@ -4,8 +4,6 @@ import 'package:flutter/material.dart';
const DART_LOG = "Dart_LOG"; const DART_LOG = "Dart_LOG";
class Log { class Log {
// static const enableLog = bool.hasEnvironment(DART_LOG);
// static final shared = Log();
static const enableLog = false; static const enableLog = false;
static void info(String? message) { static void info(String? message) {
@ -16,19 +14,19 @@ class Log {
static void debug(String? message) { static void debug(String? message) {
if (enableLog) { if (enableLog) {
debugPrint('🐛[Debug]=> $message'); debugPrint('🐛[Debug] - ${DateTime.now().second}=> $message');
} }
} }
static void warn(String? message) { static void warn(String? message) {
if (enableLog) { if (enableLog) {
debugPrint('🐛[Warn]=> $message'); debugPrint('🐛[Warn] - ${DateTime.now().second} => $message');
} }
} }
static void trace(String? message) { static void trace(String? message) {
if (enableLog) { if (enableLog) {
// debugPrint('❗️[Trace]=> $message'); debugPrint('❗️[Trace] - ${DateTime.now().second}=> $message');
} }
} }
} }

View File

@ -159,7 +159,7 @@ class _BoardContentState extends State<BoardContent> {
dataSource: widget.dataController, dataSource: widget.dataController,
direction: Axis.horizontal, direction: Axis.horizontal,
interceptor: interceptor, interceptor: interceptor,
children: _buildColumns(), children: _buildColumns(interceptor.columnKeys),
); );
return Stack( return Stack(
@ -191,7 +191,7 @@ class _BoardContentState extends State<BoardContent> {
); );
} }
List<Widget> _buildColumns() { List<Widget> _buildColumns(List<ColumnKey> columnKeys) {
final List<Widget> children = final List<Widget> children =
widget.dataController.columnDatas.asMap().entries.map( widget.dataController.columnDatas.asMap().entries.map(
(item) { (item) {
@ -208,9 +208,7 @@ class _BoardContentState extends State<BoardContent> {
value: widget.dataController.columnController(columnData.id), value: widget.dataController.columnController(columnData.id),
child: Consumer<AFBoardColumnDataController>( child: Consumer<AFBoardColumnDataController>(
builder: (context, value, child) { builder: (context, value, child) {
return ConstrainedBox( final boardColumn = AFBoardColumnWidget(
constraints: widget.columnConstraints,
child: AFBoardColumnWidget(
margin: _marginFromIndex(columnIndex), margin: _marginFromIndex(columnIndex),
itemMargin: widget.config.columnItemPadding, itemMargin: widget.config.columnItemPadding,
headerBuilder: widget.headerBuilder, headerBuilder: widget.headerBuilder,
@ -222,7 +220,21 @@ class _BoardContentState extends State<BoardContent> {
onReorder: widget.dataController.moveColumnItem, onReorder: widget.dataController.moveColumnItem,
cornerRadius: widget.config.cornerRadius, cornerRadius: widget.config.cornerRadius,
backgroundColor: widget.config.columnBackgroundColor, backgroundColor: widget.config.columnBackgroundColor,
), );
// columnKeys
// .removeWhere((element) => element.columnId == columnData.id);
// columnKeys.add(
// ColumnKey(
// columnId: columnData.id,
// key: boardColumn.columnGlobalKey,
// ),
// );
return ConstrainedBox(
constraints: widget.columnConstraints,
child: boardColumn,
); );
}, },
), ),

View File

@ -87,7 +87,9 @@ class AFBoardColumnWidget extends StatefulWidget {
final Color backgroundColor; final Color backgroundColor;
const AFBoardColumnWidget({ final GlobalKey columnGlobalKey = GlobalKey();
AFBoardColumnWidget({
Key? key, Key? key,
this.headerBuilder, this.headerBuilder,
this.footBuilder, this.footBuilder,
@ -136,8 +138,8 @@ class _AFBoardColumnWidgetState extends State<AFBoardColumnWidget> {
draggableTargetBuilder: PhantomDraggableBuilder(), draggableTargetBuilder: PhantomDraggableBuilder(),
); );
final reorderFlex = ReorderFlex( Widget reorderFlex = ReorderFlex(
key: widget.key, key: widget.columnGlobalKey,
scrollController: widget.scrollController, scrollController: widget.scrollController,
config: widget.config, config: widget.config,
onDragStarted: (index) { onDragStarted: (index) {
@ -160,6 +162,9 @@ class _AFBoardColumnWidgetState extends State<AFBoardColumnWidget> {
children: children, children: children,
); );
// reorderFlex =
// KeyedSubtree(key: widget.columnGlobalKey, child: reorderFlex);
return Container( return Container(
margin: widget.margin, margin: widget.margin,
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,

View File

@ -197,7 +197,8 @@ class AFBoardDataController extends ChangeNotifier
assert(index != -1); assert(index != -1);
if (index != -1) { if (index != -1) {
if (index != newIndex) { if (index != newIndex) {
// Log.debug('[$BoardPhantomController] update $toColumnId:$index to $toColumnId:$phantomIndex'); Log.trace(
'[$BoardPhantomController] update $columnId:$index to $columnId:$newIndex');
final item = columnDataController.removeAt(index, notify: false); final item = columnDataController.removeAt(index, notify: false);
columnDataController.insert(newIndex, item, notify: false); columnDataController.insert(newIndex, item, notify: false);
} }

View File

@ -43,7 +43,7 @@ class FlexDragTargetData extends DragTargetData {
} }
class DraggingState { class DraggingState {
final String id; final String reorderFlexId;
/// The member of widget.children currently being dragged. /// The member of widget.children currently being dragged.
Widget? _draggingWidget; Widget? _draggingWidget;
@ -72,7 +72,7 @@ class DraggingState {
/// The additional margin to place around a computed drop area. /// The additional margin to place around a computed drop area.
static const double _dropAreaMargin = 0.0; static const double _dropAreaMargin = 0.0;
DraggingState(this.id); DraggingState(this.reorderFlexId);
Size get dropAreaSize { Size get dropAreaSize {
if (feedbackSize == null) { if (feedbackSize == null) {
@ -132,7 +132,7 @@ class DraggingState {
} }
void updateNextIndex(int index) { void updateNextIndex(int index) {
Log.trace('updateNextIndex: $index'); Log.debug('$reorderFlexId updateNextIndex: $index');
nextIndex = index; nextIndex = index;
} }

View File

@ -222,10 +222,10 @@ 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: 100)); value: 0.0, vsync: vsync, duration: const Duration(milliseconds: 200));
deleteController = AnimationController( deleteController = AnimationController(
value: 0.0, vsync: vsync, duration: const Duration(milliseconds: 10)); value: 0.0, vsync: vsync, duration: const Duration(milliseconds: 1));
} }
void startDragging() { void startDragging() {
@ -276,6 +276,31 @@ class IgnorePointerWidget extends StatelessWidget {
} }
} }
class AbsorbPointerWidget extends StatelessWidget {
final Widget? child;
final bool useIntrinsicSize;
const AbsorbPointerWidget({
required this.child,
this.useIntrinsicSize = false,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final sizedChild = useIntrinsicSize
? child
: SizedBox(width: 0.0, height: 0.0, child: child);
final opacity = useIntrinsicSize ? 0.3 : 0.0;
return AbsorbPointer(
child: Opacity(
opacity: opacity,
child: sizedChild,
),
);
}
}
class PhantomWidget extends StatelessWidget { class PhantomWidget extends StatelessWidget {
final Widget? child; final Widget? child;
final double opacity; final double opacity;
@ -371,6 +396,7 @@ class _DragTargeMovePlaceholderState extends State<DragTargeMovePlaceholder> {
} }
abstract class FakeDragTargetEventTrigger { abstract class FakeDragTargetEventTrigger {
void fakeOnDragStart(void Function(int?) callback);
void fakeOnDragEnded(VoidCallback callback); void fakeOnDragEnded(VoidCallback callback);
} }
@ -421,6 +447,10 @@ class _FakeDragTargetState<T extends DragTargetData>
/// Start insert animation /// Start insert animation
widget.insertAnimationController.forward(from: 0.0); widget.insertAnimationController.forward(from: 0.0);
// widget.eventTrigger.fakeOnDragStart((insertIndex) {
// Log.trace("[$FakeDragTarget] on drag $insertIndex");
// });
widget.eventTrigger.fakeOnDragEnded(() { widget.eventTrigger.fakeOnDragEnded(() {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onDragEnded(widget.eventData.dragTargetData as T); widget.onDragEnded(widget.eventData.dragTargetData as T);
@ -436,7 +466,7 @@ class _FakeDragTargetState<T extends DragTargetData>
return SizeTransitionWithIntrinsicSize( return SizeTransitionWithIntrinsicSize(
sizeFactor: widget.deleteAnimationController, sizeFactor: widget.deleteAnimationController,
axis: Axis.vertical, axis: Axis.vertical,
child: IgnorePointerWidget( child: AbsorbPointerWidget(
child: widget.child, child: widget.child,
), ),
); );
@ -444,7 +474,7 @@ class _FakeDragTargetState<T extends DragTargetData>
return SizeTransitionWithIntrinsicSize( return SizeTransitionWithIntrinsicSize(
sizeFactor: widget.insertAnimationController, sizeFactor: widget.insertAnimationController,
axis: Axis.vertical, axis: Axis.vertical,
child: IgnorePointerWidget( child: AbsorbPointerWidget(
useIntrinsicSize: true, useIntrinsicSize: true,
child: widget.child, child: widget.child,
), ),

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../utils/log.dart'; import '../../utils/log.dart';
@ -8,6 +10,8 @@ import 'reorder_flex.dart';
/// [DragTargetInterceptor] is used to intercept the [DragTarget]'s /// [DragTargetInterceptor] is used to intercept the [DragTarget]'s
/// [onWillAccept], [OnAccept], and [onLeave] event. /// [onWillAccept], [OnAccept], and [onLeave] event.
abstract class DragTargetInterceptor { abstract class DragTargetInterceptor {
String get reorderFlexId;
/// Returns [yes] to receive the [DragTarget]'s event. /// Returns [yes] to receive the [DragTarget]'s event.
bool canHandler(FlexDragTargetData dragTargetData); bool canHandler(FlexDragTargetData dragTargetData);
@ -37,7 +41,7 @@ abstract class OverlapDragTargetDelegate {
int dragTargetIndex, int dragTargetIndex,
); );
bool canMoveTo(String dragTargetId); int canMoveTo(String dragTargetId);
} }
/// [OverlappingDragTargetInterceptor] is used to receive the overlapping /// [OverlappingDragTargetInterceptor] is used to receive the overlapping
@ -47,9 +51,12 @@ abstract class OverlapDragTargetDelegate {
/// Receive the [DragTarget] event if the [acceptedReorderFlexId] contains /// Receive the [DragTarget] event if the [acceptedReorderFlexId] contains
/// the passed in dragTarget' reorderFlexId. /// the passed in dragTarget' reorderFlexId.
class OverlappingDragTargetInterceptor extends DragTargetInterceptor { class OverlappingDragTargetInterceptor extends DragTargetInterceptor {
@override
final String reorderFlexId; final String reorderFlexId;
final List<String> acceptedReorderFlexId; final List<String> acceptedReorderFlexId;
final OverlapDragTargetDelegate delegate; final OverlapDragTargetDelegate delegate;
final List<ColumnKey> columnKeys = [];
Timer? _delayOperation;
OverlappingDragTargetInterceptor({ OverlappingDragTargetInterceptor({
required this.delegate, required this.delegate,
@ -72,15 +79,38 @@ class OverlappingDragTargetInterceptor extends DragTargetInterceptor {
if (dragTargetId == dragTargetData.reorderFlexId) { if (dragTargetId == dragTargetData.reorderFlexId) {
delegate.cancel(); delegate.cancel();
} else { } else {
if (delegate.canMoveTo(dragTargetId)) { /// The priority of the column interactions is high than the cross column.
delegate.moveTo(dragTargetId, dragTargetData, 0); /// Workaround: delay 100 milliseconds to lower the cross column event priority.
_delayOperation?.cancel();
_delayOperation = Timer(const Duration(milliseconds: 100), () {
final index = delegate.canMoveTo(dragTargetId);
if (index != -1) {
Log.trace(
'[$OverlappingDragTargetInterceptor] move to $dragTargetId at $index');
delegate.moveTo(dragTargetId, dragTargetData, index);
// final columnIndex = columnKeys
// .indexWhere((element) => element.columnId == dragTargetId);
// if (columnIndex != -1) {
// final state = columnKeys[columnIndex].key.currentState;
// if (state is ReorderFlexState) {
// state.handleOnWillAccept(context, index);
// }
// }
} }
});
} }
return true; return true;
} }
} }
class ColumnKey {
String columnId;
GlobalKey key;
ColumnKey({required this.columnId, required this.key});
}
abstract class CrossReorderFlexDragTargetDelegate { abstract class CrossReorderFlexDragTargetDelegate {
/// * [reorderFlexId] is the id that the [ReorderFlex] passed in. /// * [reorderFlexId] is the id that the [ReorderFlex] passed in.
bool acceptNewDragTargetData( bool acceptNewDragTargetData(
@ -96,6 +126,7 @@ abstract class CrossReorderFlexDragTargetDelegate {
} }
class CrossReorderFlexDragTargetInterceptor extends DragTargetInterceptor { class CrossReorderFlexDragTargetInterceptor extends DragTargetInterceptor {
@override
final String reorderFlexId; final String reorderFlexId;
final List<String> acceptedReorderFlexIds; final List<String> acceptedReorderFlexIds;
final CrossReorderFlexDragTargetDelegate delegate; final CrossReorderFlexDragTargetDelegate delegate;
@ -119,8 +150,12 @@ class CrossReorderFlexDragTargetInterceptor extends DragTargetInterceptor {
/// If the columnId equal to the dragTargetData's columnId, /// If the columnId equal to the dragTargetData's columnId,
/// 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(
"[$CrossReorderFlexDragTargetInterceptor] $reorderFlexId accept ${dragTargetData.reorderFlexId} ${reorderFlexId != dragTargetData.reorderFlexId}");
return reorderFlexId != dragTargetData.reorderFlexId; return reorderFlexId != dragTargetData.reorderFlexId;
} else { } else {
Log.trace(
"[$CrossReorderFlexDragTargetInterceptor] not accept ${dragTargetData.reorderFlexId}");
return false; return false;
} }
} }
@ -151,6 +186,9 @@ class CrossReorderFlexDragTargetInterceptor extends DragTargetInterceptor {
dragTargetIndex, dragTargetIndex,
); );
Log.debug(
'[$CrossReorderFlexDragTargetInterceptor] dargTargetIndex: $dragTargetIndex, reorderFlexId: $reorderFlexId');
if (isNewDragTarget == false) { if (isNewDragTarget == false) {
delegate.updateDragTargetData(reorderFlexId, dragTargetIndex); delegate.updateDragTargetData(reorderFlexId, dragTargetIndex);
reorderFlexState.handleOnWillAccept(context, dragTargetIndex); reorderFlexState.handleOnWillAccept(context, dragTargetIndex);

View File

@ -36,10 +36,10 @@ class ReorderFlexConfig {
final double draggingWidgetOpacity = 0.3; final double draggingWidgetOpacity = 0.3;
// How long an animation to reorder an element // How long an animation to reorder an element
final Duration reorderAnimationDuration = const Duration(milliseconds: 250); final Duration reorderAnimationDuration = const Duration(milliseconds: 300);
// 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: 250); final Duration scrollAnimationDuration = const Duration(milliseconds: 300);
final bool useMoveAnimation; final bool useMoveAnimation;
@ -214,7 +214,7 @@ class ReorderFlexState extends State<ReorderFlex>
} }
Log.trace( Log.trace(
'Rebuild: Column:[${dragState.id}] ${dragState.toString()}, childIndex: $childIndex shiftedIndex: $shiftedIndex'); 'Rebuild: Column:[${dragState.reorderFlexId}] ${dragState.toString()}, childIndex: $childIndex shiftedIndex: $shiftedIndex');
final currentIndex = dragState.currentIndex; final currentIndex = dragState.currentIndex;
final dragPhantomIndex = dragState.phantomIndex; final dragPhantomIndex = dragState.phantomIndex;
@ -330,6 +330,8 @@ class ReorderFlexState extends State<ReorderFlex>
widget.onDragStarted?.call(draggingIndex); widget.onDragStarted?.call(draggingIndex);
}, },
onDragEnded: (dragTargetData) { onDragEnded: (dragTargetData) {
if (!mounted) return;
Log.debug( Log.debug(
"[DragTarget]: Column:[${widget.dataSource.identifier}] end dragging"); "[DragTarget]: Column:[${widget.dataSource.identifier}] end dragging");
_notifier.updateDragTargetIndex(-1); _notifier.updateDragTargetIndex(-1);
@ -346,21 +348,21 @@ class ReorderFlexState extends State<ReorderFlex>
}); });
}, },
onWillAccept: (FlexDragTargetData dragTargetData) { onWillAccept: (FlexDragTargetData dragTargetData) {
// Do not receive any events if the Insert item is animating.
if (_animation.deleteController.isAnimating) { if (_animation.deleteController.isAnimating) {
return false; return false;
} }
assert(widget.dataSource.items.length > dragTargetIndex); assert(widget.dataSource.items.length > dragTargetIndex);
if (_interceptDragTarget( if (_interceptDragTarget(dragTargetData, (interceptor) {
dragTargetData, interceptor.onWillAccept(
(interceptor) => interceptor.onWillAccept(
context: builderContext, context: builderContext,
reorderFlexState: this, reorderFlexState: this,
dragTargetData: dragTargetData, dragTargetData: dragTargetData,
dragTargetId: reorderFlexItem.id, dragTargetId: reorderFlexItem.id,
dragTargetIndex: dragTargetIndex, dragTargetIndex: dragTargetIndex,
), );
)) { })) {
return true; return true;
} else { } else {
return handleOnWillAccept(builderContext, dragTargetIndex); return handleOnWillAccept(builderContext, dragTargetIndex);
@ -435,7 +437,7 @@ class ReorderFlexState extends State<ReorderFlex>
/// The [willAccept] will be true if the dargTarget is the widget that gets /// The [willAccept] will be true if the dargTarget is the widget that gets
/// dragged and it is dragged on top of the other dragTargets. /// dragged and it is dragged on top of the other dragTargets.
/// ///
Log.debug( Log.trace(
'[$ReorderDragTarget] ${widget.dataSource.identifier} on will accept, dragIndex:$dragIndex, dragTargetIndex:$dragTargetIndex, count: ${widget.dataSource.items.length}'); '[$ReorderDragTarget] ${widget.dataSource.identifier} on will accept, dragIndex:$dragIndex, dragTargetIndex:$dragTargetIndex, count: ${widget.dataSource.items.length}');
bool willAccept = bool willAccept =
@ -524,7 +526,7 @@ class ReorderFlexState extends State<ReorderFlex>
// screen, then it is already on-screen. // screen, then it is already on-screen.
final double margin = widget.direction == Axis.horizontal final double margin = widget.direction == Axis.horizontal
? dragState.dropAreaSize.width ? dragState.dropAreaSize.width
: dragState.dropAreaSize.height; : dragState.dropAreaSize.height / 2.0;
if (_scrollController.hasClients) { if (_scrollController.hasClients) {
final double scrollOffset = _scrollController.offset; final double scrollOffset = _scrollController.offset;
final double topOffset = max( final double topOffset = max(

View File

@ -59,12 +59,13 @@ class BoardPhantomController extends OverlapDragTargetDelegate
} }
void columnStartDragging(String columnId) { void columnStartDragging(String columnId) {
columnsState.setColumnIsDragging(columnId, false); columnsState.setColumnIsDragging(columnId, true);
} }
/// Remove the phantom in the column when the column is end dragging. /// Remove the phantom in the column when the column is end dragging.
void columnEndDragging(String columnId) { void columnEndDragging(String columnId) {
columnsState.setColumnIsDragging(columnId, true); columnsState.setColumnIsDragging(columnId, false);
if (phantomRecord == null) return; if (phantomRecord == null) return;
final fromColumnId = phantomRecord!.fromColumnId; final fromColumnId = phantomRecord!.fromColumnId;
@ -73,10 +74,7 @@ class BoardPhantomController extends OverlapDragTargetDelegate
columnsState.notifyDidRemovePhantom(toColumnId); columnsState.notifyDidRemovePhantom(toColumnId);
} }
if (columnsState.isDragging(fromColumnId) == false) { if (phantomRecord!.toColumnId == columnId) {
return;
}
delegate.swapColumnItem( delegate.swapColumnItem(
fromColumnId, fromColumnId,
phantomRecord!.fromColumnIndex, phantomRecord!.fromColumnIndex,
@ -84,9 +82,11 @@ class BoardPhantomController extends OverlapDragTargetDelegate
phantomRecord!.toColumnIndex, phantomRecord!.toColumnIndex,
); );
Log.debug("[$BoardPhantomController] did move ${phantomRecord.toString()}"); Log.debug(
"[$BoardPhantomController] did move ${phantomRecord.toString()}");
phantomRecord = null; phantomRecord = null;
} }
}
/// Remove the phantom in the column if it contains phantom /// Remove the phantom in the column if it contains phantom
void _removePhantom(String columnId) { void _removePhantom(String columnId) {
@ -113,7 +113,7 @@ class BoardPhantomController extends OverlapDragTargetDelegate
PhantomColumnItem(phantomContext), PhantomColumnItem(phantomContext),
); );
columnsState.notifyDidInsertPhantom(toColumnId); columnsState.notifyDidInsertPhantom(toColumnId, phantomIndex);
} }
/// Reset or initial the [PhantomRecord] /// Reset or initial the [PhantomRecord]
@ -128,8 +128,9 @@ class BoardPhantomController extends OverlapDragTargetDelegate
FlexDragTargetData dragTargetData, FlexDragTargetData dragTargetData,
int dragTargetIndex, int dragTargetIndex,
) { ) {
// Log.debug('[$BoardPhantomController] move Column:[${dragTargetData.reorderFlexId}]:${dragTargetData.draggingIndex} ' // Log.debug(
// 'to Column:[$columnId]:$index'); // '[$BoardPhantomController] move Column:[${dragTargetData.reorderFlexId}]:${dragTargetData.draggingIndex} '
// 'to Column:[$columnId]:$dragTargetIndex');
phantomRecord = PhantomRecord( phantomRecord = PhantomRecord(
toColumnId: columnId, toColumnId: columnId,
@ -202,8 +203,17 @@ class BoardPhantomController extends OverlapDragTargetDelegate
} }
@override @override
bool canMoveTo(String dragTargetId) { int canMoveTo(String dragTargetId) {
return delegate.controller(dragTargetId)?.columnData.items.isEmpty ?? false; if (columnsState.isDragging(dragTargetId)) {
return -1;
}
final controller = delegate.controller(dragTargetId);
if (controller != null) {
return controller.columnData.items.length;
} else {
return 0;
}
} }
} }
@ -294,7 +304,7 @@ class PassthroughPhantomContext extends FakeDragTargetEventTrigger
AFColumnItem get itemData => dragTargetData.reorderFlexItem as AFColumnItem; AFColumnItem get itemData => dragTargetData.reorderFlexItem as AFColumnItem;
@override @override
VoidCallback? onInserted; void Function(int?)? onInserted;
@override @override
VoidCallback? onDragEnded; VoidCallback? onDragEnded;
@ -308,6 +318,11 @@ class PassthroughPhantomContext extends FakeDragTargetEventTrigger
void fakeOnDragEnded(VoidCallback callback) { void fakeOnDragEnded(VoidCallback callback) {
onDragEnded = callback; onDragEnded = callback;
} }
@override
void fakeOnDragStart(void Function(int? index) callback) {
onInserted = callback;
}
} }
class PassthroughPhantomWidget extends PhantomWidget { class PassthroughPhantomWidget extends PhantomWidget {

View File

@ -14,7 +14,7 @@ class ColumnPhantomStateController {
void addColumnListener(String columnId, PassthroughPhantomListener listener) { void addColumnListener(String columnId, PassthroughPhantomListener listener) {
_stateWithId(columnId).notifier.addListener( _stateWithId(columnId).notifier.addListener(
onInserted: (c) => listener.onInserted?.call(), onInserted: (index) => listener.onInserted?.call(index),
onDeleted: () => listener.onDragEnded?.call(), onDeleted: () => listener.onDragEnded?.call(),
); );
} }
@ -24,8 +24,8 @@ class ColumnPhantomStateController {
_states.remove(columnId); _states.remove(columnId);
} }
void notifyDidInsertPhantom(String columnId) { void notifyDidInsertPhantom(String columnId, int index) {
_stateWithId(columnId).notifier.insert(); _stateWithId(columnId).notifier.insert(index);
} }
void notifyDidRemovePhantom(String columnId) { void notifyDidRemovePhantom(String columnId) {
@ -48,7 +48,7 @@ class ColumnState {
} }
abstract class PassthroughPhantomListener { abstract class PassthroughPhantomListener {
VoidCallback? get onInserted; void Function(int?)? get onInserted;
VoidCallback? get onDragEnded; VoidCallback? get onDragEnded;
} }
@ -57,8 +57,8 @@ class PassthroughPhantomNotifier {
final removeNotifier = PhantomDeleteNotifier(); final removeNotifier = PhantomDeleteNotifier();
void insert() { void insert(int index) {
insertNotifier.insert(); insertNotifier.insert(index);
} }
void remove() { void remove() {
@ -66,12 +66,12 @@ class PassthroughPhantomNotifier {
} }
void addListener({ void addListener({
void Function(PassthroughPhantomContext? insertedPhantom)? onInserted, void Function(int? insertedIndex)? onInserted,
void Function()? onDeleted, void Function()? onDeleted,
}) { }) {
if (onInserted != null) { if (onInserted != null) {
insertNotifier.addListener(() { insertNotifier.addListener(() {
onInserted(insertNotifier.insertedPhantom); onInserted(insertNotifier.insertedIndex);
}); });
} }
@ -89,9 +89,11 @@ class PassthroughPhantomNotifier {
} }
class PhantomInsertNotifier extends ChangeNotifier { class PhantomInsertNotifier extends ChangeNotifier {
int insertedIndex = -1;
PassthroughPhantomContext? insertedPhantom; PassthroughPhantomContext? insertedPhantom;
void insert() { void insert(int index) {
insertedIndex = index;
notifyListeners(); notifyListeners();
} }
} }

View File

@ -1,6 +1,6 @@
name: appflowy_board name: appflowy_board
description: AppFlowy board implementation. description: AppFlowy board implementation.
version: 0.0.4 version: 0.0.5
homepage: https://github.com/AppFlowy-IO/AppFlowy homepage: https://github.com/AppFlowy-IO/AppFlowy
repository: https://github.com/AppFlowy-IO/AppFlowy/tree/main/frontend/app_flowy/packages/appflowy_board repository: https://github.com/AppFlowy-IO/AppFlowy/tree/main/frontend/app_flowy/packages/appflowy_board