Pārlūkot izejas kodu

chore: config board ui

appflowy 2 gadi atpakaļ
vecāks
revīzija
a4b4b20cfc
25 mainītis faili ar 2450 papildinājumiem un 99 dzēšanām
  1. 37 0
      frontend/app_flowy/packages/flowy_board/example/.vscode/launch.json
  2. 32 36
      frontend/app_flowy/packages/flowy_board/example/lib/main.dart
  3. 78 0
      frontend/app_flowy/packages/flowy_board/example/lib/multi_board_list_example.dart
  4. 60 0
      frontend/app_flowy/packages/flowy_board/example/lib/single_board_list_example.dart
  5. 22 0
      frontend/app_flowy/packages/flowy_board/example/macos/Podfile.lock
  6. 61 1
      frontend/app_flowy/packages/flowy_board/example/macos/Runner.xcodeproj/project.pbxproj
  7. 3 0
      frontend/app_flowy/packages/flowy_board/example/macos/Runner.xcworkspace/contents.xcworkspacedata
  8. 1 1
      frontend/app_flowy/packages/flowy_board/example/pubspec.yaml
  9. 4 7
      frontend/app_flowy/packages/flowy_board/lib/flowy_board.dart
  10. 0 0
      frontend/app_flowy/packages/flowy_board/lib/src/rendering/board_overlay.dart
  11. 28 0
      frontend/app_flowy/packages/flowy_board/lib/src/utils/log.dart
  12. 149 0
      frontend/app_flowy/packages/flowy_board/lib/src/widgets/board.dart
  13. 142 0
      frontend/app_flowy/packages/flowy_board/lib/src/widgets/board_column/board_column.dart
  14. 85 0
      frontend/app_flowy/packages/flowy_board/lib/src/widgets/board_column/data_controller.dart
  15. 91 0
      frontend/app_flowy/packages/flowy_board/lib/src/widgets/column_container.dart
  16. 171 0
      frontend/app_flowy/packages/flowy_board/lib/src/widgets/flex/drag_state.dart
  17. 369 0
      frontend/app_flowy/packages/flowy_board/lib/src/widgets/flex/drag_target.dart
  18. 524 0
      frontend/app_flowy/packages/flowy_board/lib/src/widgets/flex/reorder_flex.dart
  19. 78 0
      frontend/app_flowy/packages/flowy_board/lib/src/widgets/flex/reorder_flex_ext.dart
  20. 65 0
      frontend/app_flowy/packages/flowy_board/lib/src/widgets/flex/reorder_mixin.dart
  21. 338 0
      frontend/app_flowy/packages/flowy_board/lib/src/widgets/phantom/phantom_controller.dart
  22. 109 0
      frontend/app_flowy/packages/flowy_board/lib/src/widgets/phantom/phantom_state.dart
  23. 3 1
      frontend/app_flowy/packages/flowy_board/pubspec.yaml
  24. 0 24
      frontend/app_flowy/packages/flowy_board/test/flowy_board_method_channel_test.dart
  25. 0 29
      frontend/app_flowy/packages/flowy_board/test/flowy_board_test.dart

+ 37 - 0
frontend/app_flowy/packages/flowy_board/example/.vscode/launch.json

@@ -0,0 +1,37 @@
+{
+    // Use IntelliSense to learn about possible attributes.
+    // Hover to view descriptions of existing attributes.
+    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+    "version": "0.2.0",
+    "configurations": [
+        {
+            "name": "example",
+            "cwd": "example",
+            "request": "launch",
+            "env": {
+                "Dart_LOG": "true"
+            },
+            "type": "dart"
+        },
+        {
+            "name": "example (profile mode)",
+            "cwd": "example",
+            "request": "launch",
+            "type": "dart",
+            "env": {
+                "Dart_LOG": "true"
+            },
+            "flutterMode": "profile"
+        },
+        {
+            "name": "example (release mode)",
+            "cwd": "example",
+            "request": "launch",
+            "type": "dart",
+            "env": {
+                "Dart_LOG": "true"
+            },
+            "flutterMode": "release"
+        }
+    ]
+}

+ 32 - 36
frontend/app_flowy/packages/flowy_board/example/lib/main.dart

@@ -1,8 +1,6 @@
 import 'package:flutter/material.dart';
-import 'dart:async';
-
-import 'package:flutter/services.dart';
-import 'package:flowy_board/flowy_board.dart';
+import 'single_board_list_example.dart';
+import 'multi_board_list_example.dart';
 
 void main() {
   runApp(const MyApp());
@@ -16,48 +14,46 @@ class MyApp extends StatefulWidget {
 }
 
 class _MyAppState extends State<MyApp> {
-  String _platformVersion = 'Unknown';
-  final _flowyBoardPlugin = FlowyBoard();
+  int _currentIndex = 0;
+  final _bottomNavigationColor = Colors.blue;
+
+  final List<Widget> _examples = [
+    const MultiBoardListExample(),
+    const SingleBoardListExample(),
+  ];
 
   @override
   void initState() {
     super.initState();
-    initPlatformState();
-  }
-
-  // Platform messages are asynchronous, so we initialize in an async method.
-  Future<void> initPlatformState() async {
-    String platformVersion;
-    // Platform messages may fail, so we use a try/catch PlatformException.
-    // We also handle the message potentially returning null.
-    try {
-      platformVersion =
-          await _flowyBoardPlugin.getPlatformVersion() ?? 'Unknown platform version';
-    } on PlatformException {
-      platformVersion = 'Failed to get platform version.';
-    }
-
-    // If the widget was removed from the tree while the asynchronous platform
-    // message was in flight, we want to discard the reply rather than calling
-    // setState to update our non-existent appearance.
-    if (!mounted) return;
-
-    setState(() {
-      _platformVersion = platformVersion;
-    });
   }
 
   @override
   Widget build(BuildContext context) {
     return MaterialApp(
       home: Scaffold(
-        appBar: AppBar(
-          title: const Text('Plugin example app'),
-        ),
-        body: Center(
-          child: Text('Running on: $_platformVersion\n'),
-        ),
-      ),
+          appBar: AppBar(
+            title: const Text('FlowyBoard example'),
+          ),
+          body: _examples[_currentIndex],
+          bottomNavigationBar: BottomNavigationBar(
+            fixedColor: _bottomNavigationColor,
+            showSelectedLabels: true,
+            showUnselectedLabels: false,
+            currentIndex: _currentIndex,
+            items: [
+              BottomNavigationBarItem(
+                  icon: Icon(Icons.grid_on, color: _bottomNavigationColor),
+                  label: "MultiBoardList"),
+              BottomNavigationBarItem(
+                  icon: Icon(Icons.grid_on, color: _bottomNavigationColor),
+                  label: "SingleBoardList"),
+            ],
+            onTap: (int index) {
+              setState(() {
+                _currentIndex = index;
+              });
+            },
+          )),
     );
   }
 }

+ 78 - 0
frontend/app_flowy/packages/flowy_board/example/lib/multi_board_list_example.dart

@@ -0,0 +1,78 @@
+import 'package:flowy_board/flowy_board.dart';
+import 'package:flutter/material.dart';
+
+class MultiBoardListExample extends StatefulWidget {
+  const MultiBoardListExample({Key? key}) : super(key: key);
+
+  @override
+  State<MultiBoardListExample> createState() => _MultiBoardListExampleState();
+}
+
+class _MultiBoardListExampleState extends State<MultiBoardListExample> {
+  final BoardDataController boardData = BoardDataController();
+
+  @override
+  void initState() {
+    final column1 = BoardColumnData(id: "1", items: [
+      TextItem("a"),
+      TextItem("b"),
+      TextItem("c"),
+      TextItem("d"),
+    ]);
+    final column2 = BoardColumnData(id: "2", items: [
+      TextItem("1"),
+      TextItem("2"),
+      TextItem("3"),
+      TextItem("4"),
+      TextItem("5"),
+    ]);
+
+    final column3 = BoardColumnData(id: "3", items: [
+      TextItem("A"),
+      TextItem("B"),
+      TextItem("C"),
+      TextItem("D"),
+    ]);
+
+    boardData.setColumnData(column1);
+    boardData.setColumnData(column2);
+    boardData.setColumnData(column3);
+
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Board(
+      dataController: boardData,
+      background: Container(color: Colors.red),
+      builder: (context, item) {
+        return _RowWidget(item: item as TextItem, key: ObjectKey(item));
+      },
+    );
+  }
+}
+
+class _RowWidget extends StatelessWidget {
+  final TextItem item;
+  const _RowWidget({Key? key, required this.item}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      key: ObjectKey(item),
+      height: 60,
+      color: Colors.green,
+      child: Center(child: Text(item.s)),
+    );
+  }
+}
+
+class TextItem extends ColumnItem {
+  final String s;
+
+  TextItem(this.s);
+
+  @override
+  String get id => s;
+}

+ 60 - 0
frontend/app_flowy/packages/flowy_board/example/lib/single_board_list_example.dart

@@ -0,0 +1,60 @@
+import 'package:flutter/material.dart';
+import 'package:flowy_board/flowy_board.dart';
+
+class SingleBoardListExample extends StatefulWidget {
+  const SingleBoardListExample({Key? key}) : super(key: key);
+
+  @override
+  State<SingleBoardListExample> createState() => _SingleBoardListExampleState();
+}
+
+class _SingleBoardListExampleState extends State<SingleBoardListExample> {
+  final BoardDataController boardData = BoardDataController();
+
+  @override
+  void initState() {
+    final column = BoardColumnData(id: "1", items: [
+      TextItem("a"),
+      TextItem("b"),
+      TextItem("c"),
+      TextItem("d"),
+    ]);
+
+    boardData.setColumnData(column);
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Board(
+      dataController: boardData,
+      builder: (context, item) {
+        return _RowWidget(item: item as TextItem, key: ObjectKey(item));
+      },
+    );
+  }
+}
+
+class _RowWidget extends StatelessWidget {
+  final TextItem item;
+  const _RowWidget({Key? key, required this.item}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      key: ObjectKey(item),
+      height: 60,
+      color: Colors.green,
+      child: Center(child: Text(item.s)),
+    );
+  }
+}
+
+class TextItem extends ColumnItem {
+  final String s;
+
+  TextItem(this.s);
+
+  @override
+  String get id => throw UnimplementedError();
+}

+ 22 - 0
frontend/app_flowy/packages/flowy_board/example/macos/Podfile.lock

@@ -0,0 +1,22 @@
+PODS:
+  - flowy_board (0.0.1):
+    - FlutterMacOS
+  - FlutterMacOS (1.0.0)
+
+DEPENDENCIES:
+  - flowy_board (from `Flutter/ephemeral/.symlinks/plugins/flowy_board/macos`)
+  - FlutterMacOS (from `Flutter/ephemeral`)
+
+EXTERNAL SOURCES:
+  flowy_board:
+    :path: Flutter/ephemeral/.symlinks/plugins/flowy_board/macos
+  FlutterMacOS:
+    :path: Flutter/ephemeral
+
+SPEC CHECKSUMS:
+  flowy_board: e93adfa305df65f1ac860f2cf9dc7188f50c9b66
+  FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
+
+PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c
+
+COCOAPODS: 1.11.3

