mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
# Whats new * Add RoundedSuperellipseInputBorder fixes: #176987 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing.
810 lines
28 KiB
Dart
810 lines
28 KiB
Dart
// 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.
|
|
|
|
/// @docImport 'input_decorator.dart';
|
|
library;
|
|
|
|
import 'dart:math' as math;
|
|
import 'dart:ui' show lerpDouble;
|
|
|
|
import 'package:flutter/foundation.dart' show clampDouble;
|
|
import 'package:flutter/widgets.dart';
|
|
|
|
/// Defines the appearance of an [InputDecorator]'s border.
|
|
///
|
|
/// An input decorator's border is specified by [InputDecoration.border].
|
|
///
|
|
/// The border is drawn relative to the input decorator's "container" which
|
|
/// is the optionally filled area above the decorator's helper, error,
|
|
/// and counter.
|
|
///
|
|
/// Input border's are decorated with a line whose weight and color are defined
|
|
/// by [borderSide]. The input decorator's renderer animates the input border's
|
|
/// appearance in response to state changes, like gaining or losing the focus,
|
|
/// by creating new copies of its input border with [copyWith].
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [UnderlineInputBorder], the default [InputDecorator] border which
|
|
/// draws a horizontal line at the bottom of the input decorator's container.
|
|
/// * [OutlineInputBorder], an [InputDecorator] border which draws a
|
|
/// rounded rectangle around the input decorator's container.
|
|
/// * [InputDecoration], which is used to configure an [InputDecorator].
|
|
abstract class InputBorder extends ShapeBorder {
|
|
/// Creates a border for an [InputDecorator].
|
|
///
|
|
/// Applications typically do not specify a [borderSide] parameter because the
|
|
/// [InputDecorator] substitutes its own, using [copyWith], based on the
|
|
/// current theme and [InputDecorator.isFocused].
|
|
const InputBorder({this.borderSide = BorderSide.none});
|
|
|
|
/// No input border.
|
|
///
|
|
/// Use this value with [InputDecoration.border] to specify that no border
|
|
/// should be drawn. The [InputDecoration.collapsed] constructor sets
|
|
/// its border to this value.
|
|
static const InputBorder none = _NoInputBorder();
|
|
|
|
/// Defines the border line's color and weight.
|
|
///
|
|
/// The [InputDecorator] creates copies of its input border, using [copyWith],
|
|
/// based on the current theme and [InputDecorator.isFocused].
|
|
final BorderSide borderSide;
|
|
|
|
/// Creates a copy of this input border with the specified `borderSide`.
|
|
InputBorder copyWith({BorderSide? borderSide});
|
|
|
|
/// True if this border will enclose the [InputDecorator]'s container.
|
|
///
|
|
/// This property affects the alignment of container's contents. For example
|
|
/// when an input decorator is configured with an [OutlineInputBorder] its
|
|
/// label is centered with its container.
|
|
bool get isOutline;
|
|
|
|
/// Paint this input border on [canvas].
|
|
///
|
|
/// The [rect] parameter bounds the [InputDecorator]'s container.
|
|
///
|
|
/// The additional `gap` parameters reflect the state of the [InputDecorator]'s
|
|
/// floating label. When an input decorator gains the focus, its label
|
|
/// animates upwards, to make room for the input child. The [gapStart] and
|
|
/// [gapExtent] parameters define a floating label width interval, and
|
|
/// [gapPercentage] defines the animation's progress (0.0 to 1.0).
|
|
@override
|
|
void paint(
|
|
Canvas canvas,
|
|
Rect rect, {
|
|
double? gapStart,
|
|
double gapExtent = 0.0,
|
|
double gapPercentage = 0.0,
|
|
TextDirection? textDirection,
|
|
});
|
|
}
|
|
|
|
// Used to create the InputBorder.none singleton.
|
|
class _NoInputBorder extends InputBorder {
|
|
const _NoInputBorder() : super(borderSide: BorderSide.none);
|
|
|
|
@override
|
|
_NoInputBorder copyWith({BorderSide? borderSide}) => const _NoInputBorder();
|
|
|
|
@override
|
|
bool get isOutline => false;
|
|
|
|
@override
|
|
EdgeInsetsGeometry get dimensions => EdgeInsets.zero;
|
|
|
|
@override
|
|
_NoInputBorder scale(double t) => const _NoInputBorder();
|
|
|
|
@override
|
|
Path getInnerPath(Rect rect, {TextDirection? textDirection}) {
|
|
return Path()..addRect(rect);
|
|
}
|
|
|
|
@override
|
|
Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
|
|
return Path()..addRect(rect);
|
|
}
|
|
|
|
@override
|
|
void paintInterior(Canvas canvas, Rect rect, Paint paint, {TextDirection? textDirection}) {
|
|
canvas.drawRect(rect, paint);
|
|
}
|
|
|
|
@override
|
|
bool get preferPaintInterior => true;
|
|
|
|
@override
|
|
void paint(
|
|
Canvas canvas,
|
|
Rect rect, {
|
|
double? gapStart,
|
|
double gapExtent = 0.0,
|
|
double gapPercentage = 0.0,
|
|
TextDirection? textDirection,
|
|
}) {
|
|
// Do not paint.
|
|
}
|
|
}
|
|
|
|
/// Draws a horizontal line at the bottom of an [InputDecorator]'s container and
|
|
/// defines the container's shape.
|
|
///
|
|
/// The input decorator's "container" is the optionally filled area above the
|
|
/// decorator's helper, error, and counter.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [OutlineInputBorder], an [InputDecorator] border which draws a
|
|
/// rounded rectangle around the input decorator's container.
|
|
/// * [InputDecoration], which is used to configure an [InputDecorator].
|
|
class UnderlineInputBorder extends InputBorder {
|
|
/// Creates an underline border for an [InputDecorator].
|
|
///
|
|
/// The [borderSide] parameter defaults to [BorderSide.none] (it must not be
|
|
/// null). Applications typically do not specify a [borderSide] parameter
|
|
/// because the input decorator substitutes its own, using [copyWith], based
|
|
/// on the current theme and [InputDecorator.isFocused].
|
|
///
|
|
/// The [borderRadius] parameter defaults to a value where the top left
|
|
/// and right corners have a circular radius of 4.0.
|
|
const UnderlineInputBorder({
|
|
super.borderSide = const BorderSide(),
|
|
this.borderRadius = const BorderRadius.only(
|
|
topLeft: Radius.circular(4.0),
|
|
topRight: Radius.circular(4.0),
|
|
),
|
|
});
|
|
|
|
/// The radii of the border's rounded rectangle corners.
|
|
///
|
|
/// When this border is used with a filled input decorator, see
|
|
/// [InputDecoration.filled], the border radius defines the shape
|
|
/// of the background fill as well as the bottom left and right
|
|
/// edges of the underline itself.
|
|
///
|
|
/// By default the top right and top left corners have a circular radius
|
|
/// of 4.0.
|
|
final BorderRadius borderRadius;
|
|
|
|
@override
|
|
bool get isOutline => false;
|
|
|
|
@override
|
|
UnderlineInputBorder copyWith({BorderSide? borderSide, BorderRadius? borderRadius}) {
|
|
return UnderlineInputBorder(
|
|
borderSide: borderSide ?? this.borderSide,
|
|
borderRadius: borderRadius ?? this.borderRadius,
|
|
);
|
|
}
|
|
|
|
@override
|
|
EdgeInsetsGeometry get dimensions {
|
|
return EdgeInsets.only(bottom: borderSide.width);
|
|
}
|
|
|
|
@override
|
|
UnderlineInputBorder scale(double t) {
|
|
return UnderlineInputBorder(borderSide: borderSide.scale(t));
|
|
}
|
|
|
|
@override
|
|
Path getInnerPath(Rect rect, {TextDirection? textDirection}) {
|
|
return Path()..addRect(
|
|
Rect.fromLTWH(rect.left, rect.top, rect.width, math.max(0.0, rect.height - borderSide.width)),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
|
|
return Path()..addRRect(borderRadius.resolve(textDirection).toRRect(rect));
|
|
}
|
|
|
|
@override
|
|
void paintInterior(Canvas canvas, Rect rect, Paint paint, {TextDirection? textDirection}) {
|
|
canvas.drawRRect(borderRadius.resolve(textDirection).toRRect(rect), paint);
|
|
}
|
|
|
|
@override
|
|
bool get preferPaintInterior => true;
|
|
|
|
@override
|
|
ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
|
|
if (a is UnderlineInputBorder) {
|
|
return UnderlineInputBorder(
|
|
borderSide: BorderSide.lerp(a.borderSide, borderSide, t),
|
|
borderRadius: BorderRadius.lerp(a.borderRadius, borderRadius, t)!,
|
|
);
|
|
}
|
|
return super.lerpFrom(a, t);
|
|
}
|
|
|
|
@override
|
|
ShapeBorder? lerpTo(ShapeBorder? b, double t) {
|
|
if (b is UnderlineInputBorder) {
|
|
return UnderlineInputBorder(
|
|
borderSide: BorderSide.lerp(borderSide, b.borderSide, t),
|
|
borderRadius: BorderRadius.lerp(borderRadius, b.borderRadius, t)!,
|
|
);
|
|
}
|
|
return super.lerpTo(b, t);
|
|
}
|
|
|
|
/// Draw a horizontal line at the bottom of [rect].
|
|
///
|
|
/// The [borderSide] defines the line's color and weight. The `textDirection`
|
|
/// `gap` and `textDirection` parameters are ignored.
|
|
@override
|
|
void paint(
|
|
Canvas canvas,
|
|
Rect rect, {
|
|
double? gapStart,
|
|
double gapExtent = 0.0,
|
|
double gapPercentage = 0.0,
|
|
TextDirection? textDirection,
|
|
}) {
|
|
if (borderSide.style == BorderStyle.none) {
|
|
return;
|
|
}
|
|
|
|
if (borderRadius.bottomLeft != Radius.zero || borderRadius.bottomRight != Radius.zero) {
|
|
// This prevents the border from leaking the color due to anti-aliasing rounding errors.
|
|
final updatedBorderRadius = BorderRadius.only(
|
|
bottomLeft: borderRadius.bottomLeft.clamp(maximum: Radius.circular(rect.height / 2)),
|
|
bottomRight: borderRadius.bottomRight.clamp(maximum: Radius.circular(rect.height / 2)),
|
|
);
|
|
|
|
BoxBorder.paintNonUniformBorder(
|
|
canvas,
|
|
rect,
|
|
textDirection: textDirection,
|
|
borderRadius: updatedBorderRadius,
|
|
bottom: borderSide.copyWith(strokeAlign: BorderSide.strokeAlignInside),
|
|
color: borderSide.color,
|
|
);
|
|
} else {
|
|
final alignInsideOffset = Offset(0, borderSide.width / 2);
|
|
canvas.drawLine(
|
|
rect.bottomLeft - alignInsideOffset,
|
|
rect.bottomRight - alignInsideOffset,
|
|
borderSide.toPaint(),
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool operator ==(Object other) {
|
|
if (identical(this, other)) {
|
|
return true;
|
|
}
|
|
if (other.runtimeType != runtimeType) {
|
|
return false;
|
|
}
|
|
return other is UnderlineInputBorder &&
|
|
other.borderSide == borderSide &&
|
|
other.borderRadius == borderRadius;
|
|
}
|
|
|
|
@override
|
|
int get hashCode => Object.hash(borderSide, borderRadius);
|
|
}
|
|
|
|
/// Draws a rounded rectangle around an [InputDecorator]'s container.
|
|
///
|
|
/// When the input decorator's label is floating, for example because its
|
|
/// input child has the focus, the label appears in a gap in the border outline.
|
|
///
|
|
/// The input decorator's "container" is the optionally filled area above the
|
|
/// decorator's helper, error, and counter.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [UnderlineInputBorder], the default [InputDecorator] border which
|
|
/// draws a horizontal line at the bottom of the input decorator's container.
|
|
/// * [ShapedInputBorder], an [InputDecorator] border which draws a custom
|
|
/// [ShapeBorder] around the input decorator's container.
|
|
/// * [InputDecoration], which is used to configure an [InputDecorator].
|
|
class OutlineInputBorder extends InputBorder {
|
|
/// Creates a rounded rectangle outline border for an [InputDecorator].
|
|
///
|
|
/// If the [borderSide] parameter is [BorderSide.none], it will not draw a
|
|
/// border. However, it will still define a shape (which you can see if
|
|
/// [InputDecoration.filled] is true).
|
|
///
|
|
/// If an application does not specify a [borderSide] parameter of
|
|
/// value [BorderSide.none], the input decorator substitutes its own, using
|
|
/// [copyWith], based on the current theme and [InputDecorator.isFocused].
|
|
///
|
|
/// The [borderRadius] parameter defaults to a value where all four corners
|
|
/// have a circular radius of 4.0. The corner radii must be circular, i.e.
|
|
/// their [Radius.x] and [Radius.y] values must be the same.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [InputDecoration.floatingLabelBehavior], which should be set to
|
|
/// [FloatingLabelBehavior.never] when the [borderSide] is
|
|
/// [BorderSide.none]. If left as [FloatingLabelBehavior.auto], the label
|
|
/// will extend beyond the container as if the border were still being
|
|
/// drawn.
|
|
const OutlineInputBorder({
|
|
super.borderSide = const BorderSide(),
|
|
this.borderRadius = const BorderRadius.all(Radius.circular(4.0)),
|
|
this.gapPadding = 4.0,
|
|
}) : assert(gapPadding >= 0.0);
|
|
|
|
// The label text's gap can extend into the corners (even both the top left
|
|
// and the top right corner). To avoid the more complicated problem of finding
|
|
// how far the gap penetrates into an elliptical corner, just require them
|
|
// to be circular.
|
|
//
|
|
// This can't be checked by the constructor because const constructor.
|
|
static bool _cornersAreCircular(BorderRadius borderRadius) {
|
|
return borderRadius.topLeft.x == borderRadius.topLeft.y &&
|
|
borderRadius.bottomLeft.x == borderRadius.bottomLeft.y &&
|
|
borderRadius.topRight.x == borderRadius.topRight.y &&
|
|
borderRadius.bottomRight.x == borderRadius.bottomRight.y;
|
|
}
|
|
|
|
/// Horizontal padding on either side of the border's
|
|
/// [InputDecoration.labelText] width gap.
|
|
///
|
|
/// This value is used by the [paint] method to compute the actual gap width.
|
|
final double gapPadding;
|
|
|
|
/// The radii of the border's rounded rectangle corners.
|
|
///
|
|
/// The corner radii must be circular, i.e. their [Radius.x] and [Radius.y]
|
|
/// values must be the same.
|
|
final BorderRadius borderRadius;
|
|
|
|
@override
|
|
bool get isOutline => true;
|
|
|
|
@override
|
|
OutlineInputBorder copyWith({
|
|
BorderSide? borderSide,
|
|
BorderRadius? borderRadius,
|
|
double? gapPadding,
|
|
}) {
|
|
return OutlineInputBorder(
|
|
borderSide: borderSide ?? this.borderSide,
|
|
borderRadius: borderRadius ?? this.borderRadius,
|
|
gapPadding: gapPadding ?? this.gapPadding,
|
|
);
|
|
}
|
|
|
|
@override
|
|
EdgeInsetsGeometry get dimensions {
|
|
return EdgeInsets.all(borderSide.width);
|
|
}
|
|
|
|
@override
|
|
OutlineInputBorder scale(double t) {
|
|
return OutlineInputBorder(
|
|
borderSide: borderSide.scale(t),
|
|
borderRadius: borderRadius * t,
|
|
gapPadding: gapPadding * t,
|
|
);
|
|
}
|
|
|
|
@override
|
|
ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
|
|
if (a is OutlineInputBorder) {
|
|
final OutlineInputBorder outline = a;
|
|
return OutlineInputBorder(
|
|
borderRadius: BorderRadius.lerp(outline.borderRadius, borderRadius, t)!,
|
|
borderSide: BorderSide.lerp(outline.borderSide, borderSide, t),
|
|
gapPadding: outline.gapPadding,
|
|
);
|
|
}
|
|
return super.lerpFrom(a, t);
|
|
}
|
|
|
|
@override
|
|
ShapeBorder? lerpTo(ShapeBorder? b, double t) {
|
|
if (b is OutlineInputBorder) {
|
|
final OutlineInputBorder outline = b;
|
|
return OutlineInputBorder(
|
|
borderRadius: BorderRadius.lerp(borderRadius, outline.borderRadius, t)!,
|
|
borderSide: BorderSide.lerp(borderSide, outline.borderSide, t),
|
|
gapPadding: outline.gapPadding,
|
|
);
|
|
}
|
|
return super.lerpTo(b, t);
|
|
}
|
|
|
|
@override
|
|
Path getInnerPath(Rect rect, {TextDirection? textDirection}) {
|
|
return Path()
|
|
..addRRect(borderRadius.resolve(textDirection).toRRect(rect).deflate(borderSide.width));
|
|
}
|
|
|
|
@override
|
|
Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
|
|
return Path()..addRRect(borderRadius.resolve(textDirection).toRRect(rect));
|
|
}
|
|
|
|
@override
|
|
void paintInterior(Canvas canvas, Rect rect, Paint paint, {TextDirection? textDirection}) {
|
|
canvas.drawRRect(borderRadius.resolve(textDirection).toRRect(rect), paint);
|
|
}
|
|
|
|
@override
|
|
bool get preferPaintInterior => true;
|
|
|
|
Path _gapBorderPath(Canvas canvas, RRect center, double outerWidth, double start, double extent) {
|
|
// When the corner radii on any side add up to be greater than the
|
|
// given height, each radius has to be scaled to not exceed the
|
|
// size of the width/height of the RRect.
|
|
final RRect scaledRRect = center.scaleRadii();
|
|
|
|
final tlCorner = Rect.fromLTWH(
|
|
scaledRRect.left,
|
|
scaledRRect.top,
|
|
scaledRRect.tlRadiusX * 2.0,
|
|
scaledRRect.tlRadiusY * 2.0,
|
|
);
|
|
final trCorner = Rect.fromLTWH(
|
|
scaledRRect.right - scaledRRect.trRadiusX * 2.0,
|
|
scaledRRect.top,
|
|
scaledRRect.trRadiusX * 2.0,
|
|
scaledRRect.trRadiusY * 2.0,
|
|
);
|
|
final brCorner = Rect.fromLTWH(
|
|
scaledRRect.right - scaledRRect.brRadiusX * 2.0,
|
|
scaledRRect.bottom - scaledRRect.brRadiusY * 2.0,
|
|
scaledRRect.brRadiusX * 2.0,
|
|
scaledRRect.brRadiusY * 2.0,
|
|
);
|
|
final blCorner = Rect.fromLTWH(
|
|
scaledRRect.left,
|
|
scaledRRect.bottom - scaledRRect.blRadiusY * 2.0,
|
|
scaledRRect.blRadiusX * 2.0,
|
|
scaledRRect.blRadiusY * 2.0,
|
|
);
|
|
|
|
// This assumes that the radius is circular (x and y radius are equal).
|
|
// Currently, BorderRadius only supports circular radii.
|
|
const double cornerArcSweep = math.pi / 2.0;
|
|
final path = Path();
|
|
|
|
// Top left corner
|
|
if (scaledRRect.tlRadius != Radius.zero) {
|
|
final double tlCornerArcSweep = math.acos(
|
|
clampDouble(1 - start / scaledRRect.tlRadiusX, 0.0, 1.0),
|
|
);
|
|
path.addArc(tlCorner, math.pi, tlCornerArcSweep);
|
|
} else {
|
|
// Because the path is painted with Paint.strokeCap = StrokeCap.butt, horizontal coordinate is moved
|
|
// to the left using borderSide.width / 2.
|
|
path.moveTo(scaledRRect.left - borderSide.width / 2, scaledRRect.top);
|
|
}
|
|
|
|
// Draw top border from top left corner to gap start.
|
|
if (start > scaledRRect.tlRadiusX) {
|
|
path.lineTo(start, scaledRRect.top);
|
|
}
|
|
|
|
// Draw top border from gap end to top right corner and draw top right corner.
|
|
const double trCornerArcStart = (3 * math.pi) / 2.0;
|
|
const trCornerArcSweep = cornerArcSweep;
|
|
if (start + extent < outerWidth - scaledRRect.trRadiusX) {
|
|
path.moveTo(start + extent, scaledRRect.top);
|
|
path.lineTo(scaledRRect.right - scaledRRect.trRadiusX, scaledRRect.top);
|
|
if (scaledRRect.trRadius != Radius.zero) {
|
|
path.addArc(trCorner, trCornerArcStart, trCornerArcSweep);
|
|
}
|
|
} else if (start + extent < outerWidth) {
|
|
final double dx = outerWidth - (start + extent);
|
|
final double sweep = math.asin(clampDouble(1 - dx / scaledRRect.trRadiusX, 0.0, 1.0));
|
|
path.addArc(trCorner, trCornerArcStart + sweep, trCornerArcSweep - sweep);
|
|
}
|
|
|
|
// Draw right border and bottom right corner.
|
|
if (scaledRRect.brRadius != Radius.zero) {
|
|
path.moveTo(scaledRRect.right, scaledRRect.top + scaledRRect.trRadiusY);
|
|
}
|
|
path.lineTo(scaledRRect.right, scaledRRect.bottom - scaledRRect.brRadiusY);
|
|
if (scaledRRect.brRadius != Radius.zero) {
|
|
path.addArc(brCorner, 0.0, cornerArcSweep);
|
|
}
|
|
|
|
// Draw bottom border and bottom left corner.
|
|
path.lineTo(scaledRRect.left + scaledRRect.blRadiusX, scaledRRect.bottom);
|
|
if (scaledRRect.blRadius != Radius.zero) {
|
|
path.addArc(blCorner, math.pi / 2.0, cornerArcSweep);
|
|
}
|
|
|
|
// Draw left border
|
|
path.lineTo(scaledRRect.left, scaledRRect.top + scaledRRect.tlRadiusY);
|
|
|
|
return path;
|
|
}
|
|
|
|
/// Draw a rounded rectangle around [rect] using [borderRadius].
|
|
///
|
|
/// The [borderSide] defines the line's color and weight.
|
|
///
|
|
/// The top side of the rounded rectangle may be interrupted by a single gap
|
|
/// if [gapExtent] is non-null. In that case the gap begins at
|
|
/// `gapStart - gapPadding` (assuming that the [textDirection] is [TextDirection.ltr]).
|
|
/// The gap's width is `(gapPadding + gapExtent + gapPadding) * gapPercentage`.
|
|
@override
|
|
void paint(
|
|
Canvas canvas,
|
|
Rect rect, {
|
|
double? gapStart,
|
|
double gapExtent = 0.0,
|
|
double gapPercentage = 0.0,
|
|
TextDirection? textDirection,
|
|
}) {
|
|
assert(gapPercentage >= 0.0 && gapPercentage <= 1.0);
|
|
assert(_cornersAreCircular(borderRadius));
|
|
|
|
final Paint paint = borderSide.toPaint();
|
|
final RRect outer = borderRadius.toRRect(rect);
|
|
final RRect center = outer.deflate(borderSide.width / 2.0);
|
|
if (gapStart == null || gapExtent <= 0.0 || gapPercentage == 0.0) {
|
|
canvas.drawRRect(center, paint);
|
|
} else {
|
|
final double extent = lerpDouble(0.0, gapExtent + gapPadding * 2.0, gapPercentage)!;
|
|
final double start = switch (textDirection!) {
|
|
TextDirection.rtl => gapStart + gapPadding - extent,
|
|
TextDirection.ltr => gapStart - gapPadding,
|
|
};
|
|
final Path path = _gapBorderPath(canvas, center, outer.width, math.max(0.0, start), extent);
|
|
canvas.drawPath(path, paint);
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool operator ==(Object other) {
|
|
if (identical(this, other)) {
|
|
return true;
|
|
}
|
|
if (other.runtimeType != runtimeType) {
|
|
return false;
|
|
}
|
|
return other is OutlineInputBorder &&
|
|
other.borderSide == borderSide &&
|
|
other.borderRadius == borderRadius &&
|
|
other.gapPadding == gapPadding;
|
|
}
|
|
|
|
@override
|
|
int get hashCode => Object.hash(borderSide, borderRadius, gapPadding);
|
|
}
|
|
|
|
/// Draws a custom shape around an [InputDecorator]'s container.
|
|
///
|
|
/// This border allows any [ShapeBorder] to be used as an input decorator border.
|
|
/// This provides maximum flexibility for custom border shapes while maintaining
|
|
/// the gap functionality for floating labels.
|
|
///
|
|
/// When the input decorator's label is floating, for example because its
|
|
/// input child has the focus, the label appears in a gap in the border outline.
|
|
///
|
|
/// The input decorator's "container" is the optionally filled area above the
|
|
/// decorator's helper, error, and counter.
|
|
///
|
|
/// {@tool dartpad}
|
|
/// This sample shows how to use [ShapedInputBorder] with different
|
|
/// [ShapeBorder] implementations.
|
|
///
|
|
/// ** See code in examples/api/lib/material/shaped_input_border/shaped_input_border.0.dart **
|
|
/// {@end-tool}
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [OutlineInputBorder], a traditional rounded rectangle border.
|
|
/// * [UnderlineInputBorder], the default [InputDecorator] border which
|
|
/// draws a horizontal line at the bottom of the input decorator's container.
|
|
/// * [RoundedSuperellipseBorder], which can be used with this border for iOS-style shapes.
|
|
/// * [InputDecoration], which is used to configure an [InputDecorator].
|
|
class ShapedInputBorder extends InputBorder {
|
|
/// Creates a shaped outline border for an [InputDecorator].
|
|
///
|
|
/// The [shape] parameter defines the custom border shape. It can be any
|
|
/// [ShapeBorder] such as [RoundedSuperellipseBorder], [StadiumBorder],
|
|
/// [BeveledRectangleBorder], or a custom shape.
|
|
///
|
|
/// If the [borderSide] parameter is [BorderSide.none], it will not draw a
|
|
/// border. However, it will still define a shape (which you can see if
|
|
/// [InputDecoration.filled] is true).
|
|
///
|
|
/// If an application does not specify a [borderSide] parameter of
|
|
/// value [BorderSide.none], the input decorator substitutes its own, using
|
|
/// [copyWith], based on the current theme and [InputDecorator.isFocused].
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [InputDecoration.floatingLabelBehavior], which should be set to
|
|
/// [FloatingLabelBehavior.never] when the [borderSide] is
|
|
/// [BorderSide.none]. If left as [FloatingLabelBehavior.auto], the label
|
|
/// will extend beyond the container as if the border were still being
|
|
/// drawn.
|
|
const ShapedInputBorder({
|
|
super.borderSide = const BorderSide(),
|
|
required this.shape,
|
|
this.gapPadding = 4.0,
|
|
}) : assert(gapPadding >= 0.0);
|
|
|
|
/// Horizontal padding on either side of the border's
|
|
/// [InputDecoration.labelText] width gap.
|
|
///
|
|
/// This value is used by the [paint] method to compute the actual gap width.
|
|
final double gapPadding;
|
|
|
|
/// The shape of the border.
|
|
final ShapeBorder shape;
|
|
|
|
@override
|
|
bool get isOutline => true;
|
|
|
|
@override
|
|
ShapedInputBorder copyWith({BorderSide? borderSide, ShapeBorder? shape, double? gapPadding}) {
|
|
return ShapedInputBorder(
|
|
borderSide: borderSide ?? this.borderSide,
|
|
shape: shape ?? this.shape,
|
|
gapPadding: gapPadding ?? this.gapPadding,
|
|
);
|
|
}
|
|
|
|
@override
|
|
EdgeInsetsGeometry get dimensions {
|
|
return EdgeInsets.all(borderSide.width);
|
|
}
|
|
|
|
@override
|
|
ShapedInputBorder scale(double t) {
|
|
return ShapedInputBorder(
|
|
borderSide: borderSide.scale(t),
|
|
shape: shape.scale(t),
|
|
gapPadding: gapPadding * t,
|
|
);
|
|
}
|
|
|
|
@override
|
|
ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
|
|
if (a is ShapedInputBorder) {
|
|
return ShapedInputBorder(
|
|
borderSide: BorderSide.lerp(a.borderSide, borderSide, t),
|
|
shape: ShapeBorder.lerp(a.shape, shape, t)!,
|
|
gapPadding: a.gapPadding,
|
|
);
|
|
}
|
|
return super.lerpFrom(a, t);
|
|
}
|
|
|
|
@override
|
|
ShapeBorder? lerpTo(ShapeBorder? b, double t) {
|
|
if (b is ShapedInputBorder) {
|
|
return ShapedInputBorder(
|
|
borderSide: BorderSide.lerp(borderSide, b.borderSide, t),
|
|
shape: ShapeBorder.lerp(shape, b.shape, t)!,
|
|
gapPadding: b.gapPadding,
|
|
);
|
|
}
|
|
return super.lerpTo(b, t);
|
|
}
|
|
|
|
@override
|
|
Path getInnerPath(Rect rect, {TextDirection? textDirection}) {
|
|
return shape.getInnerPath(rect.deflate(borderSide.width), textDirection: textDirection);
|
|
}
|
|
|
|
@override
|
|
Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
|
|
return shape.getOuterPath(rect, textDirection: textDirection);
|
|
}
|
|
|
|
@override
|
|
void paintInterior(Canvas canvas, Rect rect, Paint paint, {TextDirection? textDirection}) {
|
|
if (shape is OutlinedBorder) {
|
|
(shape as OutlinedBorder).paintInterior(canvas, rect, paint, textDirection: textDirection);
|
|
} else {
|
|
// Fallback for shapes that don't support paintInterior.
|
|
canvas.drawPath(shape.getOuterPath(rect, textDirection: textDirection), paint);
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool get preferPaintInterior => shape.preferPaintInterior;
|
|
|
|
Path _gapBorderPath(Rect rect, double start, double extent, {TextDirection? textDirection}) {
|
|
// Create a continuous path for the border with a gap in the top edge.
|
|
final Path outerPath = shape.getOuterPath(rect, textDirection: textDirection);
|
|
|
|
// If there's no meaningful gap, return the full outline.
|
|
if (start <= 0 && extent <= 0) {
|
|
return outerPath;
|
|
}
|
|
|
|
// Create a rectangle that represents the gap area.
|
|
// The gap is on the top edge, so we create a rect that covers the gap region.
|
|
final gapLeft = start;
|
|
final double gapRight = start + extent;
|
|
|
|
// Create a path that excludes the gap area by combining with a difference operation.
|
|
// We'll subtract a small rectangle at the top where the gap should be.
|
|
final gapRect = Path()
|
|
..addRect(
|
|
Rect.fromLTRB(
|
|
clampDouble(gapLeft, rect.left, rect.right),
|
|
rect.top - 1.0, // Extend slightly beyond to ensure clean cut.
|
|
clampDouble(gapRight, rect.left, rect.right),
|
|
rect.top + 1.0, // Small height to only affect top edge.
|
|
),
|
|
);
|
|
|
|
return Path.combine(PathOperation.difference, outerPath, gapRect);
|
|
}
|
|
|
|
/// Draw the custom shape around [rect].
|
|
///
|
|
/// The [borderSide] defines the line's color and weight.
|
|
///
|
|
/// The top side of the border may be interrupted by a single gap
|
|
/// if [gapExtent] is non-null. In that case the gap begins at
|
|
/// `gapStart - gapPadding` (assuming that the [textDirection] is [TextDirection.ltr]).
|
|
/// The gap's width is `(gapPadding + gapExtent + gapPadding) * gapPercentage`.
|
|
@override
|
|
void paint(
|
|
Canvas canvas,
|
|
Rect rect, {
|
|
double? gapStart,
|
|
double gapExtent = 0.0,
|
|
double gapPercentage = 0.0,
|
|
TextDirection? textDirection,
|
|
}) {
|
|
assert(gapPercentage >= 0.0 && gapPercentage <= 1.0);
|
|
|
|
final Paint paint = borderSide.toPaint();
|
|
final Rect deflatedRect = rect.deflate(borderSide.width / 2.0);
|
|
|
|
if (gapStart == null || gapExtent <= 0.0 || gapPercentage == 0.0) {
|
|
// Draw the shape without a gap.
|
|
if (shape is OutlinedBorder) {
|
|
final outlinedShape = shape as OutlinedBorder;
|
|
// Create a copy with our border side.
|
|
final OutlinedBorder shapedBorder = outlinedShape.copyWith(side: borderSide);
|
|
shapedBorder.paint(canvas, deflatedRect, textDirection: textDirection);
|
|
} else {
|
|
canvas.drawPath(shape.getOuterPath(deflatedRect, textDirection: textDirection), paint);
|
|
}
|
|
} else {
|
|
final double extent = lerpDouble(0.0, gapExtent + gapPadding * 2.0, gapPercentage)!;
|
|
final double start = switch (textDirection!) {
|
|
TextDirection.rtl => gapStart + gapPadding - extent,
|
|
TextDirection.ltr => gapStart - gapPadding,
|
|
};
|
|
final Path path = _gapBorderPath(
|
|
deflatedRect,
|
|
math.max(0.0, start),
|
|
extent,
|
|
textDirection: textDirection,
|
|
);
|
|
canvas.drawPath(path, paint);
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool operator ==(Object other) {
|
|
if (identical(this, other)) {
|
|
return true;
|
|
}
|
|
if (other.runtimeType != runtimeType) {
|
|
return false;
|
|
}
|
|
return other is ShapedInputBorder &&
|
|
other.borderSide == borderSide &&
|
|
other.shape == shape &&
|
|
other.gapPadding == gapPadding;
|
|
}
|
|
|
|
@override
|
|
int get hashCode => Object.hash(borderSide, shape, gapPadding);
|
|
}
|