diff --git a/frontend/app_flowy/packages/appflowy_board/CHANGELOG.md b/frontend/app_flowy/packages/appflowy_board/CHANGELOG.md index 119d25f89d..7cf059202f 100644 --- a/frontend/app_flowy/packages/appflowy_board/CHANGELOG.md +++ b/frontend/app_flowy/packages/appflowy_board/CHANGELOG.md @@ -1,3 +1,8 @@ +# 0.0.3 +* Support customize UI +* Update example +* Add AppFlowy style widget + ## 0.0.2 * Update documentation diff --git a/frontend/app_flowy/packages/appflowy_board/README.md b/frontend/app_flowy/packages/appflowy_board/README.md index 922cdd01b2..893bc3ed64 100644 --- a/frontend/app_flowy/packages/appflowy_board/README.md +++ b/frontend/app_flowy/packages/appflowy_board/README.md @@ -6,30 +6,25 @@ The **appflowy_board** is a package that is used in [AppFlowy](https://github.co **appflowy_board** will be a standard git repository when it becomes stable. ## Getting Started +

+ +

```dart @override void initState() { - final column1 = BoardColumnData(id: "1", items: [ - TextItem("a"), - TextItem("b"), - TextItem("c"), - TextItem("d"), + final column1 = BoardColumnData(id: "To Do", items: [ + TextItem("Card 1"), + TextItem("Card 2"), + TextItem("Card 3"), + TextItem("Card 4"), ]); - final column2 = BoardColumnData(id: "2", items: [ - TextItem("1"), - TextItem("2"), - TextItem("3"), - TextItem("4"), - TextItem("5"), + final column2 = BoardColumnData(id: "In Progress", items: [ + TextItem("Card 5"), + TextItem("Card 6"), ]); - final column3 = BoardColumnData(id: "3", items: [ - TextItem("A"), - TextItem("B"), - TextItem("C"), - TextItem("D"), - ]); + final column3 = BoardColumnData(id: "Done", items: []); boardDataController.addColumn(column1); boardDataController.addColumn(column2); @@ -40,25 +35,52 @@ The **appflowy_board** is a package that is used in [AppFlowy](https://github.co @override Widget build(BuildContext context) { - return Board( - dataController: boardDataController, - background: Container(color: Colors.red), - footBuilder: (context, columnData) { - return Container( - color: Colors.purple, - height: 30, - ); - }, - headerBuilder: (context, columnData) { - return Container( - color: Colors.yellow, - height: 30, - ); - }, - cardBuilder: (context, item) { - return _RowWidget(item: item as TextItem, key: ObjectKey(item)); - }, - columnConstraints: const BoxConstraints.tightFor(width: 240), + final config = BoardConfig( + columnBackgroundColor: HexColor.fromHex('#F7F8FC'), + ); + return Container( + color: Colors.white, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20), + child: Board( + dataController: boardDataController, + footBuilder: (context, columnData) { + return AppFlowyColumnFooter( + icon: const Icon(Icons.add, size: 20), + title: const Text('New'), + height: 50, + margin: config.columnItemPadding, + ); + }, + headerBuilder: (context, columnData) { + return AppFlowyColumnHeader( + icon: const Icon(Icons.lightbulb_circle), + title: Text(columnData.id), + addIcon: const Icon(Icons.add, size: 20), + moreIcon: const Icon(Icons.more_horiz, size: 20), + height: 50, + margin: config.columnItemPadding, + ); + }, + cardBuilder: (context, item) { + final textItem = item as TextItem; + return AppFlowyColumnItemCard( + key: ObjectKey(item), + child: Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text(textItem.s), + ), + ), + ); + }, + columnConstraints: const BoxConstraints.tightFor(width: 240), + config: BoardConfig( + columnBackgroundColor: HexColor.fromHex('#F7F8FC'), + ), + ), + ), ); } ``` \ No newline at end of file diff --git a/frontend/app_flowy/packages/appflowy_board/example/gifs/appflowy_board_video_1.gif b/frontend/app_flowy/packages/appflowy_board/example/gifs/appflowy_board_video_1.gif new file mode 100644 index 0000000000..bf1345608e Binary files /dev/null and b/frontend/app_flowy/packages/appflowy_board/example/gifs/appflowy_board_video_1.gif differ diff --git a/frontend/app_flowy/packages/appflowy_board/example/lib/main.dart b/frontend/app_flowy/packages/appflowy_board/example/lib/main.dart index 975cbb6ca8..c881370e03 100644 --- a/frontend/app_flowy/packages/appflowy_board/example/lib/main.dart +++ b/frontend/app_flowy/packages/appflowy_board/example/lib/main.dart @@ -32,7 +32,7 @@ class _MyAppState extends State { return MaterialApp( home: Scaffold( appBar: AppBar( - title: const Text('FlowyBoard example'), + title: const Text('AppFlowy Board'), ), body: _examples[_currentIndex], bottomNavigationBar: BottomNavigationBar( @@ -43,10 +43,10 @@ class _MyAppState extends State { items: [ BottomNavigationBarItem( icon: Icon(Icons.grid_on, color: _bottomNavigationColor), - label: "MultiBoardList"), + label: "MultiColumn"), BottomNavigationBarItem( icon: Icon(Icons.grid_on, color: _bottomNavigationColor), - label: "SingleBoardList"), + label: "SingleColumn"), ], onTap: (int index) { setState(() { diff --git a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart index 8715a4450c..7fe24362d2 100644 --- a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart +++ b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart @@ -23,26 +23,18 @@ class _MultiBoardListExampleState extends State { @override void initState() { - final column1 = BoardColumnData(id: "1", items: [ - TextItem("a"), - TextItem("b"), - TextItem("c"), - TextItem("d"), + final column1 = BoardColumnData(id: "To Do", items: [ + TextItem("Card 1"), + TextItem("Card 2"), + TextItem("Card 3"), + TextItem("Card 4"), ]); - final column2 = BoardColumnData(id: "2", items: [ - TextItem("1"), - TextItem("2"), - TextItem("3"), - TextItem("4"), - TextItem("5"), + final column2 = BoardColumnData(id: "In Progress", items: [ + TextItem("Card 5"), + TextItem("Card 6"), ]); - final column3 = BoardColumnData(id: "3", items: [ - TextItem("A"), - TextItem("B"), - TextItem("C"), - TextItem("D"), - ]); + final column3 = BoardColumnData(id: "Done", items: []); boardDataController.addColumn(column1); boardDataController.addColumn(column2); @@ -53,40 +45,52 @@ class _MultiBoardListExampleState extends State { @override Widget build(BuildContext context) { - return Board( - dataController: boardDataController, - background: Container(color: Colors.red), - footBuilder: (context, columnData) { - return Container( - color: Colors.purple, - height: 30, - ); - }, - headerBuilder: (context, columnData) { - return Container( - color: Colors.yellow, - height: 30, - ); - }, - cardBuilder: (context, item) { - return _RowWidget(item: item as TextItem, key: ObjectKey(item)); - }, - columnConstraints: const BoxConstraints.tightFor(width: 240), + final config = BoardConfig( + columnBackgroundColor: HexColor.fromHex('#F7F8FC'), ); - } -} - -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)), + color: Colors.white, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20), + child: Board( + dataController: boardDataController, + footBuilder: (context, columnData) { + return AppFlowyColumnFooter( + icon: const Icon(Icons.add, size: 20), + title: const Text('New'), + height: 50, + margin: config.columnItemPadding, + ); + }, + headerBuilder: (context, columnData) { + return AppFlowyColumnHeader( + icon: const Icon(Icons.lightbulb_circle), + title: Text(columnData.id), + addIcon: const Icon(Icons.add, size: 20), + moreIcon: const Icon(Icons.more_horiz, size: 20), + height: 50, + margin: config.columnItemPadding, + ); + }, + cardBuilder: (context, item) { + final textItem = item as TextItem; + return AppFlowyColumnItemCard( + key: ObjectKey(item), + child: Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text(textItem.s), + ), + ), + ); + }, + columnConstraints: const BoxConstraints.tightFor(width: 240), + config: BoardConfig( + columnBackgroundColor: HexColor.fromHex('#F7F8FC'), + ), + ), + ), ); } } @@ -99,3 +103,12 @@ class TextItem extends ColumnItem { @override String get id => s; } + +extension HexColor on Color { + static Color fromHex(String hexString) { + final buffer = StringBuffer(); + if (hexString.length == 6 || hexString.length == 7) buffer.write('ff'); + buffer.write(hexString.replaceFirst('#', '')); + return Color(int.parse(buffer.toString(), radix: 16)); + } +} diff --git a/frontend/app_flowy/packages/appflowy_board/lib/appflowy_board.dart b/frontend/app_flowy/packages/appflowy_board/lib/appflowy_board.dart index 684868a2a3..fc8f3c662f 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/appflowy_board.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/appflowy_board.dart @@ -2,4 +2,5 @@ library appflowy_board; export 'src/widgets/board_column/board_column_data.dart'; export 'src/widgets/board_data.dart'; +export 'src/widgets/styled_widgets/appflowy_styled_widgets.dart'; export 'src/widgets/board.dart'; diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart index 6f923ddf16..b9f766f961 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart @@ -6,7 +6,7 @@ const DART_LOG = "Dart_LOG"; class Log { // static const enableLog = bool.hasEnvironment(DART_LOG); // static final shared = Log(); - static const enableLog = false; + static const enableLog = true; static void info(String? message) { if (enableLog) { diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart index bef98842c0..3cd2a331f1 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart @@ -3,23 +3,29 @@ import 'package:provider/provider.dart'; import 'board_column/board_column.dart'; import 'board_column/board_column_data.dart'; import 'board_data.dart'; -import 'flex/drag_target_inteceptor.dart'; -import 'flex/reorder_flex.dart'; -import 'phantom/phantom_controller.dart'; +import 'reorder_flex/drag_target_inteceptor.dart'; +import 'reorder_flex/reorder_flex.dart'; +import 'reorder_phantom/phantom_controller.dart'; import '../rendering/board_overlay.dart'; +class BoardConfig { + final double cornerRadius; + final EdgeInsets columnPadding; + final EdgeInsets columnItemPadding; + final Color columnBackgroundColor; + + const BoardConfig({ + this.cornerRadius = 6.0, + this.columnPadding = const EdgeInsets.symmetric(horizontal: 8), + this.columnItemPadding = const EdgeInsets.symmetric(horizontal: 10), + this.columnBackgroundColor = Colors.transparent, + }); +} + 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; @@ -40,15 +46,16 @@ class Board extends StatelessWidget { /// final BoardPhantomController phantomController; + final BoardConfig config; + Board({ required this.dataController, required this.cardBuilder, - this.spacing = 10.0, - this.runSpacing = 0.0, this.background, this.footBuilder, this.headerBuilder, this.columnConstraints = const BoxConstraints(maxWidth: 200), + this.config = const BoardConfig(), Key? key, }) : phantomController = BoardPhantomController(delegate: dataController), super(key: key); @@ -60,9 +67,9 @@ class Board extends StatelessWidget { child: Consumer( builder: (context, notifier, child) { return BoardContent( + config: config, dataController: dataController, background: background, - spacing: spacing, delegate: phantomController, columnConstraints: columnConstraints, cardBuilder: cardBuilder, @@ -84,8 +91,8 @@ class BoardContent extends StatefulWidget { final OnDragEnded? onDragEnded; final BoardDataController dataController; final Widget? background; - final double spacing; - final ReorderFlexConfig config; + final BoardConfig config; + final ReorderFlexConfig reorderFlexConfig; final BoxConstraints columnConstraints; /// @@ -101,7 +108,8 @@ class BoardContent extends StatefulWidget { final BoardPhantomController phantomController; - BoardContent({ + const BoardContent({ + required this.config, required this.onReorder, required this.delegate, required this.dataController, @@ -109,14 +117,13 @@ class BoardContent extends StatefulWidget { this.onDragEnded, this.scrollController, this.background, - this.spacing = 10.0, required this.columnConstraints, required this.cardBuilder, this.footBuilder, this.headerBuilder, required this.phantomController, Key? key, - }) : config = ReorderFlexConfig(spacing: spacing), + }) : reorderFlexConfig = const ReorderFlexConfig(), super(key: key); @override @@ -140,7 +147,7 @@ class _BoardContentState extends State { final reorderFlex = ReorderFlex( key: widget.key, - config: widget.config, + config: widget.reorderFlexConfig, scrollController: widget.scrollController, onDragStarted: widget.onDragStarted, onReorder: widget.onReorder, @@ -154,7 +161,15 @@ class _BoardContentState extends State { return Stack( alignment: AlignmentDirectional.topStart, children: [ - if (widget.background != null) widget.background!, + if (widget.background != null) + Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(widget.config.cornerRadius), + ), + child: widget.background, + ), reorderFlex, ], ); @@ -173,8 +188,12 @@ class _BoardContentState extends State { } List _buildColumns() { - final List children = widget.dataController.columnDatas.map( - (columnData) { + final List children = + widget.dataController.columnDatas.asMap().entries.map( + (item) { + final columnData = item.value; + final columnIndex = item.key; + final dataSource = _BoardColumnDataSourceImpl( columnId: columnData.id, dataController: widget.dataController, @@ -188,6 +207,8 @@ class _BoardContentState extends State { return ConstrainedBox( constraints: widget.columnConstraints, child: BoardColumnWidget( + margin: _marginFromIndex(columnIndex), + itemMargin: widget.config.columnItemPadding, headerBuilder: widget.headerBuilder, footBuilder: widget.footBuilder, cardBuilder: widget.cardBuilder, @@ -195,7 +216,8 @@ class _BoardContentState extends State { scrollController: ScrollController(), phantomController: widget.phantomController, onReorder: widget.dataController.moveColumnItem, - spacing: 10, + cornerRadius: widget.config.cornerRadius, + backgroundColor: widget.config.columnBackgroundColor, ), ); }, @@ -206,6 +228,22 @@ class _BoardContentState extends State { return children; } + + EdgeInsets _marginFromIndex(int index) { + if (widget.dataController.columnDatas.isEmpty) { + return widget.config.columnPadding; + } + + if (index == 0) { + return EdgeInsets.only(right: widget.config.columnPadding.right); + } + + if (index == widget.dataController.columnDatas.length - 1) { + return EdgeInsets.only(left: widget.config.columnPadding.left); + } + + return widget.config.columnPadding; + } } class _BoardColumnDataSourceImpl extends BoardColumnDataDataSource { diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart index f95f4dae5c..d8981096e3 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart @@ -3,9 +3,9 @@ import 'dart:collection'; 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_target_inteceptor.dart'; +import '../reorder_phantom/phantom_controller.dart'; +import '../reorder_flex/reorder_flex.dart'; +import '../reorder_flex/drag_target_inteceptor.dart'; import 'board_column_data.dart'; typedef OnColumnDragStarted = void Function(int index); @@ -79,7 +79,15 @@ class BoardColumnWidget extends StatefulWidget { final BoardColumnFooterBuilder? footBuilder; - BoardColumnWidget({ + final EdgeInsets margin; + + final EdgeInsets itemMargin; + + final double cornerRadius; + + final Color backgroundColor; + + const BoardColumnWidget({ Key? key, this.headerBuilder, this.footBuilder, @@ -90,8 +98,11 @@ class BoardColumnWidget extends StatefulWidget { this.onDragStarted, this.scrollController, this.onDragEnded, - double? spacing, - }) : config = ReorderFlexConfig(spacing: spacing), + this.margin = EdgeInsets.zero, + this.itemMargin = EdgeInsets.zero, + this.cornerRadius = 0.0, + this.backgroundColor = Colors.transparent, + }) : config = const ReorderFlexConfig(), super(key: key); @override @@ -149,12 +160,25 @@ class _BoardColumnWidgetState extends State { children: children, ); - return Column( - children: [ - if (header != null) header, - Expanded(child: reorderFlex), - if (footer != null) footer, - ], + return Container( + margin: widget.margin, + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: widget.backgroundColor, + borderRadius: BorderRadius.circular(widget.cornerRadius), + ), + child: Column( + children: [ + if (header != null) header, + Expanded( + child: Padding( + padding: widget.itemMargin, + child: reorderFlex, + ), + ), + if (footer != null) footer, + ], + ), ); }, opaque: false, diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart index 24d3cc1a96..2ce739220e 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart @@ -3,7 +3,7 @@ import 'dart:collection'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import '../../utils/log.dart'; -import '../flex/reorder_flex.dart'; +import '../reorder_flex/reorder_flex.dart'; abstract class ColumnItem extends ReoderFlexItem { bool get isPhantom => false; @@ -92,10 +92,16 @@ class BoardColumnDataController extends ChangeNotifier with EquatableMixin { /// Replace the item at index with the [newItem]. void replace(int index, ColumnItem newItem) { - final removedItem = columnData._items.removeAt(index); - columnData._items.insert(index, newItem); - Log.debug( - '[$BoardColumnDataController] $columnData replace $removedItem with $newItem at $index'); + if (columnData._items.isEmpty) { + columnData._items.add(newItem); + Log.debug('[$BoardColumnDataController] $columnData add $newItem'); + } else { + final removedItem = columnData._items.removeAt(index); + columnData._items.insert(index, newItem); + Log.debug( + '[$BoardColumnDataController] $columnData replace $removedItem with $newItem at $index'); + } + notifyListeners(); } } @@ -119,6 +125,6 @@ class BoardColumnData extends ReoderFlexItem with EquatableMixin { @override String toString() { - return 'Column$id'; + return 'Column:[$id]'; } } diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart index fe2fca2c92..06e8ff1a57 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart @@ -4,9 +4,9 @@ import 'package:equatable/equatable.dart'; import '../utils/log.dart'; import 'board_column/board_column_data.dart'; -import 'flex/reorder_flex.dart'; +import 'reorder_flex/reorder_flex.dart'; import 'package:flutter/material.dart'; -import 'phantom/phantom_controller.dart'; +import 'reorder_phantom/phantom_controller.dart'; typedef OnMoveColumn = void Function(int fromIndex, int toIndex); @@ -79,8 +79,11 @@ class BoardDataController extends ChangeNotifier int toColumnIndex, ) { final item = columnController(fromColumnId).removeAt(fromColumnIndex); - assert( - columnController(toColumnId).items[toColumnIndex] is PhantomColumnItem); + + if (columnController(toColumnId).items.length > toColumnIndex) { + assert(columnController(toColumnId).items[toColumnIndex] + is PhantomColumnItem); + } columnController(toColumnId).replace(toColumnIndex, item); @@ -120,7 +123,7 @@ class BoardDataController extends ChangeNotifier columnController.removeAt(index); Log.debug( - '[$BoardDataController] Column$columnId remove phantom, current count: ${columnController.items.length}'); + '[$BoardDataController] Column:[$columnId] remove phantom, current count: ${columnController.items.length}'); } return isExist; } diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart index fafdcef774..ea8cc91fab 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:provider/provider.dart'; import '../transitions.dart'; abstract class DragTargetData { @@ -65,6 +67,8 @@ class ReorderDragTarget extends StatefulWidget { final AnimationController insertAnimationController; final AnimationController deleteAnimationController; + final bool useMoveAnimation; + ReorderDragTarget({ Key? key, required this.child, @@ -74,6 +78,7 @@ class ReorderDragTarget extends StatefulWidget { required this.onWillAccept, required this.insertAnimationController, required this.deleteAnimationController, + required this.useMoveAnimation, this.onAccept, this.onLeave, this.draggableTargetBuilder, @@ -140,7 +145,10 @@ class _ReorderDragTargetState data: widget.dragTargetData, ignoringFeedbackSemantics: false, feedback: feedbackBuilder, - childWhenDragging: IgnorePointerWidget(child: widget.child), + childWhenDragging: IgnorePointerWidget( + useIntrinsicSize: !widget.useMoveAnimation, + child: widget.child, + ), onDragStarted: () { _draggingFeedbackSize = widget._indexGlobalKey.currentContext?.size; widget.onDragStarted( @@ -174,11 +182,13 @@ class _ReorderDragTargetState transform: Matrix4.rotationZ(0), alignment: FractionalOffset.topLeft, child: Material( - elevation: 3.0, color: Colors.transparent, borderRadius: BorderRadius.zero, clipBehavior: Clip.hardEdge, - child: ConstrainedBox(constraints: constraints, child: child), + child: ConstrainedBox( + constraints: constraints, + child: Opacity(opacity: 0.6, child: child), + ), ), ); } @@ -254,10 +264,12 @@ class IgnorePointerWidget extends StatelessWidget { final sizedChild = useIntrinsicSize ? child : SizedBox(width: 0.0, height: 0.0, child: child); + + final opacity = useIntrinsicSize ? 0.3 : 0.0; return IgnorePointer( ignoring: true, child: Opacity( - opacity: 0, + opacity: opacity, child: sizedChild, ), ); @@ -282,6 +294,82 @@ class PhantomWidget extends StatelessWidget { } } +abstract class DragTargetMovePlaceholderDelegate { + void registerPlaceholder( + int dragTargetIndex, + void Function(int currentDragTargetIndex) callback, + ); + + void unregisterPlaceholder(int dragTargetIndex); +} + +class DragTargeMovePlaceholder extends StatefulWidget { + final double height; + final Color color; + final Color highlightColor; + final int dragTargetIndex; + final DragTargetMovePlaceholderDelegate delegate; + + const DragTargeMovePlaceholder({ + required this.delegate, + required this.dragTargetIndex, + this.height = 4, + this.color = Colors.transparent, + this.highlightColor = Colors.lightBlue, + Key? key, + }) : super(key: key); + + @override + State createState() => + _DragTargeMovePlaceholderState(); +} + +class _DragTargeMovePlaceholderState extends State { + ValueNotifier isHighlight = ValueNotifier(false); + + @override + void initState() { + widget.delegate.registerPlaceholder( + widget.dragTargetIndex, + (currentDragTargetIndex) { + if (!mounted) return; + + SchedulerBinding.instance.addPostFrameCallback((Duration duration) { + if (currentDragTargetIndex == -1) { + isHighlight.value = false; + } else { + isHighlight.value = + widget.dragTargetIndex == currentDragTargetIndex; + } + }); + }, + ); + super.initState(); + } + + @override + void dispose() { + isHighlight.dispose(); + widget.delegate.unregisterPlaceholder(widget.dragTargetIndex); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider.value( + value: isHighlight, + child: Consumer>( + builder: (context, notifier, child) { + return Container( + height: widget.height, + color: notifier.value ? widget.highlightColor : widget.color, + ); + }, + ), + ); + } +} + abstract class FakeDragTargetEventTrigger { void fakeOnDragEnded(VoidCallback callback); } diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_inteceptor.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_inteceptor.dart index 86152ed0de..da529819dd 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_inteceptor.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_inteceptor.dart @@ -30,12 +30,14 @@ abstract class DragTargetInterceptor { } abstract class OverlapDragTargetDelegate { - void didReturnOriginalDragTarget(); - void didCrossOtherDragTarget( + void cancel(); + void moveTo( String reorderFlexId, FlexDragTargetData dragTargetData, int dragTargetIndex, ); + + bool canMoveTo(String dragTargetId); } /// [OverlappingDragTargetInteceptor] is used to receive the overlapping @@ -68,13 +70,11 @@ class OverlappingDragTargetInteceptor extends DragTargetInterceptor { required String dragTargetId, required int dragTargetIndex}) { if (dragTargetId == dragTargetData.reorderFlexId) { - delegate.didReturnOriginalDragTarget(); + delegate.cancel(); } else { - delegate.didCrossOtherDragTarget( - dragTargetId, - dragTargetData, - dragTargetIndex, - ); + if (delegate.canMoveTo(dragTargetId)) { + delegate.moveTo(dragTargetId, dragTargetData, 0); + } } return true; @@ -128,13 +128,13 @@ class CrossReorderFlexDragTargetInterceptor extends DragTargetInterceptor { @override void onAccept(FlexDragTargetData dragTargetData) { Log.trace( - '[$CrossReorderFlexDragTargetInterceptor] Column$reorderFlexId on onAccept'); + '[$CrossReorderFlexDragTargetInterceptor] Column:[$reorderFlexId] on onAccept'); } @override void onLeave(FlexDragTargetData dragTargetData) { Log.trace( - '[$CrossReorderFlexDragTargetInterceptor] Column$reorderFlexId on leave'); + '[$CrossReorderFlexDragTargetInterceptor] Column:[$reorderFlexId] on leave'); } @override diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart index 9066c987f0..04de8cae7b 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart @@ -41,16 +41,19 @@ class ReorderFlexConfig { // How long an animation to scroll to an off-screen element final Duration scrollAnimationDuration = const Duration(milliseconds: 250); - final double? spacing; + final bool useMoveAnimation; - const ReorderFlexConfig({this.spacing}); + final bool useMovePlaceholder; + + const ReorderFlexConfig({ + this.useMoveAnimation = true, + }) : useMovePlaceholder = !useMoveAnimation; } class ReorderFlex extends StatefulWidget { final ReorderFlexConfig config; final List children; - final EdgeInsets? padding; /// [direction] How to place the children, default is Axis.vertical final Axis direction; @@ -81,7 +84,6 @@ class ReorderFlex extends StatefulWidget { this.onDragStarted, this.onDragEnded, this.interceptor, - this.padding, this.direction = Axis.vertical, }) : super(key: key); @@ -108,8 +110,11 @@ class ReorderFlexState extends State /// [_animation] controls the dragging animations late DragTargetAnimation _animation; + late ReorderFlexNotifier _notifier; + @override void initState() { + _notifier = ReorderFlexNotifier(); dragState = DraggingState(widget.reorderFlexId); _animation = DragTargetAnimation( @@ -154,13 +159,14 @@ class ReorderFlexState extends State for (int i = 0; i < widget.children.length; i += 1) { Widget child = widget.children[i]; + children.add(_wrap(child, i)); - if (widget.config.spacing != null) { - children.add(SizedBox(width: widget.config.spacing!)); - } - - final wrapChild = _wrap(child, i); - children.add(wrapChild); + // if (widget.config.useMovePlaceholder) { + // children.add(DragTargeMovePlaceholder( + // dragTargetIndex: i, + // delegate: _notifier, + // )); + // } } final child = _wrapContainer(children); @@ -199,7 +205,8 @@ class ReorderFlexState extends State /// [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); + final ReorderDragTarget dragTarget = + _buildDragTarget(context, child, childIndex); int shiftedIndex = childIndex; if (dragState.isOverlapWithPhantom()) { @@ -207,7 +214,7 @@ class ReorderFlexState extends State } Log.trace( - 'Rebuild: Column${dragState.id} ${dragState.toString()}, childIndex: $childIndex shiftedIndex: $shiftedIndex'); + 'Rebuild: Column:[${dragState.id}] ${dragState.toString()}, childIndex: $childIndex shiftedIndex: $shiftedIndex'); final currentIndex = dragState.currentIndex; final dragPhantomIndex = dragState.phantomIndex; @@ -234,15 +241,18 @@ class ReorderFlexState extends State } /// Determine the size of the drop area to show under the dragging widget. - final feedbackSize = dragState.feedbackSize; + Size? feedbackSize = Size.zero; + if (widget.config.useMoveAnimation) { + feedbackSize = dragState.feedbackSize; + } + Widget appearSpace = _makeAppearSpace(dragSpace, feedbackSize); Widget disappearSpace = _makeDisappearSpace(dragSpace, feedbackSize); /// When start dragging, the dragTarget, [ReorderDragTarget], 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'); + _notifier.updateDragTargetIndex(currentIndex); if (shiftedIndex == currentIndex && childIndex == dragPhantomIndex) { return _buildDraggingContainer(children: [ disappearSpace, @@ -264,8 +274,7 @@ class ReorderFlexState extends State /// 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'); + _notifier.updateDragTargetIndex(currentIndex); if (shiftedIndex == currentIndex && childIndex == dragPhantomIndex) { return _buildDraggingContainer(children: [ appearSpace, @@ -303,10 +312,7 @@ class ReorderFlexState extends State } ReorderDragTarget _buildDragTarget( - BuildContext builderContext, - Widget child, - int dragTargetIndex, - ) { + BuildContext builderContext, Widget child, int dragTargetIndex) { final ReoderFlexItem reorderFlexItem = widget.dataSource.items[dragTargetIndex]; return ReorderDragTarget( @@ -319,14 +325,14 @@ class ReorderFlexState extends State ), onDragStarted: (draggingWidget, draggingIndex, size) { Log.debug( - "[DragTarget] Column${widget.dataSource.identifier} start dragging item at $draggingIndex"); + "[DragTarget] Column:[${widget.dataSource.identifier}] start dragging item at $draggingIndex"); _startDragging(draggingWidget, draggingIndex, size); widget.onDragStarted?.call(draggingIndex); }, onDragEnded: (dragTargetData) { Log.debug( - "[DragTarget]: Column${widget.dataSource.identifier} end dragging"); - + "[DragTarget]: Column:[${widget.dataSource.identifier}] end dragging"); + _notifier.updateDragTargetIndex(-1); setState(() { if (dragTargetData.reorderFlexId == widget.reorderFlexId) { _onReordered( @@ -340,14 +346,11 @@ class ReorderFlexState extends State }); }, onWillAccept: (FlexDragTargetData dragTargetData) { - Log.debug('Insert animation: ${_animation.deleteController.status}'); - if (_animation.deleteController.isAnimating) { return false; } assert(widget.dataSource.items.length > dragTargetIndex); - if (_interceptDragTarget( dragTargetData, (interceptor) => interceptor.onWillAccept( @@ -370,6 +373,7 @@ class ReorderFlexState extends State ); }, onLeave: (dragTargetData) { + _notifier.updateDragTargetIndex(-1); _interceptDragTarget( dragTargetData, (interceptor) => interceptor.onLeave(dragTargetData), @@ -378,6 +382,7 @@ class ReorderFlexState extends State insertAnimationController: _animation.insertController, deleteAnimationController: _animation.deleteController, draggableTargetBuilder: widget.interceptor?.draggableTargetBuilder, + useMoveAnimation: widget.config.useMoveAnimation, child: child, ); } @@ -430,7 +435,7 @@ class ReorderFlexState extends State /// The [willAccept] will be true if the dargTarget is the widget that gets /// dragged and it is dragged on top of the other dragTargets. /// - Log.trace( + Log.debug( '[$ReorderDragTarget] ${widget.dataSource.identifier} on will accept, dragIndex:$dragIndex, dragTargetIndex:$dragTargetIndex, count: ${widget.dataSource.items.length}'); bool willAccept = @@ -442,7 +447,6 @@ class ReorderFlexState extends State } else { dragState.updateNextIndex(dragTargetIndex); } - _requestAnimationToNextIndex(isAcceptingNewTarget: true); }); @@ -467,7 +471,6 @@ class ReorderFlexState extends State } else { return SingleChildScrollView( scrollDirection: widget.direction, - padding: widget.padding, controller: _scrollController, child: child, ); diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_mixin.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_mixin.dart index a90ee6a83a..accdaa866b 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_mixin.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_mixin.dart @@ -1,6 +1,7 @@ import 'package:flutter/widgets.dart'; import '../transitions.dart'; +import 'drag_target.dart'; mixin ReorderFlexMinxi { @protected @@ -86,3 +87,56 @@ extension CurveAnimationController on AnimationController { ); } } + +class ReorderFlexNotifier extends DragTargetMovePlaceholderDelegate { + Map dragTargeEventNotifier = {}; + + void updateDragTargetIndex(int index) { + for (var notifier in dragTargeEventNotifier.values) { + notifier.setDragTargetIndex(index); + } + } + + DragTargetEventNotifier _notifierFromIndex(int dragTargetIndex) { + DragTargetEventNotifier? notifier = dragTargeEventNotifier[dragTargetIndex]; + if (notifier == null) { + final newNotifier = DragTargetEventNotifier(); + dragTargeEventNotifier[dragTargetIndex] = newNotifier; + notifier = newNotifier; + } + + return notifier; + } + + void dispose() { + for (var notifier in dragTargeEventNotifier.values) { + notifier.dispose(); + } + } + + @override + void registerPlaceholder( + int dragTargetIndex, + void Function(int dragTargetIndex) callback, + ) { + _notifierFromIndex(dragTargetIndex).addListener(() { + callback.call(_notifierFromIndex(dragTargetIndex).currentDragTargetIndex); + }); + } + + @override + void unregisterPlaceholder(int dragTargetIndex) { + dragTargeEventNotifier.remove(dragTargetIndex); + } +} + +class DragTargetEventNotifier extends ChangeNotifier { + int currentDragTargetIndex = -1; + + void setDragTargetIndex(int index) { + if (currentDragTargetIndex != index) { + currentDragTargetIndex = index; + notifyListeners(); + } + } +} diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart index 6b68eefd52..266c83d873 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart @@ -1,9 +1,11 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import '../../utils/log.dart'; import '../board_column/board_column_data.dart'; -import '../flex/drag_state.dart'; -import '../flex/drag_target.dart'; -import '../flex/drag_target_inteceptor.dart'; +import '../reorder_flex/drag_state.dart'; +import '../reorder_flex/drag_target.dart'; +import '../reorder_flex/drag_target_inteceptor.dart'; import 'phantom_state.dart'; abstract class BoardPhantomControllerDelegate { @@ -127,8 +129,8 @@ class BoardPhantomController extends OverlapDragTargetDelegate FlexDragTargetData dragTargetData, int dragTargetIndex, ) { - // Log.debug('[$BoardPhantomController] move Column${dragTargetData.reorderFlexId}:${dragTargetData.draggingIndex} ' - // 'to Column$columnId:$index'); + // Log.debug('[$BoardPhantomController] move Column:[${dragTargetData.reorderFlexId}]:${dragTargetData.draggingIndex} ' + // 'to Column:[$columnId]:$index'); phantomRecord = PhantomRecord( toColumnId: columnId, @@ -177,7 +179,7 @@ class BoardPhantomController extends OverlapDragTargetDelegate } @override - void didReturnOriginalDragTarget() { + void cancel() { if (phantomRecord == null) { return; } @@ -188,7 +190,7 @@ class BoardPhantomController extends OverlapDragTargetDelegate } @override - void didCrossOtherDragTarget( + void moveTo( String reorderFlexId, FlexDragTargetData dragTargetData, int dragTargetIndex, @@ -199,6 +201,12 @@ class BoardPhantomController extends OverlapDragTargetDelegate dragTargetIndex, ); } + + @override + bool canMoveTo(String dragTargetId) { + // TODO: implement shouldReceive + return delegate.controller(dragTargetId)?.columnData.items.length == 0; + } } /// Use [PhantomRecord] to record where to remove the column item and where to @@ -228,7 +236,7 @@ class PhantomRecord { return; } Log.debug( - '[$PhantomRecord] Update Column$fromColumnId remove position to $index'); + '[$PhantomRecord] Update Column:[$fromColumnId] remove position to $index'); fromColumnIndex = index; } @@ -238,13 +246,13 @@ class PhantomRecord { } Log.debug( - '[$PhantomRecord] Column$toColumnId update position $toColumnIndex -> $index'); + '[$PhantomRecord] Column:[$toColumnId] update position $toColumnIndex -> $index'); toColumnIndex = index; } @override String toString() { - return 'Column$fromColumnId:$fromColumnIndex to Column$toColumnId:$toColumnIndex'; + return 'Column:[$fromColumnId]:$fromColumnIndex to Column:[$toColumnId]:$toColumnIndex'; } } diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/appflowy_styled_widgets.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/appflowy_styled_widgets.dart new file mode 100644 index 0000000000..b802d15dae --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/appflowy_styled_widgets.dart @@ -0,0 +1,3 @@ +export 'card.dart'; +export 'footer.dart'; +export 'header.dart'; diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/card.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/card.dart new file mode 100644 index 0000000000..b2e5085649 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/card.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; + +class AppFlowyColumnItemCard extends StatefulWidget { + final Widget? child; + final Color backgroundColor; + final double cornerRadius; + final BoxConstraints boxConstraints; + + const AppFlowyColumnItemCard({ + this.child, + this.backgroundColor = Colors.white, + this.cornerRadius = 0.0, + this.boxConstraints = const BoxConstraints.tightFor(height: 60), + Key? key, + }) : super(key: key); + + @override + State createState() => _AppFlowyColumnItemCardState(); +} + +class _AppFlowyColumnItemCardState extends State { + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(4.0), + child: Container( + constraints: widget.boxConstraints, + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: widget.backgroundColor, + borderRadius: BorderRadius.circular(widget.cornerRadius), + ), + child: widget.child, + ), + ); + } +} diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/footer.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/footer.dart new file mode 100644 index 0000000000..7f5655fe60 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/footer.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; + +typedef OnFooterAddButtonClick = void Function(); + +class AppFlowyColumnFooter extends StatefulWidget { + final double height; + final Widget? icon; + final Widget? title; + final EdgeInsets margin; + final OnFooterAddButtonClick? onAddButtonClick; + + const AppFlowyColumnFooter({ + this.icon, + this.title, + this.margin = EdgeInsets.zero, + required this.height, + this.onAddButtonClick, + Key? key, + }) : super(key: key); + + @override + State createState() => _AppFlowyColumnFooterState(); +} + +class _AppFlowyColumnFooterState extends State { + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: widget.onAddButtonClick, + child: SizedBox( + height: widget.height, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (widget.icon != null) widget.icon!, + if (widget.title != null) widget.title!, + ], + ), + ), + ), + ); + } +} diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/header.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/header.dart new file mode 100644 index 0000000000..fdebc7ef21 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/header.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; + +typedef OnHeaderAddButtonClick = void Function(); +typedef OnHeaderMoreButtonClick = void Function(); + +class AppFlowyColumnHeader extends StatefulWidget { + final double height; + final Widget? icon; + final Widget? title; + final Widget? addIcon; + final Widget? moreIcon; + final EdgeInsets margin; + final OnHeaderAddButtonClick? onAddButtonClick; + final OnHeaderMoreButtonClick? onMoreButtonClick; + + const AppFlowyColumnHeader({ + required this.height, + this.icon, + this.title, + this.addIcon, + this.moreIcon, + this.margin = EdgeInsets.zero, + this.onAddButtonClick, + this.onMoreButtonClick, + Key? key, + }) : super(key: key); + + @override + State createState() => _AppFlowyColumnHeaderState(); +} + +class _AppFlowyColumnHeaderState extends State { + @override + Widget build(BuildContext context) { + List children = []; + + if (widget.icon != null) { + children.add(widget.icon!); + children.add(_hSpace()); + } + + if (widget.title != null) { + children.add(widget.title!); + children.add(_hSpace()); + } + + if (widget.moreIcon != null) { + children.add(const Spacer()); + children.add( + IconButton(onPressed: widget.onMoreButtonClick, icon: widget.moreIcon!), + ); + } + + if (widget.addIcon != null) { + children.add( + IconButton(onPressed: widget.onAddButtonClick, icon: widget.addIcon!), + ); + } + + return SizedBox( + height: widget.height, + child: Padding( + padding: widget.margin, + child: Row( + children: children, + ), + ), + ); + } + + Widget _hSpace() { + return const SizedBox(width: 6); + } +}