Skip to content

Instantly share code, notes, and snippets.

@fzyzcjy
Created July 29, 2023 10:32
Show Gist options
  • Save fzyzcjy/4d9238c5be3f49ab45de9a00436b4743 to your computer and use it in GitHub Desktop.
Save fzyzcjy/4d9238c5be3f49ab45de9a00436b4743 to your computer and use it in GitHub Desktop.
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