flutter 的滚动控制 —— 滚动类组件的内部与整体滚动
效果:
代码:
class DetailScreen extends StatefulWidget {
const DetailScreen({super.key});
@override
State<DetailScreen> createState() => _DetailScreenState();
}
class _DetailScreenState extends State<DetailScreen> {
@override
Widget build(BuildContext context) {
//DraggableScrollableNotification
return Scaffold(
appBar: AppBar(
foregroundColor: Colors.white,
),
body: Stack(
children: [
Image.network(
"https://r11.realme.net/CN/thread/1555770224727732224.jpg",
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
),
DraggableScrollableSheet(
minChildSize: 0.8,
initialChildSize: 0.8,
maxChildSize: 1.0,
builder: (context, scrollController) {
return Container(
color: Colors.transparent,
child: Column(
children: [
Container(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 10),
decoration: const BoxDecoration(
borderRadius: BorderRadius.vertical(top: Radius.circular(15)),
color: Colors.red,
),
width: double.infinity,
height: 200,
child: const Text("标题"),
),
Expanded(
child: ListView(
physics: const BouncingScrollPhysics(),
controller: scrollController,
children: [
Container(
decoration: const BoxDecoration(
color: Colors.green,
),
height: 1200,
child: const Text("内容"),
),
],
),
)
],
),
);
},
),
],
),
);
}
}
源码分析:
_DraggableScrollableSheetState 源码
class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
late _DraggableScrollableSheetScrollController _scrollController;
late _DraggableSheetExtent _extent;
@override
void initState() {
super.initState();
_extent = _DraggableSheetExtent(
minSize: widget.minChildSize,
maxSize: widget.maxChildSize,
snap: widget.snap,
snapSizes: _impliedSnapSizes(),
snapAnimationDuration: widget.snapAnimationDuration,
initialSize: widget.initialChildSize,
shouldCloseOnMinExtent: widget.shouldCloseOnMinExtent,
);
//初始化一个 ScrollController
_scrollController = _DraggableScrollableSheetScrollController(extent: _extent);
widget.controller?._attach(_scrollController);
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<double>(
valueListenable: _extent._currentSize,
builder: (BuildContext context, double currentSize, Widget? child) => LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
_extent.availablePixels = widget.maxChildSize * constraints.biggest.height;
final Widget sheet = FractionallySizedBox(
heightFactor: currentSize,
alignment: Alignment.bottomCenter,
child: child,
);
return widget.expand ? SizedBox.expand(child: sheet) : sheet;
},
),
child: widget.builder(context, _scrollController),
);
}
}
_DraggableScrollableSheetScrollController 源码
class _DraggableScrollableSheetScrollController extends ScrollController {
_DraggableScrollableSheetScrollController({
required this.extent,
});
_DraggableSheetExtent extent;
VoidCallback? onPositionDetached;
@override
_DraggableScrollableSheetScrollPosition createScrollPosition(
ScrollPhysics physics,
ScrollContext context,
ScrollPosition? oldPosition,
) {
return _DraggableScrollableSheetScrollPosition(
physics: physics.applyTo(const AlwaysScrollableScrollPhysics()),
context: context,
oldPosition: oldPosition,
getExtent: () => extent,
);
}
@override
_DraggableScrollableSheetScrollPosition get position =>
super.position as _DraggableScrollableSheetScrollPosition;
@override
void detach(ScrollPosition position) {
onPositionDetached?.call();
super.detach(position);
}
}
_DraggableScrollableSheetScrollPosition 源码
class _DraggableScrollableSheetScrollPosition extends ScrollPositionWithSingleContext {
_DraggableScrollableSheetScrollPosition({
required super.physics,
required super.context,
super.oldPosition,
required this.getExtent,
});
@override
void applyUserOffset(double delta) {
//关键方法。在 DraggableScrollableSheet 拖动到最低位置之前是组件自身整体滚动,
//否则是子组件的 ListView 内部滚动
if (!listShouldScroll &&
(!(extent.isAtMin || extent.isAtMax) ||
(extent.isAtMin && delta < 0) ||
(extent.isAtMax && delta > 0))) {
//组件整体滚动
extent.addPixelDelta(-delta, context.notificationContext!);
} else {
//组件内部滚动(默认处理)
super.applyUserOffset(delta);
}
}
}
_DraggableSheetExtent 源码
class _DraggableSheetExtent {
_DraggableSheetExtent({
required this.minSize,
required this.maxSize,
required this.snap,
required this.snapSizes,
required this.initialSize,
this.snapAnimationDuration,
ValueNotifier<double>? currentSize,
bool? hasDragged,
bool? hasChanged,
this.shouldCloseOnMinExtent = true,
}) : assert(minSize >= 0),
assert(maxSize <= 1),
assert(minSize <= initialSize),
assert(initialSize <= maxSize),
_currentSize = currentSize ?? ValueNotifier<double>(initialSize),
availablePixels = double.infinity,
hasDragged = hasDragged ?? false,
hasChanged = hasChanged ?? false;
VoidCallback? _cancelActivity;
final double minSize;
final double maxSize;
final bool snap;
final List<double> snapSizes;
final Duration? snapAnimationDuration;
final double initialSize;
final bool shouldCloseOnMinExtent;
final ValueNotifier<double> _currentSize;
double availablePixels;
bool hasDragged;
bool hasChanged;
bool get isAtMin => minSize >= _currentSize.value;
bool get isAtMax => maxSize <= _currentSize.value;
double get currentSize => _currentSize.value;
double get currentPixels => sizeToPixels(_currentSize.value);
List<double> get pixelSnapSizes => snapSizes.map(sizeToPixels).toList();
void addPixelDelta(double delta, BuildContext context) {
// Stop any playing sheet animations.
_cancelActivity?.call();
_cancelActivity = null;
// The user has interacted with the sheet, set `hasDragged` to true so that
// we'll snap if applicable.
hasDragged = true;
hasChanged = true;
if (availablePixels == 0) {
return;
}
updateSize(currentSize + pixelsToSize(delta), context);
}
void updateSize(double newSize, BuildContext context) {
final double clampedSize = clampDouble(newSize, minSize, maxSize);
if (_currentSize.value == clampedSize) {
return;
}
_currentSize.value = clampedSize;
//发送通知,告诉父组件(ScrollBar)当前滚动位置
DraggableScrollableNotification(
minExtent: minSize,
maxExtent: maxSize,
extent: currentSize,
initialExtent: initialSize,
context: context,
shouldCloseOnMinExtent: shouldCloseOnMinExtent,
).dispatch(context);
}
double pixelsToSize(double pixels) {
return pixels / availablePixels * maxSize;
}
double sizeToPixels(double size) {
return size / maxSize * availablePixels;
}
void dispose() {
_currentSize.dispose();
}
}
233