chore: update ui

This commit is contained in:
appflowy 2022-08-08 17:12:34 +08:00
parent 971a5e245a
commit db5b3e3bd3
20 changed files with 615 additions and 190 deletions

View File

@ -1,3 +1,8 @@
# 0.0.3
* Support customize UI
* Update example
* Add AppFlowy style widget
## 0.0.2
* Update documentation

View File

@ -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
<p>
<img src="" width="180" title="AppFlowyBoard">
</p>
```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'),
),
),
),
);
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

View File

@ -32,7 +32,7 @@ class _MyAppState extends State<MyApp> {
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<MyApp> {
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(() {

View File

@ -23,26 +23,18 @@ class _MultiBoardListExampleState extends State<MultiBoardListExample> {
@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<MultiBoardListExample> {
@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));
}
}

View File

@ -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';

View File

@ -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) {

View File

@ -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<BoardDataController>(
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<BoardContent> {
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<BoardContent> {
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<BoardContent> {
}
List<Widget> _buildColumns() {
final List<Widget> children = widget.dataController.columnDatas.map(
(columnData) {
final List<Widget> 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<BoardContent> {
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<BoardContent> {
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<BoardContent> {
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 {

View File

@ -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<BoardColumnWidget> {
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,

View File

@ -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]';
}
}

View File

@ -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;
}

View File

@ -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<T extends DragTargetData> extends StatefulWidget {
final AnimationController insertAnimationController;
final AnimationController deleteAnimationController;
final bool useMoveAnimation;
ReorderDragTarget({
Key? key,
required this.child,
@ -74,6 +78,7 @@ class ReorderDragTarget<T extends DragTargetData> 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<T extends DragTargetData>
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<T extends DragTargetData>
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<DragTargeMovePlaceholder> createState() =>
_DragTargeMovePlaceholderState();
}
class _DragTargeMovePlaceholderState extends State<DragTargeMovePlaceholder> {
ValueNotifier<bool> 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<ValueNotifier<bool>>(
builder: (context, notifier, child) {
return Container(
height: widget.height,
color: notifier.value ? widget.highlightColor : widget.color,
);
},
),
);
}
}
abstract class FakeDragTargetEventTrigger {
void fakeOnDragEnded(VoidCallback callback);
}

View File

@ -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

View File

@ -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<Widget> 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<ReorderFlex>
/// [_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<ReorderFlex>
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<ReorderFlex>
/// [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<ReorderFlex>
}
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<ReorderFlex>
}
/// 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<ReorderFlex>
///
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<ReorderFlex>
}
ReorderDragTarget _buildDragTarget(
BuildContext builderContext,
Widget child,
int dragTargetIndex,
) {
BuildContext builderContext, Widget child, int dragTargetIndex) {
final ReoderFlexItem reorderFlexItem =
widget.dataSource.items[dragTargetIndex];
return ReorderDragTarget<FlexDragTargetData>(
@ -319,14 +325,14 @@ class ReorderFlexState extends State<ReorderFlex>
),
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<ReorderFlex>
});
},
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<ReorderFlex>
);
},
onLeave: (dragTargetData) {
_notifier.updateDragTargetIndex(-1);
_interceptDragTarget(
dragTargetData,
(interceptor) => interceptor.onLeave(dragTargetData),
@ -378,6 +382,7 @@ class ReorderFlexState extends State<ReorderFlex>
insertAnimationController: _animation.insertController,
deleteAnimationController: _animation.deleteController,
draggableTargetBuilder: widget.interceptor?.draggableTargetBuilder,
useMoveAnimation: widget.config.useMoveAnimation,
child: child,
);
}
@ -430,7 +435,7 @@ class ReorderFlexState extends State<ReorderFlex>
/// 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<ReorderFlex>
} else {
dragState.updateNextIndex(dragTargetIndex);
}
_requestAnimationToNextIndex(isAcceptingNewTarget: true);
});
@ -467,7 +471,6 @@ class ReorderFlexState extends State<ReorderFlex>
} else {
return SingleChildScrollView(
scrollDirection: widget.direction,
padding: widget.padding,
controller: _scrollController,
child: child,
);

View File

@ -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<int, DragTargetEventNotifier> 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();
}
}
}

View File

@ -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';
}
}

View File

@ -0,0 +1,3 @@
export 'card.dart';
export 'footer.dart';
export 'header.dart';

View File

@ -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<AppFlowyColumnItemCard> createState() => _AppFlowyColumnItemCardState();
}
class _AppFlowyColumnItemCardState extends State<AppFlowyColumnItemCard> {
@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,
),
);
}
}

View File

@ -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<AppFlowyColumnFooter> createState() => _AppFlowyColumnFooterState();
}
class _AppFlowyColumnFooterState extends State<AppFlowyColumnFooter> {
@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!,
],
),
),
),
);
}
}

View File

@ -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<AppFlowyColumnHeader> createState() => _AppFlowyColumnHeaderState();
}
class _AppFlowyColumnHeaderState extends State<AppFlowyColumnHeader> {
@override
Widget build(BuildContext context) {
List<Widget> 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);
}
}