"Fossies" - the Fresh Open Source Software Archive

Member "flutter-1.22.4/packages/flutter/lib/src/cupertino/scrollbar.dart" (13 Nov 2020, 19247 Bytes) of package /linux/misc/flutter-1.22.4.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Dart source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file.

    1 // Copyright 2014 The Flutter Authors. All rights reserved.
    2 // Use of this source code is governed by a BSD-style license that can be
    3 // found in the LICENSE file.
    4 
    5 // @dart = 2.8
    6 
    7 import 'dart:async';
    8 
    9 import 'package:flutter/gestures.dart';
   10 import 'package:flutter/services.dart';
   11 import 'package:flutter/widgets.dart';
   12 
   13 import 'colors.dart';
   14 
   15 // All values eyeballed.
   16 const double _kScrollbarMinLength = 36.0;
   17 const double _kScrollbarMinOverscrollLength = 8.0;
   18 const Duration _kScrollbarTimeToFade = Duration(milliseconds: 1200);
   19 const Duration _kScrollbarFadeDuration = Duration(milliseconds: 250);
   20 const Duration _kScrollbarResizeDuration = Duration(milliseconds: 100);
   21 
   22 // Extracted from iOS 13.1 beta using Debug View Hierarchy.
   23 const Color _kScrollbarColor = CupertinoDynamicColor.withBrightness(
   24   color: Color(0x59000000),
   25   darkColor: Color(0x80FFFFFF),
   26 );
   27 
   28 // This is the amount of space from the top of a vertical scrollbar to the
   29 // top edge of the scrollable, measured when the vertical scrollbar overscrolls
   30 // to the top.
   31 // TODO(LongCatIsLooong): fix https://github.com/flutter/flutter/issues/32175
   32 const double _kScrollbarMainAxisMargin = 3.0;
   33 const double _kScrollbarCrossAxisMargin = 3.0;
   34 
   35 /// An iOS style scrollbar.
   36 ///
   37 /// A scrollbar indicates which portion of a [Scrollable] widget is actually
   38 /// visible.
   39 ///
   40 /// To add a scrollbar to a [ScrollView], simply wrap the scroll view widget in
   41 /// a [CupertinoScrollbar] widget.
   42 ///
   43 /// By default, the CupertinoScrollbar will be draggable (a feature introduced
   44 /// in iOS 13), it uses the PrimaryScrollController. For multiple scrollbars, or
   45 /// other more complicated situations, see the [controller] parameter.
   46 ///
   47 /// See also:
   48 ///
   49 ///  * [ListView], which display a linear, scrollable list of children.
   50 ///  * [GridView], which display a 2 dimensional, scrollable array of children.
   51 ///  * [Scrollbar], a Material Design scrollbar that dynamically adapts to the
   52 ///    platform showing either an Android style or iOS style scrollbar.
   53 class CupertinoScrollbar extends StatefulWidget {
   54   /// Creates an iOS style scrollbar that wraps the given [child].
   55   ///
   56   /// The [child] should be a source of [ScrollNotification] notifications,
   57   /// typically a [Scrollable] widget.
   58   const CupertinoScrollbar({
   59     Key key,
   60     this.controller,
   61     this.isAlwaysShown = false,
   62     this.thickness = defaultThickness,
   63     this.thicknessWhileDragging = defaultThicknessWhileDragging,
   64     this.radius = defaultRadius,
   65     this.radiusWhileDragging = defaultRadiusWhileDragging,
   66     @required this.child,
   67   }) : assert(thickness != null),
   68        assert(thickness < double.infinity),
   69        assert(thicknessWhileDragging != null),
   70        assert(thicknessWhileDragging < double.infinity),
   71        assert(radius != null),
   72        assert(radiusWhileDragging != null),
   73        assert(!isAlwaysShown || controller != null, 'When isAlwaysShown is true, must pass a controller that is attached to a scroll view'),
   74        super(key: key);
   75 
   76   /// Default value for [thickness] if it's not specified in [new CupertinoScrollbar].
   77   static const double defaultThickness = 3;
   78 
   79   /// Default value for [thicknessWhileDragging] if it's not specified in [new CupertinoScrollbar].
   80   static const double defaultThicknessWhileDragging = 8.0;
   81 
   82   /// Default value for [radius] if it's not specified in [new CupertinoScrollbar].
   83   static const Radius defaultRadius = Radius.circular(1.5);
   84 
   85   /// Default value for [radiusWhileDragging] if it's not specified in [new CupertinoScrollbar].
   86   static const Radius defaultRadiusWhileDragging = Radius.circular(4.0);
   87 
   88   /// The subtree to place inside the [CupertinoScrollbar].
   89   ///
   90   /// This should include a source of [ScrollNotification] notifications,
   91   /// typically a [Scrollable] widget.
   92   final Widget child;
   93 
   94   /// {@template flutter.cupertino.cupertinoScrollbar.controller}
   95   /// The [ScrollController] used to implement Scrollbar dragging.
   96   ///
   97   /// introduced in iOS 13.
   98   ///
   99   /// If nothing is passed to controller, the default behavior is to automatically
  100   /// enable scrollbar dragging on the nearest ScrollController using
  101   /// [PrimaryScrollController.of].
  102   ///
  103   /// If a ScrollController is passed, then scrollbar dragging will be enabled on
  104   /// the given ScrollController. A stateful ancestor of this CupertinoScrollbar
  105   /// needs to manage the ScrollController and either pass it to a scrollable
  106   /// descendant or use a PrimaryScrollController to share it.
  107   ///
  108   /// Here is an example of using the `controller` parameter to enable
  109   /// scrollbar dragging for multiple independent ListViews:
  110   ///
  111   /// {@tool snippet}
  112   ///
  113   /// ```dart
  114   /// final ScrollController _controllerOne = ScrollController();
  115   /// final ScrollController _controllerTwo = ScrollController();
  116   ///
  117   /// build(BuildContext context) {
  118   /// return Column(
  119   ///   children: <Widget>[
  120   ///     Container(
  121   ///        height: 200,
  122   ///        child: CupertinoScrollbar(
  123   ///          controller: _controllerOne,
  124   ///          child: ListView.builder(
  125   ///            controller: _controllerOne,
  126   ///            itemCount: 120,
  127   ///            itemBuilder: (BuildContext context, int index) => Text('item $index'),
  128   ///          ),
  129   ///        ),
  130   ///      ),
  131   ///      Container(
  132   ///        height: 200,
  133   ///        child: CupertinoScrollbar(
  134   ///          controller: _controllerTwo,
  135   ///          child: ListView.builder(
  136   ///            controller: _controllerTwo,
  137   ///            itemCount: 120,
  138   ///            itemBuilder: (BuildContext context, int index) => Text('list 2 item $index'),
  139   ///          ),
  140   ///        ),
  141   ///      ),
  142   ///    ],
  143   ///   );
  144   /// }
  145   /// ```
  146   /// {@end-tool}
  147   /// {@endtemplate}
  148   final ScrollController controller;
  149 
  150   /// {@template flutter.cupertino.cupertinoScrollbar.isAlwaysShown}
  151   /// Indicates whether the [Scrollbar] should always be visible.
  152   ///
  153   /// When false, the scrollbar will be shown during scrolling
  154   /// and will fade out otherwise.
  155   ///
  156   /// When true, the scrollbar will always be visible and never fade out.
  157   ///
  158   /// The [controller] property must be set in this case.
  159   /// It should be passed the relevant [Scrollable]'s [ScrollController].
  160   ///
  161   /// Defaults to false.
  162   ///
  163   /// {@tool snippet}
  164   ///
  165   /// ```dart
  166   /// final ScrollController _controllerOne = ScrollController();
  167   /// final ScrollController _controllerTwo = ScrollController();
  168   ///
  169   /// build(BuildContext context) {
  170   /// return Column(
  171   ///   children: <Widget>[
  172   ///     Container(
  173   ///        height: 200,
  174   ///        child: Scrollbar(
  175   ///          isAlwaysShown: true,
  176   ///          controller: _controllerOne,
  177   ///          child: ListView.builder(
  178   ///            controller: _controllerOne,
  179   ///            itemCount: 120,
  180   ///            itemBuilder: (BuildContext context, int index)
  181   ///                => Text('item $index'),
  182   ///          ),
  183   ///        ),
  184   ///      ),
  185   ///      Container(
  186   ///        height: 200,
  187   ///        child: CupertinoScrollbar(
  188   ///          isAlwaysShown: true,
  189   ///          controller: _controllerTwo,
  190   ///          child: SingleChildScrollView(
  191   ///            controller: _controllerTwo,
  192   ///            child: SizedBox(height: 2000, width: 500,),
  193   ///          ),
  194   ///        ),
  195   ///      ),
  196   ///    ],
  197   ///   );
  198   /// }
  199   /// ```
  200   /// {@end-tool}
  201   /// {@endtemplate}
  202   final bool isAlwaysShown;
  203 
  204   /// The thickness of the scrollbar when it's not being dragged by the user.
  205   ///
  206   /// When the user starts dragging the scrollbar, the thickness will animate
  207   /// to [thicknessWhileDragging], then animate back when the user stops
  208   /// dragging the scrollbar.
  209   final double thickness;
  210 
  211   /// The thickness of the scrollbar when it's being dragged by the user.
  212   ///
  213   /// When the user starts dragging the scrollbar, the thickness will animate
  214   /// from [thickness] to this value, then animate back when the user stops
  215   /// dragging the scrollbar.
  216   final double thicknessWhileDragging;
  217 
  218   /// The radius of the scrollbar edges when the scrollbar is not being dragged
  219   /// by the user.
  220   ///
  221   /// When the user starts dragging the scrollbar, the radius will animate
  222   /// to [radiusWhileDragging], then animate back when the user stops dragging
  223   /// the scrollbar.
  224   final Radius radius;
  225 
  226   /// The radius of the scrollbar edges when the scrollbar is being dragged by
  227   /// the user.
  228   ///
  229   /// When the user starts dragging the scrollbar, the radius will animate
  230   /// from [radius] to this value, then animate back when the user stops
  231   /// dragging the scrollbar.
  232   final Radius radiusWhileDragging;
  233 
  234   @override
  235   _CupertinoScrollbarState createState() => _CupertinoScrollbarState();
  236 }
  237 
  238 class _CupertinoScrollbarState extends State<CupertinoScrollbar> with TickerProviderStateMixin {
  239   final GlobalKey _customPaintKey = GlobalKey();
  240   ScrollbarPainter _painter;
  241 
  242   AnimationController _fadeoutAnimationController;
  243   Animation<double> _fadeoutOpacityAnimation;
  244   AnimationController _thicknessAnimationController;
  245   Timer _fadeoutTimer;
  246   double _dragScrollbarPositionY;
  247   Drag _drag;
  248 
  249   double get _thickness {
  250     return widget.thickness + _thicknessAnimationController.value * (widget.thicknessWhileDragging - widget.thickness);
  251   }
  252 
  253   Radius get _radius {
  254     return Radius.lerp(widget.radius, widget.radiusWhileDragging, _thicknessAnimationController.value);
  255   }
  256 
  257   ScrollController _currentController;
  258   ScrollController get _controller =>
  259       widget.controller ?? PrimaryScrollController.of(context);
  260 
  261   @override
  262   void initState() {
  263     super.initState();
  264     _fadeoutAnimationController = AnimationController(
  265       vsync: this,
  266       duration: _kScrollbarFadeDuration,
  267     );
  268     _fadeoutOpacityAnimation = CurvedAnimation(
  269       parent: _fadeoutAnimationController,
  270       curve: Curves.fastOutSlowIn,
  271     );
  272     _thicknessAnimationController = AnimationController(
  273       vsync: this,
  274       duration: _kScrollbarResizeDuration,
  275     );
  276     _thicknessAnimationController.addListener(() {
  277       _painter.updateThickness(_thickness, _radius);
  278     });
  279   }
  280 
  281   @override
  282   void didChangeDependencies() {
  283     super.didChangeDependencies();
  284     if (_painter == null) {
  285       _painter = _buildCupertinoScrollbarPainter(context);
  286     } else {
  287       _painter
  288         ..textDirection = Directionality.of(context)
  289         ..color = CupertinoDynamicColor.resolve(_kScrollbarColor, context)
  290         ..padding = MediaQuery.of(context).padding;
  291     }
  292     _triggerScrollbar();
  293   }
  294 
  295   @override
  296   void didUpdateWidget(CupertinoScrollbar oldWidget) {
  297     super.didUpdateWidget(oldWidget);
  298     assert(_painter != null);
  299     _painter.updateThickness(_thickness, _radius);
  300     if (widget.isAlwaysShown != oldWidget.isAlwaysShown) {
  301       if (widget.isAlwaysShown == true) {
  302         _triggerScrollbar();
  303         _fadeoutAnimationController.animateTo(1.0);
  304       } else {
  305         _fadeoutAnimationController.reverse();
  306       }
  307     }
  308   }
  309 
  310   /// Returns a [ScrollbarPainter] visually styled like the iOS scrollbar.
  311   ScrollbarPainter _buildCupertinoScrollbarPainter(BuildContext context) {
  312     return ScrollbarPainter(
  313       color: CupertinoDynamicColor.resolve(_kScrollbarColor, context),
  314       textDirection: Directionality.of(context),
  315       thickness: _thickness,
  316       fadeoutOpacityAnimation: _fadeoutOpacityAnimation,
  317       mainAxisMargin: _kScrollbarMainAxisMargin,
  318       crossAxisMargin: _kScrollbarCrossAxisMargin,
  319       radius: _radius,
  320       padding: MediaQuery.of(context).padding,
  321       minLength: _kScrollbarMinLength,
  322       minOverscrollLength: _kScrollbarMinOverscrollLength,
  323     );
  324   }
  325 
  326   // Wait one frame and cause an empty scroll event.  This allows the thumb to
  327   // show immediately when isAlwaysShown is true.  A scroll event is required in
  328   // order to paint the thumb.
  329   void _triggerScrollbar() {
  330     WidgetsBinding.instance.addPostFrameCallback((Duration duration) {
  331       if (widget.isAlwaysShown) {
  332         _fadeoutTimer?.cancel();
  333         widget.controller.position.didUpdateScrollPositionBy(0);
  334       }
  335     });
  336   }
  337 
  338   // Handle a gesture that drags the scrollbar by the given amount.
  339   void _dragScrollbar(double primaryDelta) {
  340     assert(_currentController != null);
  341 
  342     // Convert primaryDelta, the amount that the scrollbar moved since the last
  343     // time _dragScrollbar was called, into the coordinate space of the scroll
  344     // position, and create/update the drag event with that position.
  345     final double scrollOffsetLocal = _painter.getTrackToScroll(primaryDelta);
  346     final double scrollOffsetGlobal = scrollOffsetLocal + _currentController.position.pixels;
  347 
  348     if (_drag == null) {
  349       _drag = _currentController.position.drag(
  350         DragStartDetails(
  351           globalPosition: Offset(0.0, scrollOffsetGlobal),
  352         ),
  353         () {},
  354       );
  355     } else {
  356       _drag.update(DragUpdateDetails(
  357         globalPosition: Offset(0.0, scrollOffsetGlobal),
  358         delta: Offset(0.0, -scrollOffsetLocal),
  359         primaryDelta: -scrollOffsetLocal,
  360       ));
  361     }
  362   }
  363 
  364   void _startFadeoutTimer() {
  365     if (!widget.isAlwaysShown) {
  366       _fadeoutTimer?.cancel();
  367       _fadeoutTimer = Timer(_kScrollbarTimeToFade, () {
  368         _fadeoutAnimationController.reverse();
  369         _fadeoutTimer = null;
  370       });
  371     }
  372   }
  373 
  374   bool _checkVertical() {
  375     try {
  376       return _currentController.position.axis == Axis.vertical;
  377     } catch (_) {
  378       // Ignore the gesture if we cannot determine the direction.
  379       return false;
  380     }
  381   }
  382 
  383   double _pressStartY = 0.0;
  384 
  385   // Long press event callbacks handle the gesture where the user long presses
  386   // on the scrollbar thumb and then drags the scrollbar without releasing.
  387   void _handleLongPressStart(LongPressStartDetails details) {
  388     _currentController = _controller;
  389     if (!_checkVertical()) {
  390       return;
  391     }
  392     _pressStartY = details.localPosition.dy;
  393     _fadeoutTimer?.cancel();
  394     _fadeoutAnimationController.forward();
  395     _dragScrollbar(details.localPosition.dy);
  396     _dragScrollbarPositionY = details.localPosition.dy;
  397   }
  398 
  399   void _handleLongPress() {
  400     if (!_checkVertical()) {
  401       return;
  402     }
  403     _fadeoutTimer?.cancel();
  404     _thicknessAnimationController.forward().then<void>(
  405           (_) => HapticFeedback.mediumImpact(),
  406     );
  407   }
  408 
  409   void _handleLongPressMoveUpdate(LongPressMoveUpdateDetails details) {
  410     if (!_checkVertical()) {
  411       return;
  412     }
  413     _dragScrollbar(details.localPosition.dy - _dragScrollbarPositionY);
  414     _dragScrollbarPositionY = details.localPosition.dy;
  415   }
  416 
  417   void _handleLongPressEnd(LongPressEndDetails details) {
  418     if (!_checkVertical()) {
  419       return;
  420     }
  421     _handleDragScrollEnd(details.velocity.pixelsPerSecond.dy);
  422     if (details.velocity.pixelsPerSecond.dy.abs() < 10 &&
  423         (details.localPosition.dy - _pressStartY).abs() > 0) {
  424       HapticFeedback.mediumImpact();
  425     }
  426     _currentController = null;
  427   }
  428 
  429   void _handleDragScrollEnd(double trackVelocityY) {
  430     _startFadeoutTimer();
  431     _thicknessAnimationController.reverse();
  432     _dragScrollbarPositionY = null;
  433     final double scrollVelocityY = _painter.getTrackToScroll(trackVelocityY);
  434     _drag?.end(DragEndDetails(
  435       primaryVelocity: -scrollVelocityY,
  436       velocity: Velocity(
  437         pixelsPerSecond: Offset(
  438           0.0,
  439           -scrollVelocityY,
  440         ),
  441       ),
  442     ));
  443     _drag = null;
  444   }
  445 
  446   bool _handleScrollNotification(ScrollNotification notification) {
  447     final ScrollMetrics metrics = notification.metrics;
  448     if (metrics.maxScrollExtent <= metrics.minScrollExtent) {
  449       return false;
  450     }
  451 
  452     if (notification is ScrollUpdateNotification ||
  453         notification is OverscrollNotification) {
  454       // Any movements always makes the scrollbar start showing up.
  455       if (_fadeoutAnimationController.status != AnimationStatus.forward) {
  456         _fadeoutAnimationController.forward();
  457       }
  458 
  459       _fadeoutTimer?.cancel();
  460       _painter.update(notification.metrics, notification.metrics.axisDirection);
  461     } else if (notification is ScrollEndNotification) {
  462       // On iOS, the scrollbar can only go away once the user lifted the finger.
  463       if (_dragScrollbarPositionY == null) {
  464         _startFadeoutTimer();
  465       }
  466     }
  467     return false;
  468   }
  469 
  470   // Get the GestureRecognizerFactories used to detect gestures on the scrollbar
  471   // thumb.
  472   Map<Type, GestureRecognizerFactory> get _gestures {
  473     final Map<Type, GestureRecognizerFactory> gestures =
  474         <Type, GestureRecognizerFactory>{};
  475 
  476     gestures[_ThumbPressGestureRecognizer] =
  477         GestureRecognizerFactoryWithHandlers<_ThumbPressGestureRecognizer>(
  478       () => _ThumbPressGestureRecognizer(
  479         debugOwner: this,
  480         customPaintKey: _customPaintKey,
  481       ),
  482       (_ThumbPressGestureRecognizer instance) {
  483         instance
  484           ..onLongPressStart = _handleLongPressStart
  485           ..onLongPress = _handleLongPress
  486           ..onLongPressMoveUpdate = _handleLongPressMoveUpdate
  487           ..onLongPressEnd = _handleLongPressEnd;
  488       },
  489     );
  490 
  491     return gestures;
  492   }
  493 
  494   @override
  495   void dispose() {
  496     _fadeoutAnimationController.dispose();
  497     _thicknessAnimationController.dispose();
  498     _fadeoutTimer?.cancel();
  499     _painter.dispose();
  500     super.dispose();
  501   }
  502 
  503   @override
  504   Widget build(BuildContext context) {
  505     return NotificationListener<ScrollNotification>(
  506       onNotification: _handleScrollNotification,
  507       child: RepaintBoundary(
  508         child: RawGestureDetector(
  509           gestures: _gestures,
  510           child: CustomPaint(
  511             key: _customPaintKey,
  512             foregroundPainter: _painter,
  513             child: RepaintBoundary(child: widget.child),
  514           ),
  515         ),
  516       ),
  517     );
  518   }
  519 }
  520 
  521 // A longpress gesture detector that only responds to events on the scrollbar's
  522 // thumb and ignores everything else.
  523 class _ThumbPressGestureRecognizer extends LongPressGestureRecognizer {
  524   _ThumbPressGestureRecognizer({
  525     double postAcceptSlopTolerance,
  526     PointerDeviceKind kind,
  527     Object debugOwner,
  528     GlobalKey customPaintKey,
  529   }) :  _customPaintKey = customPaintKey,
  530         super(
  531           postAcceptSlopTolerance: postAcceptSlopTolerance,
  532           kind: kind,
  533           debugOwner: debugOwner,
  534           duration: const Duration(milliseconds: 100),
  535         );
  536 
  537   final GlobalKey _customPaintKey;
  538 
  539   @override
  540   bool isPointerAllowed(PointerDownEvent event) {
  541     if (!_hitTestInteractive(_customPaintKey, event.position)) {
  542       return false;
  543     }
  544     return super.isPointerAllowed(event);
  545   }
  546 }
  547 
  548 // foregroundPainter also hit tests its children by default, but the
  549 // scrollbar should only respond to a gesture directly on its thumb, so
  550 // manually check for a hit on the thumb here.
  551 bool _hitTestInteractive(GlobalKey customPaintKey, Offset offset) {
  552   if (customPaintKey.currentContext == null) {
  553     return false;
  554   }
  555   final CustomPaint customPaint = customPaintKey.currentContext.widget as CustomPaint;
  556   final ScrollbarPainter painter = customPaint.foregroundPainter as ScrollbarPainter;
  557   final RenderBox renderBox = customPaintKey.currentContext.findRenderObject() as RenderBox;
  558   final Offset localOffset = renderBox.globalToLocal(offset);
  559   return painter.hitTestInteractive(localOffset);
  560 }