board.dart 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. import 'package:appflowy_board/src/utils/log.dart';
  2. import 'package:flutter/material.dart';
  3. import 'package:provider/provider.dart';
  4. import 'board_column/board_column.dart';
  5. import 'board_column/board_column_data.dart';
  6. import 'board_data.dart';
  7. import 'reorder_flex/drag_state.dart';
  8. import 'reorder_flex/drag_target_interceptor.dart';
  9. import 'reorder_flex/reorder_flex.dart';
  10. import 'reorder_phantom/phantom_controller.dart';
  11. import '../rendering/board_overlay.dart';
  12. class AFBoardScrollManager {
  13. BoardColumnsState? _columnState;
  14. // AFBoardScrollManager();
  15. void scrollToBottom(String columnId, VoidCallback? completed) {
  16. _columnState
  17. ?.getReorderFlexState(columnId: columnId)
  18. ?.scrollToBottom(completed);
  19. }
  20. }
  21. class AFBoardConfig {
  22. final double cornerRadius;
  23. final EdgeInsets columnPadding;
  24. final EdgeInsets columnItemPadding;
  25. final EdgeInsets footerPadding;
  26. final EdgeInsets headerPadding;
  27. final EdgeInsets cardPadding;
  28. final Color columnBackgroundColor;
  29. const AFBoardConfig({
  30. this.cornerRadius = 6.0,
  31. this.columnPadding = const EdgeInsets.symmetric(horizontal: 8),
  32. this.columnItemPadding = const EdgeInsets.symmetric(horizontal: 12),
  33. this.footerPadding = const EdgeInsets.symmetric(horizontal: 12),
  34. this.headerPadding = const EdgeInsets.symmetric(horizontal: 16),
  35. this.cardPadding = const EdgeInsets.symmetric(horizontal: 3, vertical: 4),
  36. this.columnBackgroundColor = Colors.transparent,
  37. });
  38. }
  39. class AFBoard extends StatelessWidget {
  40. /// The direction to use as the main axis.
  41. final Axis direction = Axis.vertical;
  42. ///
  43. final Widget? background;
  44. ///
  45. final AFBoardColumnCardBuilder cardBuilder;
  46. ///
  47. final AFBoardColumnHeaderBuilder? headerBuilder;
  48. ///
  49. final AFBoardColumnFooterBuilder? footBuilder;
  50. ///
  51. final AFBoardDataController dataController;
  52. final BoxConstraints columnConstraints;
  53. ///
  54. late final BoardPhantomController phantomController;
  55. final ScrollController? scrollController;
  56. final AFBoardConfig config;
  57. final AFBoardScrollManager? scrollManager;
  58. final BoardColumnsState _columnState = BoardColumnsState();
  59. AFBoard({
  60. required this.dataController,
  61. required this.cardBuilder,
  62. this.background,
  63. this.footBuilder,
  64. this.headerBuilder,
  65. this.scrollController,
  66. this.scrollManager,
  67. this.columnConstraints = const BoxConstraints(maxWidth: 200),
  68. this.config = const AFBoardConfig(),
  69. Key? key,
  70. }) : super(key: key) {
  71. phantomController = BoardPhantomController(
  72. delegate: dataController,
  73. columnsState: _columnState,
  74. );
  75. }
  76. @override
  77. Widget build(BuildContext context) {
  78. return ChangeNotifierProvider.value(
  79. value: dataController,
  80. child: Consumer<AFBoardDataController>(
  81. builder: (context, notifier, child) {
  82. if (scrollManager != null) {
  83. scrollManager!._columnState = _columnState;
  84. }
  85. return AFBoardContent(
  86. config: config,
  87. dataController: dataController,
  88. scrollController: scrollController,
  89. scrollManager: scrollManager,
  90. columnsState: _columnState,
  91. background: background,
  92. delegate: phantomController,
  93. columnConstraints: columnConstraints,
  94. cardBuilder: cardBuilder,
  95. footBuilder: footBuilder,
  96. headerBuilder: headerBuilder,
  97. phantomController: phantomController,
  98. onReorder: dataController.moveColumn,
  99. );
  100. },
  101. ),
  102. );
  103. }
  104. }
  105. class AFBoardContent extends StatefulWidget {
  106. final ScrollController? scrollController;
  107. final OnDragStarted? onDragStarted;
  108. final OnReorder onReorder;
  109. final OnDragEnded? onDragEnded;
  110. final AFBoardDataController dataController;
  111. final Widget? background;
  112. final AFBoardConfig config;
  113. final ReorderFlexConfig reorderFlexConfig;
  114. final BoxConstraints columnConstraints;
  115. final AFBoardScrollManager? scrollManager;
  116. final BoardColumnsState columnsState;
  117. ///
  118. final AFBoardColumnCardBuilder cardBuilder;
  119. ///
  120. final AFBoardColumnHeaderBuilder? headerBuilder;
  121. ///
  122. final AFBoardColumnFooterBuilder? footBuilder;
  123. final OverlapDragTargetDelegate delegate;
  124. final BoardPhantomController phantomController;
  125. const AFBoardContent({
  126. required this.config,
  127. required this.onReorder,
  128. required this.delegate,
  129. required this.dataController,
  130. required this.scrollManager,
  131. required this.columnsState,
  132. this.onDragStarted,
  133. this.onDragEnded,
  134. this.scrollController,
  135. this.background,
  136. required this.columnConstraints,
  137. required this.cardBuilder,
  138. this.footBuilder,
  139. this.headerBuilder,
  140. required this.phantomController,
  141. Key? key,
  142. }) : reorderFlexConfig = const ReorderFlexConfig(),
  143. super(key: key);
  144. @override
  145. State<AFBoardContent> createState() => _AFBoardContentState();
  146. }
  147. class _AFBoardContentState extends State<AFBoardContent> {
  148. final GlobalKey _boardContentKey =
  149. GlobalKey(debugLabel: '$AFBoardContent overlay key');
  150. late BoardOverlayEntry _overlayEntry;
  151. @override
  152. void initState() {
  153. _overlayEntry = BoardOverlayEntry(
  154. builder: (BuildContext context) {
  155. final interceptor = OverlappingDragTargetInterceptor(
  156. reorderFlexId: widget.dataController.identifier,
  157. acceptedReorderFlexId: widget.dataController.columnIds,
  158. delegate: widget.delegate,
  159. columnsState: widget.columnsState,
  160. );
  161. final reorderFlex = ReorderFlex(
  162. config: widget.reorderFlexConfig,
  163. scrollController: widget.scrollController,
  164. onDragStarted: widget.onDragStarted,
  165. onReorder: widget.onReorder,
  166. onDragEnded: widget.onDragEnded,
  167. dataSource: widget.dataController,
  168. direction: Axis.horizontal,
  169. interceptor: interceptor,
  170. reorderable: false,
  171. children: _buildColumns(),
  172. );
  173. return Stack(
  174. alignment: AlignmentDirectional.topStart,
  175. children: [
  176. if (widget.background != null)
  177. Container(
  178. clipBehavior: Clip.hardEdge,
  179. decoration: BoxDecoration(
  180. borderRadius:
  181. BorderRadius.circular(widget.config.cornerRadius),
  182. ),
  183. child: widget.background,
  184. ),
  185. reorderFlex,
  186. ],
  187. );
  188. },
  189. opaque: false,
  190. );
  191. super.initState();
  192. }
  193. @override
  194. Widget build(BuildContext context) {
  195. return BoardOverlay(
  196. key: _boardContentKey,
  197. initialEntries: [_overlayEntry],
  198. );
  199. }
  200. List<Widget> _buildColumns() {
  201. final List<Widget> children =
  202. widget.dataController.columnDatas.asMap().entries.map(
  203. (item) {
  204. final columnData = item.value;
  205. final columnIndex = item.key;
  206. final dataSource = _BoardColumnDataSourceImpl(
  207. columnId: columnData.id,
  208. dataController: widget.dataController,
  209. );
  210. return ChangeNotifierProvider.value(
  211. key: ValueKey(columnData.id),
  212. value: widget.dataController.getColumnController(columnData.id),
  213. child: Consumer<AFBoardColumnDataController>(
  214. builder: (context, value, child) {
  215. final boardColumn = AFBoardColumnWidget(
  216. // key: PageStorageKey<String>(columnData.id),
  217. // key: GlobalObjectKey(columnData.id),
  218. margin: _marginFromIndex(columnIndex),
  219. itemMargin: widget.config.columnItemPadding,
  220. headerBuilder: _buildHeader,
  221. footBuilder: widget.footBuilder,
  222. cardBuilder: widget.cardBuilder,
  223. dataSource: dataSource,
  224. scrollController: ScrollController(),
  225. phantomController: widget.phantomController,
  226. onReorder: widget.dataController.moveColumnItem,
  227. cornerRadius: widget.config.cornerRadius,
  228. backgroundColor: widget.config.columnBackgroundColor,
  229. dragStateStorage: widget.columnsState,
  230. dragTargetIndexKeyStorage: widget.columnsState,
  231. );
  232. widget.columnsState.addColumn(columnData.id, boardColumn);
  233. return ConstrainedBox(
  234. constraints: widget.columnConstraints,
  235. child: boardColumn,
  236. );
  237. },
  238. ),
  239. );
  240. },
  241. ).toList();
  242. return children;
  243. }
  244. Widget? _buildHeader(
  245. BuildContext context, AFBoardColumnHeaderData headerData) {
  246. if (widget.headerBuilder == null) {
  247. return null;
  248. }
  249. return Selector<AFBoardColumnDataController, AFBoardColumnHeaderData>(
  250. selector: (context, controller) => controller.columnData.headerData,
  251. builder: (context, headerData, _) {
  252. return widget.headerBuilder!(context, headerData)!;
  253. },
  254. );
  255. }
  256. EdgeInsets _marginFromIndex(int index) {
  257. if (widget.dataController.columnDatas.isEmpty) {
  258. return widget.config.columnPadding;
  259. }
  260. if (index == 0) {
  261. return EdgeInsets.only(right: widget.config.columnPadding.right);
  262. }
  263. if (index == widget.dataController.columnDatas.length - 1) {
  264. return EdgeInsets.only(left: widget.config.columnPadding.left);
  265. }
  266. return widget.config.columnPadding;
  267. }
  268. }
  269. class _BoardColumnDataSourceImpl extends AFBoardColumnDataDataSource {
  270. String columnId;
  271. final AFBoardDataController dataController;
  272. _BoardColumnDataSourceImpl({
  273. required this.columnId,
  274. required this.dataController,
  275. });
  276. @override
  277. AFBoardColumnData get columnData =>
  278. dataController.getColumnController(columnId)!.columnData;
  279. @override
  280. List<String> get acceptedColumnIds => dataController.columnIds;
  281. }
  282. class BoardColumnContext {
  283. GlobalKey? columnKey;
  284. DraggingState? draggingState;
  285. }
  286. class BoardColumnsState extends DraggingStateStorage
  287. with ReorderDragTargetIndexKeyStorage {
  288. /// Quick access to the [AFBoardColumnWidget]
  289. final Map<String, GlobalKey> columnKeys = {};
  290. final Map<String, DraggingState> columnDragStates = {};
  291. final Map<String, Map<String, GlobalObjectKey>> columnDragDragTargets = {};
  292. void addColumn(String columnId, AFBoardColumnWidget columnWidget) {
  293. columnKeys[columnId] = columnWidget.globalKey;
  294. }
  295. ReorderFlexState? getReorderFlexState({required String columnId}) {
  296. final flexGlobalKey = columnKeys[columnId];
  297. if (flexGlobalKey == null) return null;
  298. if (flexGlobalKey.currentState is! ReorderFlexState) return null;
  299. final state = flexGlobalKey.currentState as ReorderFlexState;
  300. return state;
  301. }
  302. ReorderFlex? getReorderFlex({required String columnId}) {
  303. final flexGlobalKey = columnKeys[columnId];
  304. if (flexGlobalKey == null) return null;
  305. if (flexGlobalKey.currentWidget is! ReorderFlex) return null;
  306. final widget = flexGlobalKey.currentWidget as ReorderFlex;
  307. return widget;
  308. }
  309. @override
  310. DraggingState? read(String reorderFlexId) {
  311. return columnDragStates[reorderFlexId];
  312. }
  313. @override
  314. void write(String reorderFlexId, DraggingState state) {
  315. Log.trace('$reorderFlexId Write dragging state: $state');
  316. columnDragStates[reorderFlexId] = state;
  317. }
  318. @override
  319. void remove(String reorderFlexId) {
  320. columnDragStates.remove(reorderFlexId);
  321. }
  322. @override
  323. void addKey(
  324. String reorderFlexId,
  325. String key,
  326. GlobalObjectKey<State<StatefulWidget>> value,
  327. ) {
  328. Map<String, GlobalObjectKey>? column = columnDragDragTargets[reorderFlexId];
  329. if (column == null) {
  330. column = {};
  331. columnDragDragTargets[reorderFlexId] = column;
  332. }
  333. column[key] = value;
  334. }
  335. @override
  336. GlobalObjectKey<State<StatefulWidget>>? readKey(
  337. String reorderFlexId, String key) {
  338. Map<String, GlobalObjectKey>? column = columnDragDragTargets[reorderFlexId];
  339. if (column != null) {
  340. return column[key];
  341. } else {
  342. return null;
  343. }
  344. }
  345. }