reorderable_flex.dart 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107
  1. // Copyright 2018 The Chromium Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4. import 'dart:math';
  5. import 'package:flutter/material.dart';
  6. import 'package:flutter/rendering.dart';
  7. import './passthrough_overlay.dart';
  8. import './reorderable_mixin.dart';
  9. import './reorderable_widget.dart';
  10. import './typedefs.dart';
  11. /// Reorderable (drag and drop) version of [Flex], a widget that displays its
  12. /// draggable children in a one-dimensional array.
  13. ///
  14. /// The [ReorderableFlex] widget has allows you to control the axis along which
  15. /// the children are placed (horizontal or vertical). This is referred to as the
  16. /// [direction]. If you know the main axis in advance, then consider using
  17. /// a [ReorderableRow] (if it's horizontal) or [ReorderableColumn] (if it's
  18. /// vertical) instead, because that will be less verbose.
  19. ///
  20. /// In addition to other parameters in [Flex]'s constructor, this widget also
  21. /// has [header] and [footer] for placing non-reorderable widgets at the
  22. /// top/left and bottom/right of the widget. If further control is needed, you
  23. /// can use [buildItemsContainer] to customize how each item is contained, or
  24. /// use [buildDraggableFeedback] to customize the [feedback] of the internal
  25. /// [LongPressDraggable]. Consider using [ReorderableRow] or [ReorderableColumn]
  26. /// instead using this widget directly.
  27. ///
  28. /// All [children] must have a key.
  29. ///
  30. /// See also:
  31. ///
  32. /// * [ReorderableRow], for a version of this widget that is always horizontal.
  33. /// * [ReorderableColumn], for a version of this widget that is always vertical.
  34. class ReorderableFlex extends StatefulWidget {
  35. /// Creates a reorderable list.
  36. ReorderableFlex({
  37. Key? key,
  38. this.header,
  39. this.footer,
  40. required this.children,
  41. required this.onReorder,
  42. required this.direction,
  43. this.scrollDirection = Axis.vertical,
  44. this.padding,
  45. this.buildItemsContainer,
  46. this.buildDraggableFeedback,
  47. this.mainAxisAlignment = MainAxisAlignment.start,
  48. this.onNoReorder,
  49. this.onReorderStarted,
  50. this.scrollController,
  51. this.needsLongPressDraggable = true,
  52. this.draggingWidgetOpacity = 0.2,
  53. this.reorderAnimationDuration,
  54. this.scrollAnimationDuration,
  55. this.draggedItemBuilder,
  56. this.ignorePrimaryScrollController = false,
  57. }) : assert(
  58. children.every((Widget w) => w.key != null),
  59. 'All children of this widget must have a key.',
  60. ),
  61. super(key: key);
  62. /// A non-reorderable header widget to show before the list.
  63. ///
  64. /// If null, no header will appear at the top/left of the widget.
  65. final Widget? header;
  66. final Widget Function(BuildContext context, int index)? draggedItemBuilder;
  67. /// A non-reorderable footer widget to show after the list.
  68. ///
  69. /// If null, no footer will appear at the bottom/right of the widget.
  70. final Widget? footer;
  71. /// The widgets to display.
  72. final List<Widget> children;
  73. /// The [Axis] along which the list scrolls.
  74. ///
  75. /// List [children] can only drag along this [Axis].
  76. final Axis direction;
  77. final Axis scrollDirection;
  78. final ScrollController? scrollController;
  79. /// The amount of space by which to inset the [children].
  80. final EdgeInsets? padding;
  81. /// Called when a child is dropped into a new position to shuffle the
  82. /// children.
  83. final ReorderCallback onReorder;
  84. final NoReorderCallback? onNoReorder;
  85. /// Called when the draggable starts being dragged.
  86. final ReorderStartedCallback? onReorderStarted;
  87. final BuildItemsContainer? buildItemsContainer;
  88. final BuildDraggableFeedback? buildDraggableFeedback;
  89. final MainAxisAlignment mainAxisAlignment;
  90. final bool needsLongPressDraggable;
  91. final double draggingWidgetOpacity;
  92. final Duration? reorderAnimationDuration;
  93. final Duration? scrollAnimationDuration;
  94. final bool ignorePrimaryScrollController;
  95. @override
  96. _ReorderableFlexState createState() => _ReorderableFlexState();
  97. }
  98. // This top-level state manages an Overlay that contains the list and
  99. // also any Draggables it creates.
  100. //
  101. // _ReorderableListContent manages the list itself and reorder operations.
  102. //
  103. // The Overlay doesn't properly keep state by building new overlay entries,
  104. // and so we cache a single OverlayEntry for use as the list layer.
  105. // That overlay entry then builds a _ReorderableListContent which may
  106. // insert Draggables into the Overlay above itself.
  107. class _ReorderableFlexState extends State<ReorderableFlex> {
  108. // We use an inner overlay so that the dragging list item doesn't draw outside of the list itself.
  109. final GlobalKey _overlayKey =
  110. GlobalKey(debugLabel: '$ReorderableFlex overlay key');
  111. // This entry contains the scrolling list itself.
  112. late PassthroughOverlayEntry _listOverlayEntry;
  113. @override
  114. void initState() {
  115. super.initState();
  116. _listOverlayEntry = PassthroughOverlayEntry(
  117. opaque: false,
  118. builder: (BuildContext context) {
  119. return _ReorderableFlexContent(
  120. header: widget.header,
  121. footer: widget.footer,
  122. children: widget.children,
  123. direction: widget.direction,
  124. scrollDirection: widget.scrollDirection,
  125. onReorder: widget.onReorder,
  126. onNoReorder: widget.onNoReorder,
  127. onReorderStarted: widget.onReorderStarted,
  128. padding: widget.padding,
  129. buildItemsContainer: widget.buildItemsContainer,
  130. buildDraggableFeedback: widget.buildDraggableFeedback,
  131. mainAxisAlignment: widget.mainAxisAlignment,
  132. scrollController: widget.scrollController,
  133. needsLongPressDraggable: widget.needsLongPressDraggable,
  134. draggingWidgetOpacity: widget.draggingWidgetOpacity,
  135. draggedItemBuilder: widget.draggedItemBuilder,
  136. reorderAnimationDuration: widget.reorderAnimationDuration ??
  137. const Duration(milliseconds: 200),
  138. scrollAnimationDuration: widget.scrollAnimationDuration ??
  139. const Duration(milliseconds: 200),
  140. );
  141. },
  142. );
  143. }
  144. @override
  145. Widget build(BuildContext context) {
  146. final PassthroughOverlay passthroughOverlay = PassthroughOverlay(
  147. key: _overlayKey,
  148. initialEntries: <PassthroughOverlayEntry>[
  149. _listOverlayEntry,
  150. ]);
  151. return widget.ignorePrimaryScrollController
  152. ? PrimaryScrollController.none(child: passthroughOverlay)
  153. : passthroughOverlay;
  154. }
  155. }
  156. // This widget is responsible for the inside of the Overlay in the
  157. // ReorderableFlex.
  158. class _ReorderableFlexContent extends StatefulWidget {
  159. const _ReorderableFlexContent({
  160. this.header,
  161. this.footer,
  162. required this.children,
  163. required this.direction,
  164. required this.scrollDirection,
  165. required this.onReorder,
  166. required this.onNoReorder,
  167. required this.onReorderStarted,
  168. required this.mainAxisAlignment,
  169. required this.scrollController,
  170. required this.needsLongPressDraggable,
  171. required this.draggingWidgetOpacity,
  172. required this.buildItemsContainer,
  173. required this.buildDraggableFeedback,
  174. required this.padding,
  175. this.draggedItemBuilder,
  176. this.reorderAnimationDuration = const Duration(milliseconds: 200),
  177. this.scrollAnimationDuration = const Duration(milliseconds: 200),
  178. });
  179. final Widget? header;
  180. final Widget? footer;
  181. final List<Widget> children;
  182. final Axis direction;
  183. final Axis scrollDirection;
  184. final ReorderCallback onReorder;
  185. final NoReorderCallback? onNoReorder;
  186. final ReorderStartedCallback? onReorderStarted;
  187. final BuildItemsContainer? buildItemsContainer;
  188. final BuildDraggableFeedback? buildDraggableFeedback;
  189. final ScrollController? scrollController;
  190. final EdgeInsets? padding;
  191. final Widget Function(BuildContext context, int index)? draggedItemBuilder;
  192. final MainAxisAlignment mainAxisAlignment;
  193. final bool needsLongPressDraggable;
  194. final double draggingWidgetOpacity;
  195. final Duration reorderAnimationDuration;
  196. final Duration scrollAnimationDuration;
  197. @override
  198. _ReorderableFlexContentState createState() => _ReorderableFlexContentState();
  199. }
  200. class _ReorderableFlexContentState extends State<_ReorderableFlexContent>
  201. with TickerProviderStateMixin<_ReorderableFlexContent>, ReorderableMixin {
  202. // The extent along the [widget.scrollDirection] axis to allow a child to
  203. // drop into when the user reorders list children.
  204. //
  205. // This value is used when the extents haven't yet been calculated from
  206. // the currently dragging widget, such as when it first builds.
  207. // static const double _defaultDropAreaExtent = 1.0;
  208. // The additional margin to place around a computed drop area.
  209. static const double _dropAreaMargin = 0.0;
  210. // How long an animation to reorder an element in the list takes.
  211. late Duration _reorderAnimationDuration;
  212. // How long an animation to scroll to an off-screen element in the
  213. // list takes.
  214. late Duration _scrollAnimationDuration;
  215. // Controls scrolls and measures scroll progress.
  216. late ScrollController _scrollController;
  217. ScrollPosition? _attachedScrollPosition;
  218. // This controls the entrance of the dragging widget into a new place.
  219. late AnimationController _entranceController;
  220. // This controls the 'ghost' of the dragging widget, which is left behind
  221. // where the widget used to be.
  222. late AnimationController _ghostController;
  223. // The member of widget.children currently being dragged.
  224. //
  225. // Null if no drag is underway.
  226. Widget? _draggingWidget;
  227. // The last computed size of the feedback widget being dragged.
  228. Size? _draggingFeedbackSize = Size(0, 0);
  229. // The location that the dragging widget occupied before it started to drag.
  230. int _dragStartIndex = -1;
  231. // The index that the dragging widget most recently left.
  232. // This is used to show an animation of the widget's position.
  233. int _ghostIndex = -1;
  234. // The index that the dragging widget currently occupies.
  235. int _currentIndex = -1;
  236. // The widget to move the dragging widget too after the current index.
  237. int _nextIndex = 0;
  238. // Whether or not we are currently scrolling this view to show a widget.
  239. bool _scrolling = false;
  240. // final GlobalKey _contentKey = GlobalKey(debugLabel: '$ReorderableFlex content key');
  241. Size get _dropAreaSize {
  242. if (_draggingFeedbackSize == null) {
  243. return Size(0, 0);
  244. }
  245. return _draggingFeedbackSize! + Offset(_dropAreaMargin, _dropAreaMargin);
  246. // double dropAreaWithoutMargin;
  247. // switch (widget.direction) {
  248. // case Axis.horizontal:
  249. // dropAreaWithoutMargin = _draggingFeedbackSize.width;
  250. // break;
  251. // case Axis.vertical:
  252. // default:
  253. // dropAreaWithoutMargin = _draggingFeedbackSize.height;
  254. // break;
  255. // }
  256. // return dropAreaWithoutMargin + _dropAreaMargin;
  257. }
  258. @override
  259. void initState() {
  260. super.initState();
  261. _reorderAnimationDuration = widget.reorderAnimationDuration;
  262. _scrollAnimationDuration = widget.scrollAnimationDuration;
  263. _entranceController = AnimationController(
  264. value: 1.0, vsync: this, duration: _reorderAnimationDuration);
  265. _ghostController = AnimationController(
  266. value: 0, vsync: this, duration: _reorderAnimationDuration);
  267. _entranceController.addStatusListener(_onEntranceStatusChanged);
  268. }
  269. @override
  270. void didChangeDependencies() {
  271. if (_attachedScrollPosition != null) {
  272. _scrollController.detach(_attachedScrollPosition!);
  273. _attachedScrollPosition = null;
  274. }
  275. _scrollController = widget.scrollController ??
  276. PrimaryScrollController.maybeOf(context) ??
  277. ScrollController();
  278. if (_scrollController.hasClients) {
  279. _attachedScrollPosition = Scrollable.maybeOf(context)?.position;
  280. } else {
  281. _attachedScrollPosition = null;
  282. }
  283. if (_attachedScrollPosition != null) {
  284. _scrollController.attach(_attachedScrollPosition!);
  285. }
  286. super.didChangeDependencies();
  287. }
  288. @override
  289. void dispose() {
  290. if (_attachedScrollPosition != null) {
  291. _scrollController.detach(_attachedScrollPosition!);
  292. _attachedScrollPosition = null;
  293. }
  294. _entranceController.dispose();
  295. _ghostController.dispose();
  296. super.dispose();
  297. }
  298. // Animates the droppable space from _currentIndex to _nextIndex.
  299. void _requestAnimationToNextIndex({bool isAcceptingNewTarget = false}) {
  300. // debugPrint('${DateTime.now().toString().substring(5, 22)} reorderable_flex.dart(285) $this._requestAnimationToNextIndex: '
  301. // '_dragStartIndex:$_dragStartIndex _ghostIndex:$_ghostIndex _currentIndex:$_currentIndex _nextIndex:$_nextIndex isAcceptingNewTarget:$isAcceptingNewTarget isCompleted:${_entranceController.isCompleted}');
  302. if (_entranceController.isCompleted) {
  303. _ghostIndex = _currentIndex;
  304. if (!isAcceptingNewTarget && _nextIndex == _currentIndex) {
  305. // && _dragStartIndex == _ghostIndex
  306. return;
  307. }
  308. _currentIndex = _nextIndex;
  309. _ghostController.reverse(from: 1.0);
  310. _entranceController.forward(from: 0.0);
  311. }
  312. }
  313. // Requests animation to the latest next index if it changes during an animation.
  314. void _onEntranceStatusChanged(AnimationStatus status) {
  315. if (status == AnimationStatus.completed) {
  316. setState(() {
  317. _requestAnimationToNextIndex();
  318. });
  319. }
  320. }
  321. // Scrolls to a target context if that context is not on the screen.
  322. void _scrollTo(BuildContext context) {
  323. if (_scrolling) return;
  324. final RenderObject contextObject = context.findRenderObject()!;
  325. final RenderAbstractViewport viewport =
  326. RenderAbstractViewport.of(contextObject);
  327. // If and only if the current scroll offset falls in-between the offsets
  328. // necessary to reveal the selected context at the top or bottom of the
  329. // screen, then it is already on-screen.
  330. final double margin = widget.direction == Axis.horizontal
  331. ? _dropAreaSize.width
  332. : _dropAreaSize.height;
  333. if (_scrollController.hasClients) {
  334. final double scrollOffset = _scrollController.offset;
  335. final double topOffset = max(
  336. _scrollController.position.minScrollExtent,
  337. viewport.getOffsetToReveal(contextObject, 0.0).offset - margin,
  338. );
  339. final double bottomOffset = min(
  340. _scrollController.position.maxScrollExtent,
  341. viewport.getOffsetToReveal(contextObject, 1.0).offset + margin,
  342. );
  343. final bool onScreen =
  344. scrollOffset <= topOffset && scrollOffset >= bottomOffset;
  345. // If the context is off screen, then we request a scroll to make it visible.
  346. if (!onScreen) {
  347. _scrolling = true;
  348. _scrollController.position
  349. .animateTo(
  350. scrollOffset < bottomOffset ? bottomOffset : topOffset,
  351. duration: _scrollAnimationDuration,
  352. curve: Curves.easeInOut,
  353. )
  354. .then((void value) {
  355. setState(() {
  356. _scrolling = false;
  357. });
  358. });
  359. }
  360. }
  361. }
  362. // Wraps children in Row or Column, so that the children flow in
  363. // the widget's scrollDirection.
  364. Widget _buildContainerForMainAxis({required List<Widget> children}) {
  365. switch (widget.direction) {
  366. case Axis.horizontal:
  367. return Row(
  368. mainAxisSize: MainAxisSize.min,
  369. children: children,
  370. mainAxisAlignment: widget.mainAxisAlignment);
  371. case Axis.vertical:
  372. default:
  373. return Column(
  374. mainAxisSize: MainAxisSize.min,
  375. children: children,
  376. mainAxisAlignment: widget.mainAxisAlignment);
  377. }
  378. }
  379. // Wraps one of the widget's children in a DragTarget and Draggable.
  380. // Handles up the logic for dragging and reordering items in the list.
  381. Widget _wrap(Widget toWrap, int index) {
  382. assert(toWrap.key != null);
  383. final GlobalObjectKey keyIndexGlobalKey = GlobalObjectKey(toWrap.key!);
  384. // We pass the toWrapWithGlobalKey into the Draggable so that when a list
  385. // item gets dragged, the accessibility framework can preserve the selected
  386. // state of the dragging item.
  387. final draggedItem =
  388. widget.draggedItemBuilder?.call(context, index) ?? toWrap;
  389. // Starts dragging toWrap.
  390. void onDragStarted() {
  391. setState(() {
  392. _draggingWidget = draggedItem;
  393. _dragStartIndex = index;
  394. _ghostIndex = index;
  395. _currentIndex = index;
  396. _entranceController.value = 1.0;
  397. _draggingFeedbackSize = keyIndexGlobalKey.currentContext?.size;
  398. });
  399. widget.onReorderStarted?.call(index);
  400. }
  401. // Places the value from startIndex one space before the element at endIndex.
  402. void _reorder(int startIndex, int endIndex) {
  403. // debugPrint('startIndex:$startIndex endIndex:$endIndex');
  404. if (startIndex != endIndex)
  405. widget.onReorder(startIndex, endIndex);
  406. else if (widget.onNoReorder != null) widget.onNoReorder!(startIndex);
  407. // Animates leftover space in the drop area closed.
  408. // TODO(djshuckerow): bring the animation in line with the Material
  409. // specifications.
  410. _ghostController.reverse(from: 0.1);
  411. _entranceController.reverse(from: 0);
  412. }
  413. void reorder(int startIndex, int endIndex) {
  414. // debugPrint('startIndex:$startIndex endIndex:$endIndex');
  415. setState(() {
  416. _reorder(startIndex, endIndex);
  417. });
  418. }
  419. // Drops toWrap into the last position it was hovering over.
  420. void onDragEnded() {
  421. // reorder(_dragStartIndex, _currentIndex);
  422. setState(() {
  423. _reorder(_dragStartIndex, _currentIndex);
  424. _dragStartIndex = -1;
  425. _ghostIndex = -1;
  426. _currentIndex = -1;
  427. _draggingWidget = null;
  428. });
  429. }
  430. Widget wrapWithSemantics() {
  431. // First, determine which semantics actions apply.
  432. final Map<CustomSemanticsAction, VoidCallback> semanticsActions =
  433. <CustomSemanticsAction, VoidCallback>{};
  434. // Create the appropriate semantics actions.
  435. void moveToStart() => reorder(index, 0);
  436. void moveToEnd() => reorder(index, widget.children.length - 1);
  437. void moveBefore() => reorder(index, index - 1);
  438. // To move after, we go to index+2 because we are moving it to the space
  439. // before index+2, which is after the space at index+1.
  440. void moveAfter() => reorder(index, index + 2);
  441. final MaterialLocalizations localizations =
  442. MaterialLocalizations.of(context);
  443. if (index > 0) {
  444. semanticsActions[CustomSemanticsAction(
  445. label: localizations.reorderItemToStart)] = moveToStart;
  446. String reorderItemBefore = localizations.reorderItemUp;
  447. if (widget.direction == Axis.horizontal) {
  448. reorderItemBefore = Directionality.of(context) == TextDirection.ltr
  449. ? localizations.reorderItemLeft
  450. : localizations.reorderItemRight;
  451. }
  452. semanticsActions[CustomSemanticsAction(label: reorderItemBefore)] =
  453. moveBefore;
  454. }
  455. // If the item can move to after its current position in the list.
  456. if (index < widget.children.length - 1) {
  457. String reorderItemAfter = localizations.reorderItemDown;
  458. if (widget.direction == Axis.horizontal) {
  459. reorderItemAfter = Directionality.of(context) == TextDirection.ltr
  460. ? localizations.reorderItemRight
  461. : localizations.reorderItemLeft;
  462. }
  463. semanticsActions[CustomSemanticsAction(label: reorderItemAfter)] =
  464. moveAfter;
  465. semanticsActions[
  466. CustomSemanticsAction(label: localizations.reorderItemToEnd)] =
  467. moveToEnd;
  468. }
  469. // We pass toWrap with a GlobalKey into the Draggable so that when a list
  470. // item gets dragged, the accessibility framework can preserve the selected
  471. // state of the dragging item.
  472. //
  473. // We also apply the relevant custom accessibility actions for moving the item
  474. // up, down, to the start, and to the end of the list.
  475. return MergeSemantics(
  476. child: Semantics(
  477. customSemanticsActions: semanticsActions,
  478. child: toWrap,
  479. ),
  480. );
  481. // return KeyedSubtree(
  482. // key: keyIndexGlobalKey,
  483. // child: MergeSemantics(
  484. // child: Semantics(
  485. // customSemanticsActions: semanticsActions,
  486. // child: toWrap,
  487. // ),
  488. // ),
  489. // );
  490. }
  491. Widget _makeAppearingWidget(Widget child) {
  492. return makeAppearingWidget(
  493. child,
  494. _entranceController,
  495. _draggingFeedbackSize,
  496. widget.direction,
  497. );
  498. }
  499. Widget _makeDisappearingWidget(Widget child) {
  500. return makeDisappearingWidget(
  501. child,
  502. _ghostController,
  503. _draggingFeedbackSize,
  504. widget.direction,
  505. );
  506. }
  507. Widget buildDragTarget(BuildContext context, List<int?> acceptedCandidates,
  508. List<dynamic> rejectedCandidates) {
  509. final Widget toWrapWithSemantics = wrapWithSemantics();
  510. Widget feedbackBuilder = Builder(builder: (BuildContext context) {
  511. // RenderRepaintBoundary renderObject = _contentKey.currentContext.findRenderObject();
  512. // BoxConstraints contentSizeConstraints = BoxConstraints.loose(renderObject.size);
  513. BoxConstraints contentSizeConstraints = BoxConstraints.loose(
  514. _draggingFeedbackSize!); //renderObject.constraints
  515. // debugPrint('${DateTime.now().toString().substring(5, 22)} reorderable_flex.dart(515) $this.buildDragTarget: contentConstraints:$contentSizeConstraints _draggingFeedbackSize:$_draggingFeedbackSize');
  516. return (widget.buildDraggableFeedback ?? defaultBuildDraggableFeedback)(
  517. context, contentSizeConstraints, draggedItem);
  518. });
  519. // We build the draggable inside of a layout builder so that we can
  520. // constrain the size of the feedback dragging widget.
  521. bool isReorderable = true;
  522. if (toWrap is ReorderableWidget) {
  523. isReorderable = toWrap.reorderable;
  524. }
  525. Widget child;
  526. if (!isReorderable) {
  527. child = toWrap;
  528. } else {
  529. child = widget.needsLongPressDraggable
  530. ? LongPressDraggable<int>(
  531. maxSimultaneousDrags: 1,
  532. axis: widget.direction,
  533. data: index,
  534. ignoringFeedbackSemantics: false,
  535. // feedback: Container(
  536. // alignment: Alignment.topLeft,
  537. // // These constraints will limit the cross axis of the drawn widget.
  538. // constraints: constraints,
  539. // child: Material(
  540. // elevation: 6.0,
  541. // child: IntrinsicWidth(child: toWrapWithSemantics),
  542. // ),
  543. // ),
  544. feedback: feedbackBuilder,
  545. // feedback: Transform(
  546. // transform: new Matrix4.rotationZ(0),
  547. // alignment: FractionalOffset.topLeft,
  548. // child: Material(
  549. // child: Card(child: ConstrainedBox(constraints: BoxConstraints.tightFor(width: 100), child: toWrapWithSemantics)),
  550. // elevation: 6.0,
  551. // color: Colors.transparent,
  552. // borderRadius: BorderRadius.zero,
  553. // ),
  554. // ),
  555. // Wrap toWrapWithSemantics with a widget that supports HitTestBehavior
  556. // to make sure the whole toWrapWithSemantics responds to pointer events, i.e. dragging
  557. child: MetaData(
  558. child: toWrapWithSemantics,
  559. behavior: HitTestBehavior.opaque),
  560. //toWrapWithSemantics,//_dragging == toWrap.key ? const SizedBox() : toWrapWithSemantics,
  561. childWhenDragging: IgnorePointer(
  562. ignoring: true,
  563. child: Opacity(
  564. opacity: 0,
  565. child: Container(width: 0, height: 0, child: toWrap))),
  566. onDragStarted: onDragStarted,
  567. dragAnchorStrategy: childDragAnchorStrategy,
  568. // When the drag ends inside a DragTarget widget, the drag
  569. // succeeds, and we reorder the widget into position appropriately.
  570. onDragCompleted: onDragEnded,
  571. // When the drag does not end inside a DragTarget widget, the
  572. // drag fails, but we still reorder the widget to the last position it
  573. // had been dragged to.
  574. onDraggableCanceled: (Velocity velocity, Offset offset) =>
  575. onDragEnded(),
  576. )
  577. : Draggable<int>(
  578. maxSimultaneousDrags: 1,
  579. axis: widget.direction,
  580. data: index,
  581. ignoringFeedbackSemantics: false,
  582. feedback: feedbackBuilder,
  583. // Wrap toWrapWithSemantics with a widget that supports HitTestBehavior
  584. // to make sure the whole toWrapWithSemantics responds to pointer events, i.e. dragging
  585. child: MetaData(
  586. child: toWrapWithSemantics,
  587. behavior: HitTestBehavior.opaque),
  588. childWhenDragging: IgnorePointer(
  589. ignoring: true,
  590. child: Opacity(
  591. opacity: 0,
  592. child: Container(width: 0, height: 0, child: toWrap))),
  593. onDragStarted: onDragStarted,
  594. dragAnchorStrategy: childDragAnchorStrategy,
  595. // When the drag ends inside a DragTarget widget, the drag
  596. // succeeds, and we reorder the widget into position appropriately.
  597. onDragCompleted: onDragEnded,
  598. // When the drag does not end inside a DragTarget widget, the
  599. // drag fails, but we still reorder the widget to the last position it
  600. // had been dragged to.
  601. onDraggableCanceled: (Velocity velocity, Offset offset) =>
  602. onDragEnded(),
  603. );
  604. }
  605. // The target for dropping at the end of the list doesn't need to be
  606. // draggable.
  607. if (index >= widget.children.length) {
  608. child = toWrap;
  609. }
  610. // // Determine the size of the drop area to show under the dragging widget.
  611. // Widget spacing;
  612. // switch (widget.direction) {
  613. // case Axis.horizontal:
  614. // spacing = SizedBox(width: _dropAreaExtent);
  615. // break;
  616. // case Axis.vertical:
  617. // default:
  618. // spacing = SizedBox(height: _dropAreaExtent);
  619. // break;
  620. // }
  621. // // We open up a space under where the dragging widget currently is to
  622. // // show it can be dropped.
  623. // if (_currentIndex == index) {
  624. // Widget entranceSpacing = SizeTransition(
  625. // sizeFactor: _entranceController,
  626. // axis: widget.direction,
  627. // child: Row(children: [spacing, Text('eeeeeeeeeeeee $index')])
  628. // );
  629. //
  630. // return _buildContainerForScrollDirection(children: _currentIndex > _ghostIndex ? [child, entranceSpacing] : [entranceSpacing, child]);
  631. // }
  632. // // We close up the space under where the dragging widget previously was
  633. // // with the ghostController animation.
  634. // if (_ghostIndex == index) {
  635. // Widget ghostSpacing = SizeTransition(
  636. // sizeFactor: _ghostController,
  637. // axis: widget.direction,
  638. // child: Row(children: [spacing, Text('gggggggg $index')]),
  639. // );
  640. // return _buildContainerForScrollDirection(children: _currentIndex > _ghostIndex ? [child, ghostSpacing] : [ghostSpacing, child]);
  641. // }
  642. return child;
  643. }
  644. // We wrap the drag target in a Builder so that we can scroll to its specific context.
  645. return Builder(builder: (BuildContext context) {
  646. Widget dragTarget = DragTarget<int>(
  647. builder: buildDragTarget,
  648. onWillAccept: (int? toAccept) {
  649. bool willAccept = _dragStartIndex == toAccept && toAccept != index;
  650. // debugPrint('${DateTime.now().toString().substring(5, 22)} reorderable_flex.dart(609) $this._wrap: '
  651. // 'onWillAccept: toAccept:$toAccept return:$willAccept _nextIndex:$_nextIndex index:$index _currentIndex:$_currentIndex _dragStartIndex:$_dragStartIndex');
  652. setState(() {
  653. if (willAccept) {
  654. int shiftedIndex = index;
  655. if (index == _dragStartIndex) {
  656. shiftedIndex = _ghostIndex;
  657. } else if (index > _dragStartIndex && index <= _ghostIndex) {
  658. shiftedIndex--;
  659. } else if (index < _dragStartIndex && index >= _ghostIndex) {
  660. shiftedIndex++;
  661. }
  662. _nextIndex = shiftedIndex;
  663. } else {
  664. _nextIndex = index;
  665. }
  666. _requestAnimationToNextIndex(isAcceptingNewTarget: true);
  667. });
  668. _scrollTo(context);
  669. // If the target is not the original starting point, then we will accept the drop.
  670. return willAccept; //_dragging == toAccept && toAccept != toWrap.key;
  671. },
  672. onAccept: (int accepted) {},
  673. onLeave: (Object? leaving) {},
  674. );
  675. dragTarget = KeyedSubtree(key: keyIndexGlobalKey, child: dragTarget);
  676. // Determine the size of the drop area to show under the dragging widget.
  677. Widget spacing = _draggingWidget == null
  678. ? SizedBox.fromSize(size: _dropAreaSize)
  679. : Opacity(
  680. opacity: widget.draggingWidgetOpacity, child: _draggingWidget);
  681. // Widget spacing = SizedBox.fromSize(
  682. // size: _dropAreaSize,
  683. // child: _draggingWidget != null ? Opacity(opacity: 0.2, child: _draggingWidget) : null,
  684. // );
  685. // We open up a space under where the dragging widget currently is to
  686. // show it can be dropped.
  687. int shiftedIndex = index;
  688. if (_currentIndex != _ghostIndex) {
  689. if (index == _dragStartIndex) {
  690. shiftedIndex = _ghostIndex;
  691. } else if (index > _dragStartIndex && index <= _ghostIndex) {
  692. shiftedIndex--;
  693. } else if (index < _dragStartIndex && index >= _ghostIndex) {
  694. shiftedIndex++;
  695. }
  696. }
  697. // debugPrint('${DateTime.now().toString().substring(5, 22)} reorderable_flex.dart(659) $this._wrap: '
  698. // 'index:$index shiftedIndex:$shiftedIndex _nextIndex:$_nextIndex _currentIndex:$_currentIndex _ghostIndex:$_ghostIndex _dragStartIndex:$_dragStartIndex');
  699. if (shiftedIndex == _currentIndex || index == _ghostIndex) {
  700. Widget entranceSpacing = _makeAppearingWidget(spacing);
  701. Widget ghostSpacing = _makeDisappearingWidget(spacing);
  702. if (_dragStartIndex == -1) {
  703. return _buildContainerForMainAxis(children: [dragTarget]);
  704. } else if (_currentIndex > _ghostIndex) {
  705. //the ghost is moving down, i.e. the tile below the ghost is moving up
  706. // debugPrint('index:$index item moving up / ghost moving down');
  707. if (shiftedIndex == _currentIndex && index == _ghostIndex) {
  708. return _buildContainerForMainAxis(
  709. children: [ghostSpacing, dragTarget, entranceSpacing]);
  710. } else if (shiftedIndex == _currentIndex) {
  711. return _buildContainerForMainAxis(
  712. children: [dragTarget, entranceSpacing]);
  713. } else if (index == _ghostIndex) {
  714. return _buildContainerForMainAxis(
  715. children: shiftedIndex <= index
  716. ? [dragTarget, ghostSpacing]
  717. : [ghostSpacing, dragTarget]);
  718. }
  719. } else if (_currentIndex < _ghostIndex) {
  720. //the ghost is moving up, i.e. the tile above the ghost is moving down
  721. // debugPrint('index:$index item moving down / ghost moving up');
  722. if (shiftedIndex == _currentIndex && index == _ghostIndex) {
  723. return _buildContainerForMainAxis(
  724. children: [entranceSpacing, dragTarget, ghostSpacing]);
  725. } else if (shiftedIndex == _currentIndex) {
  726. return _buildContainerForMainAxis(
  727. children: [entranceSpacing, dragTarget]);
  728. } else if (index == _ghostIndex) {
  729. return _buildContainerForMainAxis(
  730. children: shiftedIndex >= index
  731. ? [ghostSpacing, dragTarget]
  732. : [dragTarget, ghostSpacing]);
  733. }
  734. } else {
  735. // debugPrint('index:$index using _entranceController: spacing on top:${!(_dragStartIndex < _currentIndex)}');
  736. return _buildContainerForMainAxis(
  737. children: _dragStartIndex < _currentIndex
  738. ? [dragTarget, entranceSpacing]
  739. : [entranceSpacing, dragTarget]);
  740. }
  741. }
  742. //we still wrap dragTarget with a container so that widget's depths are the same and it prevent's layout alignment issue
  743. return _buildContainerForMainAxis(children: [dragTarget]);
  744. });
  745. }
  746. @override
  747. Widget build(BuildContext context) {
  748. // assert(debugCheckHasMaterialLocalizations(context));
  749. // We use the layout builder to constrain the cross-axis size of dragging child widgets.
  750. // return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
  751. final List<Widget> wrappedChildren = <Widget>[];
  752. if (widget.header != null) {
  753. wrappedChildren.add(widget.header!);
  754. }
  755. for (int i = 0; i < widget.children.length; i += 1) {
  756. wrappedChildren.add(_wrap(widget.children[i], i));
  757. }
  758. if (widget.footer != null) {
  759. wrappedChildren.add(widget.footer!);
  760. }
  761. // const Key endWidgetKey = Key('DraggableList - End Widget');
  762. // Widget finalDropArea;
  763. // switch (widget.direction) {
  764. // case Axis.horizontal:
  765. // finalDropArea = SizedBox(
  766. // key: endWidgetKey,
  767. // width: _defaultDropAreaExtent,
  768. // height: double.infinity,//constraints.maxHeight,
  769. // );
  770. // break;
  771. // case Axis.vertical:
  772. // default:
  773. // finalDropArea = SizedBox(
  774. // key: endWidgetKey,
  775. // height: _defaultDropAreaExtent,
  776. // width: double.infinity,//constraints.maxWidth,
  777. // );
  778. // break;
  779. // }
  780. // wrappedChildren.add(_wrap(
  781. // finalDropArea,
  782. // widget.children.length,
  783. // constraints),
  784. // );
  785. // return _buildContainerForScrollDirection(children: wrappedChildren);
  786. // return SingleChildScrollView(
  787. // scrollDirection: widget.direction,
  788. // child: _buildContainerForScrollDirection(children: wrappedChildren),
  789. // padding: widget.padding,
  790. // controller: _scrollController,
  791. // );
  792. if (widget.scrollController != null &&
  793. PrimaryScrollController.maybeOf(context) == null) {
  794. return (widget.buildItemsContainer ?? defaultBuildItemsContainer)(
  795. context, widget.direction, wrappedChildren);
  796. } else {
  797. return SingleChildScrollView(
  798. // key: _contentKey,
  799. scrollDirection: widget.scrollDirection,
  800. child: (widget.buildItemsContainer ?? defaultBuildItemsContainer)(
  801. context, widget.direction, wrappedChildren),
  802. padding: widget.padding,
  803. controller: _scrollController,
  804. );
  805. }
  806. // });
  807. }
  808. // @override
  809. // void afterFirstLayout(BuildContext context) {
  810. // // Calling the same function "after layout" to resolve the issue.
  811. //// showHelloWorld();
  812. // debugPrint('size:' + context.size.toString());
  813. // debugPrint('constraints:' + context.findRenderObject().constraints.toString());
  814. // context.visitChildElements((Element element) {
  815. // debugPrint('element $element size:' + element.size.toString());
  816. // debugPrint('element $element constraints:' + element.findRenderObject().constraints.toString());
  817. // });
  818. //// if (widget.header != null) {
  819. //// Row headerRow = widget.header;
  820. //// headerRow.
  821. //// }
  822. // }
  823. Widget defaultBuildItemsContainer(
  824. BuildContext context, Axis direction, List<Widget> children) {
  825. switch (direction) {
  826. case Axis.horizontal:
  827. return Row(children: children);
  828. case Axis.vertical:
  829. default:
  830. return Column(children: children);
  831. }
  832. }
  833. Widget defaultBuildDraggableFeedback(
  834. BuildContext context, BoxConstraints constraints, Widget child) {
  835. return Transform(
  836. transform: Matrix4.rotationZ(0),
  837. alignment: FractionalOffset.topLeft,
  838. child: Material(
  839. child:
  840. Card(child: ConstrainedBox(constraints: constraints, child: child)),
  841. elevation: 6.0,
  842. color: Colors.transparent,
  843. borderRadius: BorderRadius.zero,
  844. ),
  845. );
  846. }
  847. }
  848. /// Reorderable (drag and drop) version of [Row], a widget that displays its
  849. /// draggable children in horizontally.
  850. ///
  851. /// In addition to other parameters in [Row]'s constructor, this widget also
  852. /// has [header] and [footer] for placing non-reorderable widgets at the top and
  853. /// bottom of the widget, and [buildDraggableFeedback] to customize the
  854. /// [feedback] widget of the internal [LongPressDraggable].
  855. ///
  856. /// The [onReorder] function must be defined. A typical onReorder function looks
  857. /// like the following:
  858. ///
  859. /// ``` dart
  860. /// void _onReorder(int oldIndex, int newIndex) {
  861. /// setState(() {
  862. /// Widget row = _rows.removeAt(oldIndex);
  863. /// _rows.insert(newIndex, row);
  864. /// });
  865. /// }
  866. /// ```
  867. ///
  868. /// All [children] must have a key.
  869. ///
  870. /// See also:
  871. ///
  872. /// * [ReorderableColumn], for a version of this widget that is always vertical.
  873. class ReorderableRow extends ReorderableFlex {
  874. ReorderableRow({
  875. required ReorderCallback onReorder,
  876. Key? key,
  877. Widget? header,
  878. Widget? footer,
  879. EdgeInsets? padding,
  880. MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
  881. MainAxisSize mainAxisSize = MainAxisSize.max,
  882. CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
  883. TextDirection? textDirection,
  884. VerticalDirection verticalDirection = VerticalDirection.down,
  885. TextBaseline? textBaseline,
  886. List<Widget> children = const <Widget>[],
  887. BuildDraggableFeedback? buildDraggableFeedback,
  888. NoReorderCallback? onNoReorder,
  889. ReorderStartedCallback? onReorderStarted,
  890. ScrollController? scrollController,
  891. bool needsLongPressDraggable = true,
  892. double draggingWidgetOpacity = 0.2,
  893. Duration? reorderAnimationDuration,
  894. Duration? scrollAnimationDuration,
  895. Widget Function(BuildContext context, int index)? draggedItemBuilder,
  896. bool ignorePrimaryScrollController = false,
  897. }) : super(
  898. key: key,
  899. header: header,
  900. footer: footer,
  901. children: children,
  902. onReorder: onReorder,
  903. onNoReorder: onNoReorder,
  904. onReorderStarted: onReorderStarted,
  905. direction: Axis.horizontal,
  906. scrollDirection: Axis.horizontal,
  907. padding: padding,
  908. draggedItemBuilder: draggedItemBuilder,
  909. buildItemsContainer:
  910. (BuildContext context, Axis direction, List<Widget> children) {
  911. return Flex(
  912. direction: direction,
  913. mainAxisAlignment: mainAxisAlignment,
  914. mainAxisSize: mainAxisSize,
  915. crossAxisAlignment: crossAxisAlignment,
  916. textDirection: textDirection,
  917. verticalDirection: verticalDirection,
  918. textBaseline: textBaseline,
  919. children: children);
  920. },
  921. buildDraggableFeedback: buildDraggableFeedback,
  922. mainAxisAlignment: mainAxisAlignment,
  923. scrollController: scrollController,
  924. needsLongPressDraggable: needsLongPressDraggable,
  925. draggingWidgetOpacity: draggingWidgetOpacity,
  926. reorderAnimationDuration: reorderAnimationDuration,
  927. scrollAnimationDuration: scrollAnimationDuration,
  928. ignorePrimaryScrollController: ignorePrimaryScrollController);
  929. }
  930. /// Reorderable (drag and drop) version of [Column], a widget that displays its
  931. /// draggable children in horizontally.
  932. ///
  933. /// In addition to other parameters in [Column]'s constructor, this widget also
  934. /// has [header] and [footer] for placing non-reorderable widgets at the left and
  935. /// right of the widget, and [buildDraggableFeedback] to customize the
  936. /// [feedback] widget of the internal [LongPressDraggable].
  937. ///
  938. /// The [onReorder] function must be defined. A typical onReorder function looks
  939. /// like the following:
  940. ///
  941. /// ``` dart
  942. /// void _onReorder(int oldIndex, int newIndex) {
  943. /// setState(() {
  944. /// Widget row = _rows.removeAt(oldIndex);
  945. /// _rows.insert(newIndex, row);
  946. /// });
  947. /// }
  948. /// ```
  949. ///
  950. /// All [children] must have a key.
  951. ///
  952. /// See also:
  953. ///
  954. /// * [ReorderableRow], for a version of this widget that is always horizontal.
  955. class ReorderableColumn extends ReorderableFlex {
  956. ReorderableColumn({
  957. required ReorderCallback onReorder,
  958. Key? key,
  959. Widget? header,
  960. Widget? footer,
  961. EdgeInsets? padding,
  962. MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
  963. MainAxisSize mainAxisSize = MainAxisSize.max,
  964. CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
  965. TextDirection? textDirection,
  966. VerticalDirection verticalDirection = VerticalDirection.down,
  967. TextBaseline? textBaseline,
  968. List<Widget> children = const <Widget>[],
  969. BuildDraggableFeedback? buildDraggableFeedback,
  970. NoReorderCallback? onNoReorder,
  971. ReorderStartedCallback? onReorderStarted,
  972. ScrollController? scrollController,
  973. bool needsLongPressDraggable = true,
  974. double draggingWidgetOpacity = 0.2,
  975. Duration? reorderAnimationDuration,
  976. Duration? scrollAnimationDuration,
  977. Widget Function(BuildContext context, int index)? draggedItemBuilder,
  978. bool ignorePrimaryScrollController = false,
  979. }) : super(
  980. key: key,
  981. header: header,
  982. footer: footer,
  983. children: children,
  984. onReorder: onReorder,
  985. onNoReorder: onNoReorder,
  986. onReorderStarted: onReorderStarted,
  987. direction: Axis.vertical,
  988. padding: padding,
  989. buildItemsContainer:
  990. (BuildContext context, Axis direction, List<Widget> children) {
  991. return Flex(
  992. direction: direction,
  993. mainAxisAlignment: mainAxisAlignment,
  994. mainAxisSize: mainAxisSize,
  995. crossAxisAlignment: crossAxisAlignment,
  996. textDirection: textDirection,
  997. verticalDirection: verticalDirection,
  998. textBaseline: textBaseline,
  999. children: children);
  1000. },
  1001. buildDraggableFeedback: buildDraggableFeedback,
  1002. mainAxisAlignment: mainAxisAlignment,
  1003. scrollController: scrollController,
  1004. needsLongPressDraggable: needsLongPressDraggable,
  1005. draggingWidgetOpacity: draggingWidgetOpacity,
  1006. reorderAnimationDuration: reorderAnimationDuration,
  1007. scrollAnimationDuration: scrollAnimationDuration,
  1008. draggedItemBuilder: draggedItemBuilder,
  1009. ignorePrimaryScrollController: ignorePrimaryScrollController);
  1010. }