-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
1d24e20
commit e61f3f5
Showing
3 changed files
with
420 additions
and
21 deletions.
There are no files selected for viewing
223 changes: 223 additions & 0 deletions
223
mobile/lib/modules/home/ui/asset_grid/asset_drag_region.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
Oops, something went wrong.