blob: b4ad42605748f6e100e1ce78cc13b8b90b82fd0a [file]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'theme.dart';
class Scrollbar extends StatefulWidget {
Scrollbar({
Key key,
this.child,
}) : super(key: key);
/// The subtree to place inside the [Scrollbar]. This should include
/// a source of [ScrollNotification2] notifications, typically a [Scrollable2]
/// widget.
final Widget child;
@override
_ScrollbarState createState() => new _ScrollbarState();
}
class _ScrollbarState extends State<Scrollbar> with TickerProviderStateMixin {
_ScrollbarController _controller;
@override
void dependenciesChanged() {
super.dependenciesChanged();
_controller ??= new _ScrollbarController(this);
_controller.color = Theme.of(context).highlightColor;
}
bool _handleScrollNotification(ScrollNotification2 notification) {
if (notification is ScrollUpdateNotification ||
notification is OverscrollNotification)
_controller.update(notification.metrics, notification.axisDirection);
return false;
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return new NotificationListener<ScrollNotification2>(
onNotification: _handleScrollNotification,
// TODO(ianh): Maybe we should try to collapse out these repaint
// boundaries when the scroll bars are invisible.
child: new RepaintBoundary(
child: new CustomPaint(
foregroundPainter: new _ScrollbarPainter(_controller),
child: new RepaintBoundary(
child: config.child,
),
),
),
);
}
}
class _ScrollbarController extends ChangeNotifier {
_ScrollbarController(TickerProvider vsync) {
assert(vsync != null);
_fadeController = new AnimationController(duration: _kThumbFadeDuration, vsync: vsync);
_opacity = new CurvedAnimation(parent: _fadeController, curve: Curves.fastOutSlowIn)
..addListener(notifyListeners);
}
// animation of the main axis direction
AnimationController _fadeController;
Animation<double> _opacity;
// fade-out timer
Timer _fadeOut;
Color get color => _color;
Color _color;
set color(Color value) {
assert(value != null);
if (color == value)
return;
_color = value;
notifyListeners();
}
@override
void dispose() {
_fadeOut?.cancel();
_fadeController.dispose();
super.dispose();
}
ScrollMetrics _lastMetrics;
AxisDirection _lastAxisDirection;
static const double _kMinThumbExtent = 18.0;
static const double _kThumbGirth = 6.0;
static const Duration _kThumbFadeDuration = const Duration(milliseconds: 300);
static const Duration _kFadeOutTimeout = const Duration(milliseconds: 600);
void update(ScrollMetrics metrics, AxisDirection axisDirection) {
_lastMetrics = metrics;
_lastAxisDirection = axisDirection;
if (_fadeController.status == AnimationStatus.completed) {
notifyListeners();
} else if (_fadeController.status != AnimationStatus.forward) {
_fadeController.forward();
}
_fadeOut?.cancel();
_fadeOut = new Timer(_kFadeOutTimeout, startFadeOut);
}
void startFadeOut() {
_fadeOut = null;
_fadeController.reverse();
}
Paint get _paint => new Paint()..color = color.withOpacity(_opacity.value);
void _paintVerticalThumb(Canvas canvas, Size size, double thumbOffset, double thumbExtent) {
final Point thumbOrigin = new Point(size.width - _kThumbGirth, thumbOffset);
final Size thumbSize = new Size(_kThumbGirth, thumbExtent);
canvas.drawRect(thumbOrigin & thumbSize, _paint);
}
void _paintHorizontalThumb(Canvas canvas, Size size, double thumbOffset, double thumbExtent) {
final Point thumbOrigin = new Point(thumbOffset, size.height - _kThumbGirth);
final Size thumbSize = new Size(thumbExtent, _kThumbGirth);
canvas.drawRect(thumbOrigin & thumbSize, _paint);
}
void _paintThumb(double before, double inside, double after, double viewport, Canvas canvas, Size size,
void painter(Canvas canvas, Size size, double thumbOffset, double thumbExtent)) {
final double thumbExtent = math.max(math.min(viewport, _kMinThumbExtent), viewport * inside / (before + inside + after));
final double thumbOffset = before * (viewport - thumbExtent) / (before + after);
painter(canvas, size, thumbOffset, thumbExtent);
}
void paint(Canvas canvas, Size size) {
if (_lastAxisDirection == null || _lastMetrics == null || _opacity.value == 0.0)
return;
switch (_lastAxisDirection) {
case AxisDirection.down:
_paintThumb(_lastMetrics.extentBefore, _lastMetrics.extentInside, _lastMetrics.extentAfter, size.height, canvas, size, _paintVerticalThumb);
break;
case AxisDirection.up:
_paintThumb(_lastMetrics.extentAfter, _lastMetrics.extentInside, _lastMetrics.extentBefore, size.height, canvas, size, _paintVerticalThumb);
break;
case AxisDirection.right:
_paintThumb(_lastMetrics.extentBefore, _lastMetrics.extentInside, _lastMetrics.extentAfter, size.width, canvas, size, _paintHorizontalThumb);
break;
case AxisDirection.left:
_paintThumb(_lastMetrics.extentAfter, _lastMetrics.extentInside, _lastMetrics.extentBefore, size.width, canvas, size, _paintHorizontalThumb);
break;
}
}
}
class _ScrollbarPainter extends CustomPainter {
_ScrollbarPainter(this.controller) : super(repaint: controller);
final _ScrollbarController controller;
@override
void paint(Canvas canvas, Size size) {
controller.paint(canvas, size);
}
@override
bool shouldRepaint(_ScrollbarPainter oldDelegate) {
return oldDelegate.controller != controller;
}
}