Skip to content

Commit

Permalink
feat(mobile): drag to select assets (#8004)
Browse files Browse the repository at this point in the history
fear(mobile): drag to select assets

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
  • Loading branch information
3 people committed Mar 19, 2024
1 parent 9274c07 commit 9e4bab7
Show file tree
Hide file tree
Showing 3 changed files with 420 additions and 21 deletions.
223 changes: 223 additions & 0 deletions mobile/lib/modules/home/ui/asset_grid/asset_drag_region.dart
@@ -0,0 +1,223 @@
// ignore_for_file: library_private_types_in_public_api
// Based on https://stackoverflow.com/a/52625182

import 'dart:async';

import 'package:collection/collection.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

class AssetDragRegion extends StatefulWidget {
final Widget child;

final void Function(AssetIndex valueKey)? onStart;
final void Function(AssetIndex valueKey)? onAssetEnter;
final void Function()? onEnd;
final void Function()? onScrollStart;
final void Function(ScrollDirection direction)? onScroll;

const AssetDragRegion({
super.key,
required this.child,
this.onStart,
this.onAssetEnter,
this.onEnd,
this.onScrollStart,
this.onScroll,
});
@override
State createState() => _AssetDragRegionState();
}

class _AssetDragRegionState extends State<AssetDragRegion> {
late AssetIndex? assetUnderPointer;
late AssetIndex? anchorAsset;

// Scroll related state
static const double scrollOffset = 0.10;
double? topScrollOffset;
double? bottomScrollOffset;
Timer? scrollTimer;
late bool scrollNotified;

@override
void initState() {
super.initState();
assetUnderPointer = null;
anchorAsset = null;
scrollNotified = false;
}

@override
void didChangeDependencies() {
super.didChangeDependencies();
topScrollOffset = null;
bottomScrollOffset = null;
}

@override
void dispose() {
scrollTimer?.cancel();
super.dispose();
}

@override
Widget build(BuildContext context) {
return RawGestureDetector(
gestures: {
_CustomLongPressGestureRecognizer: GestureRecognizerFactoryWithHandlers<
_CustomLongPressGestureRecognizer>(
() => _CustomLongPressGestureRecognizer(),
_registerCallbacks,
),
},
child: widget.child,
);
}

void _registerCallbacks(_CustomLongPressGestureRecognizer recognizer) {
recognizer.onLongPressMoveUpdate = (details) => _onLongPressMove(details);
recognizer.onLongPressStart = (details) => _onLongPressStart(details);
recognizer.onLongPressUp = _onLongPressEnd;
recognizer.onLongPressCancel = _onLongPressEnd;
}

AssetIndex? _getValueKeyAtPositon(Offset position) {
final box = context.findAncestorRenderObjectOfType<RenderBox>();
if (box == null) return null;

final hitTestResult = BoxHitTestResult();
final local = box.globalToLocal(position);
if (!box.hitTest(hitTestResult, position: local)) return null;

return (hitTestResult.path
.firstWhereOrNull((hit) => hit.target is _AssetIndexProxy)
?.target as _AssetIndexProxy?)
?.index;
}

void _onLongPressStart(LongPressStartDetails event) {
/// Calculate widget height and scroll offset when long press starting instead of in [initState]
/// or [didChangeDependencies] as the grid might still be rendering into view to get the actual size
final height = context.size?.height;
if (height != null &&
(topScrollOffset == null || bottomScrollOffset == null)) {
topScrollOffset = height * scrollOffset;
bottomScrollOffset = height - topScrollOffset!;
}

final initialHit = _getValueKeyAtPositon(event.globalPosition);
anchorAsset = initialHit;
if (initialHit == null) return;

if (anchorAsset != null) {
widget.onStart?.call(anchorAsset!);
}
}

void _onLongPressEnd() {
scrollNotified = false;
scrollTimer?.cancel();
widget.onEnd?.call();
}

void _onLongPressMove(LongPressMoveUpdateDetails event) {
if (anchorAsset == null) return;
if (topScrollOffset == null || bottomScrollOffset == null) return;

final currentDy = event.localPosition.dy;

if (currentDy > bottomScrollOffset!) {
scrollTimer ??= Timer.periodic(
const Duration(milliseconds: 50),
(_) => widget.onScroll?.call(ScrollDirection.forward),
);
} else if (currentDy < topScrollOffset!) {
scrollTimer ??= Timer.periodic(
const Duration(milliseconds: 50),
(_) => widget.onScroll?.call(ScrollDirection.reverse),
);
} else {
scrollTimer?.cancel();
scrollTimer = null;
}

final currentlyTouchingAsset = _getValueKeyAtPositon(event.globalPosition);
if (currentlyTouchingAsset == null) return;

if (assetUnderPointer != currentlyTouchingAsset) {
if (!scrollNotified) {
scrollNotified = true;
widget.onScrollStart?.call();
}

widget.onAssetEnter?.call(currentlyTouchingAsset);
assetUnderPointer = currentlyTouchingAsset;
}
}
}

class _CustomLongPressGestureRecognizer extends LongPressGestureRecognizer {
@override
void rejectGesture(int pointer) {
acceptGesture(pointer);
}
}

// ignore: prefer-single-widget-per-file
class AssetIndexWrapper extends SingleChildRenderObjectWidget {
final int rowIndex;
final int sectionIndex;

const AssetIndexWrapper({
required Widget super.child,
required this.rowIndex,
required this.sectionIndex,
super.key,
});

@override
_AssetIndexProxy createRenderObject(BuildContext context) {
return _AssetIndexProxy(
index: AssetIndex(rowIndex: rowIndex, sectionIndex: sectionIndex),
);
}

@override
void updateRenderObject(
BuildContext context,
_AssetIndexProxy renderObject,
) {
renderObject.index =
AssetIndex(rowIndex: rowIndex, sectionIndex: sectionIndex);
}
}

class _AssetIndexProxy extends RenderProxyBox {
AssetIndex index;

_AssetIndexProxy({
required this.index,
});
}

class AssetIndex {
final int rowIndex;
final int sectionIndex;

const AssetIndex({
required this.rowIndex,
required this.sectionIndex,
});

@override
bool operator ==(covariant AssetIndex other) {
if (identical(this, other)) return true;

return other.rowIndex == rowIndex && other.sectionIndex == sectionIndex;
}

@override
int get hashCode => rowIndex.hashCode ^ sectionIndex.hashCode;
}

0 comments on commit 9e4bab7

Please sign in to comment.