+ 61 - 1
frontend/app_flowy/packages/flowy_board/example/macos/Runner.xcodeproj/project.pbxproj

@@ -21,6 +21,7 @@
 /* End PBXAggregateTarget section */
 
 /* Begin PBXBuildFile section */
+		2AD48017E3EAE6142B6E265B /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 368626E4047E7783820AEC34 /* Pods_Runner.framework */; };
 		335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
 		33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
 		33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
@@ -52,9 +53,10 @@
 /* End PBXCopyFilesBuildPhase section */
 
 /* Begin PBXFileReference section */
+		02BDE4CD9C63CA2562B2FDD1 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
 		333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
 		335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
-		33CC10ED2044A3C60003C045 /* flowy_board_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "flowy_board_example.app"; sourceTree = BUILT_PRODUCTS_DIR; };
+		33CC10ED2044A3C60003C045 /* flowy_board_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = flowy_board_example.app; sourceTree = BUILT_PRODUCTS_DIR; };
 		33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
 		33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
 		33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
@@ -66,8 +68,11 @@
 		33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
 		33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
 		33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
+		368626E4047E7783820AEC34 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		59D896B3478D0A2144E570BB /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
 		7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
 		9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
+		F368AC3EE3CE4F2FCEC85166 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -75,6 +80,7 @@
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				2AD48017E3EAE6142B6E265B /* Pods_Runner.framework in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -99,6 +105,7 @@
 				33CEB47122A05771004F2AC0 /* Flutter */,
 				33CC10EE2044A3C60003C045 /* Products */,
 				D73912EC22F37F3D000D13A0 /* Frameworks */,
+				6BBA375F5E43A645EC061EA0 /* Pods */,
 			);
 			sourceTree = "<group>";
 		};
@@ -145,9 +152,21 @@
 			path = Runner;
 			sourceTree = "<group>";
 		};
+		6BBA375F5E43A645EC061EA0 /* Pods */ = {
+			isa = PBXGroup;
+			children = (
+				F368AC3EE3CE4F2FCEC85166 /* Pods-Runner.debug.xcconfig */,
+				02BDE4CD9C63CA2562B2FDD1 /* Pods-Runner.release.xcconfig */,
+				59D896B3478D0A2144E570BB /* Pods-Runner.profile.xcconfig */,
+			);
+			name = Pods;
+			path = Pods;
+			sourceTree = "<group>";
+		};
 		D73912EC22F37F3D000D13A0 /* Frameworks */ = {
 			isa = PBXGroup;
 			children = (
+				368626E4047E7783820AEC34 /* Pods_Runner.framework */,
 			);
 			name = Frameworks;
 			sourceTree = "<group>";
@@ -159,11 +178,13 @@
 			isa = PBXNativeTarget;
 			buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
 			buildPhases = (
+				DF704441B638BDD90A6EF3BC /* [CP] Check Pods Manifest.lock */,
 				33CC10E92044A3C60003C045 /* Sources */,
 				33CC10EA2044A3C60003C045 /* Frameworks */,
 				33CC10EB2044A3C60003C045 /* Resources */,
 				33CC110E2044A8840003C045 /* Bundle Framework */,
 				3399D490228B24CF009A79C7 /* ShellScript */,
+				B89DDA2E6C13BED8F107400D /* [CP] Embed Pods Frameworks */,
 			);
 			buildRules = (
 			);
@@ -270,6 +291,45 @@
 			shellPath = /bin/sh;
 			shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
 		};
+		B89DDA2E6C13BED8F107400D /* [CP] Embed Pods Frameworks */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputFileListPaths = (
+				"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
+			);
+			name = "[CP] Embed Pods Frameworks";
+			outputFileListPaths = (
+				"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
+			showEnvVarsInLog = 0;
+		};
+		DF704441B638BDD90A6EF3BC /* [CP] Check Pods Manifest.lock */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputFileListPaths = (
+			);
+			inputPaths = (
+				"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+				"${PODS_ROOT}/Manifest.lock",
+			);
+			name = "[CP] Check Pods Manifest.lock";
+			outputFileListPaths = (
+			);
+			outputPaths = (
+				"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n    # print error to STDERR\n    echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n    exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+			showEnvVarsInLog = 0;
+		};
 /* End PBXShellScriptBuildPhase section */
 
 /* Begin PBXSourcesBuildPhase section */

+ 3 - 0
frontend/app_flowy/packages/flowy_board/example/macos/Runner.xcworkspace/contents.xcworkspacedata

@@ -4,4 +4,7 @@
    <FileRef
       location = "group:Runner.xcodeproj">
    </FileRef>
+   <FileRef
+      location = "group:Pods/Pods.xcodeproj">
+   </FileRef>
 </Workspace>

+ 1 - 1
frontend/app_flowy/packages/flowy_board/example/pubspec.yaml

@@ -6,7 +6,7 @@ description: Demonstrates how to use the flowy_board plugin.
 publish_to: 'none' # Remove this line if you wish to publish to pub.dev
 
 environment:
-  sdk: ">=2.17.6 <3.0.0"
+  sdk: ">=2.17.1 <3.0.0"
 
 # Dependencies specify other packages that your package needs in order to work.
 # To automatically upgrade your package dependencies to the latest versions

+ 4 - 7
frontend/app_flowy/packages/flowy_board/lib/flowy_board.dart

@@ -1,8 +1,5 @@
+library flowy_board;
 
-import 'flowy_board_platform_interface.dart';
-
-class FlowyBoard {
-  Future<String?> getPlatformVersion() {
-    return FlowyBoardPlatform.instance.getPlatformVersion();
-  }
-}
+export 'src/widgets/board_column/board_column.dart';
+export 'src/widgets/board_column/data_controller.dart';
+export 'src/widgets/board.dart';

+ 0 - 0
frontend/app_flowy/packages/flowy_board/lib/src/widgets/board_overlay.dart → frontend/app_flowy/packages/flowy_board/lib/src/rendering/board_overlay.dart


+ 28 - 0
frontend/app_flowy/packages/flowy_board/lib/src/utils/log.dart

@@ -0,0 +1,28 @@
+import 'package:flutter/material.dart';
+
+// ignore: constant_identifier_names
+const DART_LOG = "Dart_LOG";
+
+class Log {
+  // static const enableLog = bool.hasEnvironment(DART_LOG);
+  // static final shared = Log();
+  static const enableLog = true;
+
+  static void info(String? message) {
+    if (enableLog) {
+      debugPrint('ℹ️[Info]=> $message');
+    }
+  }
+
+  static void debug(String? message) {
+    if (enableLog) {
+      debugPrint('🐛[Debug]=> $message');
+    }
+  }
+
+  static void trace(String? message) {
+    if (enableLog) {
+      // debugPrint('❗️[Trace]=> $message');
+    }
+  }
+}

+ 149 - 0
frontend/app_flowy/packages/flowy_board/lib/src/widgets/board.dart

@@ -0,0 +1,149 @@
+import 'dart:collection';
+import 'package:equatable/equatable.dart';
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import '../../flowy_board.dart';
+
+import 'column_container.dart';
+import 'flex/reorder_flex.dart';
+import 'phantom/phantom_controller.dart';
+
+class Board extends StatelessWidget {
+  /// The direction to use as the main axis.
+  final Axis direction = Axis.vertical;
+
+  /// How much space to place between children in a run in the main axis.
+  /// Defaults to 10.0.
+  final double spacing;
+
+  /// How much space to place between the runs themselves in the cross axis.
+  /// Defaults to 0.0.
+  final double runSpacing;
+
+  final Widget? background;
+
+  final BoardColumnItemWidgetBuilder builder;
+
+  ///
+  final BoardDataController dataController;
+
+  ///
+  final BoardPhantomController passthroughPhantomContorller;
+
+  Board({
+    required this.dataController,
+    required this.builder,
+    this.spacing = 10.0,
+    this.runSpacing = 0.0,
+    this.background,
+    Key? key,
+  })  : passthroughPhantomContorller =
+            BoardPhantomController(delegate: dataController),
+        super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return ChangeNotifierProvider.value(
+      value: dataController,
+      child: Consumer(
+        builder: (context, notifier, child) {
+          List<Widget> children = [];
+          List<String> acceptColumns =
+              dataController.columnControllers.keys.toList();
+
+          dataController.columnControllers.forEach((columnId, dataController) {
+            Widget child =
+                buildBoardColumn(columnId, acceptColumns, dataController);
+            if (children.isEmpty) {
+              // children.add(SizedBox(width: spacing));
+            }
+            // if (background != null) {
+            //   child = Stack(children: [
+            //     background!,
+            //     child,
+            //   ]);
+            // }
+            // children.add(Expanded(key: ValueKey(columnId), child: child));
+            children.add(child);
+            // children.add(SizedBox(width: spacing));
+          });
+
+          return BoardColumnContainer(
+            onReorder: (fromIndex, toIndex) {},
+            boardDataController: dataController,
+            children: children,
+          );
+
+          // return Row(
+          //   crossAxisAlignment: CrossAxisAlignment.start,
+          //   mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+          //   children: children,
+          // );
+        },
+      ),
+    );
+  }
+
+  ///
+  Widget buildBoardColumn(
+    String columnId,
+    List<String> acceptColumns,
+    BoardColumnDataController dataController,
+  ) {
+    return ChangeNotifierProvider.value(
+      key: ValueKey(columnId),
+      value: dataController,
+      child: Consumer<BoardColumnDataController>(
+        builder: (context, value, child) {
+          return SizedBox(
+            width: 200,
+            child: BoardColumnWidget(
+              header: Container(color: Colors.yellow, height: 30),
+              builder: builder,
+              acceptColumns: acceptColumns,
+              dataController: dataController,
+              scrollController: ScrollController(),
+              onReorder: (_, int fromIndex, int toIndex) {
+                dataController.move(fromIndex, toIndex);
+              },
+              phantomController: passthroughPhantomContorller,
+            ),
+          );
+        },
+      ),
+    );
+  }
+}
+
+class BoardDataController extends ChangeNotifier
+    with EquatableMixin, BoardPhantomControllerDelegate, ReoderFlextDataSource {
+  final LinkedHashMap<String, BoardColumnData> columnDatas = LinkedHashMap();
+  final LinkedHashMap<String, BoardColumnDataController> columnControllers =
+      LinkedHashMap();
+
+  BoardDataController();
+
+  void setColumnData(BoardColumnData columnData) {
+    final controller = BoardColumnDataController(columnData: columnData);
+    columnDatas[columnData.id] = columnData;
+    columnControllers[columnData.id] = controller;
+  }
+
+  @override
+  List<Object?> get props {
+    return [columnDatas.values];
+  }
+
+  @override
+  BoardColumnDataController? controller(String columnId) {
+    return columnControllers[columnId];
+  }
+
+  @override
+  String get identifier => '$BoardDataController';
+
+  @override
+  List<ReoderFlextItem> get items => columnDatas.values.toList();
+}
+
+class BoardDataIdentifier {}

