blob: 79a5b3a7f2db96634f06cafbf1390687948cb41f [file] [log] [blame]
// Copyright 2017 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 'dart:ui' show lerpDouble;
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 {
/// 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 = const _NoInputBorder();
/// Creates a border for an [InputDecorator].
///
/// The [borderSide] parameter 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].
const InputBorder({
this.borderSide = BorderSide.none,
}) : assert(borderSide != null);
/// 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 new Path()..addRect(rect);
}
@override
Path getOuterPath(Rect rect, { TextDirection textDirection }) {
return new Path()..addRect(rect);
}
@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. The [borderRadius]
/// parameter must not be null.
const UnderlineInputBorder({
BorderSide borderSide = BorderSide.none,
this.borderRadius = const BorderRadius.only(
topLeft: const Radius.circular(4.0),
topRight: const Radius.circular(4.0),
),
}) : assert(borderRadius != null),
super(borderSide: borderSide);
/// 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 new UnderlineInputBorder(
borderSide: borderSide ?? this.borderSide,
borderRadius: borderRadius ?? this.borderRadius,
);
}
@override
EdgeInsetsGeometry get dimensions {
return new EdgeInsets.only(bottom: borderSide.width);
}
@override
UnderlineInputBorder scale(double t) {
return new UnderlineInputBorder(borderSide: borderSide.scale(t));
}
@override
Path getInnerPath(Rect rect, { TextDirection textDirection }) {
return new Path()
..addRect(new Rect.fromLTWH(rect.left, rect.top, rect.width, math.max(0.0, rect.height - borderSide.width)));
}
@override
Path getOuterPath(Rect rect, { TextDirection textDirection }) {
return new Path()..addRRect(borderRadius.resolve(textDirection).toRRect(rect));
}
@override
ShapeBorder lerpFrom(ShapeBorder a, double t) {
if (a is UnderlineInputBorder) {
return new UnderlineInputBorder(
borderSide: BorderSide.lerp(a.borderSide, borderSide, t),
);
}
return super.lerpFrom(a, t);
}
@override
ShapeBorder lerpTo(ShapeBorder b, double t) {
if (b is UnderlineInputBorder) {
return new UnderlineInputBorder(
borderSide: BorderSide.lerp(borderSide, b.borderSide, 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 (borderRadius.bottomLeft != Radius.zero || borderRadius.bottomRight != Radius.zero)
canvas.clipPath(getOuterPath(rect, textDirection: textDirection));
canvas.drawLine(rect.bottomLeft, rect.bottomRight, borderSide.toPaint());
}
@override
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (runtimeType != other.runtimeType)
return false;
final InputBorder typedOther = other;
return typedOther.borderSide == borderSide;
}
@override
int get hashCode => borderSide.hashCode;
}
/// 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.
/// * [InputDecoration], which is used to configure an [InputDecorator].
class OutlineInputBorder extends InputBorder {
/// Creates a rounded rectangle outline 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 all four
/// corners have a circular radius of 4.0. The [borderRadius] parameter
/// must not be null and the corner radii must be circular, i.e. their
/// [Radius.x] and [Radius.y] values must be the same.
const OutlineInputBorder({
BorderSide borderSide = BorderSide.none,
this.borderRadius = const BorderRadius.all(const Radius.circular(4.0)),
this.gapPadding = 4.0,
}) : assert(borderRadius != null),
assert(gapPadding != null && gapPadding >= 0.0),
super(borderSide: borderSide);
// 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 new OutlineInputBorder(
borderSide: borderSide ?? this.borderSide,
borderRadius: borderRadius ?? this.borderRadius,
gapPadding: gapPadding ?? this.gapPadding,
);
}
@override
EdgeInsetsGeometry get dimensions {
return new EdgeInsets.all(borderSide.width);
}
@override
OutlineInputBorder scale(double t) {
return new 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 new 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 new 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 new Path()
..addRRect(borderRadius.resolve(textDirection).toRRect(rect).deflate(borderSide.width));
}
@override
Path getOuterPath(Rect rect, { TextDirection textDirection }) {
return new Path()
..addRRect(borderRadius.resolve(textDirection).toRRect(rect));
}
Path _gapBorderPath(Canvas canvas, RRect center, double start, double extent) {
final Rect tlCorner = new Rect.fromLTWH(
center.left,
center.top,
center.tlRadiusX * 2.0,
center.tlRadiusY * 2.0,
);
final Rect trCorner = new Rect.fromLTWH(
center.right - center.trRadiusX * 2.0,
center.top,
center.trRadiusX * 2.0,
center.trRadiusY * 2.0,
);
final Rect brCorner = new Rect.fromLTWH(
center.right - center.brRadiusX * 2.0,
center.bottom - center.brRadiusY * 2.0,
center.brRadiusX * 2.0,
center.brRadiusY * 2.0,
);
final Rect blCorner = new Rect.fromLTWH(
center.left,
center.bottom - center.brRadiusY * 2.0,
center.blRadiusX * 2.0,
center.blRadiusY * 2.0,
);
const double cornerArcSweep = math.pi / 2.0;
final double tlCornerArcSweep = start < center.tlRadiusX
? math.asin(start / center.tlRadiusX)
: math.pi / 2.0;
final Path path = new Path()
..addArc(tlCorner, math.pi, tlCornerArcSweep)
..moveTo(center.left + center.tlRadiusX, center.top);
if (start > center.tlRadiusX)
path.lineTo(center.left + start, center.top);
const double trCornerArcStart = (3 * math.pi) / 2.0;
const double trCornerArcSweep = cornerArcSweep;
if (start + extent < center.width - center.trRadiusX) {
path
..relativeMoveTo(extent, 0.0)
..lineTo(center.right - center.trRadiusX, center.top)
..addArc(trCorner, trCornerArcStart, trCornerArcSweep);
} else if (start + extent < center.width) {
final double dx = center.width - (start + extent);
final double sweep = math.acos(dx / center.trRadiusX);
path.addArc(trCorner, trCornerArcStart + sweep, trCornerArcSweep - sweep);
}
return path
..moveTo(center.right, center.top + center.trRadiusY)
..lineTo(center.right, center.bottom - center.brRadiusY)
..addArc(brCorner, 0.0, cornerArcSweep)
..lineTo(center.left + center.blRadiusX, center.bottom)
..addArc(blCorner, math.pi / 2.0, cornerArcSweep)
..lineTo(center.left, center.top + center.trRadiusY);
}
/// 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(gapExtent != null);
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);
switch (textDirection) {
case TextDirection.rtl: {
final Path path = _gapBorderPath(canvas, center, gapStart + gapPadding - extent, extent);
canvas.drawPath(path, paint);
break;
}
case TextDirection.ltr: {
final Path path = _gapBorderPath(canvas, center, gapStart - gapPadding, extent);
canvas.drawPath(path, paint);
break;
}
}
}
}
@override
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (runtimeType != other.runtimeType)
return false;
final OutlineInputBorder typedOther = other;
return typedOther.borderSide == borderSide
&& typedOther.borderRadius == borderRadius
&& typedOther.gapPadding == gapPadding;
}
@override
int get hashCode => hashValues(borderSide, borderRadius, gapPadding);
}