mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: enable mobile view item drag-and-drop reordering (#3812)
* feat: enable mobile view item draggable * fix: dragging the view doesn't disable hover effect
This commit is contained in:
parent
653c831473
commit
09b4e19c9d
@ -2,6 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/base/box_container.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class BottomSheetActionWidget extends StatelessWidget {
|
||||
const BottomSheetActionWidget({
|
||||
@ -19,7 +20,10 @@ class BottomSheetActionWidget extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyBoxContainer(
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
onTap: () {
|
||||
HapticFeedback.mediumImpact();
|
||||
onTap();
|
||||
},
|
||||
enableFeedback: true,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
|
@ -46,7 +46,7 @@ class MobilePersonalFolder extends StatelessWidget {
|
||||
key: ValueKey(
|
||||
'${FolderCategoryType.personal.name} ${view.id}',
|
||||
),
|
||||
isDraggable: false,
|
||||
isDraggable: true,
|
||||
categoryType: FolderCategoryType.personal,
|
||||
isFirstChild: view.id == views.first.id,
|
||||
view: view,
|
||||
|
@ -231,7 +231,10 @@ class InnerMobileViewItem extends StatelessWidget {
|
||||
child = DraggableViewItem(
|
||||
isFirstChild: isFirstChild,
|
||||
view: view,
|
||||
child: child,
|
||||
// FIXME: use better color
|
||||
centerHighlightColor: Colors.blue.shade200,
|
||||
topHighlightColor: Colors.blue.shade200,
|
||||
bottomHighlightColor: Colors.blue.shade200,
|
||||
feedback: (context) {
|
||||
return MobileViewItem(
|
||||
view: view,
|
||||
@ -246,6 +249,7 @@ class InnerMobileViewItem extends StatelessWidget {
|
||||
endActionPane: endActionPane,
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@ -319,13 +323,14 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
|
||||
// ··· more action button
|
||||
// children.add(_buildViewMoreActionButton(context));
|
||||
// only support add button for document layout
|
||||
if (widget.view.layout == ViewLayoutPB.Document) {
|
||||
if (!widget.isFeedback && widget.view.layout == ViewLayoutPB.Document) {
|
||||
// + button
|
||||
children.add(_buildViewAddButton(context));
|
||||
}
|
||||
|
||||
Widget child = GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
Widget child = InkWell(
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
enableFeedback: true,
|
||||
onTap: () => widget.onSelected(widget.view),
|
||||
child: SizedBox(
|
||||
height: _itemHeight,
|
||||
|
@ -1,9 +1,11 @@
|
||||
import 'package:appflowy/util/platform_extension.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/draggable_item/draggable_item.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
enum DraggableHoverPosition {
|
||||
@ -20,12 +22,20 @@ class DraggableViewItem extends StatefulWidget {
|
||||
this.feedback,
|
||||
required this.child,
|
||||
this.isFirstChild = false,
|
||||
this.centerHighlightColor,
|
||||
this.topHighlightColor,
|
||||
this.bottomHighlightColor,
|
||||
this.onDragging,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final WidgetBuilder? feedback;
|
||||
final ViewPB view;
|
||||
final bool isFirstChild;
|
||||
final Color? centerHighlightColor;
|
||||
final Color? topHighlightColor;
|
||||
final Color? bottomHighlightColor;
|
||||
final void Function(bool isDragging)? onDragging;
|
||||
|
||||
@override
|
||||
State<DraggableViewItem> createState() => _DraggableViewItemState();
|
||||
@ -34,41 +44,20 @@ class DraggableViewItem extends StatefulWidget {
|
||||
class _DraggableViewItemState extends State<DraggableViewItem> {
|
||||
DraggableHoverPosition position = DraggableHoverPosition.none;
|
||||
|
||||
final _dividerHeight = 2.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// add top border if the draggable item is on the top of the list
|
||||
// highlight the draggable item if the draggable item is on the center
|
||||
// add bottom border if the draggable item is on the bottom of the list
|
||||
final child = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// only show the top border when the draggable item is the first child
|
||||
if (widget.isFirstChild)
|
||||
Divider(
|
||||
height: 2,
|
||||
thickness: 2,
|
||||
color: position == DraggableHoverPosition.top
|
||||
? Theme.of(context).colorScheme.secondary
|
||||
: Colors.transparent,
|
||||
),
|
||||
Container(
|
||||
color: position == DraggableHoverPosition.center
|
||||
? Theme.of(context).colorScheme.secondary.withOpacity(0.5)
|
||||
: Colors.transparent,
|
||||
child: widget.child,
|
||||
),
|
||||
Divider(
|
||||
height: 2,
|
||||
thickness: 2,
|
||||
color: position == DraggableHoverPosition.bottom
|
||||
? Theme.of(context).colorScheme.secondary
|
||||
: Colors.transparent,
|
||||
),
|
||||
],
|
||||
);
|
||||
final child = PlatformExtension.isMobile
|
||||
? _buildMobileDraggableItem()
|
||||
: _buildDesktopDraggableItem();
|
||||
|
||||
return DraggableItem<ViewPB>(
|
||||
data: widget.view,
|
||||
onDragging: widget.onDragging,
|
||||
onWillAccept: (data) => true,
|
||||
onMove: (data) {
|
||||
final renderBox = context.findRenderObject() as RenderBox;
|
||||
@ -77,20 +66,21 @@ class _DraggableViewItemState extends State<DraggableViewItem> {
|
||||
if (!_shouldAccept(data.data, position)) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
Log.debug(
|
||||
'offset: $offset, position: $position, size: ${renderBox.size}',
|
||||
);
|
||||
this.position = position;
|
||||
});
|
||||
Log.debug(
|
||||
'offset: $offset, position: $position, size: ${renderBox.size}',
|
||||
);
|
||||
_updatePosition(position);
|
||||
},
|
||||
onLeave: (_) => setState(
|
||||
() => position = DraggableHoverPosition.none,
|
||||
onLeave: (_) => _updatePosition(
|
||||
DraggableHoverPosition.none,
|
||||
),
|
||||
onAccept: (data) {
|
||||
_move(data, widget.view);
|
||||
setState(
|
||||
() => position = DraggableHoverPosition.none,
|
||||
_move(
|
||||
data,
|
||||
widget.view,
|
||||
);
|
||||
_updatePosition(
|
||||
DraggableHoverPosition.none,
|
||||
);
|
||||
},
|
||||
feedback: IntrinsicWidth(
|
||||
@ -103,6 +93,97 @@ class _DraggableViewItemState extends State<DraggableViewItem> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDesktopDraggableItem() {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// only show the top border when the draggable item is the first child
|
||||
if (widget.isFirstChild)
|
||||
Divider(
|
||||
height: _dividerHeight,
|
||||
thickness: _dividerHeight,
|
||||
color: position == DraggableHoverPosition.top
|
||||
? widget.topHighlightColor ??
|
||||
Theme.of(context).colorScheme.secondary
|
||||
: Colors.transparent,
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6.0),
|
||||
color: position == DraggableHoverPosition.center
|
||||
? widget.centerHighlightColor ??
|
||||
Theme.of(context).colorScheme.secondary.withOpacity(0.5)
|
||||
: Colors.transparent,
|
||||
),
|
||||
child: widget.child,
|
||||
),
|
||||
Divider(
|
||||
height: _dividerHeight,
|
||||
thickness: _dividerHeight,
|
||||
color: position == DraggableHoverPosition.bottom
|
||||
? widget.bottomHighlightColor ??
|
||||
Theme.of(context).colorScheme.secondary
|
||||
: Colors.transparent,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMobileDraggableItem() {
|
||||
return Stack(
|
||||
children: [
|
||||
if (widget.isFirstChild)
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: _dividerHeight,
|
||||
child: Divider(
|
||||
height: _dividerHeight,
|
||||
thickness: _dividerHeight,
|
||||
color: position == DraggableHoverPosition.top
|
||||
? widget.topHighlightColor ??
|
||||
Theme.of(context).colorScheme.secondary
|
||||
: Colors.transparent,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
color: position == DraggableHoverPosition.center
|
||||
? widget.centerHighlightColor ??
|
||||
Theme.of(context).colorScheme.secondary.withOpacity(0.5)
|
||||
: Colors.transparent,
|
||||
),
|
||||
child: widget.child,
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: _dividerHeight,
|
||||
child: Divider(
|
||||
height: _dividerHeight,
|
||||
thickness: _dividerHeight,
|
||||
color: position == DraggableHoverPosition.bottom
|
||||
? widget.bottomHighlightColor ??
|
||||
Theme.of(context).colorScheme.secondary
|
||||
: Colors.transparent,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _updatePosition(DraggableHoverPosition position) {
|
||||
if (PlatformExtension.isMobile && position != this.position) {
|
||||
HapticFeedback.mediumImpact();
|
||||
}
|
||||
setState(
|
||||
() => this.position = position,
|
||||
);
|
||||
}
|
||||
|
||||
void _move(ViewPB from, ViewPB to) {
|
||||
if (position == DraggableHoverPosition.center &&
|
||||
to.layout != ViewLayoutPB.Document) {
|
||||
@ -144,7 +225,7 @@ class _DraggableViewItemState extends State<DraggableViewItem> {
|
||||
}
|
||||
|
||||
DraggableHoverPosition _computeHoverPosition(Offset offset, Size size) {
|
||||
final threshold = size.height / 3.0;
|
||||
final threshold = size.height / 5.0;
|
||||
if (widget.isFirstChild && offset.dy < -5.0) {
|
||||
return DraggableHoverPosition.top;
|
||||
}
|
||||
|
@ -103,6 +103,8 @@ class ViewItem extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
bool _isDragging = false;
|
||||
|
||||
class InnerViewItem extends StatelessWidget {
|
||||
const InnerViewItem({
|
||||
super.key,
|
||||
@ -188,6 +190,9 @@ class InnerViewItem extends StatelessWidget {
|
||||
isFirstChild: isFirstChild,
|
||||
view: view,
|
||||
child: child,
|
||||
onDragging: (isDragging) {
|
||||
_isDragging = isDragging;
|
||||
},
|
||||
feedback: (context) {
|
||||
return ViewItem(
|
||||
view: view,
|
||||
@ -261,7 +266,7 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
|
||||
hoverColor: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
resetHoverOnRebuild: widget.showActions,
|
||||
buildWhenOnHover: () => !widget.showActions,
|
||||
buildWhenOnHover: () => !widget.showActions && !_isDragging,
|
||||
builder: (_, onHover) => _buildViewItem(onHover),
|
||||
isSelected: () =>
|
||||
widget.showActions ||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:appflowy/util/platform_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DraggableItem<T extends Object> extends StatefulWidget {
|
||||
@ -13,6 +14,7 @@ class DraggableItem<T extends Object> extends StatefulWidget {
|
||||
this.onLeave,
|
||||
this.enableAutoScroll = true,
|
||||
this.hitTestSize = const Size(100, 100),
|
||||
this.onDragging,
|
||||
});
|
||||
|
||||
final T data;
|
||||
@ -32,6 +34,8 @@ class DraggableItem<T extends Object> extends StatefulWidget {
|
||||
final bool enableAutoScroll;
|
||||
final Size hitTestSize;
|
||||
|
||||
final void Function(bool isDragging)? onDragging;
|
||||
|
||||
@override
|
||||
State<DraggableItem<T>> createState() => _DraggableItemState<T>();
|
||||
}
|
||||
@ -57,7 +61,7 @@ class _DraggableItemState<T extends Object> extends State<DraggableItem<T>> {
|
||||
onWillAccept: widget.onWillAccept,
|
||||
onMove: widget.onMove,
|
||||
onLeave: widget.onLeave,
|
||||
builder: (_, __, ___) => Draggable<T>(
|
||||
builder: (_, __, ___) => _Draggable<T>(
|
||||
data: widget.data,
|
||||
feedback: widget.feedback ?? widget.child,
|
||||
childWhenDragging: widget.childWhenDragging ?? widget.child,
|
||||
@ -67,14 +71,17 @@ class _DraggableItemState<T extends Object> extends State<DraggableItem<T>> {
|
||||
dragTarget = details.globalPosition & widget.hitTestSize;
|
||||
autoScroller?.startAutoScrollIfNecessary(dragTarget!);
|
||||
}
|
||||
widget.onDragging?.call(true);
|
||||
},
|
||||
onDragEnd: (details) {
|
||||
autoScroller?.stopAutoScroll();
|
||||
dragTarget = null;
|
||||
widget.onDragging?.call(false);
|
||||
},
|
||||
onDraggableCanceled: (_, __) {
|
||||
autoScroller?.stopAutoScroll();
|
||||
dragTarget = null;
|
||||
widget.onDragging?.call(false);
|
||||
},
|
||||
),
|
||||
);
|
||||
@ -105,3 +112,58 @@ class _DraggableItemState<T extends Object> extends State<DraggableItem<T>> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Draggable<T extends Object> extends StatelessWidget {
|
||||
const _Draggable({
|
||||
required this.child,
|
||||
required this.feedback,
|
||||
this.data,
|
||||
this.childWhenDragging,
|
||||
this.onDragStarted,
|
||||
this.onDragUpdate,
|
||||
this.onDraggableCanceled,
|
||||
this.onDragEnd,
|
||||
this.onDragCompleted,
|
||||
});
|
||||
|
||||
/// The data that will be dropped by this draggable.
|
||||
final T? data;
|
||||
|
||||
final Widget child;
|
||||
|
||||
final Widget? childWhenDragging;
|
||||
final Widget feedback;
|
||||
|
||||
/// Called when the draggable starts being dragged.
|
||||
final VoidCallback? onDragStarted;
|
||||
|
||||
final DragUpdateCallback? onDragUpdate;
|
||||
|
||||
final DraggableCanceledCallback? onDraggableCanceled;
|
||||
|
||||
final VoidCallback? onDragCompleted;
|
||||
final DragEndCallback? onDragEnd;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PlatformExtension.isMobile
|
||||
? LongPressDraggable<T>(
|
||||
data: data,
|
||||
feedback: feedback,
|
||||
childWhenDragging: childWhenDragging,
|
||||
onDragUpdate: onDragUpdate,
|
||||
onDragEnd: onDragEnd,
|
||||
onDraggableCanceled: onDraggableCanceled,
|
||||
child: child,
|
||||
)
|
||||
: Draggable<T>(
|
||||
data: data,
|
||||
feedback: feedback,
|
||||
childWhenDragging: childWhenDragging,
|
||||
onDragUpdate: onDragUpdate,
|
||||
onDragEnd: onDragEnd,
|
||||
onDraggableCanceled: onDraggableCanceled,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user