Created
July 29, 2023 10:32
-
-
Save fzyzcjy/4d9238c5be3f49ab45de9a00436b4743 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
import 'dart:async'; | |
import 'dart:math'; | |
import 'package:common_flutter/ui/services/navigator_ui_service.dart'; | |
import 'package:common_flutter/utils/colors.dart'; | |
import 'package:flutter/gestures.dart'; | |
import 'package:flutter/material.dart'; | |
import 'package:front_log/front_log.dart'; | |
class PopGestureWidget extends StatefulWidget { | |
final Widget child; | |
const PopGestureWidget({super.key, required this.child}); | |
@override | |
State<PopGestureWidget> createState() => _PopGestureWidgetState(); | |
} | |
class _PopGestureWidgetState extends State<PopGestureWidget> { | |
Offset? _lastStartPosition; | |
_Direction? _lastDirection; | |
final _dragDistance = ValueNotifier(0.0); | |
@override | |
Widget build(BuildContext context) { | |
const kWidth = 12.0; | |
const kHeight = 32.0; | |
return _MyBackGestureDetector<void>( | |
enabledCallback: () => true, | |
onStartPopGesture: (details, direction) { | |
setState(() { | |
_lastStartPosition = details.localPosition; | |
_lastDirection = direction; | |
}); | |
return _MyBackGestureController(_dragDistance); | |
}, | |
child: Stack( | |
fit: StackFit.passthrough, | |
children: [ | |
widget.child, | |
Positioned( | |
left: _lastDirection == _Direction.left ? 0.0 : null, | |
right: _lastDirection == _Direction.right ? 0.0 : null, | |
width: kWidth, | |
top: (_lastStartPosition?.dy ?? 0.0) - kHeight / 2, | |
height: kHeight, | |
child: IgnorePointer( | |
child: Transform.scale( | |
scaleX: _lastDirection == _Direction.right ? -1.0 : 1.0, | |
scaleY: 1.0, | |
child: CustomPaint( | |
painter: _PopArrowPainter( | |
dragDistance: _dragDistance, | |
color: Theme.of(context).autoGrey.shade700, | |
), | |
), | |
), | |
), | |
), | |
], | |
), | |
); | |
} | |
} | |
class _PopArrowPainter extends CustomPainter { | |
final ValueNotifier<double> dragDistance; | |
final Color color; | |
_PopArrowPainter({required this.dragDistance, required this.color}) : super(repaint: dragDistance); | |
@override | |
void paint(Canvas canvas, Size size) { | |
final xBase = -size.width + dragDistance.value * 0.25; | |
final x1 = xBase; | |
final x2 = xBase + size.width; | |
final painter = Paint() | |
..color = color | |
..strokeWidth = 1.0; | |
canvas.drawLine(Offset(x1, 0), Offset(x2, size.height / 2), painter); | |
canvas.drawLine(Offset(x1, size.height), Offset(x2, size.height / 2), painter); | |
} | |
@override | |
bool shouldRepaint(covariant CustomPainter oldDelegate) => true; | |
} | |
// NOTE COPIED FROM Flutter cupertino/route.dart :: [_kBackGestureWidth] | |
const double _kBackGestureWidth = 20.0; | |
// NOTE MODIFIED FROM Flutter cupertino/route.dart :: [_CupertinoBackGestureDetector] | |
class _MyBackGestureDetector<T> extends StatefulWidget { | |
const _MyBackGestureDetector({ | |
super.key, | |
required this.enabledCallback, | |
required this.onStartPopGesture, | |
required this.child, | |
}); | |
final Widget child; | |
final ValueGetter<bool> enabledCallback; | |
final _MyBackGestureController<T> Function(DragStartDetails details, _Direction direction) onStartPopGesture; | |
@override | |
_MyBackGestureDetectorState<T> createState() => _MyBackGestureDetectorState<T>(); | |
} | |
class _MyBackGestureDetectorState<T> extends State<_MyBackGestureDetector<T>> { | |
_MyBackGestureController<T>? _backGestureController; | |
_Direction? _direction; | |
late HorizontalDragGestureRecognizer _recognizer; | |
@override | |
void initState() { | |
super.initState(); | |
_recognizer = HorizontalDragGestureRecognizer(debugOwner: this) | |
..onStart = _handleDragStart | |
..onUpdate = _handleDragUpdate | |
..onEnd = _handleDragEnd | |
..onCancel = _handleDragCancel; | |
} | |
@override | |
void dispose() { | |
_recognizer.dispose(); | |
super.dispose(); | |
} | |
void _handleDragStart(DragStartDetails details) { | |
assert(mounted); | |
assert(_backGestureController == null); | |
_backGestureController = widget.onStartPopGesture(details, _direction!); | |
} | |
void _handleDragUpdate(DragUpdateDetails details) { | |
assert(mounted); | |
assert(_backGestureController != null); | |
// no need to divide by `context.size!.width` since want absolute delta in our case | |
_backGestureController!.dragUpdate(_convertToLogical(details.primaryDelta!)); | |
} | |
void _handleDragEnd(DragEndDetails details) { | |
assert(mounted); | |
assert(_backGestureController != null); | |
_backGestureController!.dragEnd(_convertToLogical(details.velocity.pixelsPerSecond.dx)); | |
_backGestureController = null; | |
_direction = null; | |
} | |
void _handleDragCancel() { | |
assert(mounted); | |
// This can be called even if start is not called, paired with the "down" event | |
// that we don't consider here. | |
_backGestureController?.dragEnd(0.0); | |
_backGestureController = null; | |
_direction = null; | |
} | |
void _handlePointerDown(PointerDownEvent event, _Direction direction) { | |
if (widget.enabledCallback()) { | |
_recognizer.addPointer(event); | |
_direction = direction; | |
} | |
} | |
double _convertToLogical(double value) { | |
switch (_direction ?? _Direction.left) { | |
case _Direction.left: | |
return value; | |
case _Direction.right: | |
return -value; | |
} | |
} | |
@override | |
Widget build(BuildContext context) { | |
assert(debugCheckHasDirectionality(context)); | |
// For devices with notches, the drag area needs to be larger on the side | |
// that has the notch. | |
double dragAreaWidth = Directionality.of(context) == TextDirection.ltr | |
? MediaQuery.paddingOf(context).left | |
: MediaQuery.paddingOf(context).right; | |
dragAreaWidth = max(dragAreaWidth, _kBackGestureWidth); | |
return Stack( | |
fit: StackFit.passthrough, | |
children: <Widget>[ | |
widget.child, | |
for (final direction in _Direction.values) | |
Positioned( | |
left: direction == _Direction.left ? 0.0 : null, | |
right: direction == _Direction.right ? 0.0 : null, | |
width: dragAreaWidth, | |
top: 0.0, | |
bottom: 0.0, | |
child: Listener( | |
onPointerDown: (e) => _handlePointerDown(e, direction), | |
behavior: HitTestBehavior.translucent, | |
), | |
), | |
], | |
); | |
} | |
} | |
enum _Direction { left, right } | |
// NOTE MODIFIED FROM Flutter cupertino/route.dart :: [_CupertinoBackGestureDetector] | |
class _MyBackGestureController<T> { | |
static const _kTag = 'MyBackGestureController'; | |
final ValueNotifier<double> _dragDistance; | |
_MyBackGestureController(this._dragDistance); | |
static const _kThreshold = 24.0; | |
void dragUpdate(double delta) { | |
_dragDistance.value += delta; | |
} | |
void dragEnd(double velocity) { | |
final dragDistanceValue = _dragDistance.value; | |
_dragDistance.value = 0.0; // reset | |
final shouldPop = dragDistanceValue >= _kThreshold; | |
Log.d(_kTag, 'dragEnd shouldPop=$shouldPop dragDistance=$dragDistanceValue velocity=$velocity', self: this); | |
if (shouldPop) { | |
final context = LongLiveContextSource.primary.context; | |
if (context == null) { | |
Log.i(_kTag, 'dragEnd skip maybePop since context==null', self: this); | |
return; | |
} | |
unawaited(Navigator.of(context).maybePop()); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment