| // 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'; |
| |
| /// A material design scrollbar. |
| /// |
| /// A scrollbar indicates which portion of a [Scrollable] widget is actually |
| /// visible. |
| /// |
| /// To add a scrollbar to a [ScrollView], simply wrap the scroll view widget in |
| /// a [Scrollbar] widget. |
| /// |
| /// See also: |
| /// |
| /// * [ListView], which display a linear, scrollable list of children. |
| /// * [GridView], which display a 2 dimensional, scrollable array of children. |
| class Scrollbar extends StatefulWidget { |
| /// Creates a material design scrollbar that wraps the given [child]. |
| /// |
| /// The [child] should be a source of [ScrollNotification] notifications, |
| /// typically a [Scrollable] widget. |
| const Scrollbar({ |
| Key key, |
| @required this.child, |
| }) : super(key: key); |
| |
| /// The subtree to place inside the [Scrollbar]. |
| /// |
| /// This should include a source of [ScrollNotification] notifications, |
| /// typically a [Scrollable] widget. |
| final Widget child; |
| |
| @override |
| _ScrollbarState createState() => new _ScrollbarState(); |
| } |
| |
| class _ScrollbarState extends State<Scrollbar> with TickerProviderStateMixin { |
| _ScrollbarPainter _painter; |
| |
| @override |
| void didChangeDependencies() { |
| super.didChangeDependencies(); |
| _painter ??= new _ScrollbarPainter(this); |
| _painter |
| ..color = Theme.of(context).highlightColor |
| ..textDirection = Directionality.of(context); |
| } |
| |
| bool _handleScrollNotification(ScrollNotification notification) { |
| if (notification is ScrollUpdateNotification || |
| notification is OverscrollNotification) |
| _painter.update(notification.metrics, notification.metrics.axisDirection); |
| return false; |
| } |
| |
| @override |
| void dispose() { |
| _painter.dispose(); |
| super.dispose(); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| return new NotificationListener<ScrollNotification>( |
| 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: _painter, |
| child: new RepaintBoundary( |
| child: widget.child, |
| ), |
| ), |
| ), |
| ); |
| } |
| } |
| |
| class _ScrollbarPainter extends ChangeNotifier implements CustomPainter { |
| _ScrollbarPainter(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(); |
| } |
| |
| TextDirection get textDirection => _textDirection; |
| TextDirection _textDirection; |
| set textDirection(TextDirection value) { |
| assert(value != null); |
| if (_textDirection == value) |
| return; |
| _textDirection = 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); |
| |
| double _getThumbX(Size size) { |
| assert(textDirection != null); |
| switch (textDirection) { |
| case TextDirection.rtl: |
| return 0.0; |
| case TextDirection.ltr: |
| return size.width - _kThumbGirth; |
| } |
| return null; |
| } |
| |
| void _paintVerticalThumb(Canvas canvas, Size size, double thumbOffset, double thumbExtent) { |
| final Offset thumbOrigin = new Offset(_getThumbX(size), thumbOffset); |
| final Size thumbSize = new Size(_kThumbGirth, thumbExtent); |
| canvas.drawRect(thumbOrigin & thumbSize, _paint); |
| } |
| |
| void _paintHorizontalThumb(Canvas canvas, Size size, double thumbOffset, double thumbExtent) { |
| final Offset thumbOrigin = new Offset(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)) { |
| double thumbExtent = math.min(viewport, _kMinThumbExtent); |
| if (before + inside + after > 0.0) |
| thumbExtent = math.max(thumbExtent, viewport * inside / (before + inside + after)); |
| |
| final double thumbOffset = (before + after > 0.0) ? |
| before * (viewport - thumbExtent) / (before + after) : 0.0; |
| |
| painter(canvas, size, thumbOffset, thumbExtent); |
| } |
| |
| @override |
| 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; |
| } |
| } |
| |
| @override |
| bool hitTest(Offset position) => null; |
| |
| @override |
| bool shouldRepaint(_ScrollbarPainter oldDelegate) => false; |
| } |