// 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. import 'dart:math' as math; import 'package:flutter/animation.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'scroll_metrics.dart'; const double _kMinThumbExtent = 18.0; /// A [CustomPainter] for painting scrollbars. /// /// Unlike [CustomPainter]s that subclasses [CustomPainter] and only repaint /// when [shouldRepaint] returns true (which requires this [CustomPainter] to /// be rebuilt), this painter has the added optimization of repainting and not /// rebuilding when: /// /// * the scroll position changes; and /// * when the scrollbar fades away. /// /// Calling [update] with the new [ScrollMetrics] will repaint the new scrollbar /// position. /// /// Updating the value on the provided [fadeoutOpacityAnimation] will repaint /// with the new opacity. /// /// You must call [dispose] on this [ScrollbarPainter] when it's no longer used. /// /// See also: /// /// * [Scrollbar] for a widget showing a scrollbar around a [Scrollable] in the /// Material Design style. /// * [CupertinoScrollbar] for a widget showing a scrollbar around a /// [Scrollable] in the iOS style. class ScrollbarPainter extends ChangeNotifier implements CustomPainter { /// Creates a scrollbar with customizations given by construction arguments. ScrollbarPainter({ @required this.color, @required this.textDirection, @required this.thickness, @required this.fadeoutOpacityAnimation, this.mainAxisMargin: 0.0, this.crossAxisMargin: 0.0, this.radius, this.minLength: _kMinThumbExtent, }) : assert(color != null), assert(textDirection != null), assert(thickness != null), assert(fadeoutOpacityAnimation != null), assert(mainAxisMargin != null), assert(crossAxisMargin != null), assert(minLength != null) { fadeoutOpacityAnimation.addListener(notifyListeners); } /// [Color] of the thumb. Mustn't be null. final Color color; /// [TextDirection] of the [BuildContext] which dictates the side of the /// screen the scrollbar appears in (the trailing side). Mustn't be null. final TextDirection textDirection; /// Thickness of the scrollbar in its cross-axis in pixels. Mustn't be null. final double thickness; /// An opacity [Animation] that dictates the opacity of the thumb. /// Changes in value of this [Listenable] will automatically trigger repaints. /// Mustn't be null. final Animation fadeoutOpacityAnimation; /// Distance from the scrollbar's start and end to the edge of the viewport in /// pixels. Mustn't be null. final double mainAxisMargin; /// Distance from the scrollbar's side to the nearest edge in pixels. Musn't /// be null. final double crossAxisMargin; /// [Radius] of corners if the scrollbar should have rounded corners. /// /// Scrollbar will be rectangular if [radius] is null. final Radius radius; /// The smallest size the scrollbar can shrink to when the total scrollable /// extent is large and the current visible viewport is small. Mustn't be /// null. final double minLength; ScrollMetrics _lastMetrics; AxisDirection _lastAxisDirection; /// Update with new [ScrollMetrics]. The scrollbar will show and redraw itself /// based on these new metrics. /// /// The scrollbar will remain on screen. void update( ScrollMetrics metrics, AxisDirection axisDirection, ) { _lastMetrics = metrics; _lastAxisDirection = axisDirection; notifyListeners(); } Paint get _paint { return new Paint()..color = color.withOpacity(color.opacity * fadeoutOpacityAnimation.value); } double _getThumbX(Size size) { assert(textDirection != null); switch (textDirection) { case TextDirection.rtl: return crossAxisMargin; case TextDirection.ltr: return size.width - thickness - crossAxisMargin; } return null; } void _paintVerticalThumb(Canvas canvas, Size size, double thumbOffset, double thumbExtent) { final Offset thumbOrigin = new Offset(_getThumbX(size), thumbOffset); final Size thumbSize = new Size(thickness, thumbExtent); final Rect thumbRect = thumbOrigin & thumbSize; if (radius == null) canvas.drawRect(thumbRect, _paint); else canvas.drawRRect(new RRect.fromRectAndRadius(thumbRect, radius), _paint); } void _paintHorizontalThumb(Canvas canvas, Size size, double thumbOffset, double thumbExtent) { final Offset thumbOrigin = new Offset(thumbOffset, size.height - thickness); final Size thumbSize = new Size(thumbExtent, thickness); final Rect thumbRect = thumbOrigin & thumbSize; if (radius == null) canvas.drawRect(thumbRect, _paint); else canvas.drawRRect(new RRect.fromRectAndRadius(thumbRect, radius), _paint); } void _paintThumb( double before, double inside, double after, double viewport, Canvas canvas, Size size, void painter(Canvas canvas, Size size, double thumbOffset, double thumbExtent), ) { // Establish the minimum size possible. double thumbExtent = math.min(viewport, minLength); if (before + inside + after > 0.0) { final double fractionVisible = inside / (before + inside + after); thumbExtent = math.max( thumbExtent, viewport * fractionVisible - 2 * mainAxisMargin, ); } final double fractionPast = before / (before + after); final double thumbOffset = (before + after > 0.0) ? fractionPast * (viewport - thumbExtent - 2 * mainAxisMargin) + mainAxisMargin : mainAxisMargin; painter(canvas, size, thumbOffset, thumbExtent); } @override void dispose() { fadeoutOpacityAnimation.removeListener(notifyListeners); super.dispose(); } @override void paint(Canvas canvas, Size size) { if (_lastAxisDirection == null || _lastMetrics == null || fadeoutOpacityAnimation.value == 0.0) return; switch (_lastAxisDirection) { case AxisDirection.down: _paintThumb(_lastMetrics.extentBefore, _lastMetrics.extentInside, _lastMetrics.extentAfter, size.height, canvas, size, _paintVerticalThumb); break; case AxisDirection.up: _paintThumb(_lastMetrics.extentAfter, _lastMetrics.extentInside, _lastMetrics.extentBefore, size.height, canvas, size, _paintVerticalThumb); break; case AxisDirection.right: _paintThumb(_lastMetrics.extentBefore, _lastMetrics.extentInside, _lastMetrics.extentAfter, size.width, canvas, size, _paintHorizontalThumb); break; case AxisDirection.left: _paintThumb(_lastMetrics.extentAfter, _lastMetrics.extentInside, _lastMetrics.extentBefore, size.width, canvas, size, _paintHorizontalThumb); break; } } // Scrollbars are (currently) not interactive. @override bool hitTest(Offset position) => null; @override bool shouldRepaint(ScrollbarPainter old) { // Should repaint if any properties changed. return color != old.color || textDirection != old.textDirection || thickness != old.thickness || fadeoutOpacityAnimation != old.fadeoutOpacityAnimation || mainAxisMargin != old.mainAxisMargin || crossAxisMargin != old.crossAxisMargin || radius != old.radius || minLength != old.minLength; } @override bool shouldRebuildSemantics(CustomPainter oldDelegate) => false; @override SemanticsBuilderCallback get semanticsBuilder => null; }