diff --git a/packages/flutter/lib/material.dart b/packages/flutter/lib/material.dart index 0484fb90200..fff0a91b88c 100644 --- a/packages/flutter/lib/material.dart +++ b/packages/flutter/lib/material.dart @@ -159,6 +159,7 @@ export 'src/material/selection_area.dart'; export 'src/material/shadows.dart'; export 'src/material/slider.dart'; export 'src/material/slider_theme.dart'; +export 'src/material/slider_value_indicator_shape.dart'; export 'src/material/snack_bar.dart'; export 'src/material/snack_bar_theme.dart'; export 'src/material/spell_check_suggestions_toolbar.dart'; diff --git a/packages/flutter/lib/src/material/range_slider.dart b/packages/flutter/lib/src/material/range_slider.dart index 69cec8f336d..ba4a5cd1a0b 100644 --- a/packages/flutter/lib/src/material/range_slider.dart +++ b/packages/flutter/lib/src/material/range_slider.dart @@ -27,6 +27,7 @@ import 'constants.dart'; import 'debug.dart'; import 'material_state.dart'; import 'slider_theme.dart'; +import 'slider_value_indicator_shape.dart'; import 'theme.dart'; // Examples can assume: diff --git a/packages/flutter/lib/src/material/slider.dart b/packages/flutter/lib/src/material/slider.dart index f80e52f1b97..e4977a00bc1 100644 --- a/packages/flutter/lib/src/material/slider.dart +++ b/packages/flutter/lib/src/material/slider.dart @@ -26,6 +26,7 @@ import 'debug.dart'; import 'material.dart'; import 'material_state.dart'; import 'slider_theme.dart'; +import 'slider_value_indicator_shape.dart'; import 'theme.dart'; // Examples can assume: diff --git a/packages/flutter/lib/src/material/slider_theme.dart b/packages/flutter/lib/src/material/slider_theme.dart index 064355fd48f..b3cbf41cd55 100644 --- a/packages/flutter/lib/src/material/slider_theme.dart +++ b/packages/flutter/lib/src/material/slider_theme.dart @@ -2957,280 +2957,6 @@ class RoundSliderOverlayShape extends SliderComponentShape { } } -/// The default shape of a [Slider]'s value indicator. -/// -/// ![A slider widget, consisting of 5 divisions and showing the rectangular slider value indicator shape.](https://flutter.github.io/assets-for-api-docs/assets/material/rectangular_slider_value_indicator_shape.png) -/// -/// See also: -/// -/// * [Slider], which includes a value indicator defined by this shape. -/// * [SliderTheme], which can be used to configure the slider value indicator -/// of all sliders in a widget subtree. -class RectangularSliderValueIndicatorShape extends SliderComponentShape { - /// Create a slider value indicator that resembles a rectangular tooltip. - const RectangularSliderValueIndicatorShape(); - - static const _RectangularSliderValueIndicatorPathPainter _pathPainter = - _RectangularSliderValueIndicatorPathPainter(); - - @override - Size getPreferredSize( - bool isEnabled, - bool isDiscrete, { - TextPainter? labelPainter, - double? textScaleFactor, - }) { - assert(labelPainter != null); - assert(textScaleFactor != null && textScaleFactor >= 0); - return _pathPainter.getPreferredSize(labelPainter!, textScaleFactor!); - } - - @override - void paint( - PaintingContext context, - Offset center, { - required Animation activationAnimation, - required Animation enableAnimation, - required bool isDiscrete, - required TextPainter labelPainter, - required RenderBox parentBox, - required SliderThemeData sliderTheme, - required TextDirection textDirection, - required double value, - required double textScaleFactor, - required Size sizeWithOverflow, - }) { - final Canvas canvas = context.canvas; - final double scale = activationAnimation.value; - _pathPainter.paint( - parentBox: parentBox, - canvas: canvas, - center: center, - scale: scale, - labelPainter: labelPainter, - textScaleFactor: textScaleFactor, - sizeWithOverflow: sizeWithOverflow, - backgroundPaintColor: sliderTheme.valueIndicatorColor!, - strokePaintColor: sliderTheme.valueIndicatorStrokeColor, - ); - } -} - -/// The default shape of a [RangeSlider]'s value indicators. -/// -/// ![A slider widget, consisting of 5 divisions and showing the rectangular range slider value indicator shape.](https://flutter.github.io/assets-for-api-docs/assets/material/rectangular_range_slider_value_indicator_shape.png) -/// -/// See also: -/// -/// * [RangeSlider], which includes value indicators defined by this shape. -/// * [SliderTheme], which can be used to configure the range slider value -/// indicator of all sliders in a widget subtree. -class RectangularRangeSliderValueIndicatorShape extends RangeSliderValueIndicatorShape { - /// Create a range slider value indicator that resembles a rectangular tooltip. - const RectangularRangeSliderValueIndicatorShape(); - - static const _RectangularSliderValueIndicatorPathPainter _pathPainter = - _RectangularSliderValueIndicatorPathPainter(); - - @override - Size getPreferredSize( - bool isEnabled, - bool isDiscrete, { - required TextPainter labelPainter, - required double textScaleFactor, - }) { - assert(textScaleFactor >= 0); - return _pathPainter.getPreferredSize(labelPainter, textScaleFactor); - } - - @override - double getHorizontalShift({ - RenderBox? parentBox, - Offset? center, - TextPainter? labelPainter, - Animation? activationAnimation, - double? textScaleFactor, - Size? sizeWithOverflow, - }) { - return _pathPainter.getHorizontalShift( - parentBox: parentBox!, - center: center!, - labelPainter: labelPainter!, - textScaleFactor: textScaleFactor!, - sizeWithOverflow: sizeWithOverflow!, - scale: activationAnimation!.value, - ); - } - - @override - void paint( - PaintingContext context, - Offset center, { - Animation? activationAnimation, - Animation? enableAnimation, - bool? isDiscrete, - bool? isOnTop, - TextPainter? labelPainter, - double? textScaleFactor, - Size? sizeWithOverflow, - RenderBox? parentBox, - SliderThemeData? sliderTheme, - TextDirection? textDirection, - double? value, - Thumb? thumb, - }) { - final Canvas canvas = context.canvas; - final double scale = activationAnimation!.value; - _pathPainter.paint( - parentBox: parentBox!, - canvas: canvas, - center: center, - scale: scale, - labelPainter: labelPainter!, - textScaleFactor: textScaleFactor!, - sizeWithOverflow: sizeWithOverflow!, - backgroundPaintColor: sliderTheme!.valueIndicatorColor!, - strokePaintColor: - isOnTop! - ? sliderTheme.overlappingShapeStrokeColor - : sliderTheme.valueIndicatorStrokeColor, - ); - } -} - -class _RectangularSliderValueIndicatorPathPainter { - const _RectangularSliderValueIndicatorPathPainter(); - - static const double _triangleHeight = 8.0; - static const double _labelPadding = 16.0; - static const double _preferredHeight = 32.0; - static const double _minLabelWidth = 16.0; - static const double _bottomTipYOffset = 14.0; - static const double _preferredHalfHeight = _preferredHeight / 2; - static const double _upperRectRadius = 4; - - Size getPreferredSize(TextPainter labelPainter, double textScaleFactor) { - return Size( - _upperRectangleWidth(labelPainter, 1, textScaleFactor), - labelPainter.height + _labelPadding, - ); - } - - double getHorizontalShift({ - required RenderBox parentBox, - required Offset center, - required TextPainter labelPainter, - required double textScaleFactor, - required Size sizeWithOverflow, - required double scale, - }) { - assert(!sizeWithOverflow.isEmpty); - - const double edgePadding = 8.0; - final double rectangleWidth = _upperRectangleWidth(labelPainter, scale, textScaleFactor); - - /// Value indicator draws on the Overlay and by using the global Offset - /// we are making sure we use the bounds of the Overlay instead of the Slider. - final Offset globalCenter = parentBox.localToGlobal(center); - - // The rectangle must be shifted towards the center so that it minimizes the - // chance of it rendering outside the bounds of the render box. If the shift - // is negative, then the lobe is shifted from right to left, and if it is - // positive, then the lobe is shifted from left to right. - final double overflowLeft = math.max(0, rectangleWidth / 2 - globalCenter.dx + edgePadding); - final double overflowRight = math.max( - 0, - rectangleWidth / 2 - (sizeWithOverflow.width - globalCenter.dx - edgePadding), - ); - - if (rectangleWidth < sizeWithOverflow.width) { - return overflowLeft - overflowRight; - } else if (overflowLeft - overflowRight > 0) { - return overflowLeft - (edgePadding * textScaleFactor); - } else { - return -overflowRight + (edgePadding * textScaleFactor); - } - } - - double _upperRectangleWidth(TextPainter labelPainter, double scale, double textScaleFactor) { - final double unscaledWidth = - math.max(_minLabelWidth * textScaleFactor, labelPainter.width) + _labelPadding * 2; - return unscaledWidth * scale; - } - - void paint({ - required RenderBox parentBox, - required Canvas canvas, - required Offset center, - required double scale, - required TextPainter labelPainter, - required double textScaleFactor, - required Size sizeWithOverflow, - required Color backgroundPaintColor, - Color? strokePaintColor, - }) { - if (scale == 0.0) { - // Zero scale essentially means "do not draw anything", so it's safe to just return. - return; - } - assert(!sizeWithOverflow.isEmpty); - - final double rectangleWidth = _upperRectangleWidth(labelPainter, scale, textScaleFactor); - final double horizontalShift = getHorizontalShift( - parentBox: parentBox, - center: center, - labelPainter: labelPainter, - textScaleFactor: textScaleFactor, - sizeWithOverflow: sizeWithOverflow, - scale: scale, - ); - - final double rectHeight = labelPainter.height + _labelPadding; - final Rect upperRect = Rect.fromLTWH( - -rectangleWidth / 2 + horizontalShift, - -_triangleHeight - rectHeight, - rectangleWidth, - rectHeight, - ); - - final Path trianglePath = - Path() - ..lineTo(-_triangleHeight, -_triangleHeight) - ..lineTo(_triangleHeight, -_triangleHeight) - ..close(); - final Paint fillPaint = Paint()..color = backgroundPaintColor; - final RRect upperRRect = RRect.fromRectAndRadius( - upperRect, - const Radius.circular(_upperRectRadius), - ); - trianglePath.addRRect(upperRRect); - - canvas.save(); - // Prepare the canvas for the base of the tooltip, which is relative to the - // center of the thumb. - canvas.translate(center.dx, center.dy - _bottomTipYOffset); - canvas.scale(scale, scale); - if (strokePaintColor != null) { - final Paint strokePaint = - Paint() - ..color = strokePaintColor - ..strokeWidth = 1.0 - ..style = PaintingStyle.stroke; - canvas.drawPath(trianglePath, strokePaint); - } - canvas.drawPath(trianglePath, fillPaint); - - // The label text is centered within the value indicator. - final double bottomTipToUpperRectTranslateY = -_preferredHalfHeight / 2 - upperRect.height; - canvas.translate(0, bottomTipToUpperRectTranslateY); - final Offset boxCenter = Offset(horizontalShift, upperRect.height / 2); - final Offset halfLabelPainterOffset = Offset(labelPainter.width / 2, labelPainter.height / 2); - final Offset labelOffset = boxCenter - halfLabelPainterOffset; - labelPainter.paint(canvas, labelOffset); - canvas.restore(); - } -} - /// A variant shape of a [Slider]'s value indicator . The value indicator is in /// the shape of an upside-down pear. /// diff --git a/packages/flutter/lib/src/material/slider_value_indicator_shape.dart b/packages/flutter/lib/src/material/slider_value_indicator_shape.dart new file mode 100644 index 00000000000..2a84cab1e05 --- /dev/null +++ b/packages/flutter/lib/src/material/slider_value_indicator_shape.dart @@ -0,0 +1,282 @@ +// Copyright 2014 The Flutter 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/widgets.dart'; + +import 'slider_theme.dart'; + +/// The default shape of a [Slider]'s value indicator. +/// +/// ![A slider widget, consisting of 5 divisions and showing the rectangular slider value indicator shape.](https://flutter.github.io/assets-for-api-docs/assets/material/rectangular_slider_value_indicator_shape.png) +/// +/// See also: +/// +/// * [Slider], which includes a value indicator defined by this shape. +/// * [SliderTheme], which can be used to configure the slider value indicator +/// of all sliders in a widget subtree. +class RectangularSliderValueIndicatorShape extends SliderComponentShape { + /// Create a slider value indicator that resembles a rectangular tooltip. + const RectangularSliderValueIndicatorShape(); + + static const _RectangularSliderValueIndicatorPathPainter _pathPainter = + _RectangularSliderValueIndicatorPathPainter(); + + @override + Size getPreferredSize( + bool isEnabled, + bool isDiscrete, { + TextPainter? labelPainter, + double? textScaleFactor, + }) { + assert(labelPainter != null); + assert(textScaleFactor != null && textScaleFactor >= 0); + return _pathPainter.getPreferredSize(labelPainter!, textScaleFactor!); + } + + @override + void paint( + PaintingContext context, + Offset center, { + required Animation activationAnimation, + required Animation enableAnimation, + required bool isDiscrete, + required TextPainter labelPainter, + required RenderBox parentBox, + required SliderThemeData sliderTheme, + required TextDirection textDirection, + required double value, + required double textScaleFactor, + required Size sizeWithOverflow, + }) { + final Canvas canvas = context.canvas; + final double scale = activationAnimation.value; + _pathPainter.paint( + parentBox: parentBox, + canvas: canvas, + center: center, + scale: scale, + labelPainter: labelPainter, + textScaleFactor: textScaleFactor, + sizeWithOverflow: sizeWithOverflow, + backgroundPaintColor: sliderTheme.valueIndicatorColor!, + strokePaintColor: sliderTheme.valueIndicatorStrokeColor, + ); + } +} + +/// The default shape of a [RangeSlider]'s value indicators. +/// +/// ![A slider widget, consisting of 5 divisions and showing the rectangular range slider value indicator shape.](https://flutter.github.io/assets-for-api-docs/assets/material/rectangular_range_slider_value_indicator_shape.png) +/// +/// See also: +/// +/// * [RangeSlider], which includes value indicators defined by this shape. +/// * [SliderTheme], which can be used to configure the range slider value +/// indicator of all sliders in a widget subtree. +class RectangularRangeSliderValueIndicatorShape extends RangeSliderValueIndicatorShape { + /// Create a range slider value indicator that resembles a rectangular tooltip. + const RectangularRangeSliderValueIndicatorShape(); + + static const _RectangularSliderValueIndicatorPathPainter _pathPainter = + _RectangularSliderValueIndicatorPathPainter(); + + @override + Size getPreferredSize( + bool isEnabled, + bool isDiscrete, { + required TextPainter labelPainter, + required double textScaleFactor, + }) { + assert(textScaleFactor >= 0); + return _pathPainter.getPreferredSize(labelPainter, textScaleFactor); + } + + @override + double getHorizontalShift({ + RenderBox? parentBox, + Offset? center, + TextPainter? labelPainter, + Animation? activationAnimation, + double? textScaleFactor, + Size? sizeWithOverflow, + }) { + return _pathPainter.getHorizontalShift( + parentBox: parentBox!, + center: center!, + labelPainter: labelPainter!, + textScaleFactor: textScaleFactor!, + sizeWithOverflow: sizeWithOverflow!, + scale: activationAnimation!.value, + ); + } + + @override + void paint( + PaintingContext context, + Offset center, { + Animation? activationAnimation, + Animation? enableAnimation, + bool? isDiscrete, + bool? isOnTop, + TextPainter? labelPainter, + double? textScaleFactor, + Size? sizeWithOverflow, + RenderBox? parentBox, + SliderThemeData? sliderTheme, + TextDirection? textDirection, + double? value, + Thumb? thumb, + }) { + final Canvas canvas = context.canvas; + final double scale = activationAnimation!.value; + _pathPainter.paint( + parentBox: parentBox!, + canvas: canvas, + center: center, + scale: scale, + labelPainter: labelPainter!, + textScaleFactor: textScaleFactor!, + sizeWithOverflow: sizeWithOverflow!, + backgroundPaintColor: sliderTheme!.valueIndicatorColor!, + strokePaintColor: + isOnTop! + ? sliderTheme.overlappingShapeStrokeColor + : sliderTheme.valueIndicatorStrokeColor, + ); + } +} + +class _RectangularSliderValueIndicatorPathPainter { + const _RectangularSliderValueIndicatorPathPainter(); + + static const double _triangleHeight = 8.0; + static const double _labelPadding = 16.0; + static const double _preferredHeight = 32.0; + static const double _minLabelWidth = 16.0; + static const double _bottomTipYOffset = 14.0; + static const double _preferredHalfHeight = _preferredHeight / 2; + static const double _upperRectRadius = 4; + + Size getPreferredSize(TextPainter labelPainter, double textScaleFactor) { + return Size( + _upperRectangleWidth(labelPainter, 1, textScaleFactor), + labelPainter.height + _labelPadding, + ); + } + + double getHorizontalShift({ + required RenderBox parentBox, + required Offset center, + required TextPainter labelPainter, + required double textScaleFactor, + required Size sizeWithOverflow, + required double scale, + }) { + assert(!sizeWithOverflow.isEmpty); + + const double edgePadding = 8.0; + final double rectangleWidth = _upperRectangleWidth(labelPainter, scale, textScaleFactor); + + /// Value indicator draws on the Overlay and by using the global Offset + /// we are making sure we use the bounds of the Overlay instead of the Slider. + final Offset globalCenter = parentBox.localToGlobal(center); + + // The rectangle must be shifted towards the center so that it minimizes the + // chance of it rendering outside the bounds of the render box. If the shift + // is negative, then the lobe is shifted from right to left, and if it is + // positive, then the lobe is shifted from left to right. + final double overflowLeft = math.max(0, rectangleWidth / 2 - globalCenter.dx + edgePadding); + final double overflowRight = math.max( + 0, + rectangleWidth / 2 - (sizeWithOverflow.width - globalCenter.dx - edgePadding), + ); + + if (rectangleWidth < sizeWithOverflow.width) { + return overflowLeft - overflowRight; + } else if (overflowLeft - overflowRight > 0) { + return overflowLeft - (edgePadding * textScaleFactor); + } else { + return -overflowRight + (edgePadding * textScaleFactor); + } + } + + double _upperRectangleWidth(TextPainter labelPainter, double scale, double textScaleFactor) { + final double unscaledWidth = + math.max(_minLabelWidth * textScaleFactor, labelPainter.width) + _labelPadding * 2; + return unscaledWidth * scale; + } + + void paint({ + required RenderBox parentBox, + required Canvas canvas, + required Offset center, + required double scale, + required TextPainter labelPainter, + required double textScaleFactor, + required Size sizeWithOverflow, + required Color backgroundPaintColor, + Color? strokePaintColor, + }) { + if (scale == 0.0) { + // Zero scale essentially means "do not draw anything", so it's safe to just return. + return; + } + assert(!sizeWithOverflow.isEmpty); + + final double rectangleWidth = _upperRectangleWidth(labelPainter, scale, textScaleFactor); + final double horizontalShift = getHorizontalShift( + parentBox: parentBox, + center: center, + labelPainter: labelPainter, + textScaleFactor: textScaleFactor, + sizeWithOverflow: sizeWithOverflow, + scale: scale, + ); + + final double rectHeight = labelPainter.height + _labelPadding; + final Rect upperRect = Rect.fromLTWH( + -rectangleWidth / 2 + horizontalShift, + -_triangleHeight - rectHeight, + rectangleWidth, + rectHeight, + ); + + final Path trianglePath = + Path() + ..lineTo(-_triangleHeight, -_triangleHeight) + ..lineTo(_triangleHeight, -_triangleHeight) + ..close(); + final Paint fillPaint = Paint()..color = backgroundPaintColor; + final RRect upperRRect = RRect.fromRectAndRadius( + upperRect, + const Radius.circular(_upperRectRadius), + ); + trianglePath.addRRect(upperRRect); + + canvas.save(); + // Prepare the canvas for the base of the tooltip, which is relative to the + // center of the thumb. + canvas.translate(center.dx, center.dy - _bottomTipYOffset); + canvas.scale(scale, scale); + if (strokePaintColor != null) { + final Paint strokePaint = + Paint() + ..color = strokePaintColor + ..strokeWidth = 1.0 + ..style = PaintingStyle.stroke; + canvas.drawPath(trianglePath, strokePaint); + } + canvas.drawPath(trianglePath, fillPaint); + + // The label text is centered within the value indicator. + final double bottomTipToUpperRectTranslateY = -_preferredHalfHeight / 2 - upperRect.height; + canvas.translate(0, bottomTipToUpperRectTranslateY); + final Offset boxCenter = Offset(horizontalShift, upperRect.height / 2); + final Offset halfLabelPainterOffset = Offset(labelPainter.width / 2, labelPainter.height / 2); + final Offset labelOffset = boxCenter - halfLabelPainterOffset; + labelPainter.paint(canvas, labelOffset); + canvas.restore(); + } +}