scrollbar.dart 6.87 KB
Newer Older
1 2 3 4
// 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.

5 6 7 8
import 'dart:async';
import 'dart:math' as math;

import 'package:flutter/foundation.dart';
9 10 11 12
import 'package:flutter/widgets.dart';

import 'theme.dart';

13 14 15 16 17 18 19 20 21 22 23 24
/// 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.
25
class Scrollbar extends StatefulWidget {
26 27 28 29
  /// Creates a material design scrollbar that wraps the given [child].
  ///
  /// The [child] should be a source of [ScrollNotification] notifications,
  /// typically a [Scrollable] widget.
30
  const Scrollbar({
31
    Key key,
32
    @required this.child,
33 34
  }) : super(key: key);

35 36 37 38
  /// The subtree to place inside the [Scrollbar].
  ///
  /// This should include a source of [ScrollNotification] notifications,
  /// typically a [Scrollable] widget.
39 40 41
  final Widget child;

  @override
42
  _ScrollbarState createState() => new _ScrollbarState();
43 44
}

45
class _ScrollbarState extends State<Scrollbar> with TickerProviderStateMixin {
46
  _ScrollbarPainter _painter;
47 48

  @override
49 50
  void didChangeDependencies() {
    super.didChangeDependencies();
51
    _painter ??= new _ScrollbarPainter(this);
52 53 54
    _painter
      ..color = Theme.of(context).highlightColor
      ..textDirection = Directionality.of(context);
55 56
  }

Adam Barth's avatar
Adam Barth committed
57
  bool _handleScrollNotification(ScrollNotification notification) {
58 59
    if (notification is ScrollUpdateNotification ||
        notification is OverscrollNotification)
60
      _painter.update(notification.metrics, notification.metrics.axisDirection);
61 62 63 64 65
    return false;
  }

  @override
  void dispose() {
66
    _painter.dispose();
67 68 69 70 71
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
Adam Barth's avatar
Adam Barth committed
72
    return new NotificationListener<ScrollNotification>(
73 74 75 76 77
      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(
78
          foregroundPainter: _painter,
79
          child: new RepaintBoundary(
80
            child: widget.child,
81 82 83 84 85 86 87
          ),
        ),
      ),
    );
  }
}

88
class _ScrollbarPainter extends ChangeNotifier implements CustomPainter {
89 90
  _ScrollbarPainter(TickerProvider vsync)
    : assert(vsync != null) {
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
    _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);
107
    if (_color == value)
108 109 110 111 112
      return;
    _color = value;
    notifyListeners();
  }

113 114 115 116 117 118 119 120 121 122
  TextDirection get textDirection => _textDirection;
  TextDirection _textDirection;
  set textDirection(TextDirection value) {
    assert(value != null);
    if (_textDirection == value)
      return;
    _textDirection = value;
    notifyListeners();
  }

123 124 125 126 127 128 129
  @override
  void dispose() {
    _fadeOut?.cancel();
    _fadeController.dispose();
    super.dispose();
  }

130
  ScrollMetrics _lastMetrics;
131 132 133 134 135 136 137
  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);

138
  void update(ScrollMetrics metrics, AxisDirection axisDirection) {
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156
    _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);

157 158 159 160 161 162 163 164 165 166 167
  double _getThumbX(Size size) {
    assert(textDirection != null);
    switch (textDirection) {
      case TextDirection.rtl:
        return 0.0;
      case TextDirection.ltr:
        return size.width - _kThumbGirth;
    }
    return null;
  }

168
  void _paintVerticalThumb(Canvas canvas, Size size, double thumbOffset, double thumbExtent) {
169
    final Offset thumbOrigin = new Offset(_getThumbX(size), thumbOffset);
170 171 172 173 174
    final Size thumbSize = new Size(_kThumbGirth, thumbExtent);
    canvas.drawRect(thumbOrigin & thumbSize, _paint);
  }

  void _paintHorizontalThumb(Canvas canvas, Size size, double thumbOffset, double thumbExtent) {
175
    final Offset thumbOrigin = new Offset(thumbOffset, size.height - _kThumbGirth);
176 177 178 179 180 181
    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)) {
182 183 184 185 186 187 188
    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;

189 190 191
    painter(canvas, size, thumbOffset, thumbExtent);
  }

192
  @override
193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212
  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
213
  bool hitTest(Offset position) => null;
214 215

  @override
216
  bool shouldRepaint(_ScrollbarPainter oldDelegate) => false;
217
}