+ 142 - 0
frontend/app_flowy/packages/flowy_board/lib/src/widgets/board_column/board_column.dart

@@ -0,0 +1,142 @@
+import 'package:flutter/material.dart';
+
+import '../../rendering/board_overlay.dart';
+import '../../utils/log.dart';
+import '../phantom/phantom_controller.dart';
+import '../flex/reorder_flex.dart';
+import '../flex/drag_state.dart';
+import '../flex/reorder_flex_ext.dart';
+import 'data_controller.dart';
+
+typedef OnDragStarted = void Function(int index);
+typedef OnDragEnded = void Function(String listId);
+typedef OnReorder = void Function(String listId, int fromIndex, int toIndex);
+typedef OnDeleted = void Function(String listId, int deletedIndex);
+typedef OnInserted = void Function(String listId, int insertedIndex);
+typedef OnPassedInPhantom = void Function(
+  String listId,
+  FlexDragTargetData dragTargetData,
+  int phantomIndex,
+);
+
+typedef BoardColumnItemWidgetBuilder = Widget Function(
+    BuildContext context, ColumnItem item);
+
+class BoardColumnWidget extends StatefulWidget {
+  final Widget? header;
+  final Widget? footer;
+  final BoardColumnDataController dataController;
+  final ScrollController? scrollController;
+  final ReorderFlexConfig config;
+
+  final OnDragStarted? onDragStarted;
+  final OnReorder onReorder;
+  final OnDragEnded? onDragEnded;
+
+  final BoardPhantomController phantomController;
+
+  String get columnId => dataController.identifier;
+
+  final List<String> acceptColumns;
+
+  final BoardColumnItemWidgetBuilder builder;
+
+  const BoardColumnWidget({
+    Key? key,
+    this.header,
+    this.footer,
+    required this.builder,
+    required this.onReorder,
+    required this.dataController,
+    required this.phantomController,
+    required this.acceptColumns,
+    this.config = const ReorderFlexConfig(),
+    this.onDragStarted,
+    this.scrollController,
+    this.onDragEnded,
+  }) : super(key: key);
+
+  @override
+  State<BoardColumnWidget> createState() => _BoardColumnWidgetState();
+}
+
+class _BoardColumnWidgetState extends State<BoardColumnWidget> {
+  final GlobalKey _columnOverlayKey =
+      GlobalKey(debugLabel: '$BoardColumnWidget overlay key');
+
+  late BoardOverlayEntry _overlayEntry;
+
+  @override
+  void initState() {
+    _overlayEntry = BoardOverlayEntry(
+        builder: (BuildContext context) {
+          final children = widget.dataController.items
+              .map((item) => _buildWidget(context, item))
+              .toList();
+
+          final dragTargetExtension = ReorderFlextDragTargetExtension(
+            reorderFlexId: widget.columnId,
+            delegate: widget.phantomController,
+            acceptReorderFlexIds: widget.acceptColumns,
+            draggableTargetBuilder: PhantomReorderDraggableBuilder(),
+          );
+
+          return ReorderFlex(
+            key: widget.key,
+            header: widget.header,
+            footer: widget.footer,
+            scrollController: widget.scrollController,
+            config: widget.config,
+            onDragStarted: (index) {
+              widget.phantomController.columnStartDragging(widget.columnId);
+              widget.onDragStarted?.call(index);
+            },
+            onReorder: ((fromIndex, toIndex) {
+              if (widget.phantomController.isFromColumn(widget.columnId)) {
+                widget.onReorder(widget.columnId, fromIndex, toIndex);
+                widget.phantomController.transformIndex(fromIndex, toIndex);
+              }
+            }),
+            onDragEnded: () {
+              widget.phantomController.columnEndDragging(widget.columnId);
+              widget.onDragEnded?.call(widget.columnId);
+              _printItems(widget.dataController);
+            },
+            dataSource: widget.dataController,
+            dragTargetExtension: dragTargetExtension,
+            children: children,
+          );
+        },
+        opaque: false);
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return BoardOverlay(
+      key: _columnOverlayKey,
+      initialEntries: [_overlayEntry],
+    );
+  }
+
+  Widget _buildWidget(BuildContext context, ColumnItem item) {
+    if (item is PhantomColumnItem) {
+      return PassthroughPhantomWidget(
+        key: UniqueKey(),
+        opacity: widget.config.draggingWidgetOpacity,
+        passthroughPhantomContext: item.phantomContext,
+      );
+    } else {
+      return widget.builder(context, item);
+    }
+  }
+}
+
+void _printItems(BoardColumnDataController dataController) {
+  String msg = '';
+  for (var element in dataController.items) {
+    msg = '$msg$element,';
+  }
+
+  Log.debug(msg);
+}

+ 85 - 0
frontend/app_flowy/packages/flowy_board/lib/src/widgets/board_column/data_controller.dart

@@ -0,0 +1,85 @@
+import 'package:equatable/equatable.dart';
+import 'package:flutter/material.dart';
+import '../../utils/log.dart';
+import '../flex/reorder_flex.dart';
+
+abstract class ColumnItem extends ReoderFlextItem {
+  String get id;
+
+  bool get isPhantom => false;
+
+  @override
+  String toString() {
+    return id;
+  }
+}
+
+class BoardColumnData extends ReoderFlextItem with EquatableMixin {
+  final String id;
+  final List<ColumnItem> items;
+
+  BoardColumnData({
+    required this.id,
+    required this.items,
+  });
+
+  @override
+  List<Object?> get props => [id, ...items];
+
+  @override
+  String toString() {
+    return 'Column$id';
+  }
+}
+
+class BoardColumnDataController extends ChangeNotifier
+    with EquatableMixin, ReoderFlextDataSource {
+  final BoardColumnData columnData;
+
+  BoardColumnDataController({
+    required this.columnData,
+  });
+
+  @override
+  List<Object?> get props => columnData.props;
+
+  ColumnItem removeAt(int index) {
+    Log.debug('[$BoardColumnDataController] $columnData remove item at $index');
+    final item = columnData.items.removeAt(index);
+    notifyListeners();
+    return item;
+  }
+
+  void move(int fromIndex, int toIndex) {
+    if (fromIndex == toIndex) {
+      return;
+    }
+    Log.debug(
+        '[$BoardColumnDataController] $columnData move item from $fromIndex to $toIndex');
+    final item = columnData.items.removeAt(fromIndex);
+    columnData.items.insert(toIndex, item);
+    notifyListeners();
+  }
+
+  void insert(int index, ColumnItem item, {bool notify = true}) {
+    Log.debug('[$BoardColumnDataController] $columnData insert item at $index');
+    columnData.items.insert(index, item);
+    if (notify) {
+      notifyListeners();
+    }
+  }
+
+  void replace(int index, ColumnItem item) {
+    Log.debug(
+        '[$BoardColumnDataController] $columnData replace item at $index');
+    columnData.items.removeAt(index);
+    columnData.items.insert(index, item);
+    notifyListeners();
+  }
+
+  @override
+  List<ColumnItem> get items => columnData.items;
+
+  @override
+  String get identifier => columnData.id;
+}

+ 91 - 0
frontend/app_flowy/packages/flowy_board/lib/src/widgets/column_container.dart

