scrollbar.dart 8.07 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
import 'package:flutter/services.dart';
6 7
import 'package:flutter/widgets.dart';

8 9
import 'colors.dart';

10
// All values eyeballed.
11 12
const double _kScrollbarMinLength = 36.0;
const double _kScrollbarMinOverscrollLength = 8.0;
13
const Duration _kScrollbarTimeToFade = Duration(milliseconds: 1200);
14
const Duration _kScrollbarFadeDuration = Duration(milliseconds: 250);
15
const Duration _kScrollbarResizeDuration = Duration(milliseconds: 100);
16

17 18 19 20 21 22
// Extracted from iOS 13.1 beta using Debug View Hierarchy.
const Color _kScrollbarColor = CupertinoDynamicColor.withBrightness(
  color: Color(0x59000000),
  darkColor: Color(0x80FFFFFF),
);

23 24 25 26 27 28 29
// This is the amount of space from the top of a vertical scrollbar to the
// top edge of the scrollable, measured when the vertical scrollbar overscrolls
// to the top.
// TODO(LongCatIsLooong): fix https://github.com/flutter/flutter/issues/32175
const double _kScrollbarMainAxisMargin = 3.0;
const double _kScrollbarCrossAxisMargin = 3.0;

30
/// An iOS style scrollbar.
31
///
32
/// To add a scrollbar to a [ScrollView], wrap the scroll view widget in
33 34
/// a [CupertinoScrollbar] widget.
///
35 36
/// {@youtube 560 315 https://www.youtube.com/watch?v=DbkIQSvwnZc}
///
37 38 39 40 41 42
/// {@macro flutter.widgets.Scrollbar}
///
/// When dragging a [CupertinoScrollbar] thumb, the thickness and radius will
/// animate from [thickness] and [radius] to [thicknessWhileDragging] and
/// [radiusWhileDragging], respectively.
///
43
/// {@tool dartpad}
44 45 46 47 48 49
/// This sample shows a [CupertinoScrollbar] that fades in and out of view as scrolling occurs.
/// The scrollbar will fade into view as the user scrolls, and fade out when scrolling stops.
/// The `thickness` of the scrollbar will animate from 6 pixels to the `thicknessWhileDragging` of 10
/// when it is dragged by the user. The `radius` of the scrollbar thumb corners will animate from 34
/// to the `radiusWhileDragging` of 0 when the scrollbar is being dragged by the user.
///
50
/// ** See code in examples/api/lib/cupertino/scrollbar/cupertino_scrollbar.0.dart **
51 52
/// {@end-tool}
///
53
/// {@tool dartpad}
54
/// When [thumbVisibility] is true, the scrollbar thumb will remain visible without the
55
/// fade animation. This requires that a [ScrollController] is provided to controller,
56
/// or that the [PrimaryScrollController] is available.
57
///
58
/// ** See code in examples/api/lib/cupertino/scrollbar/cupertino_scrollbar.1.dart **
59
/// {@end-tool}
60
///
61 62
/// See also:
///
63 64 65 66 67 68
///  * [ListView], which displays a linear, scrollable list of children.
///  * [GridView], which displays a 2 dimensional, scrollable array of children.
///  * [Scrollbar], a Material Design scrollbar.
///  * [RawScrollbar], a basic scrollbar that fades in and out, extended
///    by this class to add more animations and behaviors.
class CupertinoScrollbar extends RawScrollbar {
69 70 71 72 73
  /// Creates an iOS style scrollbar that wraps the given [child].
  ///
  /// The [child] should be a source of [ScrollNotification] notifications,
  /// typically a [Scrollable] widget.
  const CupertinoScrollbar({
74 75 76
    super.key,
    required super.child,
    super.controller,
77
    bool? thumbVisibility,
78
    double super.thickness = defaultThickness,
79
    this.thicknessWhileDragging = defaultThicknessWhileDragging,
80
    Radius super.radius = defaultRadius,
81
    this.radiusWhileDragging = defaultRadiusWhileDragging,
82
    ScrollNotificationPredicate? notificationPredicate,
83
    super.scrollbarOrientation,
84
  }) : assert(thickness < double.infinity),
85
       assert(thicknessWhileDragging < double.infinity),
86
       super(
87
         thumbVisibility: thumbVisibility ?? false,
88 89 90
         fadeDuration: _kScrollbarFadeDuration,
         timeToFade: _kScrollbarTimeToFade,
         pressDuration: const Duration(milliseconds: 100),
91
         notificationPredicate: notificationPredicate ?? defaultScrollNotificationPredicate,
92 93 94
       );

  /// Default value for [thickness] if it's not specified in [CupertinoScrollbar].
95 96
  static const double defaultThickness = 3;

97 98
  /// Default value for [thicknessWhileDragging] if it's not specified in
  /// [CupertinoScrollbar].
99 100
  static const double defaultThicknessWhileDragging = 8.0;

101
  /// Default value for [radius] if it's not specified in [CupertinoScrollbar].
102 103
  static const Radius defaultRadius = Radius.circular(1.5);

104 105
  /// Default value for [radiusWhileDragging] if it's not specified in
  /// [CupertinoScrollbar].
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
  static const Radius defaultRadiusWhileDragging = Radius.circular(4.0);

  /// The thickness of the scrollbar when it's being dragged by the user.
  ///
  /// When the user starts dragging the scrollbar, the thickness will animate
  /// from [thickness] to this value, then animate back when the user stops
  /// dragging the scrollbar.
  final double thicknessWhileDragging;

  /// The radius of the scrollbar edges when the scrollbar is being dragged by
  /// the user.
  ///
  /// When the user starts dragging the scrollbar, the radius will animate
  /// from [radius] to this value, then animate back when the user stops
  /// dragging the scrollbar.
  final Radius radiusWhileDragging;

123
  @override
124
  RawScrollbarState<CupertinoScrollbar> createState() => _CupertinoScrollbarState();
125 126
}

127
class _CupertinoScrollbarState extends RawScrollbarState<CupertinoScrollbar> {
128
  late AnimationController _thicknessAnimationController;
129 130

  double get _thickness {
131
    return widget.thickness! + _thicknessAnimationController.value * (widget.thicknessWhileDragging - widget.thickness!);
132 133 134
  }

  Radius get _radius {
135
    return Radius.lerp(widget.radius, widget.radiusWhileDragging, _thicknessAnimationController.value)!;
136
  }
137 138 139 140

  @override
  void initState() {
    super.initState();
141 142 143 144 145
    _thicknessAnimationController = AnimationController(
      vsync: this,
      duration: _kScrollbarResizeDuration,
    );
    _thicknessAnimationController.addListener(() {
146
      updateScrollbarPainter();
147
    });
148 149 150
  }

  @override
151 152 153 154 155 156 157 158
  void updateScrollbarPainter() {
    scrollbarPainter
      ..color = CupertinoDynamicColor.resolve(_kScrollbarColor, context)
      ..textDirection = Directionality.of(context)
      ..thickness = _thickness
      ..mainAxisMargin = _kScrollbarMainAxisMargin
      ..crossAxisMargin = _kScrollbarCrossAxisMargin
      ..radius = _radius
159
      ..padding = MediaQuery.paddingOf(context)
160
      ..minLength = _kScrollbarMinLength
161 162
      ..minOverscrollLength = _kScrollbarMinOverscrollLength
      ..scrollbarOrientation = widget.scrollbarOrientation;
163 164
  }

165
  double _pressStartAxisPosition = 0.0;
166

167 168
  // Long press event callbacks handle the gesture where the user long presses
  // on the scrollbar thumb and then drags the scrollbar without releasing.
169 170 171 172

  @override
  void handleThumbPressStart(Offset localPosition) {
    super.handleThumbPressStart(localPosition);
173 174 175 176
    final Axis? direction = getScrollbarDirection();
    if (direction == null) {
      return;
    }
177 178
    switch (direction) {
      case Axis.vertical:
179
        _pressStartAxisPosition = localPosition.dy;
180
      case Axis.horizontal:
181
        _pressStartAxisPosition = localPosition.dx;
182
    }
183 184
  }

185 186 187
  @override
  void handleThumbPress() {
    if (getScrollbarDirection() == null) {
188 189
      return;
    }
190
    super.handleThumbPress();
191 192
    _thicknessAnimationController.forward().then<void>(
          (_) => HapticFeedback.mediumImpact(),
193
    );
194 195
  }

196 197 198
  @override
  void handleThumbPressEnd(Offset localPosition, Velocity velocity) {
    final Axis? direction = getScrollbarDirection();
199
    if (direction == null) {
200 201
      return;
    }
202 203
    _thicknessAnimationController.reverse();
    super.handleThumbPressEnd(localPosition, velocity);
204
    switch (direction) {
205
      case Axis.vertical:
206 207
        if (velocity.pixelsPerSecond.dy.abs() < 10 &&
          (localPosition.dy - _pressStartAxisPosition).abs() > 0) {
208 209 210
          HapticFeedback.mediumImpact();
        }
      case Axis.horizontal:
211 212
        if (velocity.pixelsPerSecond.dx.abs() < 10 &&
          (localPosition.dx - _pressStartAxisPosition).abs() > 0) {
213 214
          HapticFeedback.mediumImpact();
        }
215
    }
216 217
  }

218 219
  @override
  void dispose() {
220
    _thicknessAnimationController.dispose();
221 222
    super.dispose();
  }
223
}