scrollbar.dart 5.1 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
import 'dart:async';

7
import 'package:flutter/cupertino.dart';
8 9 10 11
import 'package:flutter/widgets.dart';

import 'theme.dart';

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

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

42
  /// The widget below this widget in the tree.
43
  ///
44 45 46 47
  /// 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].
48 49
  final Widget child;

50 51 52
  /// {@macro flutter.cupertino.cupertinoScrollbar.controller}
  final ScrollController controller;

53
  @override
54
  _ScrollbarState createState() => _ScrollbarState();
55 56
}

57 58 59 60
class _ScrollbarState extends State<Scrollbar> with TickerProviderStateMixin {
  ScrollbarPainter _materialPainter;
  TextDirection _textDirection;
  Color _themeColor;
61
  bool _useCupertinoScrollbar;
62 63 64
  AnimationController _fadeoutAnimationController;
  Animation<double> _fadeoutOpacityAnimation;
  Timer _fadeoutTimer;
65 66

  @override
67 68
  void initState() {
    super.initState();
69
    _fadeoutAnimationController = AnimationController(
70 71 72
      vsync: this,
      duration: _kScrollbarFadeDuration,
    );
73
    _fadeoutOpacityAnimation = CurvedAnimation(
74
      parent: _fadeoutAnimationController,
75
      curve: Curves.fastOutSlowIn,
76
    );
77 78
  }

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

108
  ScrollbarPainter _buildMaterialScrollbarPainter() {
109
    return ScrollbarPainter(
110 111 112 113 114 115
      color: _themeColor,
      textDirection: _textDirection,
      thickness: _kScrollbarThickness,
      fadeoutOpacityAnimation: _fadeoutOpacityAnimation,
      padding: MediaQuery.of(context).padding,
    );
116 117
  }

118
  bool _handleScrollNotification(ScrollNotification notification) {
119 120 121 122 123
    final ScrollMetrics metrics = notification.metrics;
    if (metrics.maxScrollExtent <= metrics.minScrollExtent) {
      return false;
    }

124 125
    // iOS sub-delegates to the CupertinoScrollbar instead and doesn't handle
    // scroll notifications here.
126 127
    if (!_useCupertinoScrollbar &&
        (notification is ScrollUpdateNotification || notification is OverscrollNotification)) {
128 129 130 131 132 133
      if (_fadeoutAnimationController.status != AnimationStatus.forward) {
        _fadeoutAnimationController.forward();
      }

      _materialPainter.update(notification.metrics, notification.metrics.axisDirection);
      _fadeoutTimer?.cancel();
134
      _fadeoutTimer = Timer(_kScrollbarTimeToFade, () {
135 136 137 138 139
        _fadeoutAnimationController.reverse();
        _fadeoutTimer = null;
      });
    }
    return false;
140 141
  }

142 143 144 145 146 147
  @override
  void dispose() {
    _fadeoutAnimationController.dispose();
    _fadeoutTimer?.cancel();
    _materialPainter?.dispose();
    super.dispose();
148 149
  }

150
  @override
151
  Widget build(BuildContext context) {
152 153 154
    if (_useCupertinoScrollbar) {
      return CupertinoScrollbar(
        child: widget.child,
155
        controller: widget.controller,
156 157 158 159 160 161 162
      );
    }
    return NotificationListener<ScrollNotification>(
      onNotification: _handleScrollNotification,
      child: RepaintBoundary(
        child: CustomPaint(
          foregroundPainter: _materialPainter,
163
          child: RepaintBoundary(
164
            child: widget.child,
165
          ),
166 167 168
        ),
      ),
    );
169 170
  }
}