Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix-scrolling-2-stefan #2

Merged
merged 6 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class MainApp extends StatelessWidget {

@override
Widget build(BuildContext context) {
final maxHeight = MediaQuery.of(context).size.height - kToolbarHeight;
final maxHeight = MediaQuery.of(context).size.height - kToolbarHeight - 100;
return MaterialApp(
home: Scaffold(
appBar: AppBar(
Expand Down
82 changes: 82 additions & 0 deletions lib/gesture_listener.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';

class GestureListener extends StatefulWidget {
final void Function(DragUpdateDetails) onVerticalDragUpdate;
final void Function(DragEndDetails) onVerticalDragEnd;
final void Function() onVerticalDragCancel;

final bool canDrag;

final Widget child;

const GestureListener({
super.key,
required this.canDrag,
required this.onVerticalDragUpdate,
required this.onVerticalDragEnd,
required this.onVerticalDragCancel,
required this.child,
});

@override
State<GestureListener> createState() => _GestureListenerState();
}

class _GestureListenerState extends State<GestureListener> {
final _velocityTracker = VelocityTracker.withKind(PointerDeviceKind.touch);

@override
Widget build(BuildContext context) {
return Listener(
onPointerDown: (event) {
if (!widget.canDrag) return;

_velocityTracker.addPosition(event.timeStamp, event.position);
},
onPointerMove: (event) {
if (!widget.canDrag) return;

_velocityTracker.addPosition(event.timeStamp, event.position);

final delta = event.delta;
final primaryDelta = delta.dy;

/// ⚠️ If not assign the dx to 0, an assertion is not working.
final offset = Offset(0, primaryDelta);
JulianBissekkou marked this conversation as resolved.
Show resolved Hide resolved

final details = DragUpdateDetails(
globalPosition: event.position,
delta: offset,
localPosition: event.localPosition,
sourceTimeStamp: event.timeStamp,
primaryDelta: primaryDelta,
);

widget.onVerticalDragUpdate(details);
},
onPointerUp: (event) {
if (!widget.canDrag) return;

final velocity = _velocityTracker.getVelocity();

final pixelsPerSecondY = velocity.pixelsPerSecond.dy;

/// ⚠️ If not assign the dx to 0, an assertion is not working.
JulianBissekkou marked this conversation as resolved.
Show resolved Hide resolved
final offset = Offset(0, pixelsPerSecondY);
final details = DragEndDetails(
velocity: Velocity(pixelsPerSecond: offset),
primaryVelocity: pixelsPerSecondY,
);

widget.onVerticalDragEnd(details);
},
onPointerCancel: (event) {
if (!widget.canDrag) return;

widget.onVerticalDragCancel();
},
child: widget.child,
);
}
}
147 changes: 104 additions & 43 deletions lib/scrollable_bottom_sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';
import 'package:non_uniform_border/non_uniform_border.dart';
import 'package:tapped_bottom_sheet/gesture_listener.dart';

typedef ScrollableBottomSheetBuilder = Widget Function(
BuildContext context,
ScrollController scrollController,
);

const double _kMinFlingVelocity = 600.0;
const double _kCompleteFlingVelocity = 4000.0;

class ScrollableBottomSheet extends StatefulWidget {
final double maxHeight;
final double minHeight;
Expand All @@ -21,11 +25,15 @@ class ScrollableBottomSheet extends StatefulWidget {

final Color borderColor;

final Color backgroundColor;

final bool canDrag;

final Color backgroundColor;
final List<BoxShadow>? shadows;

final double minFlingVelocity;

final double completeFlingVelocity;

const ScrollableBottomSheet({
super.key,
required this.maxHeight,
Expand All @@ -40,6 +48,8 @@ class ScrollableBottomSheet extends StatefulWidget {
required this.borderColor,
required this.backgroundColor,
this.shadows,
this.minFlingVelocity = _kMinFlingVelocity,
this.completeFlingVelocity = _kCompleteFlingVelocity,
});

@override
Expand All @@ -54,10 +64,13 @@ class ScrollableBottomSheetState extends State<ScrollableBottomSheet>
with SingleTickerProviderStateMixin {
final _scrollController = ScrollController();
late AnimationController _animationController;
final _velocityTracker = VelocityTracker.withKind(PointerDeviceKind.touch);
var _scrollingEnabled = false;
var _isScrollingEnabled = false;
var _isScrollingBlocked = false;

Drag? _drag;

ScrollHoldController? _hold;

Tween<double> get _sizeTween =>
Tween(begin: widget.minHeight, end: widget.maxHeight);

Expand Down Expand Up @@ -92,19 +105,11 @@ class ScrollableBottomSheetState extends State<ScrollableBottomSheet>
final borderRadius =
BorderRadius.vertical(top: Radius.circular(widget.borderRadiusTop));

return Listener(
onPointerDown: widget.canDrag
? (p) => _velocityTracker.addPosition(p.timeStamp, p.position)
: null,
onPointerMove: widget.canDrag
? (p) {
_velocityTracker.addPosition(p.timeStamp, p.position);
_onDragUpdate(p.delta.dy);
}
: null,
onPointerUp: widget.canDrag
? (p) => _onGestureEnd(_velocityTracker.getVelocity())
: null,
return GestureListener(
canDrag: widget.canDrag,
onVerticalDragUpdate: (details) => _onDragUpdate(details),
onVerticalDragEnd: (details) => _onDragEnd(details),
onVerticalDragCancel: () => _handleDragCancel(),
child: MediaQuery.removePadding(
context: context,
removeTop: true,
Expand Down Expand Up @@ -145,10 +150,30 @@ class ScrollableBottomSheetState extends State<ScrollableBottomSheet>

// region drag updates

void _onDragUpdate(double dy) {
void _onDragUpdate(DragUpdateDetails details) {
final delta = details.delta;
final primaryDelta = delta.dy;

if (_isScrollingEnabled && _isPanelOpen) {
// _drag might be null if the drag activity ended and called _disposeDrag.
assert(_hold == null || _drag == null);
_drag?.update(details);
if (_scrollController.hasClients &&
_scrollController.position.pixels <= 0 &&
details.primaryDelta! > 0) {
setState(() => _isScrollingEnabled = false);
_handleDragCancel();
if (_scrollController.position.pixels != 0.0) {
_scrollController.position.setPixels(0.0);
}
}
return;
}

// only slide the panel if scrolling is not enabled
if (!_scrollingEnabled && !_isScrollingBlocked) {
_animationController.value -= dy / (widget.maxHeight - widget.minHeight);
if (!_isScrollingEnabled && !_isScrollingBlocked) {
_animationController.value -=
primaryDelta / (widget.maxHeight - widget.minHeight);
}

// if the panel is open and the user hasn't scrolled, we need to determine
Expand All @@ -157,14 +182,18 @@ class ScrollableBottomSheetState extends State<ScrollableBottomSheet>
if (_isPanelOpen &&
_scrollController.hasClients &&
_scrollController.offset <= 0) {
setState(() => _scrollingEnabled = dy < 0);
}

//update scrolling if panel is open
if (_isPanelOpen) {
double scrollOffset = _scrollController.offset;
final scrollTo = scrollOffset -= dy;
_scrollController.jumpTo(scrollTo);
final scrollingEnabled = primaryDelta < 0;

setState(() => _isScrollingEnabled = scrollingEnabled);

if (scrollingEnabled) {
final startDetails = DragStartDetails(
sourceTimeStamp: details.sourceTimeStamp,
globalPosition: details.globalPosition,
);
_hold = _scrollController.position.hold(_disposeHold);
_drag = _scrollController.position.drag(startDetails, _disposeDrag);
}
}
}

Expand All @@ -173,34 +202,48 @@ class ScrollableBottomSheetState extends State<ScrollableBottomSheet>
// region animation and scroll

void _onScroll() {
if (!_scrollingEnabled || _isScrollingBlocked) {
if (!_isScrollingEnabled || _isScrollingBlocked) {
_scrollController.jumpTo(0);
}
}

void _onGestureEnd(Velocity velocity) {
void _onDragEnd(DragEndDetails details) {
if (_isScrollingBlocked) return;

// let the current animation finish before starting a new one
if (_animationController.isAnimating) return;

// if scrolling is allowed and the panel is open, we don't want to close
// the panel if they swipe up on the scrollable
if (_isPanelOpen && _scrollingEnabled) return;
if (_isPanelOpen && _isScrollingEnabled) {
assert(_hold == null || _drag == null);
_drag?.end(details);
assert(_drag == null);

return;
}

final dyVelocity = velocity.pixelsPerSecond.dy;
final visualVelocity = -dyVelocity / (widget.maxHeight - widget.minHeight);
final scrollPixelPerSeconds = details.velocity.pixelsPerSecond.dy;
final flingVelocity =
-scrollPixelPerSeconds / (widget.maxHeight - widget.minHeight);

final newPosition =
final nearestSnapPoint =
_findNearestRelativeSnapPoint(target: _animationController.value);

switch (newPosition) {
case 1.0:
open();
case 0.0:
close();
default:
_flingPanelToPosition(newPosition, visualVelocity);
if (scrollPixelPerSeconds > widget.completeFlingVelocity) {
if (flingVelocity.isNegative) {
_flingPanelToPosition(0.0, flingVelocity);
} else {
_flingPanelToPosition(1.0, flingVelocity);
}
} else {
if (scrollPixelPerSeconds > widget.minFlingVelocity) {
_flingPanelToPosition(nearestSnapPoint, flingVelocity);
} else {
final pixels = _sizeTween.transform(nearestSnapPoint);

animateTo(pixels: pixels, duration: widget.animationDuration);
}
}
}

Expand Down Expand Up @@ -230,7 +273,7 @@ class ScrollableBottomSheetState extends State<ScrollableBottomSheet>
// region panel options

Future<void> close() async {
setState(() => _scrollingEnabled = false);
setState(() => _isScrollingEnabled = false);

await _scrollController.animateTo(
0.0,
Expand Down Expand Up @@ -268,7 +311,7 @@ class ScrollableBottomSheetState extends State<ScrollableBottomSheet>

// Reset the initial state, since we had some issues in the full state of the booking summary
setState(() {
_scrollingEnabled = false;
_isScrollingEnabled = false;
_isScrollingBlocked = false;
});
}
Expand All @@ -289,6 +332,24 @@ class ScrollableBottomSheetState extends State<ScrollableBottomSheet>
final size = _sizeTween.transform(_animationController.value);
widget.onSizeChanged!.call(_animationController.value, size);
}

void _handleDragCancel() {
// _hold might be null if the drag started.
// _drag might be null if the drag activity ended and called _disposeDrag.
assert(_hold == null || _drag == null);
_hold?.cancel();
_drag?.cancel();
assert(_hold == null);
assert(_drag == null);
}

void _disposeHold() {
_hold = null;
}

void _disposeDrag() {
_drag = null;
}
}

double _findClosestPosition({
Expand Down
Loading