Commit 53194ed4 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

ShapeBorder (#12335)

This class lays the groundwork for RTL borders.
parent 437e4c08
......@@ -259,6 +259,318 @@ class BorderSide {
String toString() => '$runtimeType($color, ${width.toStringAsFixed(1)}, $style)';
}
/// Base class for shape outlines.
///
/// This class handles how to add multiple borders together.
@immutable
abstract class ShapeBorder {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const ShapeBorder();
/// The widths of the sides of this border represented as an [EdgeInsets].
///
/// Specifically, this is the amount by which a rectangle should be inset so
/// as to avoid painting over any important part of the border. It is the
/// amount by which additional borders will be inset before they are drawn.
///
/// This can be used, for example, with a [Padding] widget to inset a box by
/// the size of these borders.
///
/// Shapes that have a fixed ratio regardless of the area on which they are
/// painted, or that change their rendering based on the size they are given
/// when painting (for instance [CircleBorder]), will not return valid
/// [dimensions] information because they cannot know their eventual size when
/// computing their [dimensions].
EdgeInsetsGeometry get dimensions;
/// Attempts to create a new object that represents the amalgamation of [this]
/// border and the `other` border.
///
/// If the type of the other border isn't known, or the given instance cannot
/// be reasonably added to this instance, then this should return null.
///
/// This method is used by the [operator +] implementation.
///
/// The `reversed` argument is true if this object was the right operand of
/// the `+` operator, and false if it was the left operand.
@protected
ShapeBorder add(ShapeBorder other, { bool reversed: false }) => null;
/// Creates a new border consisting of the two borders on either side of the
/// operator.
///
/// If the borders belong to classes that know how to add themselves, then
/// this results in a new border that represents the intelligent addition of
/// those two borders (see [add]). Otherwise, an object is returned that
/// merely paints the two borders sequentially, with the left hand operand on
/// the inside and the right hand operand on the outside.
ShapeBorder operator +(ShapeBorder other) {
return add(other) ?? other.add(this, reversed: true) ?? new _CompoundBorder(<ShapeBorder>[other, this]);
}
/// Creates a new border with the widths of this border multiplied by `t`.
ShapeBorder scale(double t);
/// Linearly interpolates from `a` to [this].
///
/// When implementing this method in subclasses, return null if this class
/// cannot interpolate from `a`. In that case, [lerp] will try `a`'s [lerpTo]
/// method instead. If `a` is null, this must not return null.
///
/// The base class implementation handles the case of `a` being null by
/// deferring to [scale].
///
/// Instead of calling this directly, use [ShapeBorder.lerp].
@protected
ShapeBorder lerpFrom(ShapeBorder a, double t) {
if (a == null)
return scale(t);
return null;
}
/// Linearly interpolates from [this] to `b`.
///
/// This is called if `b`'s [lerpTo] did not know how to handle this class.
///
/// When implementing this method in subclasses, return null if this class
/// cannot interpolate from `b`. In that case, [lerp] will apply a default
/// behavior instead. If `b` is null, this must not return null.
///
/// The base class implementation handles the case of `b` being null by
/// deferring to [scale].
///
/// Instead of calling this directly, use [ShapeBorder.lerp].
@protected
ShapeBorder lerpTo(ShapeBorder b, double t) {
if (b == null)
return scale(1.0 - t);
return null;
}
/// Linearly interpolates from `begin` to `end`.
///
/// This defers to `end`'s [lerpTo] function if `end` is not null. If `end` is
/// null or if its [lerpTo] returns null, it uses `begin`'s [lerpFrom]
/// function instead. If both return null, it returns `begin` before `t=0.5`
/// and `end` after `t=0.5`.
static ShapeBorder lerp(ShapeBorder begin, ShapeBorder end, double t) {
ShapeBorder result;
if (end != null)
result = end.lerpFrom(begin, t);
if (result == null && begin != null)
result = begin.lerpTo(end, t);
return result ?? (t < 0.5 ? begin : end);
}
/// Create a [Path] that describes the outer edge of the border.
///
/// This path must not cross the path given by [getInnerPath] for the same
/// [Rect].
///
/// To obtain a [Path] that describes the area of the border itself, set the
/// [Path.fillType] of the returned object to [PathFillType.evenOdd], and add
/// to this object the path returned from [getInnerPath] (using
/// [Path.addPath]).
///
/// The `textDirection` argument must be provided non-null if the border
/// has a text direction dependency (for example if it is expressed in terms
/// of "start" and "end" instead of "left" and "right"). It may be null if
/// the border will not need the text direction to paint itself.
///
/// See also:
///
/// * [getInnerPath], which creates the path for the inner edge.
/// * [Path.contains], which can tell if an [Offset] is within a [Path].
Path getOuterPath(Rect rect, { TextDirection textDirection });
/// Create a [Path] that describes the inner edge of the border.
///
/// This path must not cross the path given by [getOuterPath] for the same
/// [Rect].
///
/// To obtain a [Path] that describes the area of the border itself, set the
/// [Path.fillType] of the returned object to [PathFillType.evenOdd], and add
/// to this object the path returned from [getOuterPath] (using
/// [Path.addPath]).
///
/// The `textDirection` argument must be provided and non-null if the border
/// has a text direction dependency (for example if it is expressed in terms
/// of "start" and "end" instead of "left" and "right"). It may be null if
/// the border will not need the text direction to paint itself.
///
/// See also:
///
/// * [getOuterPath], which creates the path for the outer edge.
/// * [Path.contains], which can tell if an [Offset] is within a [Path].
Path getInnerPath(Rect rect, { TextDirection textDirection });
/// Paints the border within the given [Rect] on the given [Canvas].
///
/// The `textDirection` argument must be provided and non-null if the border
/// has a text direction dependency (for example if it is expressed in terms
/// of "start" and "end" instead of "left" and "right"). It may be null if
/// the border will not need the text direction to paint itself.
void paint(Canvas canvas, Rect rect, { TextDirection textDirection });
@override
String toString() {
return '$runtimeType()';
}
}
/// Represents the addition of two otherwise-incompatible borders.
///
/// The borders are listed from the outside to the inside.
class _CompoundBorder extends ShapeBorder {
_CompoundBorder(this.borders) {
assert(borders != null);
assert(borders.length >= 2);
assert(!borders.any((ShapeBorder border) => border is _CompoundBorder));
}
final List<ShapeBorder> borders;
@override
EdgeInsetsGeometry get dimensions {
return borders.fold<EdgeInsetsGeometry>(
EdgeInsets.zero,
(EdgeInsetsGeometry previousValue, ShapeBorder border) {
return previousValue.add(border.dimensions);
},
);
}
@override
ShapeBorder add(ShapeBorder other, { bool reversed: false }) {
// This wraps the list of borders with "other", or, if "reversed" is true,
// wraps "other" with the list of borders.
// If "reversed" is false, "other" should end up being at the start of the
// list, otherwise, if "reversed" is true, it should end up at the end.
// First, see if we can merge the new adjacent borders.
if (other is! _CompoundBorder) {
// Here, "ours" is the border at the side where we're adding the new
// border, and "merged" is the result of attempting to merge it with the
// new border. If it's null, it couldn't be merged.
final ShapeBorder ours = reversed ? borders.last : borders.first;
final ShapeBorder merged = ours.add(other, reversed: reversed)
?? other.add(ours, reversed: !reversed);
if (merged != null) {
final List<ShapeBorder> result = <ShapeBorder>[];
result.addAll(borders);
result[reversed ? result.length - 1 : 0] = merged;
return new _CompoundBorder(result);
}
}
// We can't, so fall back to just adding the new border to the list.
final List<ShapeBorder> mergedBorders = <ShapeBorder>[];
if (reversed)
mergedBorders.addAll(borders);
if (other is _CompoundBorder)
mergedBorders.addAll(other.borders);
else
mergedBorders.add(other);
if (!reversed)
mergedBorders.addAll(borders);
return new _CompoundBorder(mergedBorders);
}
@override
ShapeBorder scale(double t) {
return new _CompoundBorder(
borders.map<ShapeBorder>((ShapeBorder border) => border.scale(t)).toList()
);
}
@override
ShapeBorder lerpFrom(ShapeBorder a, double t) {
return _CompoundBorder.lerp(a, this, t);
}
@override
ShapeBorder lerpTo(ShapeBorder b, double t) {
return _CompoundBorder.lerp(this, b, t);
}
static _CompoundBorder lerp(ShapeBorder a, ShapeBorder b, double t) {
assert(a is _CompoundBorder || b is _CompoundBorder); // Not really necessary, but all call sites currently intend this.
final List<ShapeBorder> aList = a is _CompoundBorder ? a.borders : <ShapeBorder>[a];
final List<ShapeBorder> bList = b is _CompoundBorder ? b.borders : <ShapeBorder>[b];
final List<ShapeBorder> results = <ShapeBorder>[];
final int length = math.max(aList.length, bList.length);
for (int index = 0; index < length; index += 1) {
final ShapeBorder localA = index < aList.length ? aList[index] : null;
final ShapeBorder localB = index < bList.length ? bList[index] : null;
if (localA != null && localB != null) {
final ShapeBorder localResult = localA.lerpTo(localB, t) ?? localB.lerpFrom(localA, t);
if (localResult != null) {
results.add(localResult);
continue;
}
}
// If we're changing from one shape to another, make sure the shape that is coming in
// is inserted before the shape that is going away, so that the outer path changes to
// the new border earlier rather than later. (This affects, among other things, where
// the ShapeDecoration class puts its background.)
if (localB != null)
results.add(localB.scale(t));
if (localA != null)
results.add(localA.scale(1.0 - t));
}
return new _CompoundBorder(results);
}
@override
Path getInnerPath(Rect rect, { TextDirection textDirection }) {
for (int index = 0; index < borders.length - 1; index += 1)
rect = borders[index].dimensions.resolve(textDirection).deflateRect(rect);
return borders.last.getInnerPath(rect);
}
@override
Path getOuterPath(Rect rect, { TextDirection textDirection }) {
return borders.first.getOuterPath(rect);
}
@override
void paint(Canvas canvas, Rect rect, { TextDirection textDirection }) {
for (ShapeBorder border in borders) {
border.paint(canvas, rect, textDirection: textDirection);
rect = border.dimensions.resolve(textDirection).deflateRect(rect);
}
}
@override
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (runtimeType != other.runtimeType)
return false;
final _CompoundBorder typedOther = other;
if (borders == typedOther.borders)
return true;
if (borders.length != typedOther.borders.length)
return false;
for (int index = 0; index < borders.length; index += 1) {
if (borders[index] != typedOther.borders[index])
return false;
}
return true;
}
@override
int get hashCode => hashList(borders);
@override
String toString() {
// We list them in reverse order because when adding two borders they end up
// in the list in the opposite order of what the source looks like: a + b =>
// [b, a]. We do this to make the painting code more optimal, and most of
// the rest of the code doesn't care, except toString() (for debugging).
return borders.reversed.map<String>((ShapeBorder border) => border.toString()).join(' + ');
}
}
/// A border of a box, comprised of four sides.
///
/// The sides are represented by [BorderSide] objects.
......@@ -315,8 +627,7 @@ class BorderSide {
/// * [BorderSide], which is used to describe each side of the box.
/// * [Theme], from the material layer, which can be queried to obtain appropriate colors
/// to use for borders in a material app, as shown in the "divider" sample above.
@immutable
class Border {
class Border extends ShapeBorder {
/// Creates a border.
///
/// All the sides of the border default to [BorderSide.none].
......@@ -373,10 +684,7 @@ class Border {
/// The left side of this border.
final BorderSide left;
/// The widths of the sides of this border represented as an [EdgeInsets].
///
/// This can be used, for example, with a [Padding] widget to inset a box by
/// the size of these borders.
@override
EdgeInsetsGeometry get dimensions {
return new EdgeInsets.fromLTRB(left.width, top.width, right.width, bottom.width);
}
......@@ -410,7 +718,11 @@ class Border {
return true;
}
Border add(Border typedOther) {
@override
Border add(ShapeBorder other, { bool reversed: false }) {
if (other is! Border)
return null;
final Border typedOther = other;
if (BorderSide.canMerge(top, typedOther.top) &&
BorderSide.canMerge(right, typedOther.right) &&
BorderSide.canMerge(bottom, typedOther.bottom) &&
......@@ -421,6 +733,7 @@ class Border {
}
/// Creates a new border with the widths of this border multiplied by `t`.
@override
Border scale(double t) {
return new Border(
top: top.scale(t),
......@@ -430,6 +743,34 @@ class Border {
);
}
/// Linearly interpolates from `a` to [this].
///
/// If `a` is null, this defers to [scale].
///
/// If `a` is also a [Border], this uses [Border.lerp].
///
/// Otherwise, it defers to [ShapeBorder.lerpFrom].
@override
ShapeBorder lerpFrom(ShapeBorder a, double t) {
if (a is Border)
return Border.lerp(a, this, t);
return super.lerpFrom(a, t);
}
/// Linearly interpolates from [this] to `b`.
///
/// If `b` is null, this defers to [scale].
///
/// If `b` is also a [Border], this uses [Border.lerp].
///
/// Otherwise, it defers to [ShapeBorder.lerpTo].
@override
ShapeBorder lerpTo(ShapeBorder b, double t) {
if (b is Border)
return Border.lerp(this, b, t);
return super.lerpTo(b, t);
}
/// Linearly interpolate between two borders.
///
/// If a border is null, it is treated as having four [BorderSide.none]
......@@ -449,6 +790,18 @@ class Border {
);
}
@override
Path getInnerPath(Rect rect, { TextDirection textDirection }) {
return new Path()
..addRect(dimensions.resolve(textDirection).deflateRect(rect));
}
@override
Path getOuterPath(Rect rect, { TextDirection textDirection }) {
return new Path()
..addRect(rect);
}
/// Paints the border within the given [Rect] on the given [Canvas].
///
/// Uniform borders are more efficient to paint than more complex borders.
......@@ -460,10 +813,17 @@ class Border {
/// may specify a [BorderRadius]. If a `borderRadius` is specified, there is
/// the requirement that the border [isUniform].
///
/// The [getInnerPath] and [getOuterPath] methods do not know about the
/// `shape` and `borderRadius` arguments.
///
/// The `textDirection` argument is not used by this paint method.
///
/// See also:
///
/// * [paintBorder], which is used if the border is not uniform.
@override
void paint(Canvas canvas, Rect rect, {
TextDirection textDirection,
BoxShape shape: BoxShape.rectangle,
BorderRadius borderRadius,
}) {
......
......@@ -51,34 +51,34 @@ void main() {
final BorderSide yellow2 = const BorderSide(color: const Color(0xFFFFFF00), width: 2.0);
final BorderSide yellowNone0 = const BorderSide(color: const Color(0xFFFFFF00), width: 0.0, style: BorderStyle.none);
expect(
new Border(top: yellow2).add(new Border(right: magenta3)),
new Border(top: yellow2) + new Border(right: magenta3),
new Border(top: yellow2, right: magenta3),
);
expect(
new Border(bottom: magenta3).add(new Border(bottom: magenta3)),
new Border(bottom: magenta3) + new Border(bottom: magenta3),
new Border(bottom: magenta6),
);
expect(
new Border(left: magenta3, right: yellowNone0).add(new Border(right: yellow2)),
new Border(left: magenta3, right: yellowNone0) + new Border(right: yellow2),
new Border(left: magenta3, right: yellow2),
);
expect(
const Border().add(const Border()),
const Border() + const Border(),
const Border(),
);
expect(
new Border(left: magenta3).add(new Border(left: yellow2)),
isNull,
new Border(left: magenta3) + new Border(left: yellow2),
isNot(const isInstanceOf<Border>()), // see shape_border_test.dart for better tests of this case
);
final Border b3 = new Border(top: magenta3);
final Border b6 = new Border(top: magenta6);
expect(b3.add(b3), b6);
expect(b3 + b3, b6);
final Border b0 = new Border(top: yellowNone0);
final Border bZ = const Border();
expect(b0.add(b0), bZ);
expect(bZ.add(bZ), bZ);
expect(b0.add(bZ), bZ);
expect(bZ.add(b0), bZ);
expect(b0 + b0, bZ);
expect(bZ + bZ, bZ);
expect(b0 + bZ, bZ);
expect(bZ + b0, bZ);
});
test('Border.scale', () {
......
// 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 'package:flutter/painting.dart';
import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
void main() {
test('Compound borders', () {
final Border b1 = new Border.all(color: const Color(0xFF00FF00));
final Border b2 = new Border.all(color: const Color(0xFF0000FF));
expect(
(b1 + b2).toString(),
'Border.all(BorderSide(Color(0xff00ff00), 1.0, BorderStyle.solid)) + '
'Border.all(BorderSide(Color(0xff0000ff), 1.0, BorderStyle.solid))',
);
expect(
(b1 + (b2 + b2)).toString(),
'Border.all(BorderSide(Color(0xff00ff00), 1.0, BorderStyle.solid)) + '
'Border.all(BorderSide(Color(0xff0000ff), 2.0, BorderStyle.solid))',
);
expect(
((b1 + b2) + b2).toString(),
'Border.all(BorderSide(Color(0xff00ff00), 1.0, BorderStyle.solid)) + '
'Border.all(BorderSide(Color(0xff0000ff), 2.0, BorderStyle.solid))',
);
expect((b1 + b2) + b2, b1 + (b2 + b2));
expect(
(b1 + b2).scale(3.0).toString(),
'Border.all(BorderSide(Color(0xff00ff00), 3.0, BorderStyle.solid)) + '
'Border.all(BorderSide(Color(0xff0000ff), 3.0, BorderStyle.solid))',
);
expect(
(b1 + b2).scale(0.0).toString(),
'Border.all(BorderSide(Color(0xff00ff00), 0.0, BorderStyle.none)) + '
'Border.all(BorderSide(Color(0xff0000ff), 0.0, BorderStyle.none))',
);
expect(
ShapeBorder.lerp(b2 + b1, b1 + b2, 0.0).toString(),
'Border.all(BorderSide(Color(0xff0000ff), 1.0, BorderStyle.solid)) + '
'Border.all(BorderSide(Color(0xff00ff00), 1.0, BorderStyle.solid))',
);
expect(
ShapeBorder.lerp(b2 + b1, b1 + b2, 0.25).toString(),
'Border.all(BorderSide(Color(0xff003fbf), 1.0, BorderStyle.solid)) + '
'Border.all(BorderSide(Color(0xff00bf3f), 1.0, BorderStyle.solid))',
);
expect(
ShapeBorder.lerp(b2 + b1, b1 + b2, 0.5).toString(),
'Border.all(BorderSide(Color(0xff007f7f), 1.0, BorderStyle.solid)) + '
'Border.all(BorderSide(Color(0xff007f7f), 1.0, BorderStyle.solid))',
);
expect(
ShapeBorder.lerp(b2 + b1, b1 + b2, 1.0).toString(),
'Border.all(BorderSide(Color(0xff00ff00), 1.0, BorderStyle.solid)) + '
'Border.all(BorderSide(Color(0xff0000ff), 1.0, BorderStyle.solid))'
);
expect((b1 + b2).dimensions, const EdgeInsets.all(2.0));
final Rect rect = new Rect.fromLTRB(11.0, 15.0, 299.0, 175.0);
expect((Canvas canvas) => (b1 + b2).paint(canvas, rect), paints
..rect(rect: rect.deflate(0.5), color: b2.top.color)
..rect(rect: rect.deflate(1.5), color: b1.top.color)
);
expect((b1 + b2 + b1).dimensions, const EdgeInsets.all(3.0));
expect((Canvas canvas) => (b1 + b2 + b1).paint(canvas, rect), paints
..rect(rect: rect.deflate(0.5), color: b1.top.color)
..rect(rect: rect.deflate(1.5), color: b2.top.color)
..rect(rect: rect.deflate(2.5), color: b1.top.color)
);
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment