可以参考如下代码实现:
import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
//provide the same scrollController for list and draggableScrollbar
final ScrollController controller = ScrollController();
@override
Widget build(BuildContext context) {
return new MaterialApp(
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new Scaffold(
appBar: new AppBar(
title: new Text('Draggable Scroll Bar Demo'),
),
//DraggableScrollbar builds Stack with provided Scrollable List of Grid
body: new DraggableScrollbar(
child: _buildGrid(),
heightScrollThumb: 40.0,
controller: controller,
),
),
);
}
Widget _buildGrid() {
return GridView.builder(
controller: controller,//scrollController is final in this stateless widget
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 5,
),
padding: EdgeInsets.zero,
itemCount: 1000,
itemBuilder: (context, index) {
return Container(
alignment: Alignment.center,
margin: EdgeInsets.all(2.0),
color: Colors.grey[300],
//I've add index to grid cells to see more clear how it scrolls
child: new Center(child: new Text("$index")),
);
},
);
}
}
class DraggableScrollbar extends StatefulWidget {
final double heightScrollThumb;
final Widget child;
final ScrollController controller;
DraggableScrollbar({this.heightScrollThumb, this.child, this.controller});
@override
_DraggableScrollbarState createState() => new _DraggableScrollbarState();
}
class _DraggableScrollbarState extends State<DraggableScrollbar> {
//this counts offset for scroll thumb in Vertical axis
double _barOffset;
//this counts offset for list in Vertical axis
double _viewOffset;
//variable to track when scrollbar is dragged
bool _isDragInProcess;
@override
void initState() {
super.initState();
_barOffset = 0.0;
_viewOffset = 0.0;
_isDragInProcess = false;
}
//if list takes 300.0 pixels of height on screen and scrollthumb height is 40.0
//then max bar offset is 260.0
double get barMaxScrollExtent =>
context.size.height - widget.heightScrollThumb;
double get barMinScrollExtent => 0.0;
//this is usually lenght (in pixels) of list
//if list has 1000 items of 100.0 pixels each, maxScrollExtent is 100,000.0 pixels
double get viewMaxScrollExtent => widget.controller.position.maxScrollExtent;
//this is usually 0.0
double get viewMinScrollExtent => widget.controller.position.minScrollExtent;
double getScrollViewDelta(
double barDelta,
double barMaxScrollExtent,
double viewMaxScrollExtent,
) {//propotion
return barDelta * viewMaxScrollExtent / barMaxScrollExtent;
}
double getBarDelta(
double scrollViewDelta,
double barMaxScrollExtent,
double viewMaxScrollExtent,
) {//propotion
return scrollViewDelta * barMaxScrollExtent / viewMaxScrollExtent;
}
void _onVerticalDragStart(DragStartDetails details) {
setState(() {
_isDragInProcess = true;
});
}
void _onVerticalDragEnd(DragEndDetails details) {
setState(() {
_isDragInProcess = false;
});
}
void _onVerticalDragUpdate(DragUpdateDetails details) {
setState(() {
_barOffset += details.delta.dy;
if (_barOffset < barMinScrollExtent) {
_barOffset = barMinScrollExtent;
}
if (_barOffset > barMaxScrollExtent) {
_barOffset = barMaxScrollExtent;
}
double viewDelta = getScrollViewDelta(
details.delta.dy, barMaxScrollExtent, viewMaxScrollExtent);
_viewOffset = widget.controller.position.pixels + viewDelta;
if (_viewOffset < widget.controller.position.minScrollExtent) {
_viewOffset = widget.controller.position.minScrollExtent;
}
if (_viewOffset > viewMaxScrollExtent) {
_viewOffset = viewMaxScrollExtent;
}
widget.controller.jumpTo(_viewOffset);
});
}
//this function process events when scroll controller changes it's position
//by scrollController.jumpTo or scrollController.animateTo functions.
//It can be when user scrolls, drags scrollbar (see line 139)
//or any other manipulation with scrollController outside this widget
changePosition(ScrollNotification notification) {
//if notification was fired when user drags we don't need to update scrollThumb position
if (_isDragInProcess) {
return;
}
setState(() {
if (notification is ScrollUpdateNotification) {
_barOffset += getBarDelta(
notification.scrollDelta,
barMaxScrollExtent,
viewMaxScrollExtent,
);
if (_barOffset < barMinScrollExtent) {
_barOffset = barMinScrollExtent;
}
if (_barOffset > barMaxScrollExtent) {
_barOffset = barMaxScrollExtent;
}
_viewOffset += notification.scrollDelta;
if (_viewOffset < widget.controller.position.minScrollExtent) {
_viewOffset = widget.controller.position.minScrollExtent;
}
if (_viewOffset > viewMaxScrollExtent) {
_viewOffset = viewMaxScrollExtent;
}
}
});
}
@override
Widget build(BuildContext context) {
return NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
changePosition(notification);
},
child: new Stack(children: <Widget>[
widget.child,
GestureDetector(
//we've add functions for onVerticalDragStart and onVerticalDragEnd
//to track when dragging starts and finishes
onVerticalDragStart: _onVerticalDragStart,
onVerticalDragUpdate: _onVerticalDragUpdate,
onVerticalDragEnd: _onVerticalDragEnd,
child: Container(
alignment: Alignment.topRight,
margin: EdgeInsets.only(top: _barOffset),
child: _buildScrollThumb())),
]));
}
Widget _buildScrollThumb() {
return new Container(
height: widget.heightScrollThumb,
width: 20.0,
color: Colors.blue,
);
}
}