Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 92 additions & 47 deletions lib/src/delegates/asset_grid_drag_selection_coordinator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,51 +53,16 @@ class AssetGridDragSelectionCoordinator {
smallestSelectingIndex = -1;
}

/// Long Press or horizontal drag to start the selection.
void onSelectionStart({
required BuildContext context,
required Offset globalPosition,
required int index,
required AssetEntity asset,
}) {
final scrollableState = _checkScrollableStatePresent(context);
if (scrollableState == null) {
return;
}

if (delegate.gridScrollController.position.isScrollingNotifier.value) {
return;
}

dragging = true;

_autoScroller = EdgeDraggingAutoScroller(
scrollableState,
velocityScalar: _kDefaultAutoScrollVelocityScalar,
);

initialSelectingIndex = index;
largestSelectingIndex = index;
smallestSelectingIndex = index;

addSelected = !delegate.provider.selectedAssets.contains(asset);
}

void onSelectionUpdate({
/// Calculate the asset index from global position.
/// Returns null if the position is out of bounds.
int? _calculateIndexFromPosition({
required BuildContext context,
required Offset globalPosition,
required BoxConstraints constraints,
}) {
if (!dragging) {
return;
}

final view = View.of(context);
final dimensionSize = view.physicalSize / view.devicePixelRatio;

// Get the actual top padding. Since `viewPadding` represents the
// physical pixels, it should be divided by the device pixel ratio
// to get the logical pixels.
final appBarSize =
delegate.appBarPreferredSize ?? delegate.appBar(context).preferredSize;
final viewPaddingTop = view.viewPadding.top / view.devicePixelRatio;
Expand All @@ -108,15 +73,10 @@ class AssetGridDragSelectionCoordinator {
final gridViewport =
dimensionSize.height - topSectionHeight - bottomSectionHeight;

// Calculate the coordinate of the current drag position's
// asset representation.
final gridCount = delegate.gridCount;
final itemSize = dimensionSize.width / gridCount;
final dividedSpacing = delegate.itemSpacing / gridCount;

// Row index is calculated based on the drag's global position.
// The AppBar height, status bar height, and scroll offset are subtracted
// to adjust for padding and scrolling. This gives the actual row index.
final gridRevert = delegate.effectiveShouldRevertGrid(context);
final totalRows = (provider.currentAssets.length / gridCount).ceil();
final onlyOneScreen =
Expand All @@ -134,10 +94,8 @@ class AssetGridDragSelectionCoordinator {
pathWrapper: provider.currentPath,
specialItemsFinalized: specialItems,
);
final scrolledOffset = delegate.gridScrollController.offset
.abs(); // Offset is negative when reverted.
final scrolledOffset = delegate.gridScrollController.offset.abs();

// Corrects the Y position according the reverted status.
final correctedY = switch (reverted) {
true =>
dimensionSize.height - bottomSectionHeight - globalPosition.dy,
Expand Down Expand Up @@ -165,7 +123,6 @@ class AssetGridDragSelectionCoordinator {
onlyOneScreen: onlyOneScreen,
specialItemsFinalized: specialItems,
);
// Make the index starts with the bottom if the grid is reverted.
if (reverted && placeholderCount > 0 && rowIndex > 0 && anchor < 1.0) {
rowIndex -= 1;
}
Expand All @@ -176,6 +133,94 @@ class AssetGridDragSelectionCoordinator {
}

final currentDragIndex = rowIndex * gridCount + columnIndex;

// Clamp to valid range
if (currentDragIndex < 0 ||
currentDragIndex >= provider.currentAssets.length) {
return null;
}

return currentDragIndex;
}

/// Long Press or horizontal drag to start the selection.
void onSelectionStart({
required BuildContext context,
required Offset globalPosition,
required BoxConstraints constraints,
}) {
final scrollableState = _checkScrollableStatePresent(context);
if (scrollableState == null) {
return;
}

if (delegate.gridScrollController.position.isScrollingNotifier.value) {
return;
}

final index = _calculateIndexFromPosition(
context: context,
globalPosition: globalPosition,
constraints: constraints,
);

if (index == null || index >= provider.currentAssets.length) {
return;
}

final asset = provider.currentAssets[index];

dragging = true;

_autoScroller = EdgeDraggingAutoScroller(
scrollableState,
velocityScalar: _kDefaultAutoScrollVelocityScalar,
);

initialSelectingIndex = index;
largestSelectingIndex = index;
smallestSelectingIndex = index;

addSelected = !delegate.provider.selectedAssets.contains(asset);
}

void onSelectionUpdate({
required BuildContext context,
required Offset globalPosition,
required BoxConstraints constraints,
}) {
if (!dragging) {
return;
}

final currentDragIndex = _calculateIndexFromPosition(
context: context,
globalPosition: globalPosition,
constraints: constraints,
);

if (currentDragIndex == null) {
return;
}

final view = View.of(context);
final dimensionSize = view.physicalSize / view.devicePixelRatio;
final appBarSize =
delegate.appBarPreferredSize ?? delegate.appBar(context).preferredSize;
final viewPaddingTop = view.viewPadding.top / view.devicePixelRatio;
final viewPaddingBottom = view.viewPadding.bottom / view.devicePixelRatio;
final topSectionHeight = appBarSize.height + viewPaddingTop;
final bottomSectionHeight =
delegate.bottomSectionHeight + viewPaddingBottom;
final gridCount = delegate.gridCount;
final itemSize = dimensionSize.width / gridCount;
final dividedSpacing = delegate.itemSpacing / gridCount;

// Calculate column index for auto-scroll
int getDragAxisIndex(double delta, double itemSize) {
return delta ~/ (itemSize + dividedSpacing);
}
final columnIndex = getDragAxisIndex(globalPosition.dx, itemSize);

// Check the selecting index in order to diff unselecting assets.
smallestSelectingIndex = math.min(
Expand Down
155 changes: 77 additions & 78 deletions lib/src/delegates/asset_picker_builder_delegate.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1392,89 +1392,16 @@ class DefaultAssetPickerBuilderDelegate<T extends DefaultAssetPickerProvider>
index -= placeholderCount;
}

Widget child = assetGridItemBuilder(
final Widget child = assetGridItemBuilder(
context: context,
index: index,
currentAssets: assets,
specialItemsFinalized: specialItemsFinalized,
);

// Enables drag-to-select when:
// 1. The feature is enabled manually.
// 2. The accessibility service is not being used.
// 3. The picker is not in single asset mode.
if ((dragToSelect ?? !accessibleNavigation) &&
!isSingleAssetMode) {
child = GestureDetector(
excludeFromSemantics: true,
onHorizontalDragStart: (d) {
dragSelectCoordinator.onSelectionStart(
context: context,
globalPosition: d.globalPosition,
index: index,
asset: assets[index],
);
},
onHorizontalDragUpdate: (d) {
dragSelectCoordinator.onSelectionUpdate(
context: context,
globalPosition: d.globalPosition,
constraints: constraints,
);
},
onHorizontalDragCancel:
dragSelectCoordinator.resetDraggingStatus,
onHorizontalDragEnd: (d) {
dragSelectCoordinator.onDragEnd(
globalPosition: d.globalPosition,
);
},
onLongPressStart: (d) {
dragSelectCoordinator.onSelectionStart(
context: context,
globalPosition: d.globalPosition,
index: index,
asset: assets[index],
);
},
onLongPressMoveUpdate: (d) {
dragSelectCoordinator.onSelectionUpdate(
context: context,
globalPosition: d.globalPosition,
constraints: constraints,
);
},
onLongPressCancel:
dragSelectCoordinator.resetDraggingStatus,
onLongPressEnd: (d) {
dragSelectCoordinator.onDragEnd(
globalPosition: d.globalPosition,
);
},
onPanStart: (d) {
dragSelectCoordinator.onSelectionStart(
context: context,
globalPosition: d.globalPosition,
index: index,
asset: assets[index],
);
},
onPanUpdate: (d) {
dragSelectCoordinator.onSelectionUpdate(
context: context,
globalPosition: d.globalPosition,
constraints: constraints,
);
},
onPanCancel: dragSelectCoordinator.resetDraggingStatus,
onPanEnd: (d) {
dragSelectCoordinator.onDragEnd(
globalPosition: d.globalPosition,
);
},
child: child,
);
}
// Per-item gesture detectors removed - now handled globally
// at the scroll view level to support drag selection across
// all items including those not yet mounted.

return MergeSemantics(
child: Directionality(
Expand Down Expand Up @@ -1545,7 +1472,7 @@ class DefaultAssetPickerBuilderDelegate<T extends DefaultAssetPickerProvider>
context.bottomPadding + bottomSectionHeight,
);
appBarPreferredSize ??= appBar(context).preferredSize;
return CustomScrollView(
final scrollView = CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
controller: gridScrollController,
anchor: anchor,
Expand All @@ -1567,6 +1494,78 @@ class DefaultAssetPickerBuilderDelegate<T extends DefaultAssetPickerProvider>
if (!gridRevert && isAppleOS(context)) bottomGap,
],
);

// Wrap with gesture detector for drag-to-select when enabled
if ((dragToSelect ?? !accessibleNavigation) &&
!isSingleAssetMode) {
return GestureDetector(
excludeFromSemantics: true,
onHorizontalDragStart: (d) {
dragSelectCoordinator.onSelectionStart(
context: context,
globalPosition: d.globalPosition,
constraints: constraints,
);
},
onHorizontalDragUpdate: (d) {
dragSelectCoordinator.onSelectionUpdate(
context: context,
globalPosition: d.globalPosition,
constraints: constraints,
);
},
onHorizontalDragCancel:
dragSelectCoordinator.resetDraggingStatus,
onHorizontalDragEnd: (d) {
dragSelectCoordinator.onDragEnd(
globalPosition: d.globalPosition,
);
},
onLongPressStart: (d) {
dragSelectCoordinator.onSelectionStart(
context: context,
globalPosition: d.globalPosition,
constraints: constraints,
);
},
onLongPressMoveUpdate: (d) {
dragSelectCoordinator.onSelectionUpdate(
context: context,
globalPosition: d.globalPosition,
constraints: constraints,
);
},
onLongPressCancel:
dragSelectCoordinator.resetDraggingStatus,
onLongPressEnd: (d) {
dragSelectCoordinator.onDragEnd(
globalPosition: d.globalPosition,
);
},
onPanStart: (d) {
dragSelectCoordinator.onSelectionStart(
context: context,
globalPosition: d.globalPosition,
constraints: constraints,
);
},
onPanUpdate: (d) {
dragSelectCoordinator.onSelectionUpdate(
context: context,
globalPosition: d.globalPosition,
constraints: constraints,
);
},
onPanCancel: dragSelectCoordinator.resetDraggingStatus,
onPanEnd: (d) {
dragSelectCoordinator.onDragEnd(
globalPosition: d.globalPosition,
);
},
child: scrollView,
);
}
return scrollView;
},
),
),
Expand Down