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

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

import 'theme.dart';

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

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

  @override
51
  _ScrollbarState createState() => new _ScrollbarState();
52 53 54
}


55 56 57 58 59
class _ScrollbarState extends State<Scrollbar> with TickerProviderStateMixin {
  ScrollbarPainter _materialPainter;
  TargetPlatform _currentPlatform;
  TextDirection _textDirection;
  Color _themeColor;
60

61 62 63
  AnimationController _fadeoutAnimationController;
  Animation<double> _fadeoutOpacityAnimation;
  Timer _fadeoutTimer;
64 65

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

78
  @override
79 80
  void didChangeDependencies() {
    super.didChangeDependencies();
81

82 83
    final ThemeData theme = Theme.of(context);
    _currentPlatform = theme.platform;
84

85 86 87 88 89 90 91 92 93 94 95 96 97 98
    switch (_currentPlatform) {
      case TargetPlatform.iOS:
        // On iOS, stop all local animations. CupertinoScrollbar has its own
        // animations.
        _fadeoutTimer?.cancel();
        _fadeoutTimer = null;
        _fadeoutAnimationController.reset();
        break;
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
        _themeColor = theme.highlightColor.withOpacity(1.0);
        _textDirection = Directionality.of(context);
        _materialPainter = _buildMaterialScrollbarPainter();
        break;
99 100 101
    }
  }

102 103 104 105 106 107 108
  ScrollbarPainter _buildMaterialScrollbarPainter() {
    return new ScrollbarPainter(
        color: _themeColor,
        textDirection: _textDirection,
        thickness: _kScrollbarThickness,
        fadeoutOpacityAnimation: _fadeoutOpacityAnimation,
      );
109 110
  }

111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128
  bool _handleScrollNotification(ScrollNotification notification) {
    // iOS sub-delegates to the CupertinoScrollbar instead and doesn't handle
    // scroll notifications here.
    if (_currentPlatform != TargetPlatform.iOS
        && (notification is ScrollUpdateNotification
            || notification is OverscrollNotification)) {
      if (_fadeoutAnimationController.status != AnimationStatus.forward) {
        _fadeoutAnimationController.forward();
      }

      _materialPainter.update(notification.metrics, notification.metrics.axisDirection);
      _fadeoutTimer?.cancel();
      _fadeoutTimer = new Timer(_kScrollbarTimeToFade, () {
        _fadeoutAnimationController.reverse();
        _fadeoutTimer = null;
      });
    }
    return false;
129 130
  }

131 132 133 134 135 136
  @override
  void dispose() {
    _fadeoutAnimationController.dispose();
    _fadeoutTimer?.cancel();
    _materialPainter?.dispose();
    super.dispose();
137 138
  }

139
  @override
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
  Widget build(BuildContext context) {
    switch (_currentPlatform) {
      case TargetPlatform.iOS:
        return new CupertinoScrollbar(
          child: widget.child,
        );
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
        return new NotificationListener<ScrollNotification>(
          onNotification: _handleScrollNotification,
          child: new RepaintBoundary(
            child: new CustomPaint(
              foregroundPainter: _materialPainter,
              child: new RepaintBoundary(
                child: widget.child,
              ),
            ),
          ),
        );
159
    }
160
    throw new FlutterError('Unknown platform for scrollbar insertion');
161 162
  }
}