@@ -0,0 +1,91 @@
+import 'package:flutter/material.dart';
+
+import '../rendering/board_overlay.dart';
+import 'flex/reorder_flex.dart';
+import 'board.dart';
+
+class BoardColumnContainer extends StatefulWidget {
+  final Widget? header;
+  final Widget? footer;
+  final ScrollController? scrollController;
+  final OnDragStarted? onDragStarted;
+  final OnReorder onReorder;
+  final OnDragEnded? onDragEnded;
+  final BoardDataController boardDataController;
+  final List<Widget> children;
+  final EdgeInsets? padding;
+  final Widget? background;
+  final ReorderFlexConfig config;
+
+  const BoardColumnContainer({
+    required this.boardDataController,
+    required this.onReorder,
+    required this.children,
+    this.onDragStarted,
+    this.onDragEnded,
+    this.header,
+    this.footer,
+    this.scrollController,
+    this.padding,
+    this.background,
+    this.config = const ReorderFlexConfig(),
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<BoardColumnContainer> createState() => _BoardColumnContainerState();
+}
+
+class _BoardColumnContainerState extends State<BoardColumnContainer> {
+  final GlobalKey _columnContainerOverlayKey =
+      GlobalKey(debugLabel: '$BoardColumnContainer overlay key');
+  late BoardOverlayEntry _overlayEntry;
+
+  @override
+  void initState() {
+    _overlayEntry = BoardOverlayEntry(
+        builder: (BuildContext context) {
+          Widget reorderFlex = ReorderFlex(
+            key: widget.key,
+            header: widget.header,
+            footer: widget.footer,
+            scrollController: widget.scrollController,
+            config: widget.config,
+            onDragStarted: (index) {},
+            onReorder: ((fromIndex, toIndex) {}),
+            onDragEnded: () {},
+            dataSource: widget.boardDataController,
+            direction: Axis.horizontal,
+            children: widget.children,
+          );
+
+          if (widget.padding != null) {
+            reorderFlex = Padding(
+              padding: widget.padding!,
+              child: reorderFlex,
+            );
+          }
+
+          return Expanded(
+              child: Stack(
+            alignment: AlignmentDirectional.center,
+            children: [
+              Container(
+                color: Colors.red,
+              ),
+              reorderFlex
+            ],
+          ));
+        },
+        opaque: false);
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return BoardOverlay(
+      key: _columnContainerOverlayKey,
+      initialEntries: [_overlayEntry],
+    );
+  }
+}

+ 171 - 0
frontend/app_flowy/packages/flowy_board/lib/src/widgets/flex/drag_state.dart

@@ -0,0 +1,171 @@
+import 'package:flutter/material.dart';
+
+import '../../utils/log.dart';
+import 'drag_target.dart';
+import 'reorder_flex.dart';
+
+/// [FlexDragTargetData] is used to store the custom dragging data. It can be used to
+/// locate the index of the dragging widget in the [BoardList].
+class FlexDragTargetData extends DragTargetData {
+  /// The index of the dragging target in the boardList.
+  @override
+  final int draggingIndex;
+
+  final DraggingState state;
+
+  Widget? get draggingWidget => state.draggingWidget;
+
+  Size? get draggingFeedbackSize => state.feedbackSize;
+
+  /// Indicate the dargging come from which [ReorderFlex].
+  final DraggingReorderFlex draggingReorderFlex;
+
+  ReoderFlextItem get columnItem =>
+      draggingReorderFlex.itemAtIndex(draggingIndex);
+
+  String get reorderFlexId => draggingReorderFlex.id;
+
+  FlexDragTargetData({
+    required this.draggingIndex,
+    required this.state,
+    required this.draggingReorderFlex,
+  });
+}
+
+abstract class DraggingReorderFlex {
+  String get id;
+  ReoderFlextItem itemAtIndex(int index);
+}
+
+class DraggingState {
+  final String id;
+
+  /// The member of widget.children currently being dragged.
+  ///
+  /// Null if no drag is underway.
+  Widget? _draggingWidget;
+
+  Widget? get draggingWidget => _draggingWidget;
+
+  /// The last computed size of the feedback widget being dragged.
+  Size? _draggingFeedbackSize = Size.zero;
+
+  Size? get feedbackSize => _draggingFeedbackSize;
+
+  /// The location that the dragging widget occupied before it started to drag.
+  int dragStartIndex = -1;
+
+  /// The index that the dragging widget most recently left.
+  /// This is used to show an animation of the widget's position.
+  int phantomIndex = -1;
+
+  /// The index that the dragging widget currently occupies.
+  int currentIndex = -1;
+
+  /// The widget to move the dragging widget too after the current index.
+  int nextIndex = 0;
+
+  /// Whether or not we are currently scrolling this view to show a widget.
+  bool scrolling = false;
+
+  /// The additional margin to place around a computed drop area.
+  static const double _dropAreaMargin = 0.0;
+
+  DraggingState(this.id);
+
+  Size get dropAreaSize {
+    if (_draggingFeedbackSize == null) {
+      return Size.zero;
+    }
+    return _draggingFeedbackSize! +
+        const Offset(_dropAreaMargin, _dropAreaMargin);
+  }
+
+  void startDragging(Widget draggingWidget, int draggingWidgetIndex,
+      Size? draggingWidgetSize) {
+    ///
+    assert(draggingWidgetIndex >= 0);
+
+    _draggingWidget = draggingWidget;
+    phantomIndex = draggingWidgetIndex;
+    dragStartIndex = draggingWidgetIndex;
+    currentIndex = draggingWidgetIndex;
+    _draggingFeedbackSize = draggingWidgetSize;
+  }
+
+  void endDragging() {
+    dragStartIndex = -1;
+    phantomIndex = -1;
+    currentIndex = -1;
+    _draggingWidget = null;
+  }
+
+  /// When the phantomIndex and currentIndex are the same, it means the dragging
+  /// widget did move to the destination location.
+  void removePhantom() {
+    phantomIndex = currentIndex;
+  }
+
+  /// The dragging widget overlaps with the phantom widget.
+  bool isOverlapWithPhantom() {
+    return currentIndex != phantomIndex;
+  }
+
+  bool isPhantomAboveDragTarget() {
+    return currentIndex > phantomIndex;
+  }
+
+  bool isPhantomBelowDragTarget() {
+    return currentIndex < phantomIndex;
+  }
+
+  bool didDragTargetMoveToNext() {
+    return currentIndex == nextIndex;
+  }
+
+  /// Set the currentIndex to nextIndex
+  void moveDragTargetToNext() {
+    Log.trace('moveDragTargetToNext: $nextIndex');
+    currentIndex = nextIndex;
+  }
+
+  void updateNextIndex(int index) {
+    assert(index >= 0);
+    Log.trace('updateNextIndex: $index');
+    nextIndex = index;
+  }
+
+  bool isNotDragging() {
+    return dragStartIndex == -1;
+  }
+
+  bool isDragging() {
+    return !isNotDragging();
+  }
+
+  /// When the _dragStartIndex less than the _currentIndex, it means the
+  /// dragTarget is going down to the end of the list.
+  bool isDragTargetMovingDown() {
+    return dragStartIndex < currentIndex;
+  }
+
+  /// The index represents the widget original index of the list.
+  int calculateShiftedIndex(int index) {
+    int shiftedIndex = index;
+    if (index == dragStartIndex) {
+      shiftedIndex = phantomIndex;
+    } else if (index > dragStartIndex && index <= phantomIndex) {
+      /// phantom move up
+      shiftedIndex--;
+    } else if (index < dragStartIndex && index >= phantomIndex) {
+      /// phantom move down
+      shiftedIndex++;
+    }
+    return shiftedIndex;
+  }
+
+  @override
+  String toString() {
+    return 'DragStartIndex: $dragStartIndex, PhantomIndex: $phantomIndex, CurrentIndex: $currentIndex, NextIndex: $nextIndex';
+  }
+}

+ 369 - 0
frontend/app_flowy/packages/flowy_board/lib/src/widgets/flex/drag_target.dart

@@ -0,0 +1,369 @@
+import 'package:flutter/material.dart';
+
+abstract class DragTargetData {
+  int get draggingIndex;
+}
+
+abstract class ReorderDraggableTargetBuilder {
+  Widget? build<T extends DragTargetData>(
+      BuildContext context,
+      Widget child,
+      DragTargetOnStarted onDragStarted,
+      DragTargetOnEnded<T> onDragEnded,
+      DragTargetWillAccpet<T> onWillAccept);
+}
+
+typedef DragTargetWillAccpet<T extends DragTargetData> = bool Function(
+    T dragTargetData);
+typedef DragTargetOnStarted = void Function(Widget, int, Size?);
+typedef DragTargetOnEnded<T extends DragTargetData> = void Function(
+    T dragTargetData);
+
+/// [ReorderDragTarget] is a [DragTarget] that carries the index information of
+/// the child.
+///
+/// The size of the [ReorderDragTarget] will become zero when it start dragging.
+///
+class ReorderDragTarget<T extends DragTargetData> extends StatefulWidget {
+  final Widget child;
+  final T dragTargetData;
+
+  final GlobalObjectKey _indexGlobalKey;
+
+  /// Called when dragTarget is being dragging.
+  final DragTargetOnStarted onDragStarted;
+
+  final DragTargetOnEnded<T> onDragEnded;
+
+  /// Called to determine whether this widget is interested in receiving a given
+  /// piece of data being dragged over this drag target.
+  ///
+  /// [toAccept] represents the dragTarget index, which is the value passed in
+  /// when creating the [ReorderDragTarget].
+  final DragTargetWillAccpet<T> onWillAccept;
+
+  /// Called when an acceptable piece of data was dropped over this drag target.
+  ///
+  /// Equivalent to [onAcceptWithDetails], but only includes the data.
+  final void Function(T dragTargetData)? onAccept;
+
+  /// Called when a given piece of data being dragged over this target leaves
+  /// the target.
+  final void Function(T dragTargetData)? onLeave;
+
+  final ReorderDraggableTargetBuilder? draggableTargetBuilder;
+
+  ReorderDragTarget({
+    Key? key,
+    required this.child,
+    required this.dragTargetData,
+    required this.onDragStarted,
+    required this.onDragEnded,
+    required this.onWillAccept,
+    this.onAccept,
+    this.onLeave,
+    this.draggableTargetBuilder,
+  })  : _indexGlobalKey = GlobalObjectKey(child.key!),
+        super(key: key);
+
+  @override
+  State<ReorderDragTarget<T>> createState() => _ReorderDragTargetState<T>();
+}
+
+class _ReorderDragTargetState<T extends DragTargetData>
+    extends State<ReorderDragTarget<T>> {
+  /// Return the dragTarget's size
+  Size? _draggingFeedbackSize = Size.zero;
+
+  @override
+  Widget build(BuildContext context) {
+    Widget dragTarget = DragTarget<T>(
+      builder: _buildDraggableWidget,
+      onWillAccept: (dragTargetData) {
+        assert(dragTargetData != null);
+        if (dragTargetData == null) return false;
+        return widget.onWillAccept(dragTargetData);
+      },
+      onAccept: widget.onAccept,
+      onLeave: (dragTargetData) {
+        assert(dragTargetData != null);
+        if (dragTargetData != null) {
+          widget.onLeave?.call(dragTargetData);
+        }
+      },
+    );
+
+    dragTarget = KeyedSubtree(key: widget._indexGlobalKey, child: dragTarget);
+    return dragTarget;
+  }
+
+  Widget _buildDraggableWidget(
+    BuildContext context,
+    List<T?> acceptedCandidates,
+    List<dynamic> rejectedCandidates,
+  ) {
+    Widget feedbackBuilder = Builder(builder: (BuildContext context) {
+      BoxConstraints contentSizeConstraints =
+          BoxConstraints.loose(_draggingFeedbackSize!);
+      return _buildDraggableFeedback(
+        context,
+        contentSizeConstraints,
+        widget.child,
+      );
+    });
+
+    final draggableWidget = widget.draggableTargetBuilder?.build(
+          context,
+          widget.child,
+          widget.onDragStarted,
+          widget.onDragEnded,
+          widget.onWillAccept,
+        ) ??
+        LongPressDraggable<DragTargetData>(
+          maxSimultaneousDrags: 1,
+          data: widget.dragTargetData,
+          ignoringFeedbackSemantics: false,
+          feedback: feedbackBuilder,
+          childWhenDragging: IgnorePointerWidget(child: widget.child),
+          onDragStarted: () {
+            _draggingFeedbackSize = widget._indexGlobalKey.currentContext?.size;
+            widget.onDragStarted(
+              widget.child,
+              widget.dragTargetData.draggingIndex,
+              _draggingFeedbackSize,
+            );
+          },
+          dragAnchorStrategy: childDragAnchorStrategy,
+
+          /// When the drag ends inside a DragTarget widget, the drag
+          /// succeeds, and we reorder the widget into position appropriately.
+          onDragCompleted: () {
+            widget.onDragEnded(widget.dragTargetData);
+          },
+
+          /// When the drag does not end inside a DragTarget widget, the
+          /// drag fails, but we still reorder the widget to the last position it
+          /// had been dragged to.
+          onDraggableCanceled: (Velocity velocity, Offset offset) =>
+              widget.onDragEnded(widget.dragTargetData),
+          child: widget.child,
+        );
+
+    return draggableWidget;
+  }
+
+  Widget _buildDraggableFeedback(
+      BuildContext context, BoxConstraints constraints, Widget child) {
+    return Transform(
+      transform: Matrix4.rotationZ(0),
+      alignment: FractionalOffset.topLeft,
+      child: Material(
+        elevation: 3.0,
+        color: Colors.transparent,
+        borderRadius: BorderRadius.zero,
+        child: ConstrainedBox(constraints: constraints, child: child),
+      ),
+    );
+  }
+}
+
+class DragAnimationController {
+  // How long an animation to reorder an element in the list takes.
+  final Duration reorderAnimationDuration;
+
+  // How long an animation to scroll to an off-screen element in the
+  // list takes.
+  final Duration scrollAnimationDuration;
+
+  // This controls the entrance of the dragging widget into a new place.
+  late AnimationController entranceController;
+
+  // This controls the 'phantom' of the dragging widget, which is left behind
+  // where the widget used to be.
+  late AnimationController phantomController;
+
+  DragAnimationController({
+    required this.reorderAnimationDuration,
+    required this.scrollAnimationDuration,
+    required TickerProvider vsync,
+    required void Function(AnimationStatus) entranceAnimateStatusChanged,
+  }) {
+    entranceController = AnimationController(
+        value: 1.0, vsync: vsync, duration: reorderAnimationDuration);
+    phantomController = AnimationController(
+        value: 0, vsync: vsync, duration: reorderAnimationDuration);
+    entranceController.addStatusListener(entranceAnimateStatusChanged);
+  }
+
+  bool get isEntranceAnimationCompleted => entranceController.isCompleted;
+
+  void startDargging() {
+    entranceController.value = 1.0;
+  }
+
+  void animateToNext() {
+    phantomController.reverse(from: 1.0);
+    entranceController.forward(from: 0.0);
+  }
+
+  void reverseAnimation() {
+    phantomController.reverse(from: 0.1);
+    entranceController.reverse(from: 0.0);
+  }
+
+  void dispose() {
+    entranceController.dispose();
+    phantomController.dispose();
+  }
+}
+
+class IgnorePointerWidget extends StatelessWidget {
+  final Widget? child;
+  final bool useIntrinsicSize;
+  const IgnorePointerWidget({
+    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);
+    return IgnorePointer(
+      ignoring: true,
+      child: Opacity(
+        opacity: 0,
+        child: sizedChild,
+      ),
+    );
+  }
+}
+
+class PhantomWidget extends StatelessWidget {
+  final Widget? child;
+  final double opacity;
+  const PhantomWidget({
+    this.child,
+    this.opacity = 1.0,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Opacity(
+      opacity: opacity,
+      child: child,
+    );
+  }
+}
+
+class PhantomAnimateContorller {
+  // How long an animation to reorder an element in the list takes.
+  final Duration reorderAnimationDuration;
+  late AnimationController appearController;
+  late AnimationController disappearController;
+
+  PhantomAnimateContorller({
+    required TickerProvider vsync,
+    required this.reorderAnimationDuration,
+    required void Function(AnimationStatus) appearAnimateStatusChanged,
+  }) {
+    appearController = AnimationController(
+        value: 1.0, vsync: vsync, duration: reorderAnimationDuration);
+    disappearController = AnimationController(
+        value: 0, vsync: vsync, duration: reorderAnimationDuration);
+    appearController.addStatusListener(appearAnimateStatusChanged);
+  }
+
+  bool get isAppearAnimationCompleted => appearController.isCompleted;
+
+  void animateToNext() {
+    disappearController.reverse(from: 1.0);
+    appearController.forward(from: 0.0);
+  }
+
+  void performReorderAnimation() {
+    disappearController.reverse(from: 0.1);
+    appearController.reverse(from: 0.0);
+  }
+
+  void dispose() {
+    appearController.dispose();
+    disappearController.dispose();
+  }
+}
+
+abstract class FakeDragTargetEventTrigger {
+  void fakeOnDragStarted(VoidCallback callback);
+  void fakeOnDragEnded(VoidCallback callback);
+}
+
+abstract class FakeDragTargetEventData {
+  Size? get feedbackSize;
+  int get index;
+  DragTargetData get dragTargetData;
+}
+
+class FakeDragTarget<T extends DragTargetData> extends StatefulWidget {
+  final FakeDragTargetEventTrigger eventTrigger;
+  final FakeDragTargetEventData eventData;
+  final DragTargetOnStarted onDragStarted;
+  final DragTargetOnEnded<T> onDragEnded;
+  final DragTargetWillAccpet<T> onWillAccept;
+  final Widget child;
+  const FakeDragTarget({
+    Key? key,
+    required this.eventTrigger,
+    required this.eventData,
+    required this.onDragStarted,
+    required this.onDragEnded,
+    required this.onWillAccept,
+    required this.child,
+  }) : super(key: key);
+
+  @override
+  State<FakeDragTarget<T>> createState() => _FakeDragTargetState<T>();
+}
+
+class _FakeDragTargetState<T extends DragTargetData>
+    extends State<FakeDragTarget<T>> {
+  bool isDragging = false;
+
+  @override
+  void initState() {
+    widget.eventTrigger.fakeOnDragStarted(() {
+      if (mounted) {
+        setState(() {
+          widget.onWillAccept(widget.eventData.dragTargetData as T);
+
+          widget.onDragStarted(
+            widget.child,
+            widget.eventData.index,
+            widget.eventData.feedbackSize,
+          );
+
+          isDragging = true;
+        });
+      }
+    });
+
+    widget.eventTrigger.fakeOnDragEnded(() {
+      if (mounted) {
+        widget.onDragEnded(widget.eventData.dragTargetData as T);
+      }
+    });
+
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    if (isDragging) {
+      return IgnorePointerWidget(child: widget.child);
+    } else {
+      return IgnorePointerWidget(useIntrinsicSize: true, child: widget.child);
+    }
+  }
+}

+ 524 - 0
frontend/app_flowy/packages/flowy_board/lib/src/widgets/flex/reorder_flex.dart

@@ -0,0 +1,524 @@
+import 'dart:math';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
+
+import '../../utils/log.dart';
+import 'reorder_mixin.dart';
+import 'drag_target.dart';
+import 'drag_state.dart';
+import 'reorder_flex_ext.dart';
+
+typedef OnDragStarted = void Function(int index);
+typedef OnDragEnded = void Function();
+typedef OnReorder = void Function(int fromIndex, int toIndex);
+typedef OnDeleted = void Function(int deletedIndex);
+typedef OnInserted = void Function(int insertedIndex);
+typedef OnReveivePassedInPhantom = void Function(
+    FlexDragTargetData dragTargetData, int phantomIndex);
+
+abstract class ReoderFlextDataSource {
+  String get identifier;
+  List<ReoderFlextItem> get items;
+}
+
+abstract class ReoderFlextItem {}
+
+class ReorderFlexConfig {
+  final bool needsLongPressDraggable = true;
+  final double draggingWidgetOpacity = 0.2;
+  final Duration reorderAnimationDuration = const Duration(milliseconds: 250);
+  final Duration scrollAnimationDuration = const Duration(milliseconds: 250);
+  const ReorderFlexConfig();
+}
+
+class ReorderFlex extends StatefulWidget with DraggingReorderFlex {
+  final Widget? header;
+  final Widget? footer;
+  final ReorderFlexConfig config;
+
+  final List<Widget> children;
+  final EdgeInsets? padding;
+  final Axis direction;
+  final MainAxisAlignment mainAxisAlignment = MainAxisAlignment.spaceEvenly;
+  final ScrollController? scrollController;
+
+  final OnDragStarted? onDragStarted;
+  final OnReorder onReorder;
+  final OnDragEnded? onDragEnded;
+
+  final ReoderFlextDataSource dataSource;
+
+  final ReorderFlextDragTargetExtension? dragTargetExtension;
+
+  const ReorderFlex({
+    Key? key,
+    this.header,
+    this.footer,
+    this.scrollController,
+    required this.dataSource,
+    required this.children,
+    required this.config,
+    required this.onReorder,
+    this.onDragStarted,
+    this.onDragEnded,
+    this.dragTargetExtension,
+    // ignore: unused_element
+    this.padding,
+    this.direction = Axis.vertical,
+  }) : super(key: key);
+
+  @override
+  State<ReorderFlex> createState() => ReorderFlexState();
+
+  @override
+  String get id => dataSource.identifier;
+
+  @override
+  ReoderFlextItem itemAtIndex(int index) {
+    return dataSource.items[index];
+  }
+}
+
+class ReorderFlexState extends State<ReorderFlex>
+    with ReorderFlexMinxi, TickerProviderStateMixin<ReorderFlex> {
+  /// Controls scrolls and measures scroll progress.
+  late ScrollController _scrollController;
+  ScrollPosition? _attachedScrollPosition;
+
+  /// Whether or not we are currently scrolling this view to show a widget.
+  bool _scrolling = false;
+
+  late DraggingState dragState;
+  late DragAnimationController _dragAnimationController;
+
+  @override
+  void initState() {
+    dragState = DraggingState(widget.id);
+
+    _dragAnimationController = DragAnimationController(
+      reorderAnimationDuration: widget.config.reorderAnimationDuration,
+      scrollAnimationDuration: widget.config.scrollAnimationDuration,
+      entranceAnimateStatusChanged: (status) {
+        if (status == AnimationStatus.completed) {
+          setState(() => _requestAnimationToNextIndex());
+        }
+      },
+      vsync: this,
+    );
+
+    super.initState();
+  }
+
+  @override
+  void didChangeDependencies() {
+    if (_attachedScrollPosition != null) {
+      _scrollController.detach(_attachedScrollPosition!);
+      _attachedScrollPosition = null;
+    }
+
+    _scrollController = widget.scrollController ??
+        PrimaryScrollController.of(context) ??
+        ScrollController();
+
+    if (_scrollController.hasClients) {
+      _attachedScrollPosition = Scrollable.of(context)?.position;
+    } else {
+      _attachedScrollPosition = null;
+    }
+
+    if (_attachedScrollPosition != null) {
+      _scrollController.attach(_attachedScrollPosition!);
+    }
+    super.didChangeDependencies();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final List<Widget> children = [];
+    if (widget.header != null) {
+      children.add(widget.header!);
+    }
+
+    for (int i = 0; i < widget.children.length; i += 1) {
+      Widget child = widget.children[i];
+      final wrapChild = _wrap(child, i);
+      children.add(wrapChild);
+    }
+
+    if (widget.footer != null) {
+      children.add(widget.footer!);
+    }
+
+    return _wrapScrollView(
+      child: _wrapContainer(children),
+    );
+  }
+
+  @override
+  void dispose() {
+    if (_attachedScrollPosition != null) {
+      _scrollController.detach(_attachedScrollPosition!);
+      _attachedScrollPosition = null;
+    }
+
+    _dragAnimationController.dispose();
+    super.dispose();
+  }
+
+  void _requestAnimationToNextIndex({bool isAcceptingNewTarget = false}) {
+    /// Update the dragState and animate to the next index if the current
+    /// dragging animation is completed. Otherwise, it will get called again
+    /// when the animation finishs.
+
+    if (_dragAnimationController.isEntranceAnimationCompleted) {
+      dragState.removePhantom();
+
+      if (!isAcceptingNewTarget && dragState.didDragTargetMoveToNext()) {
+        return;
+      }
+
+      dragState.moveDragTargetToNext();
+      _dragAnimationController.animateToNext();
+    }
+  }
+
+  /// [child]: the child will be wrapped with dartTarget
+  /// [childIndex]: the index of the child in a list
+  Widget _wrap(Widget child, int childIndex) {
+    return Builder(builder: (context) {
+      final dragTarget = _buildDragTarget(context, child, childIndex);
+      int shiftedIndex = childIndex;
+
+      if (dragState.isOverlapWithPhantom()) {
+        shiftedIndex = dragState.calculateShiftedIndex(childIndex);
+      }
+
+      Log.trace(
+          'Rebuild: Column${dragState.id} ${dragState.toString()}, childIndex: $childIndex shiftedIndex: $shiftedIndex');
+      final currentIndex = dragState.currentIndex;
+      final dragPhantomIndex = dragState.phantomIndex;
+
+      if (shiftedIndex == currentIndex || childIndex == dragPhantomIndex) {
+        Widget dragSpace;
+        if (dragState.draggingWidget != null) {
+          if (dragState.draggingWidget is PhantomWidget) {
+            dragSpace = dragState.draggingWidget!;
+          } else {
+            dragSpace = PhantomWidget(
+              opacity: widget.config.draggingWidgetOpacity,
+              child: dragState.draggingWidget,
+            );
+          }
+        } else {
+          dragSpace = SizedBox.fromSize(size: dragState.dropAreaSize);
+        }
+
+        /// Return the dragTarget it is not start dragging. The size of the
+        /// dragTarget is the same as the the passed in child.
+        ///
+        if (dragState.isNotDragging()) {
+          return _buildDraggingContainer(children: [dragTarget]);
+        }
+
+        /// Determine the size of the drop area to show under the dragging widget.
+        final feedbackSize = dragState.feedbackSize;
+        Widget appearSpace = _makeAppearSpace(dragSpace, feedbackSize);
+        Widget disappearSpace = _makeDisappearSpace(dragSpace, feedbackSize);
+
+        /// When start dragging, the dragTarget, [BoardDragTarget], will
+        /// return a [IgnorePointerWidget] which size is zero.
+        if (dragState.isPhantomAboveDragTarget()) {
+          //the phantom is moving down, i.e. the tile below the phantom is moving up
+          Log.trace('index:$childIndex item moving up / phantom moving down');
+          if (shiftedIndex == currentIndex && childIndex == dragPhantomIndex) {
+            return _buildDraggingContainer(children: [
+              disappearSpace,
+              dragTarget,
+              appearSpace,
+            ]);
+          } else if (shiftedIndex == currentIndex) {
+            return _buildDraggingContainer(children: [
+              dragTarget,
+              appearSpace,
+            ]);
+          } else if (childIndex == dragPhantomIndex) {
+            return _buildDraggingContainer(
+                children: shiftedIndex <= childIndex
+                    ? [dragTarget, disappearSpace]
+                    : [disappearSpace, dragTarget]);
+          }
+        }
+
+        ///
+        if (dragState.isPhantomBelowDragTarget()) {
+          //the phantom is moving up, i.e. the tile above the phantom is moving down
+          Log.trace('index:$childIndex item moving down / phantom moving up');
+          if (shiftedIndex == currentIndex && childIndex == dragPhantomIndex) {
+            return _buildDraggingContainer(children: [
+              appearSpace,
+              dragTarget,
+              disappearSpace,
+            ]);
+          } else if (shiftedIndex == currentIndex) {
+            return _buildDraggingContainer(children: [
+              appearSpace,
+              dragTarget,
+            ]);
+          } else if (childIndex == dragPhantomIndex) {
+            return _buildDraggingContainer(
+                children: shiftedIndex >= childIndex
+                    ? [disappearSpace, dragTarget]
+                    : [dragTarget, disappearSpace]);
+          }
+        }
+
+        assert(!dragState.isOverlapWithPhantom());
+
+        List<Widget> children = [];
+        if (dragState.isDragTargetMovingDown()) {
+          children.addAll([dragTarget, appearSpace]);
+        } else {
+          children.addAll([appearSpace, dragTarget]);
+        }
+        return _buildDraggingContainer(children: children);
+      }
+
+      /// We still wrap dragTarget with a container so that widget's depths are
+      /// the same and it prevent's layout alignment issue
+      return _buildDraggingContainer(children: [dragTarget]);
+    });
+  }
+
+  ReorderDragTarget _buildDragTarget(
+      BuildContext builderContext, Widget child, int childIndex) {
+    return ReorderDragTarget<FlexDragTargetData>(
+      dragTargetData: FlexDragTargetData(
+        draggingIndex: childIndex,
+        state: dragState,
+        draggingReorderFlex: widget,
+      ),
+      onDragStarted: (draggingWidget, draggingIndex, size) {
+        Log.debug("Column${widget.dataSource.identifier} start dragging");
+        _startDragging(draggingWidget, draggingIndex, size);
+        widget.onDragStarted?.call(draggingIndex);
+      },
+      onDragEnded: (dragTargetData) {
+        Log.debug("Column${widget.dataSource.identifier} end dragging");
+
+        setState(() {
+          _onReordered(
+            dragState.dragStartIndex,
+            dragState.currentIndex,
+          );
+          dragState.endDragging();
+          widget.onDragEnded?.call();
+        });
+      },
+      onWillAccept: (FlexDragTargetData dragTargetData) {
+        Log.debug(
+            '[$ReorderDragTarget] ${widget.dataSource.identifier} on will accept, count: ${widget.dataSource.items.length}');
+        assert(widget.dataSource.items.length > childIndex);
+
+        if (_requestDragExtensionToHanlder(
+          dragTargetData,
+          (extension) {
+            extension.onWillAccept(
+              this,
+              builderContext,
+              dragTargetData,
+              dragState.isDragging(),
+              dragTargetData.draggingIndex,
+              childIndex,
+            );
+          },
+        )) {
+          return true;
+        } else {
+          final dragIndex = dragTargetData.draggingIndex;
+          return onWillAccept(builderContext, dragIndex, childIndex);
+        }
+      },
+      onAccept: (dragTargetData) {
+        _requestDragExtensionToHanlder(
+          dragTargetData,
+          (extension) => extension.onAccept(dragTargetData),
+        );
+      },
+      onLeave: (dragTargetData) {
+        _requestDragExtensionToHanlder(
+          dragTargetData,
+          (extension) => extension.onLeave(dragTargetData),
+        );
+      },
+      draggableTargetBuilder:
+          widget.dragTargetExtension?.draggableTargetBuilder,
+      child: child,
+    );
+  }
+
+  bool _requestDragExtensionToHanlder(
+    FlexDragTargetData dragTargetData,
+    void Function(ReorderFlextDragTargetExtension) callback,
+  ) {
+    final extension = widget.dragTargetExtension;
+    if (extension != null && extension.canHandler(dragTargetData)) {
+      callback(extension);
+      return true;
+    } else {
+      return false;
+    }
+  }
+
+  Widget _makeAppearSpace(Widget child, Size? feedbackSize) {
+    return makeAppearingWidget(
+      child,
+      _dragAnimationController.entranceController,
+      feedbackSize,
+      widget.direction,
+    );
+  }
+
+  Widget _makeDisappearSpace(Widget child, Size? feedbackSize) {
+    return makeDisappearingWidget(
+      child,
+      _dragAnimationController.phantomController,
+      feedbackSize,
+      widget.direction,
+    );
+  }
+
+  void _startDragging(
+    Widget draggingWidget,
+    int dragIndex,
+    Size? feedbackSize,
+  ) {
+    setState(() {
+      dragState.startDragging(draggingWidget, dragIndex, feedbackSize);
+      _dragAnimationController.startDargging();
+    });
+  }
+
+  bool onWillAccept(BuildContext context, int? dragIndex, int childIndex) {
+    /// The [willAccept] will be true if the dargTarget is the widget that gets
+    /// dragged and it is dragged on top of the other dragTargets.
+    bool willAccept =
+        dragState.dragStartIndex == dragIndex && dragIndex != childIndex;
+    setState(() {
+      if (willAccept) {
+        int shiftedIndex = dragState.calculateShiftedIndex(childIndex);
+        dragState.updateNextIndex(shiftedIndex);
+      } else {
+        dragState.updateNextIndex(childIndex);
+      }
+
+      _requestAnimationToNextIndex(isAcceptingNewTarget: true);
+    });
+
+    _scrollTo(context);
+
+    /// If the target is not the original starting point, then we will accept the drop.
+    return willAccept;
+  }
+
+  void _onReordered(int fromIndex, int toIndex) {
+    if (fromIndex != toIndex) {
+      widget.onReorder.call(fromIndex, toIndex);
+    }
+
+    _dragAnimationController.reverseAnimation();
+  }
+
+  Widget _wrapScrollView({required Widget child}) {
+    if (widget.scrollController != null &&
+        PrimaryScrollController.of(context) == null) {
+      return child;
+    } else {
+      return SingleChildScrollView(
+        scrollDirection: widget.direction,
+        padding: widget.padding,
+        controller: _scrollController,
+        child: child,
+      );
+    }
+  }
+
+  Widget _wrapContainer(List<Widget> children) {
+    switch (widget.direction) {
+      case Axis.horizontal:
+        return Row(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          mainAxisAlignment: widget.mainAxisAlignment,
+          children: children,
+        );
+      case Axis.vertical:
+      default:
+        return Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          mainAxisAlignment: widget.mainAxisAlignment,
+          children: children,
+        );
+    }
+  }
+
+  Widget _buildDraggingContainer({required List<Widget> children}) {
+    switch (widget.direction) {
+      case Axis.horizontal:
+        return Row(
+          mainAxisSize: MainAxisSize.min,
+          mainAxisAlignment: MainAxisAlignment.start,
+          children: children,
+        );
+      case Axis.vertical:
+      default:
+        return Column(
+          mainAxisSize: MainAxisSize.min,
+          mainAxisAlignment: MainAxisAlignment.start,
+          children: children,
+        );
+    }
+  }
+
+// Scrolls to a target context if that context is not on the screen.
+  void _scrollTo(BuildContext context) {
+    if (_scrolling) return;
+    final RenderObject contextObject = context.findRenderObject()!;
+    final RenderAbstractViewport viewport =
+        RenderAbstractViewport.of(contextObject)!;
+    // If and only if the current scroll offset falls in-between the offsets
+    // necessary to reveal the selected context at the top or bottom of the
+    // screen, then it is already on-screen.
+    final double margin = widget.direction == Axis.horizontal
+        ? dragState.dropAreaSize.width
+        : dragState.dropAreaSize.height;
+    if (_scrollController.hasClients) {
+      final double scrollOffset = _scrollController.offset;
+      final double topOffset = max(
+        _scrollController.position.minScrollExtent,
+        viewport.getOffsetToReveal(contextObject, 0.0).offset - margin,
+      );
+      final double bottomOffset = min(
+        _scrollController.position.maxScrollExtent,
+        viewport.getOffsetToReveal(contextObject, 1.0).offset + margin,
+      );
+      final bool onScreen =
+          scrollOffset <= topOffset && scrollOffset >= bottomOffset;
+
+      // If the context is off screen, then we request a scroll to make it visible.
+      if (!onScreen) {
+        _scrolling = true;
+        _scrollController.position
+            .animateTo(
+          scrollOffset < bottomOffset ? bottomOffset : topOffset,
+          duration: _dragAnimationController.scrollAnimationDuration,
+          curve: Curves.easeInOut,
+        )
+            .then((void value) {
+          setState(() {
+            _scrolling = false;
+          });
+        });
+      }
+    }
+  }
+}

+ 78 - 0
frontend/app_flowy/packages/flowy_board/lib/src/widgets/flex/reorder_flex_ext.dart

@@ -0,0 +1,78 @@
+import 'package:flutter/material.dart';
+
+import '../../utils/log.dart';
+import 'drag_state.dart';
+import 'drag_target.dart';
+import 'reorder_flex.dart';
+
+abstract class DragTargetExtensionDelegate {
+  bool acceptNewDragTargetData(
+    String columnId,
+    FlexDragTargetData dragTargetData,
+    int index,
+  );
+  void updateDragTargetData(
+    String columnId,
+    FlexDragTargetData dragTargetData,
+    int index,
+  );
+}
+
+class ReorderFlextDragTargetExtension {
+  final String reorderFlexId;
+  final List<String> acceptReorderFlexIds;
+  final DragTargetExtensionDelegate delegate;
+  final ReorderDraggableTargetBuilder? draggableTargetBuilder;
+
+  ReorderFlextDragTargetExtension({
+    required this.reorderFlexId,
+    required this.delegate,
+    required this.acceptReorderFlexIds,
+    this.draggableTargetBuilder,
+  });
+
+  bool canHandler(FlexDragTargetData dragTargetData) {
+    /// If the columnId equal to the dragTargetData's columnId,
+    /// it means the dragTarget is dragging on the top of its own list.
+    /// Otherwise, it means the dargTarget was moved to another list.
+    ///
+    if (!acceptReorderFlexIds.contains(dragTargetData.reorderFlexId)) {
+      return false;
+    }
+
+    return reorderFlexId != dragTargetData.reorderFlexId;
+  }
+
+  bool onWillAccept(
+    ReorderFlexState reorderFlexState,
+    BuildContext context,
+    FlexDragTargetData dragTargetData,
+    bool isDragging,
+    int dragIndex,
+    int itemIndex,
+  ) {
+    final isNewDragTarget = delegate.acceptNewDragTargetData(
+        reorderFlexId, dragTargetData, itemIndex);
+
+    if (isNewDragTarget == false) {
+      delegate.updateDragTargetData(reorderFlexId, dragTargetData, itemIndex);
+      reorderFlexState.onWillAccept(context, dragIndex, itemIndex);
+    } else {
+      Log.debug(
+          '[$ReorderFlextDragTargetExtension] move Column${dragTargetData.reorderFlexId}:${dragTargetData.draggingIndex} '
+          'to Column$reorderFlexId:$itemIndex');
+    }
+
+    return true;
+  }
+
+  void onAccept(FlexDragTargetData dragTargetData) {
+    Log.trace(
+        '[$ReorderFlextDragTargetExtension] Column$reorderFlexId on onAccept');
+  }
+
+  void onLeave(FlexDragTargetData dragTargetData) {
+    Log.trace(
+        '[$ReorderFlextDragTargetExtension] Column$reorderFlexId on leave');
+  }
+}

+ 65 - 0
frontend/app_flowy/packages/flowy_board/lib/src/widgets/flex/reorder_mixin.dart

@@ -0,0 +1,65 @@
+import 'package:flutter/widgets.dart';
+
+import '../transitions.dart';
+
+mixin ReorderFlexMinxi {
+  @protected
+  Widget makeAppearingWidget(
+    Widget child,
+    AnimationController entranceController,
+    Size? draggingFeedbackSize,
+    Axis direction,
+  ) {
+    if (null == draggingFeedbackSize) {
+      return SizeTransitionWithIntrinsicSize(
+        sizeFactor: entranceController,
+        axis: direction,
+        child: FadeTransition(
+          opacity: entranceController,
+          child: child,
+        ),
+      );
+    } else {
+      var transition = SizeTransition(
+        sizeFactor: entranceController,
+        axis: direction,
+        child: FadeTransition(opacity: entranceController, child: child),
+      );
+
+      BoxConstraints contentSizeConstraints =
+          BoxConstraints.loose(draggingFeedbackSize);
+      return ConstrainedBox(
+          constraints: contentSizeConstraints, child: transition);
+    }
+  }
+
+  @protected
+  Widget makeDisappearingWidget(
+    Widget child,
+    AnimationController phantomController,
+    Size? draggingFeedbackSize,
+    Axis direction,
+  ) {
+    if (null == draggingFeedbackSize) {
+      return SizeTransitionWithIntrinsicSize(
+        sizeFactor: phantomController,
+        axis: direction,
+        child: FadeTransition(
+          opacity: phantomController,
+          child: child,
+        ),
+      );
+    } else {
+      var transition = SizeTransition(
+        sizeFactor: phantomController,
+        axis: direction,
+        child: FadeTransition(opacity: phantomController, child: child),
+      );
+
+      BoxConstraints contentSizeConstraints =
+          BoxConstraints.loose(draggingFeedbackSize);
+      return ConstrainedBox(
+          constraints: contentSizeConstraints, child: transition);
+    }
+  }
+}

+ 338 - 0
frontend/app_flowy/packages/flowy_board/lib/src/widgets/phantom/phantom_controller.dart

@@ -0,0 +1,338 @@
+import 'package:flutter/material.dart';
+import '../../../flowy_board.dart';
+import '../../utils/log.dart';
+import '../flex/drag_state.dart';
+import '../flex/drag_target.dart';
+import '../flex/reorder_flex_ext.dart';
+import 'phantom_state.dart';
+
+abstract class BoardPhantomControllerDelegate {
+  BoardColumnDataController? controller(String columnId);
+}
+
+mixin ColumnDataPhantomMixim {
+  BoardColumnDataController? get;
+}
+
+class BoardPhantomController extends DragTargetExtensionDelegate {
+  final BoardPhantomControllerDelegate delegate;
+
+  PhantomRecord? phantomRecord;
+
+  final columnsState = ColumnPassthroughStateController();
+
+  BoardPhantomController({required this.delegate});
+
+  bool get hasPhantom => phantomRecord != null;
+
+  bool isFromColumn(String columnId) {
+    if (phantomRecord != null) {
+      return phantomRecord!.fromColumnId == columnId;
+    } else {
+      return true;
+    }
+  }
+
+  void transformIndex(int fromIndex, int toIndex) {
+    if (phantomRecord == null) {
+      return;
+    }
+    assert(phantomRecord!.fromColumnIndex == fromIndex);
+    phantomRecord?.updateFromColumnIndex(toIndex);
+  }
+
+  void columnStartDragging(String columnId) {
+    columnsState.setColumnIsDragging(columnId, false);
+  }
+
+  void columnEndDragging(String columnId) {
+    columnsState.setColumnIsDragging(columnId, true);
+    if (phantomRecord != null) {
+      if (phantomRecord!.fromColumnId == columnId) {
+        columnsState.notifyDidRemovePhantom(phantomRecord!.toColumnId);
+      }
+    }
+    _swapColumnData();
+  }
+
+  void _swapColumnData() {
+    if (phantomRecord == null) {
+      return;
+    }
+
+    if (columnsState.isDragging(phantomRecord!.fromColumnId) == false) {
+      return;
+    }
+
+    Log.debug("[$BoardPhantomController] move ${phantomRecord.toString()}");
+
+    final item = delegate
+        .controller(phantomRecord!.fromColumnId)
+        ?.removeAt(phantomRecord!.fromColumnIndex);
+
+    assert(item != null);
+    assert(delegate
+        .controller(phantomRecord!.toColumnId)
+        ?.items[phantomRecord!.toColumnIndex] is PhantomColumnItem);
+
+    delegate
+        .controller(phantomRecord!.toColumnId)
+        ?.replace(phantomRecord!.toColumnIndex, item!);
+
+    phantomRecord = null;
+  }
+
+  @override
+  bool acceptNewDragTargetData(
+      String columnId, FlexDragTargetData dragTargetData, int index) {
+    if (phantomRecord == null) {
+      _updatePhantomRecord(columnId, dragTargetData, index);
+      _insertPhantom(columnId, dragTargetData, index);
+
+      return true;
+    }
+
+    final isDifferentDragTarget = phantomRecord!.toColumnId != columnId;
+    Log.debug(
+        '[$BoardPhantomController] Set inserted column id: $columnId, different target: $isDifferentDragTarget');
+    if (isDifferentDragTarget) {
+      /// Remove the phantom in the previous column.
+      _removePhantom(phantomRecord!.toColumnId);
+
+      /// Update the record and insert the phantom to new column.
+      _updatePhantomRecord(columnId, dragTargetData, index);
+      _insertPhantom(columnId, dragTargetData, index);
+    }
+
+    return isDifferentDragTarget;
+  }
+
+  @override
+  void updateDragTargetData(
+      String columnId, FlexDragTargetData dragTargetData, int index) {
+    phantomRecord?.updateInsertedIndex(index);
+
+    assert(phantomRecord != null);
+    if (phantomRecord!.toColumnId == columnId) {
+      /// Update the existing phantom index
+      _updatePhantom(phantomRecord!.toColumnId, dragTargetData, index);
+    }
+  }
+
+  void _updatePhantom(
+    String toColumnId,
+    FlexDragTargetData dragTargetData,
+    int phantomIndex,
+  ) {
+    final items = delegate.controller(toColumnId)?.items;
+    if (items == null) {
+      return;
+    }
+
+    final index = items.indexWhere((item) => item.isPhantom);
+    assert(index != -1);
+    if (index != -1) {
+      if (index != phantomIndex) {
+        Log.debug(
+            '[$BoardPhantomController] move phantom $toColumnId:$index to $toColumnId:$phantomIndex');
+        final item = items.removeAt(index);
+        items.insert(phantomIndex, item);
+      }
+    }
+  }
+
+  void _removePhantom(String columnId) {
+    final items = delegate.controller(columnId)?.items;
+    if (items == null) {
+      return;
+    }
+
+    final index = items.indexWhere((item) => item.isPhantom);
+    assert(index != -1);
+    if (index != -1) {
+      items.removeAt(index);
+      Log.debug(
+          '[$BoardPhantomController] Column$columnId remove phantom, current count: ${items.length}');
+      columnsState.notifyDidRemovePhantom(columnId);
+      columnsState.removeColumnListener(columnId);
+    }
+  }
+
+  void _insertPhantom(
+    String toColumnId,
+    FlexDragTargetData dragTargetData,
+    int phantomIndex,
+  ) {
+    final items = delegate.controller(toColumnId)?.items;
+    if (items == null) {
+      return;
+    }
+
+    final phantomContext = PassthroughPhantomContext(
+      index: phantomIndex,
+      dragTargetData: dragTargetData,
+    );
+    columnsState.addColumnListener(toColumnId, phantomContext);
+
+    Log.debug(
+        '[$BoardPhantomController] Column$toColumnId insert phantom at $phantomIndex');
+    delegate
+        .controller(toColumnId)
+        ?.insert(phantomIndex, PhantomColumnItem(phantomContext));
+
+    WidgetsBinding.instance.addPostFrameCallback((_) {
+      Future.delayed(const Duration(microseconds: 100), () {
+        columnsState.notifyDidInsertPhantom(toColumnId);
+      });
+    });
+  }
+
+  void _updatePhantomRecord(
+    String columnId,
+    FlexDragTargetData dragTargetData,
+    int index,
+  ) {
+    phantomRecord = PhantomRecord(
+      toColumnId: columnId,
+      toColumnIndex: index,
+      item: dragTargetData.columnItem as ColumnItem,
+      fromColumnId: dragTargetData.reorderFlexId,
+      fromColumnIndex: dragTargetData.draggingIndex,
+    );
+  }
+}
+
+class PhantomRecord {
+  final ColumnItem item;
+  final String fromColumnId;
+  int fromColumnIndex;
+
+  final String toColumnId;
+  int toColumnIndex;
+
+  PhantomRecord({
+    required this.item,
+    required this.toColumnId,
+    required this.toColumnIndex,
+    required this.fromColumnId,
+    required this.fromColumnIndex,
+  });
+
+  void updateFromColumnIndex(int index) {
+    if (fromColumnIndex == index) {
+      return;
+    }
+    Log.info(
+        '[$PhantomRecord] Update Column$fromColumnId remove position to $index');
+    fromColumnIndex = index;
+  }
+
+  void updateInsertedIndex(int index) {
+    if (toColumnIndex == index) {
+      return;
+    }
+
+    Log.info(
+        '[$PhantomRecord] Update Column$toColumnId phantom position to $index');
+    toColumnIndex = index;
+  }
+
+  @override
+  String toString() {
+    return '$fromColumnId:$fromColumnIndex to $toColumnId:$toColumnIndex';
+  }
+}
+
+class PhantomColumnItem extends ColumnItem {
+  final PassthroughPhantomContext phantomContext;
+
+  PhantomColumnItem(PassthroughPhantomContext insertedPhantom)
+      : phantomContext = insertedPhantom;
+
+  @override
+  bool get isPhantom => true;
+
+  @override
+  String get id => phantomContext.itemData.id;
+
+  Size? get feedbackSize => phantomContext.feedbackSize;
+
+  Widget get draggingWidget => phantomContext.draggingWidget == null
+      ? const SizedBox()
+      : phantomContext.draggingWidget!;
+}
+
+class PassthroughPhantomContext extends FakeDragTargetEventTrigger
+    with FakeDragTargetEventData, PassthroughPhantomListener {
+  @override
+  int index;
+
+  @override
+  final FlexDragTargetData dragTargetData;
+
+  @override
+  Size? get feedbackSize => dragTargetData.state.feedbackSize;
+
+  Widget? get draggingWidget => dragTargetData.draggingWidget;
+
+  ColumnItem get itemData => dragTargetData.columnItem as ColumnItem;
+
+  @override
+  VoidCallback? onInserted;
+
+  @override
+  VoidCallback? onDragEnded;
+
+  PassthroughPhantomContext({
+    required this.index,
+    required this.dragTargetData,
+  });
+
+  @override
+  void fakeOnDragEnded(VoidCallback callback) {
+    onDragEnded = callback;
+  }
+
+  @override
+  void fakeOnDragStarted(VoidCallback callback) {
+    onInserted = callback;
+  }
+}
+
+class PassthroughPhantomWidget extends PhantomWidget {
+  final PassthroughPhantomContext passthroughPhantomContext;
+
+  PassthroughPhantomWidget({
+    required double opacity,
+    required this.passthroughPhantomContext,
+    Key? key,
+  }) : super(
+          child: passthroughPhantomContext.draggingWidget,
+          opacity: opacity,
+          key: key,
+        );
+}
+
+class PhantomReorderDraggableBuilder extends ReorderDraggableTargetBuilder {
+  @override
+  Widget? build<T extends DragTargetData>(
+    BuildContext context,
+    Widget child,
+    DragTargetOnStarted onDragStarted,
+    DragTargetOnEnded<T> onDragEnded,
+    DragTargetWillAccpet<T> onWillAccept,
+  ) {
+    if (child is PassthroughPhantomWidget) {
+      return FakeDragTarget<T>(
+        eventTrigger: child.passthroughPhantomContext,
+        eventData: child.passthroughPhantomContext,
+        onDragStarted: onDragStarted,
+        onDragEnded: onDragEnded,
+        onWillAccept: onWillAccept,
+        child: child,
+      );
+    } else {
+      return null;
+    }
+  }
+}

+ 109 - 0
frontend/app_flowy/packages/flowy_board/lib/src/widgets/phantom/phantom_state.dart

@@ -0,0 +1,109 @@
+import 'phantom_controller.dart';
+import 'package:flutter/material.dart';
+
+class ColumnPassthroughStateController {
+  final _states = <String, ColumnPassthrougPhantomhState>{};
+
+  void setColumnIsDragging(String columnId, bool isDragging) {
+    _stateWithId(columnId).isDragging = isDragging;
+  }
+
+  bool isDragging(String columnId) {
+    return _stateWithId(columnId).isDragging;
+  }
+
+  void addColumnListener(String columnId, PassthroughPhantomListener listener) {
+    _stateWithId(columnId).notifier.addListener(
+          onInserted: (c) => listener.onInserted?.call(),
+          onDeleted: () => listener.onDragEnded?.call(),
+        );
+  }
+
+  void removeColumnListener(String columnId) {
+    _stateWithId(columnId).notifier.dispose();
+    _states.remove(columnId);
+  }
+
+  void notifyDidInsertPhantom(String columnId) {
+    _stateWithId(columnId).notifier.insert();
+  }
+
+  void notifyDidRemovePhantom(String columnId) {
+    _stateWithId(columnId).notifier.remove();
+  }
+
+  ColumnPassthrougPhantomhState _stateWithId(String columnId) {
+    var state = _states[columnId];
+    if (state == null) {
+      state = ColumnPassthrougPhantomhState();
+      _states[columnId] = state;
+    }
+    return state;
+  }
+}
+
+class ColumnPassthrougPhantomhState {
+  bool isDragging = false;
+  final notifier = PassthroughPhantomNotifier();
+}
+
+abstract class PassthroughPhantomListener {
+  VoidCallback? get onInserted;
+  VoidCallback? get onDragEnded;
+}
+
+class PassthroughPhantomNotifier {
+  final insertNotifier = PhantomInsertNotifier();
+
+  final removeNotifier = PhantomDeleteNotifier();
+
+  void insert() {
+    insertNotifier.insert();
+  }
+
+  void remove() {
+    removeNotifier.remove();
+  }
+
+  void addListener({
+    void Function(PassthroughPhantomContext? insertedPhantom)? onInserted,
+    void Function()? onDeleted,
+  }) {
+    if (onInserted != null) {
+      insertNotifier.addListener(() {
+        onInserted(insertNotifier.insertedPhantom);
+      });
+    }
+
+    if (onDeleted != null) {
+      removeNotifier.addListener(() {
+        onDeleted();
+      });
+    }
+  }
+
+  void dispose() {
+    insertNotifier.dispose();
+    removeNotifier.dispose();
+  }
+}
+
+class PhantomInsertNotifier extends ChangeNotifier {
+  PassthroughPhantomContext? insertedPhantom;
+
+  void insert() {
+    notifyListeners();
+  }
+}
+
+class PhantomDeleteNotifier extends ChangeNotifier {
+  // int deletedIndex = -1;
+
+  void remove() {
+    // if (this.deletedIndex != deletedIndex) {
+    //   this.deletedIndex = deletedIndex;
+    //   notifyListeners();
+    // }
+    notifyListeners();
+  }
+}

+ 3 - 1
frontend/app_flowy/packages/flowy_board/pubspec.yaml

@@ -4,13 +4,15 @@ version: 0.0.1
 homepage:
 
 environment:
-  sdk: ">=2.17.6 <3.0.0"
+  sdk: ">=2.17.1 <3.0.0"
   flutter: ">=2.5.0"
 
 dependencies:
   flutter:
     sdk: flutter
   plugin_platform_interface: ^2.0.2
+  equatable: ^2.0.3
+  provider: ^6.0.1
 
 dev_dependencies:
   flutter_test:

+ 0 - 24
frontend/app_flowy/packages/flowy_board/test/flowy_board_method_channel_test.dart

@@ -1,24 +0,0 @@
-import 'package:flutter/services.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:flowy_board/flowy_board_method_channel.dart';
-
-void main() {
-  MethodChannelFlowyBoard platform = MethodChannelFlowyBoard();
-  const MethodChannel channel = MethodChannel('flowy_board');
-
-  TestWidgetsFlutterBinding.ensureInitialized();
-
-  setUp(() {
-    channel.setMockMethodCallHandler((MethodCall methodCall) async {
-      return '42';
-    });
-  });
-
-  tearDown(() {
-    channel.setMockMethodCallHandler(null);
-  });
-
-  test('getPlatformVersion', () async {
-    expect(await platform.getPlatformVersion(), '42');
-  });
-}

+ 0 - 29
frontend/app_flowy/packages/flowy_board/test/flowy_board_test.dart

@@ -1,29 +0,0 @@
-import 'package:flutter_test/flutter_test.dart';
-import 'package:flowy_board/flowy_board.dart';
-import 'package:flowy_board/flowy_board_platform_interface.dart';
-import 'package:flowy_board/flowy_board_method_channel.dart';
-import 'package:plugin_platform_interface/plugin_platform_interface.dart';
-
-class MockFlowyBoardPlatform 
-    with MockPlatformInterfaceMixin
-    implements FlowyBoardPlatform {
-
-  @override
-  Future<String?> getPlatformVersion() => Future.value('42');
-}
-
-void main() {
-  final FlowyBoardPlatform initialPlatform = FlowyBoardPlatform.instance;
-
-  test('$MethodChannelFlowyBoard is the default instance', () {
-    expect(initialPlatform, isInstanceOf<MethodChannelFlowyBoard>());
-  });
-
-  test('getPlatformVersion', () async {
-    FlowyBoard flowyBoardPlugin = FlowyBoard();
-    MockFlowyBoardPlatform fakePlatform = MockFlowyBoardPlatform();
-    FlowyBoardPlatform.instance = fakePlatform;
-  
-    expect(await flowyBoardPlugin.getPlatformVersion(), '42');
-  });
-}