scrollbar.dart 6.39 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
// @dart = 2.8

7 8
import 'dart:async';

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

import 'theme.dart';

14
const double _kScrollbarThickness = 6.0;
15 16
const Duration _kScrollbarFadeDuration = Duration(milliseconds: 300);
const Duration _kScrollbarTimeToFade = Duration(milliseconds: 600);
17

18 19 20 21 22
/// A material design scrollbar.
///
/// A scrollbar indicates which portion of a [Scrollable] widget is actually
/// visible.
///
23 24
/// Dynamically changes to an iOS style scrollbar that looks like
/// [CupertinoScrollbar] on the iOS platform.
25
///
26 27 28 29 30 31 32
/// 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.
33
class Scrollbar extends StatefulWidget {
34 35 36 37
  /// Creates a material design scrollbar that wraps the given [child].
  ///
  /// The [child] should be a source of [ScrollNotification] notifications,
  /// typically a [Scrollable] widget.
38
  const Scrollbar({
39
    Key key,
40
    @required this.child,
41
    this.controller,
42
    this.isAlwaysShown = false,
43 44
  }) : assert(!isAlwaysShown || controller != null, 'When isAlwaysShown is true, must pass a controller that is attached to a scroll view'),
       super(key: key);
45

46
  /// The widget below this widget in the tree.
47
  ///
48 49 50 51
  /// The scrollbar will be stacked on top of this child. This child (and its
  /// subtree) should include a source of [ScrollNotification] notifications.
  ///
  /// Typically a [ListView] or [CustomScrollView].
52 53
  final Widget child;

54 55 56
  /// {@macro flutter.cupertino.cupertinoScrollbar.controller}
  final ScrollController controller;

57 58 59
  /// {@macro flutter.cupertino.cupertinoScrollbar.isAlwaysShown}
  final bool isAlwaysShown;

60
  @override
61
  _ScrollbarState createState() => _ScrollbarState();
62 63
}

64 65 66 67
class _ScrollbarState extends State<Scrollbar> with TickerProviderStateMixin {
  ScrollbarPainter _materialPainter;
  TextDirection _textDirection;
  Color _themeColor;
68
  bool _useCupertinoScrollbar;
69 70 71
  AnimationController _fadeoutAnimationController;
  Animation<double> _fadeoutOpacityAnimation;
  Timer _fadeoutTimer;
72 73

  @override
74 75
  void initState() {
    super.initState();
76
    _fadeoutAnimationController = AnimationController(
77 78 79
      vsync: this,
      duration: _kScrollbarFadeDuration,
    );
80
    _fadeoutOpacityAnimation = CurvedAnimation(
81
      parent: _fadeoutAnimationController,
82
      curve: Curves.fastOutSlowIn,
83
    );
84 85
  }

86
  @override
87 88
  void didChangeDependencies() {
    super.didChangeDependencies();
89 90 91 92
    assert((() {
      _useCupertinoScrollbar = null;
      return true;
    })());
93
    final ThemeData theme = Theme.of(context);
94
    switch (theme.platform) {
95
      case TargetPlatform.iOS:
96
      case TargetPlatform.macOS:
97 98 99 100 101
        // On iOS, stop all local animations. CupertinoScrollbar has its own
        // animations.
        _fadeoutTimer?.cancel();
        _fadeoutTimer = null;
        _fadeoutAnimationController.reset();
102
        _useCupertinoScrollbar = true;
103 104 105
        break;
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
106 107
      case TargetPlatform.linux:
      case TargetPlatform.windows:
108 109 110
        _themeColor = theme.highlightColor.withOpacity(1.0);
        _textDirection = Directionality.of(context);
        _materialPainter = _buildMaterialScrollbarPainter();
111
        _useCupertinoScrollbar = false;
112
        _triggerScrollbar();
113
        break;
114
    }
115
    assert(_useCupertinoScrollbar != null);
116 117
  }

118 119 120 121 122 123 124
  @override
  void didUpdateWidget(Scrollbar oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.isAlwaysShown != oldWidget.isAlwaysShown) {
      if (widget.isAlwaysShown == false) {
        _fadeoutAnimationController.reverse();
      } else {
125
        _triggerScrollbar();
126 127 128 129 130
        _fadeoutAnimationController.animateTo(1.0);
      }
    }
  }

131 132 133 134 135 136 137 138 139 140 141 142
  // Wait one frame and cause an empty scroll event.  This allows the thumb to
  // show immediately when isAlwaysShown is true.  A scroll event is required in
  // order to paint the thumb.
  void _triggerScrollbar() {
    WidgetsBinding.instance.addPostFrameCallback((Duration duration) {
      if (widget.isAlwaysShown) {
        _fadeoutTimer?.cancel();
        widget.controller.position.didUpdateScrollPositionBy(0);
      }
    });
  }

143
  ScrollbarPainter _buildMaterialScrollbarPainter() {
144
    return ScrollbarPainter(
145 146 147 148 149 150
      color: _themeColor,
      textDirection: _textDirection,
      thickness: _kScrollbarThickness,
      fadeoutOpacityAnimation: _fadeoutOpacityAnimation,
      padding: MediaQuery.of(context).padding,
    );
151 152
  }

153
  bool _handleScrollNotification(ScrollNotification notification) {
154 155 156 157 158
    final ScrollMetrics metrics = notification.metrics;
    if (metrics.maxScrollExtent <= metrics.minScrollExtent) {
      return false;
    }

159 160
    // iOS sub-delegates to the CupertinoScrollbar instead and doesn't handle
    // scroll notifications here.
161
    if (!_useCupertinoScrollbar &&
162 163
        (notification is ScrollUpdateNotification ||
            notification is OverscrollNotification)) {
164 165 166 167
      if (_fadeoutAnimationController.status != AnimationStatus.forward) {
        _fadeoutAnimationController.forward();
      }

168 169 170 171 172 173 174 175 176 177 178
      _materialPainter.update(
        notification.metrics,
        notification.metrics.axisDirection,
      );
      if (!widget.isAlwaysShown) {
        _fadeoutTimer?.cancel();
        _fadeoutTimer = Timer(_kScrollbarTimeToFade, () {
          _fadeoutAnimationController.reverse();
          _fadeoutTimer = null;
        });
      }
179 180
    }
    return false;
181 182
  }

183 184 185 186 187 188
  @override
  void dispose() {
    _fadeoutAnimationController.dispose();
    _fadeoutTimer?.cancel();
    _materialPainter?.dispose();
    super.dispose();
189 190
  }

191
  @override
192
  Widget build(BuildContext context) {
193 194 195
    if (_useCupertinoScrollbar) {
      return CupertinoScrollbar(
        child: widget.child,
196
        isAlwaysShown: widget.isAlwaysShown,
197
        controller: widget.controller,
198 199 200 201 202 203 204
      );
    }
    return NotificationListener<ScrollNotification>(
      onNotification: _handleScrollNotification,
      child: RepaintBoundary(
        child: CustomPaint(
          foregroundPainter: _materialPainter,
205
          child: RepaintBoundary(
206
            child: widget.child,
207
          ),
208 209 210
        ),
      ),
    );
211 212
  }
}