Skip to content

Commit

Permalink
Merge pull request #2 from tappeddev/fix-scrolling-2-stefan
Browse files Browse the repository at this point in the history
fix-scrolling-2-stefan
  • Loading branch information
JulianBissekkou authored Mar 12, 2024
2 parents b5215e0 + c2e681c commit 6a54c35
Show file tree
Hide file tree
Showing 4 changed files with 190 additions and 44 deletions.
1 change: 1 addition & 0 deletions .fvm/flutter_sdk
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
84 changes: 84 additions & 0 deletions lib/gesture_listener.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
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
/// in the constructor of [DragUpdateDetails] is thrown.
final offset = Offset(0, primaryDelta);

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
/// in the constructor of [DragEndDetails] is thrown.
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

0 comments on commit 6a54c35

Please sign in to comment.