scrollbar.dart 6.33 KB
Newer Older
1 2 3 4 5 6 7 8
// 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 'package:flutter/widgets.dart';

import 'theme.dart';

9
const double _kMinScrollbarThumbExtent = 18.0;
10 11 12
const double _kScrollbarThumbGirth = 6.0;
const Duration _kScrollbarThumbFadeDuration = const Duration(milliseconds: 300);

13 14
class _Painter extends CustomPainter {
  _Painter({
15 16 17 18
    this.scrollOffset,
    this.scrollDirection,
    this.contentExtent,
    this.containerExtent,
19
    this.color
20 21 22 23 24 25
  });

  final double scrollOffset;
  final Axis scrollDirection;
  final double contentExtent;
  final double containerExtent;
26
  final Color color;
27 28 29 30 31 32 33 34

  void paintScrollbar(Canvas canvas, Size size) {
    Point thumbOrigin;
    Size thumbSize;

    switch (scrollDirection) {
      case Axis.vertical:
        double thumbHeight = size.height * containerExtent / contentExtent;
35
        thumbHeight = thumbHeight.clamp(_kMinScrollbarThumbExtent, size.height);
36 37 38 39 40 41 42 43
        final double maxThumbTop = size.height - thumbHeight;
        double thumbTop = (scrollOffset / (contentExtent - containerExtent)) * maxThumbTop;
        thumbTop = thumbTop.clamp(0.0, maxThumbTop);
        thumbOrigin = new Point(size.width - _kScrollbarThumbGirth, thumbTop);
        thumbSize = new Size(_kScrollbarThumbGirth, thumbHeight);
        break;
      case Axis.horizontal:
        double thumbWidth = size.width * containerExtent / contentExtent;
44
        thumbWidth = thumbWidth.clamp(_kMinScrollbarThumbExtent, size.width);
45 46 47 48 49 50 51 52
        final double maxThumbLeft = size.width - thumbWidth;
        double thumbLeft = (scrollOffset / (contentExtent - containerExtent)) * maxThumbLeft;
        thumbLeft = thumbLeft.clamp(0.0, maxThumbLeft);
        thumbOrigin = new Point(thumbLeft, size.height - _kScrollbarThumbGirth);
        thumbSize = new Size(thumbWidth, _kScrollbarThumbGirth);
        break;
    }

53
    final Paint paint = new Paint()..color = color;
54 55 56 57 58
    canvas.drawRect(thumbOrigin & thumbSize, paint);
  }

  @override
  void paint(Canvas canvas, Size size) {
59
    if (scrollOffset == null || color.alpha == 0)
60 61 62 63 64
      return;
    paintScrollbar(canvas, size);
  }

  @override
65
  bool shouldRepaint(_Painter oldPainter) {
66
    return oldPainter.scrollOffset != scrollOffset
67 68 69 70
        || oldPainter.scrollDirection != scrollDirection
        || oldPainter.contentExtent != contentExtent
        || oldPainter.containerExtent != containerExtent
        || oldPainter.color != color;
71 72 73 74 75 76 77 78
  }
}

/// Displays a scrollbar that tracks the scrollOffset of its child's [Scrollable]
/// descendant. If the Scrollbar's child has more than one Scrollable descendant
/// the scrollableKey parameter can be used to identify the one the Scrollbar
/// should track.
class Scrollbar extends StatefulWidget {
79 80 81
  /// Creates a scrollbar.
  ///
  /// The child argument must not be null.
82 83 84 85
  Scrollbar({ Key key, this.scrollableKey, this.child }) : super(key: key) {
    assert(child != null);
  }

86 87
  /// Identifies the [Scrollable] descendant of child that the scrollbar will
  /// track. Can be null if there's only one [Scrollable] descendant.
88
  final Key scrollableKey;
89 90 91

  /// The scrollbar will be stacked on top of this child. The scrollbar will
  /// display when child's [Scrollable] descendant is scrolled.
92 93 94 95 96 97
  final Widget child;

  @override
  _ScrollbarState createState() => new _ScrollbarState();
}

98 99
class _ScrollbarState extends State<Scrollbar> with SingleTickerProviderStateMixin {
  AnimationController _fade;
100 101 102 103 104 105 106 107 108
  CurvedAnimation _opacity;
  double _scrollOffset;
  Axis _scrollDirection;
  double _containerExtent;
  double _contentExtent;

  @override
  void initState() {
    super.initState();
109
    _fade = new AnimationController(duration: _kScrollbarThumbFadeDuration, vsync: this);
110
    _opacity = new CurvedAnimation(parent: _fade, curve: Curves.fastOutSlowIn);
111 112
  }

113 114 115 116 117 118
  @override
  void dispose() {
    _fade.stop();
    super.dispose();
  }

119
  void _updateState(ScrollableState scrollable) {
120 121
    if (scrollable.scrollBehavior is! ExtentScrollBehavior)
      return;
122 123 124 125
    if (_scrollOffset != scrollable.scrollOffset)
      setState(() { _scrollOffset = scrollable.scrollOffset; });
    if (_scrollDirection != scrollable.config.scrollDirection)
      setState(() { _scrollDirection = scrollable.config.scrollDirection; });
126
    final ExtentScrollBehavior scrollBehavior = scrollable.scrollBehavior;
127 128 129 130
    if (_contentExtent != scrollBehavior.contentExtent)
      setState(() { _contentExtent = scrollBehavior.contentExtent; });
    if (_containerExtent != scrollBehavior.containerExtent)
      setState(() { _containerExtent = scrollBehavior.containerExtent; });
131 132 133 134 135 136 137
  }

  void _onScrollStarted(ScrollableState scrollable) {
    _updateState(scrollable);
  }

  void _onScrollUpdated(ScrollableState scrollable) {
138
    _updateState(scrollable);
139 140
    if (_fade.status != AnimationStatus.completed)
      _fade.forward();
141 142 143 144 145 146 147 148
  }

  void _onScrollEnded(ScrollableState scrollable) {
    _updateState(scrollable);
    _fade.reverse();
  }

  bool _handleScrollNotification(ScrollNotification notification) {
149
    if (config.scrollableKey == null) {
150 151
      if (notification.depth != 0)
        return false;
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
    } else if (config.scrollableKey != notification.scrollable.config.key) {
      return false;
    }

    final ScrollableState scrollable = notification.scrollable;
    switch(notification.kind) {
      case ScrollNotificationKind.started:
        _onScrollStarted(scrollable);
        break;
      case ScrollNotificationKind.updated:
        _onScrollUpdated(scrollable);
        break;
      case ScrollNotificationKind.ended:
        _onScrollEnded(scrollable);
        break;
167 168 169 170 171 172 173 174 175 176 177 178
    }
    return false;
  }

  @override
  Widget build(BuildContext context) {
    return new NotificationListener<ScrollNotification>(
      onNotification: _handleScrollNotification,
      child: new AnimatedBuilder(
        animation: _opacity,
        builder: (BuildContext context, Widget child) {
          return new CustomPaint(
179
            foregroundPainter: new _Painter(
180 181 182 183
              scrollOffset: _scrollOffset,
              scrollDirection: _scrollDirection,
              containerExtent: _containerExtent,
              contentExtent: _contentExtent,
184
              color: Theme.of(context).highlightColor.withOpacity(_opacity.value)
185 186 187 188 189 190 191 192
            ),
            child: child
          );
        },
        child: config.child
      )
    );
  }
193
}