Unverified Commit 07de84f0 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Add StarBorder and StarBorder.polygon, with example. (#108489)

parent e79e6d72
......@@ -412,7 +412,7 @@ Future<void> verifyGoldenTags(String workingDirectory, { int minimumMatches = 20
if (needsTag) {
if (!hasTagNotation) {
errors.add('${file.path}: Files containing golden tests must be tagged using '
'`@Tags(...)` at the top of the file before import statements.');
"@Tags(<String>['reduced-test-set']) at the top of the file before import statements.");
} else if (!hasReducedTag) {
errors.add('${file.path}: Files containing golden tests must be tagged with '
"'reduced-test-set'.");
......
......@@ -74,10 +74,9 @@ void main() {
test('analyze.dart - verifyGoldenTags', () async {
final String result = await capture(() => verifyGoldenTags(testRootPath, minimumMatches: 6), exitCode: 1);
const String noTag = 'Files containing golden tests must be '
'tagged using `@Tags(...)` at the top of the file before import statements.';
const String missingTag = 'Files containing golden tests must be '
"tagged with 'reduced-test-set'.";
const String noTag = "Files containing golden tests must be tagged using @Tags(<String>['reduced-test-set']) "
'at the top of the file before import statements.';
const String missingTag = "Files containing golden tests must be tagged with 'reduced-test-set'.";
String lines = <String>[
'test/analyze-test-input/root/packages/foo/golden_missing_tag.dart: $missingTag',
'test/analyze-test-input/root/packages/foo/golden_no_tag.dart: $noTag',
......
// 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 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
enum LerpTarget {
circle,
roundedRect,
rect,
stadium,
polygon,
star,
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
static final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
final OptionModel _model = OptionModel();
final TextEditingController textController = TextEditingController();
@override
void initState() {
super.initState();
_model.addListener(_modelChanged);
}
@override
void dispose() {
super.dispose();
_model.removeListener(_modelChanged);
}
void _modelChanged() {
setState(() {});
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
key: scaffoldKey,
appBar: AppBar(
title: const Text('Star Border'),
backgroundColor: const Color(0xff323232),
),
body: Column(
children: <Widget>[
Container(color: Colors.grey.shade200, child: Options(_model)),
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Container(
key: UniqueKey(),
alignment: Alignment.center,
width: 300,
height: 200,
decoration: ShapeDecoration(
color: Colors.blue.shade100,
shape: lerpBorder(
StarBorder.polygon(
side: const BorderSide(strokeAlign: StrokeAlign.center, width: 2),
sides: _model.points,
pointRounding: _model.pointRounding,
rotation: _model.rotation,
squash: _model.squash,
),
_model._lerpTarget,
_model._lerpAmount,
to: _model.lerpTo,
)!,
),
child: const Text('Polygon'),
),
Container(
key: UniqueKey(),
alignment: Alignment.center,
width: 300,
height: 200,
decoration: ShapeDecoration(
color: Colors.blue.shade100,
shape: lerpBorder(
StarBorder(
side: const BorderSide(strokeAlign: StrokeAlign.center, width: 2),
points: _model.points,
innerRadiusRatio: _model.innerRadiusRatio,
pointRounding: _model.pointRounding,
valleyRounding: _model.valleyRounding,
rotation: _model.rotation,
squash: _model.squash,
),
_model._lerpTarget,
_model._lerpAmount,
to: _model.lerpTo,
)!,
),
child: const Text('Star'),
),
],
),
),
],
),
),
);
}
}
class OptionModel extends ChangeNotifier {
double get pointRounding => _pointRounding;
double _pointRounding = 0.0;
set pointRounding(double value) {
if (value != _pointRounding) {
_pointRounding = value;
if (_valleyRounding + _pointRounding > 1) {
_valleyRounding = 1.0 - _pointRounding;
}
notifyListeners();
}
}
double get valleyRounding => _valleyRounding;
double _valleyRounding = 0.0;
set valleyRounding(double value) {
if (value != _valleyRounding) {
_valleyRounding = value;
if (_valleyRounding + _pointRounding > 1) {
_pointRounding = 1.0 - _valleyRounding;
}
notifyListeners();
}
}
double get squash => _squash;
double _squash = 0.0;
set squash(double value) {
if (value != _squash) {
_squash = value;
notifyListeners();
}
}
double get rotation => _rotation;
double _rotation = 0.0;
set rotation(double value) {
if (value != _rotation) {
_rotation = value;
notifyListeners();
}
}
double get innerRadiusRatio => _innerRadiusRatio;
double _innerRadiusRatio = 0.4;
set innerRadiusRatio(double value) {
if (value != _innerRadiusRatio) {
_innerRadiusRatio = clampDouble(value, 0.0001, double.infinity);
notifyListeners();
}
}
double get points => _points;
double _points = 5;
set points(double value) {
if (value != _points) {
_points = value;
notifyListeners();
}
}
double get lerpAmount => _lerpAmount;
double _lerpAmount = 0.0;
set lerpAmount(double value) {
if (value != _lerpAmount) {
_lerpAmount = value;
notifyListeners();
}
}
bool get lerpTo => _lerpTo;
bool _lerpTo = true;
set lerpTo(bool value) {
if (_lerpTo != value) {
_lerpTo = value;
notifyListeners();
}
}
LerpTarget get lerpTarget => _lerpTarget;
LerpTarget _lerpTarget = LerpTarget.circle;
set lerpTarget(LerpTarget value) {
if (value != _lerpTarget) {
_lerpTarget = value;
notifyListeners();
}
}
void reset() {
final OptionModel defaultModel = OptionModel();
_pointRounding = defaultModel.pointRounding;
_valleyRounding = defaultModel.valleyRounding;
_rotation = defaultModel.rotation;
_squash = defaultModel.squash;
_lerpAmount = defaultModel.lerpAmount;
_lerpTo = defaultModel.lerpTo;
_lerpTarget = defaultModel.lerpTarget;
_innerRadiusRatio = defaultModel._innerRadiusRatio;
_points = defaultModel.points;
notifyListeners();
}
}
class LabeledCheckbox extends StatelessWidget {
const LabeledCheckbox({super.key, required this.label, this.onChanged, this.value});
final String label;
final ValueChanged<bool?>? onChanged;
final bool? value;
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Checkbox(
onChanged: onChanged,
value: value,
),
Text(label),
],
);
}
}
class Options extends StatefulWidget {
const Options(this.model, {super.key});
final OptionModel model;
@override
State<Options> createState() => _OptionsState();
}
class _OptionsState extends State<Options> {
@override
void initState() {
super.initState();
widget.model.addListener(_modelChanged);
}
@override
void didUpdateWidget(Options oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.model != oldWidget.model) {
oldWidget.model.removeListener(_modelChanged);
widget.model.addListener(_modelChanged);
}
}
@override
void dispose() {
super.dispose();
widget.model.removeListener(_modelChanged);
}
void _modelChanged() {
setState(() {});
}
double sliderValue = 0.0;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(5.0, 0.0, 5.0, 10.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Row(
children: <Widget>[
Expanded(
child: ControlSlider(
label: 'Point Rounding',
value: widget.model.pointRounding,
onChanged: (double value) {
widget.model.pointRounding = value;
},
),
),
Expanded(
child: ControlSlider(
label: 'Valley Rounding',
value: widget.model.valleyRounding,
onChanged: (double value) {
widget.model.valleyRounding = value;
},
),
),
],
),
Row(
children: <Widget>[
Expanded(
child: ControlSlider(
label: 'Squash',
value: widget.model.squash,
onChanged: (double value) {
widget.model.squash = value;
},
),
),
Expanded(
child: ControlSlider(
label: 'Rotation',
value: widget.model.rotation,
max: 360,
onChanged: (double value) {
widget.model.rotation = value;
},
),
),
],
),
Row(
children: <Widget>[
Expanded(
child: Row(
children: <Widget>[
Expanded(
child: ControlSlider(
label: 'Points',
value: widget.model.points,
min: 2,
max: 20,
onChanged: (double value) {
widget.model.points = value;
},
),
),
OutlinedButton(
child: const Text('Nearest'),
onPressed: () {
widget.model.points = widget.model.points.roundToDouble();
}),
],
),
),
Expanded(
child: ControlSlider(
label: 'Inner Radius',
value: widget.model.innerRadiusRatio,
onChanged: (double value) {
widget.model.innerRadiusRatio = value;
},
),
),
],
),
Row(
children: <Widget>[
Expanded(
flex: 2,
child: Padding(
padding: const EdgeInsetsDirectional.only(end: 8.0),
child: ControlSlider(
label: 'Lerp',
value: widget.model.lerpAmount,
onChanged: (double value) {
widget.model.lerpAmount = value;
},
),
),
),
Padding(
padding: const EdgeInsetsDirectional.only(start: 8.0, end: 20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(children: <Widget>[
Radio<bool>(
value: true,
groupValue: widget.model.lerpTo,
onChanged: (bool? value) {
widget.model.lerpTo = value!;
}),
const Text('To'),
]),
Row(children: <Widget>[
Radio<bool>(
value: false,
groupValue: widget.model.lerpTo,
onChanged: (bool? value) {
widget.model.lerpTo = value!;
}),
const Text('From'),
])
],
),
),
Expanded(
child: Row(
children: <Widget>[
Expanded(
child: DropdownButton<LerpTarget>(
items: LerpTarget.values.map<DropdownMenuItem<LerpTarget>>((LerpTarget target) {
return DropdownMenuItem<LerpTarget>(value: target, child: Text(target.name));
}).toList(),
value: widget.model.lerpTarget,
onChanged: (LerpTarget? value) {
if (value == null) {
return;
}
widget.model.lerpTarget = value;
},
),
),
],
),
),
],
),
ElevatedButton(
onPressed: () {
widget.model.reset();
sliderValue = 0.0;
},
child: const Text('Reset'),
),
],
),
);
}
}
class ControlSlider extends StatelessWidget {
const ControlSlider({
super.key,
required this.label,
required this.value,
required this.onChanged,
this.min = 0.0,
this.max = 1.0,
});
final String label;
final double value;
final void Function(double value) onChanged;
final double min;
final double max;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(4.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(label),
Expanded(
child: Slider(
label: value.toStringAsFixed(1),
onChanged: onChanged,
min: min,
max: max,
value: value,
),
),
Text(
value.toStringAsFixed(3),
),
],
),
);
}
}
const Color lerpToColor = Colors.red;
const BorderSide lerpToBorder = BorderSide(width: 5, color: lerpToColor);
ShapeBorder? lerpBorder(StarBorder border, LerpTarget target, double t, {bool to = true}) {
switch (target) {
case LerpTarget.circle:
if (to) {
return border.lerpTo(const CircleBorder(side: lerpToBorder), t);
} else {
return border.lerpFrom(const CircleBorder(side: lerpToBorder), t);
}
case LerpTarget.roundedRect:
if (to) {
return border.lerpTo(
const RoundedRectangleBorder(
side: lerpToBorder,
borderRadius: BorderRadius.all(
Radius.circular(10),
),
),
t,
);
} else {
return border.lerpFrom(
const RoundedRectangleBorder(
side: lerpToBorder,
borderRadius: BorderRadius.all(
Radius.circular(10),
),
),
t,
);
}
case LerpTarget.rect:
if (to) {
return border.lerpTo(const RoundedRectangleBorder(side: lerpToBorder), t);
} else {
return border.lerpFrom(const RoundedRectangleBorder(side: lerpToBorder), t);
}
case LerpTarget.stadium:
if (to) {
return border.lerpTo(const StadiumBorder(side: lerpToBorder), t);
} else {
return border.lerpFrom(const StadiumBorder(side: lerpToBorder), t);
}
case LerpTarget.polygon:
if (to) {
return border.lerpTo(const StarBorder.polygon(side: lerpToBorder, sides: 4), t);
} else {
return border.lerpFrom(const StarBorder.polygon(side: lerpToBorder, sides: 4), t);
}
case LerpTarget.star:
if (to) {
return border.lerpTo(const StarBorder(side: lerpToBorder, innerRadiusRatio: .5), t);
} else {
return border.lerpFrom(const StarBorder(side: lerpToBorder, innerRadiusRatio: .5), t);
}
}
}
// 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.
/// An example showing usage of [StarBorder].
import 'package:flutter/material.dart';
const int _kParameterPrecision = 2;
void main() => runApp(const StarBorderApp());
class StarBorderApp extends StatelessWidget {
const StarBorderApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('StarBorder Example'),
backgroundColor: const Color(0xff323232),
),
body: const StarBorderExample(),
),
);
}
}
class StarBorderExample extends StatefulWidget {
const StarBorderExample({super.key});
@override
State<StarBorderExample> createState() => _StarBorderExampleState();
}
class _StarBorderExampleState extends State<StarBorderExample> {
final OptionModel _model = OptionModel();
final TextEditingController _textController = TextEditingController();
@override
void initState() {
super.initState();
_model.addListener(_modelChanged);
}
@override
void dispose() {
_model.removeListener(_modelChanged);
_textController.dispose();
super.dispose();
}
void _modelChanged() {
setState(() {});
}
@override
Widget build(BuildContext context) {
return DefaultTextStyle(
style: const TextStyle(
color: Colors.black,
fontSize: 14.0,
fontFamily: 'Roboto',
fontStyle: FontStyle.normal,
),
child: ListView(
children: <Widget>[
Container(
color: Colors.grey.shade200,
child: Options(_model),
),
Padding(
padding: const EdgeInsets.all(18.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Expanded(
child: ExampleBorder(
border: StarBorder(
side: const BorderSide(),
points: _model.points,
innerRadiusRatio: _model.innerRadiusRatio,
pointRounding: _model.pointRounding,
valleyRounding: _model.valleyRounding,
rotation: _model.rotation,
squash: _model.squash,
),
title: 'Star',
),
),
Expanded(
child: ExampleBorder(
border: StarBorder.polygon(
side: const BorderSide(),
sides: _model.points,
pointRounding: _model.pointRounding,
rotation: _model.rotation,
squash: _model.squash,
),
title: 'Polygon',
),
),
],
),
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Expanded(
child: Container(
color: Colors.black12,
margin: const EdgeInsets.all(16.0),
padding: const EdgeInsets.all(16.0),
child: SelectableText(_model.starCode),
),
),
Expanded(
child: Container(
color: Colors.black12,
margin: const EdgeInsets.all(16.0),
padding: const EdgeInsets.all(16.0),
child: SelectableText(_model.polygonCode),
),
),
],
),
],
),
);
}
}
class ExampleBorder extends StatelessWidget {
const ExampleBorder({
super.key,
required this.border,
required this.title,
});
final StarBorder border;
final String title;
@override
Widget build(BuildContext context) {
return Container(
key: UniqueKey(),
alignment: Alignment.center,
padding: const EdgeInsets.all(20),
width: 150,
height: 100,
decoration: ShapeDecoration(
color: Colors.blue.shade100,
shape: border,
),
child: Text(title),
);
}
}
class Options extends StatefulWidget {
const Options(this.model, {super.key});
final OptionModel model;
@override
State<Options> createState() => _OptionsState();
}
class _OptionsState extends State<Options> {
@override
void initState() {
super.initState();
widget.model.addListener(_modelChanged);
}
@override
void didUpdateWidget(Options oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.model != oldWidget.model) {
oldWidget.model.removeListener(_modelChanged);
widget.model.addListener(_modelChanged);
}
}
@override
void dispose() {
super.dispose();
widget.model.removeListener(_modelChanged);
}
void _modelChanged() {
setState(() {});
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(5.0, 0.0, 5.0, 10.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Row(
children: <Widget>[
Expanded(
child: ControlSlider(
label: 'Point Rounding',
value: widget.model.pointRounding,
onChanged: (double value) {
widget.model.pointRounding = value;
},
),
),
Expanded(
child: ControlSlider(
label: 'Valley Rounding',
value: widget.model.valleyRounding,
onChanged: (double value) {
widget.model.valleyRounding = value;
},
),
),
],
),
Row(
children: <Widget>[
Expanded(
child: ControlSlider(
label: 'Squash',
value: widget.model.squash,
onChanged: (double value) {
widget.model.squash = value;
},
),
),
Expanded(
child: ControlSlider(
label: 'Rotation',
value: widget.model.rotation,
max: 360,
onChanged: (double value) {
widget.model.rotation = value;
},
),
),
],
),
Row(
children: <Widget>[
Expanded(
child: Row(
children: <Widget>[
Expanded(
child: ControlSlider(
label: 'Points',
value: widget.model.points,
min: 3,
max: 20,
precision: 1,
onChanged: (double value) {
widget.model.points = value;
},
),
),
Tooltip(
message: 'Round the number of points to the nearest integer.',
child: Padding(
padding: const EdgeInsets.all(8.0),
child: OutlinedButton(
child: const Text('Nearest'),
onPressed: () {
widget.model.points = widget.model.points.roundToDouble();
},
),
),
),
],
),
),
Expanded(
child: ControlSlider(
label: 'Inner Radius',
value: widget.model.innerRadiusRatio,
onChanged: (double value) {
widget.model.innerRadiusRatio = value;
},
),
),
],
),
ElevatedButton(
onPressed: () {
widget.model.reset();
},
child: const Text('Reset'),
),
],
),
);
}
}
class OptionModel extends ChangeNotifier {
double get pointRounding => _pointRounding;
double _pointRounding = 0.0;
set pointRounding(double value) {
if (value != _pointRounding) {
_pointRounding = value;
if (_valleyRounding + _pointRounding > 1) {
_valleyRounding = 1.0 - _pointRounding;
}
notifyListeners();
}
}
double get valleyRounding => _valleyRounding;
double _valleyRounding = 0.0;
set valleyRounding(double value) {
if (value != _valleyRounding) {
_valleyRounding = value;
if (_valleyRounding + _pointRounding > 1) {
_pointRounding = 1.0 - _valleyRounding;
}
notifyListeners();
}
}
double get squash => _squash;
double _squash = 0.0;
set squash(double value) {
if (value != _squash) {
_squash = value;
notifyListeners();
}
}
double get rotation => _rotation;
double _rotation = 0.0;
set rotation(double value) {
if (value != _rotation) {
_rotation = value;
notifyListeners();
}
}
double get innerRadiusRatio => _innerRadiusRatio;
double _innerRadiusRatio = 0.4;
set innerRadiusRatio(double value) {
if (value != _innerRadiusRatio) {
_innerRadiusRatio = value.clamp(0.0001, double.infinity);
notifyListeners();
}
}
double get points => _points;
double _points = 5;
set points(double value) {
if (value != _points) {
_points = value;
notifyListeners();
}
}
String get starCode {
return 'Container(\n'
' decoration: ShapeDecoration(\n'
' shape: StarBorder(\n'
' points: ${points.toStringAsFixed(_kParameterPrecision)},\n'
' rotation: ${rotation.toStringAsFixed(_kParameterPrecision)},\n'
' innerRadiusRatio: ${innerRadiusRatio.toStringAsFixed(_kParameterPrecision)},\n'
' pointRounding: ${pointRounding.toStringAsFixed(_kParameterPrecision)},\n'
' valleyRounding: ${valleyRounding.toStringAsFixed(_kParameterPrecision)},\n'
' squash: ${squash.toStringAsFixed(_kParameterPrecision)},\n'
' ),\n'
' ),\n'
');';
}
String get polygonCode {
return 'Container(\n'
' decoration: ShapeDecoration(\n'
' shape: StarBorder.polygon(\n'
' sides: ${points.toStringAsFixed(_kParameterPrecision)},\n'
' rotation: ${rotation.toStringAsFixed(_kParameterPrecision)},\n'
' cornerRounding: ${pointRounding.toStringAsFixed(_kParameterPrecision)},\n'
' squash: ${squash.toStringAsFixed(_kParameterPrecision)},\n'
' ),\n'
' ),\n'
');';
}
void reset() {
final OptionModel defaultModel = OptionModel();
_pointRounding = defaultModel.pointRounding;
_valleyRounding = defaultModel.valleyRounding;
_rotation = defaultModel.rotation;
_squash = defaultModel.squash;
_innerRadiusRatio = defaultModel._innerRadiusRatio;
_points = defaultModel.points;
notifyListeners();
}
}
class ControlSlider extends StatelessWidget {
const ControlSlider({
super.key,
required this.label,
required this.value,
required this.onChanged,
this.min = 0.0,
this.max = 1.0,
this.precision = _kParameterPrecision,
});
final String label;
final double value;
final void Function(double value) onChanged;
final double min;
final double max;
final int precision;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(4.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Expanded(
flex: 2,
child: Text(
label,
textAlign: TextAlign.end,
),
),
Expanded(
flex: 5,
child: Slider(
onChanged: onChanged,
min: min,
max: max,
value: value,
),
),
Expanded(
child: Text(
value.toStringAsFixed(precision),
),
),
],
),
);
}
}
......@@ -56,6 +56,7 @@ export 'src/painting/rounded_rectangle_border.dart';
export 'src/painting/shader_warm_up.dart';
export 'src/painting/shape_decoration.dart';
export 'src/painting/stadium_border.dart';
export 'src/painting/star_border.dart';
export 'src/painting/strut_style.dart';
export 'src/painting/text_painter.dart';
export 'src/painting/text_span.dart';
......
......@@ -144,7 +144,8 @@ class Slider extends StatefulWidget {
assert(min != null),
assert(max != null),
assert(min <= max),
assert(value >= min && value <= max),
assert(value >= min && value <= max,
'Value $value is not between minimum $min and maximum $max'),
assert(divisions == null || divisions > 0);
/// Creates an adaptive [Slider] based on the target platform, following
......
// 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 'dart:ui' as ui show lerpDouble;
import 'package:flutter/foundation.dart';
import 'package:vector_math/vector_math_64.dart' show Matrix4;
import 'basic_types.dart';
import 'borders.dart';
import 'circle_border.dart';
import 'edge_insets.dart';
import 'rounded_rectangle_border.dart';
import 'stadium_border.dart';
// Conversion from radians to degrees.
const double _kRadToDeg = 180 / math.pi;
// Conversion from degrees to radians.
const double _kDegToRad = math.pi / 180;
/// A border that fits a star or polygon-shaped border within the rectangle of
/// the widget it is applied to.
///
/// Typically used with a [ShapeDecoration] to draw a polygonal or star shaped
/// border.
///
/// {@tool dartpad}
/// This example serves both as a usage example, as well as an explorer for
/// determining the parameters to use with a [StarBorder]. The resulting code
/// can be copied and pasted into your app. A [Container] is just one widget
/// which takes a [ShapeBorder]. [Dialog]s, [OutlinedButton]s,
/// [ElevatedButton]s, etc. all can be shaped with a [ShapeBorder].
///
/// ** See code in examples/api/lib/painting/star_border/star_border.0.dart **
/// {@end-tool}
///
/// See also:
///
/// * [BorderSide], which is used to describe how the edge of the shape is
/// drawn.
class StarBorder extends OutlinedBorder {
/// Create a const star-shaped border with the given number [points] on the
/// star.
const StarBorder({
super.side,
this.points = 5,
double innerRadiusRatio = 0.4,
this.pointRounding = 0,
this.valleyRounding = 0,
double rotation = 0,
this.squash = 0,
}) : assert(squash >= 0),
assert(squash <= 1),
assert(pointRounding >= 0),
assert(pointRounding <= 1),
assert(valleyRounding >= 0),
assert(valleyRounding <= 1),
assert(
(valleyRounding + pointRounding) <= 1,
'The sum of valleyRounding ($valleyRounding) and '
'pointRounding ($pointRounding) must not exceed one.'),
assert(innerRadiusRatio >= 0),
assert(innerRadiusRatio <= 1),
assert(points >= 2),
_rotationRadians = rotation * _kDegToRad,
_innerRadiusRatio = innerRadiusRatio;
/// Create a const polygon border with the given number of [sides].
const StarBorder.polygon({
super.side,
double sides = 5,
this.pointRounding = 0,
double rotation = 0,
this.squash = 0,
}) : assert(squash >= 0),
assert(squash <= 1),
assert(pointRounding >= 0),
assert(pointRounding <= 1),
assert(sides >= 2),
points = sides,
valleyRounding = 0,
_rotationRadians = rotation * _kDegToRad,
_innerRadiusRatio = null;
/// The number of points in this star, or sides on a polygon.
///
/// This is a floating point number: if this is not a whole number, then an
/// additional star point or corner shorter than the others will be added to
/// finish the shape. Only whole-numbered values will yield a symmetric shape.
///
/// For stars created with [StarBorder], this the number of points on
/// the star. For polygons created with [StarBorder.polygon], this is the
/// number of sides on the polygon.
///
/// Must be greater than or equal to two.
final double points;
/// The ratio of the inner radius of a star with the outer radius.
///
/// When making a star using [StarBorder], this is the ratio of the inner
/// radius that to the outer radius. If it is one, then the inner radius
/// will equal the outer radius.
///
/// For polygons created with [StarBorder.polygon], getting this value will
/// return the incircle radius of the polygon (the radius of a circle
/// inscribed inside the polygon).
///
/// Defaults to 0.4 for stars, and must be between zero and one, inclusive.
double get innerRadiusRatio {
// Polygons are just a special case of a star where the inner radius is the
// incircle radius of the polygon (the radius of an inscribed circle).
return _innerRadiusRatio ?? math.cos(math.pi / points);
}
final double? _innerRadiusRatio;
/// The amount of rounding on the points of stars, or the corners of polygons.
///
/// This is a value between zero and one which describes how rounded the point
/// or corner should be. A value of zero means no rounding (sharp corners),
/// and a value of one means that the entire point or corner is a portion of a
/// circle.
///
/// Defaults to zero. The sum of `pointRounding` and [valleyRounding] must be
/// less than or equal to one.
final double pointRounding;
/// The amount of rounding of the interior corners of stars.
///
/// This is a value between zero and one which describes how rounded the inner
/// corners in a star (the "valley" between points) should be. A value of zero
/// means no rounding (sharp corners), and a value of one means that the
/// entire corner is a portion of a circle.
///
/// Defaults to zero. The sum of [pointRounding] and `valleyRounding` must be
/// less than or equal to one. For polygons created with [StarBorder.polygon],
/// this will always be zero.
final double valleyRounding;
/// The rotation in clockwise degrees around the center of the shape.
///
/// The rotation occurs before the [squash] effect is applied, so that you can
/// fine tune where the points of a star or corners of a polygon start.
///
/// Defaults to zero, meaning that the first point or corner is pointing up.
double get rotation => _rotationRadians * _kRadToDeg;
final double _rotationRadians;
/// How much of the aspect ratio of the attached widget to take on.
///
/// If `squash` is non-zero, the border will match the aspect ratio of the
/// bounding box of the widget that it is attached to, which can give a
/// squashed appearance.
///
/// The `squash` parameter lets you control how much of that aspect ratio this
/// border takes on.
///
/// A value of zero means that the border will be drawn with a square aspect
/// ratio at the size of the shortest side of the bounding rectangle, ignoring
/// the aspect ratio of the widget, and a value of one means it will be drawn
/// with the aspect ratio of the widget. The value of `squash` has no effect
/// if the widget is square to begin with.
///
/// Defaults to zero, and must be between zero and one, inclusive.
final double squash;
@override
EdgeInsetsGeometry get dimensions {
switch (side.strokeAlign) {
case StrokeAlign.inside:
return EdgeInsets.all(side.width);
case StrokeAlign.center:
return EdgeInsets.all(side.width / 2);
case StrokeAlign.outside:
return EdgeInsets.zero;
}
}
@override
ShapeBorder scale(double t) {
return StarBorder(
points: points,
side: side.scale(t),
rotation: rotation,
innerRadiusRatio: innerRadiusRatio,
pointRounding: pointRounding,
valleyRounding: valleyRounding,
squash: squash,
);
}
ShapeBorder? _twoPhaseLerp(
double t,
double split,
ShapeBorder? Function(double t) first,
ShapeBorder? Function(double t) second,
) {
// If the rectangle has square corners, then skip the extra lerp to round the corners.
if (t < split) {
return first(t * (1 / split));
} else {
t = (1 / (1.0 - split)) * (t - split);
return second(t);
}
}
@override
ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
if (t == 0) {
return a;
}
if (t == 1.0) {
return this;
}
if (a is StarBorder) {
return StarBorder(
side: BorderSide.lerp(a.side, side, t),
points: ui.lerpDouble(a.points, points, t)!,
rotation: ui.lerpDouble(a._rotationRadians, _rotationRadians, t)! * _kRadToDeg,
innerRadiusRatio: ui.lerpDouble(a.innerRadiusRatio, innerRadiusRatio, t)!,
pointRounding: ui.lerpDouble(a.pointRounding, pointRounding, t)!,
valleyRounding: ui.lerpDouble(a.valleyRounding, valleyRounding, t)!,
squash: ui.lerpDouble(a.squash, squash, t)!,
);
}
if (a is CircleBorder) {
if (points >= 2.5) {
final double lerpedPoints = ui.lerpDouble(points.round(), points, t)!;
return StarBorder(
side: BorderSide.lerp(a.side, side, t),
points: lerpedPoints,
rotation: rotation,
innerRadiusRatio: ui.lerpDouble(math.cos(math.pi / lerpedPoints), innerRadiusRatio, t)!,
pointRounding: ui.lerpDouble(1.0, pointRounding, t)!,
valleyRounding: ui.lerpDouble(0.0, valleyRounding, t)!,
);
} else {
// Have a slightly different lerp for two-pointed stars, since they get
// kind of squirrelly with near-zero innerRadiusRatios.
final double lerpedPoints = ui.lerpDouble(points, 2, t)!;
return StarBorder(
side: BorderSide.lerp(a.side, side, t),
points: lerpedPoints,
rotation: rotation,
innerRadiusRatio: ui.lerpDouble(1, innerRadiusRatio, t)!,
pointRounding: ui.lerpDouble(0.5, pointRounding, t)!,
valleyRounding: ui.lerpDouble(0.5, valleyRounding, t)!,
);
}
}
if (a is StadiumBorder) {
// Lerp from a stadium to a circle first, and from there to a star.
final BorderSide lerpedSide = BorderSide.lerp(a.side, side, t);
return _twoPhaseLerp(
t,
0.5,
(double t) => a.lerpTo(CircleBorder(side: lerpedSide), t),
(double t) => lerpFrom(CircleBorder(side: lerpedSide), t),
);
}
if (a is RoundedRectangleBorder) {
// Lerp from a rectangle to a stadium, then from a Stadium to a circle,
// then from a circle to a star.
final BorderSide lerpedSide = BorderSide.lerp(a.side, side, t);
return _twoPhaseLerp(
t,
1 / 3,
(double t) {
return StadiumBorder(side: lerpedSide).lerpFrom(a, t);
},
(double t) {
return _twoPhaseLerp(
t,
0.5,
(double t) => StadiumBorder(side: lerpedSide).lerpTo(CircleBorder(side: lerpedSide), t),
(double t) => lerpFrom(CircleBorder(side: lerpedSide), t),
);
},
);
}
return super.lerpFrom(a, t);
}
@override
ShapeBorder? lerpTo(ShapeBorder? b, double t) {
if (t == 0) {
return this;
}
if (t == 1.0) {
return b;
}
if (b is StarBorder) {
return StarBorder(
side: BorderSide.lerp(side, b.side, t),
points: ui.lerpDouble(points, b.points, t)!,
rotation: ui.lerpDouble(_rotationRadians, b._rotationRadians, t)! * _kRadToDeg,
innerRadiusRatio: ui.lerpDouble(innerRadiusRatio, b.innerRadiusRatio, t)!,
pointRounding: ui.lerpDouble(pointRounding, b.pointRounding, t)!,
valleyRounding: ui.lerpDouble(valleyRounding, b.valleyRounding, t)!,
squash: ui.lerpDouble(squash, b.squash, t)!,
);
}
if (b is CircleBorder) {
// Have a slightly different lerp for two-pointed stars, since they get
// kind of squirrelly with near-zero innerRadiusRatios.
if (points >= 2.5) {
final double lerpedPoints = ui.lerpDouble(points, points.round(), t)!;
return StarBorder(
side: BorderSide.lerp(side, b.side, t),
points: lerpedPoints,
rotation: rotation,
innerRadiusRatio: ui.lerpDouble(innerRadiusRatio, math.cos(math.pi / lerpedPoints), t)!,
pointRounding: ui.lerpDouble(pointRounding, 1.0, t)!,
valleyRounding: ui.lerpDouble(valleyRounding, 0.0, t)!,
);
} else {
final double lerpedPoints = ui.lerpDouble(points, 2, t)!;
return StarBorder(
side: BorderSide.lerp(side, b.side, t),
points: lerpedPoints,
rotation: rotation,
innerRadiusRatio: ui.lerpDouble(innerRadiusRatio, 1, t)!,
pointRounding: ui.lerpDouble(pointRounding, 0.5, t)!,
valleyRounding: ui.lerpDouble(valleyRounding, 0.5, t)!,
);
}
}
if (b is StadiumBorder) {
// Lerp to a circle first, then to a stadium.
final BorderSide lerpedSide = BorderSide.lerp(side, b.side, t);
return _twoPhaseLerp(
t,
0.5,
(double t) => lerpTo(CircleBorder(side: lerpedSide), t),
(double t) => b.lerpFrom(CircleBorder(side: lerpedSide), t),
);
}
if (b is RoundedRectangleBorder) {
// Lerp to a circle, and then to a stadium, then to a rounded rect.
final BorderSide lerpedSide = BorderSide.lerp(side, b.side, t);
return _twoPhaseLerp(
t,
2 / 3,
(double t) {
return _twoPhaseLerp(
t,
0.5,
(double t) => lerpTo(CircleBorder(side: lerpedSide), t),
(double t) => StadiumBorder(side: lerpedSide).lerpFrom(CircleBorder(side: lerpedSide), t),
);
},
(double t) {
return StadiumBorder(side: lerpedSide).lerpTo(b, t);
},
);
}
return super.lerpTo(b, t);
}
@override
StarBorder copyWith({
BorderSide? side,
double? points,
double? innerRadiusRatio,
double? pointRounding,
double? valleyRounding,
double? rotation,
double? squash,
}) {
return StarBorder(
side: side ?? this.side,
points: points ?? this.points,
rotation: rotation ?? this.rotation,
innerRadiusRatio: innerRadiusRatio ?? this.innerRadiusRatio,
pointRounding: pointRounding ?? this.pointRounding,
valleyRounding: valleyRounding ?? this.valleyRounding,
squash: squash ?? this.squash,
);
}
@override
Path getInnerPath(Rect rect, {TextDirection? textDirection}) {
final Rect adjustedRect;
switch (side.strokeAlign) {
case StrokeAlign.inside:
adjustedRect = rect.deflate(side.width);
break;
case StrokeAlign.center:
adjustedRect = rect.deflate(side.width / 2);
break;
case StrokeAlign.outside:
adjustedRect = rect;
break;
}
return _StarGenerator(
points: points,
rotation: _rotationRadians,
innerRadiusRatio: innerRadiusRatio,
pointRounding: pointRounding,
valleyRounding: valleyRounding,
squash: squash,
).generate(adjustedRect);
}
@override
Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
return _StarGenerator(
points: points,
rotation: _rotationRadians,
innerRadiusRatio: innerRadiusRatio,
pointRounding: pointRounding,
valleyRounding: valleyRounding,
squash: squash,
).generate(rect);
}
@override
void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {
switch (side.style) {
case BorderStyle.none:
break;
case BorderStyle.solid:
final Rect adjustedRect;
switch (side.strokeAlign) {
case StrokeAlign.inside:
adjustedRect = rect.deflate(side.width / 2);
break;
case StrokeAlign.center:
adjustedRect = rect;
break;
case StrokeAlign.outside:
adjustedRect = rect.inflate(side.width / 2);
break;
}
final Path path = _StarGenerator(
points: points,
rotation: _rotationRadians,
innerRadiusRatio: innerRadiusRatio,
pointRounding: pointRounding,
valleyRounding: valleyRounding,
squash: squash,
).generate(adjustedRect);
canvas.drawPath(path, side.toPaint());
}
}
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is StarBorder && other.side == side;
}
@override
int get hashCode => side.hashCode;
@override
String toString() {
return '${objectRuntimeType(this, 'StarBorder')}($side, points: $points, innerRadiusRatio: $innerRadiusRatio)';
}
}
class _PointInfo {
_PointInfo({
required this.valley,
required this.point,
required this.valleyArc1,
required this.pointArc1,
required this.valleyArc2,
required this.pointArc2,
});
Offset valley;
Offset point;
Offset valleyArc1;
Offset pointArc1;
Offset pointArc2;
Offset valleyArc2;
}
class _StarGenerator {
const _StarGenerator({
required this.points,
required this.innerRadiusRatio,
required this.pointRounding,
required this.valleyRounding,
required this.rotation,
required this.squash,
}) : assert(points > 1),
assert(innerRadiusRatio == null || innerRadiusRatio <= 1),
assert(innerRadiusRatio == null || innerRadiusRatio >= 0),
assert(squash >= 0),
assert(squash <= 1),
assert(pointRounding >= 0),
assert(pointRounding <= 1),
assert(valleyRounding >= 0),
assert(valleyRounding <= 1),
assert(pointRounding + valleyRounding <= 1);
final double points;
final double innerRadiusRatio;
final double pointRounding;
final double valleyRounding;
final double rotation;
final double squash;
bool get isStar => innerRadiusRatio != null;
Path generate(Rect rect) {
final double radius = rect.shortestSide / 2;
final Offset center = rect.center;
// The minimum allowed inner radius ratio. Numerical instabilities occur near
// zero, so we just don't allow values in that range.
const double minInnerRadiusRatio = .002;
// Map the innerRadiusRatio so that we don't get values close to zero, since
// things get a little squirrelly there because the path thinks that the
// length of the conicTo is small enough that it can render it as a straight
// line, even though it will be scaled up later. This maps the range from
// [0, 1] to [minInnerRadiusRatio, 1].
final double mappedInnerRadiusRatio = (innerRadiusRatio * (1.0 - minInnerRadiusRatio)) + minInnerRadiusRatio;
// First, generate the "points" of the star.
final List<_PointInfo> points = <_PointInfo>[];
final double maxDiameter = 2.0 *
_generatePoints(
pointList: points,
center: center,
radius: radius,
innerRadius: radius * mappedInnerRadiusRatio,
);
// Calculate the endpoints of each of the arcs, then draw the arcs.
final Path path = Path();
_drawPoints(path, points);
Offset scale = Offset(rect.width / maxDiameter, rect.height / maxDiameter);
if (rect.shortestSide == rect.width) {
scale = Offset(scale.dx, squash * scale.dy + (1 - squash) * scale.dx);
} else {
scale = Offset(squash * scale.dx + (1 - squash) * scale.dy, scale.dy);
}
// Scale the border so that it matches the size of the widget rectangle, so
// that "rotation" of the shape doesn't affect how much of the rectangle it
// covers.
final Matrix4 squashMatrix = Matrix4.translationValues(rect.center.dx, rect.center.dy, 0);
squashMatrix.multiply(Matrix4.diagonal3Values(scale.dx, scale.dy, 1));
squashMatrix.multiply(Matrix4.rotationZ(rotation));
squashMatrix.multiply(Matrix4.translationValues(-rect.center.dx, -rect.center.dy, 0));
return path.transform(squashMatrix.storage);
}
double _generatePoints({
required List<_PointInfo> pointList,
required Offset center,
required double radius,
required double innerRadius,
}) {
final double step = math.pi / points;
// Start initial rotation one step before zero.
double angle = -math.pi / 2 - step;
Offset valley = Offset(
center.dx + math.cos(angle) * innerRadius,
center.dy + math.sin(angle) * innerRadius,
);
// In order to do overall scale properly, calculate the actual radius at the
// point, taking into account the rounding of the points and the weight of
// the corner point. This effectively is evaluating the rational quadratic
// bezier at the midpoint of the curve.
Offset getCurveMidpoint(Offset a, Offset b, Offset c, Offset a1, Offset c1) {
final double angle = _getAngle(a, b, c);
final double w = _getWeight(angle) / 2;
return (a1 / 4 + b * w + c1 / 4) / (0.5 + w);
}
double addPoint(
double pointAngle,
double pointStep,
double pointRadius,
double pointInnerRadius,
) {
pointAngle += pointStep;
final Offset point = Offset(
center.dx + math.cos(pointAngle) * pointRadius,
center.dy + math.sin(pointAngle) * pointRadius,
);
pointAngle += pointStep;
final Offset nextValley = Offset(
center.dx + math.cos(pointAngle) * pointInnerRadius,
center.dy + math.sin(pointAngle) * pointInnerRadius,
);
final Offset valleyArc1 = valley + (point - valley) * valleyRounding;
final Offset pointArc1 = point + (valley - point) * pointRounding;
final Offset pointArc2 = point + (nextValley - point) * pointRounding;
final Offset valleyArc2 = nextValley + (point - nextValley) * valleyRounding;
pointList.add(_PointInfo(
valley: valley,
point: point,
valleyArc1: valleyArc1,
pointArc1: pointArc1,
pointArc2: pointArc2,
valleyArc2: valleyArc2,
));
valley = nextValley;
return pointAngle;
}
final double remainder = points - points.truncateToDouble();
final bool hasIntegerSides = remainder < 1e-6;
final double wholeSides = points - (hasIntegerSides ? 0 : 1);
for (int i = 0; i < wholeSides; i += 1) {
angle = addPoint(angle, step, radius, innerRadius);
}
double valleyRadius = 0;
double pointRadius = 0;
final _PointInfo thisPoint = pointList[0];
final _PointInfo nextPoint = pointList[1];
final Offset pointMidpoint =
getCurveMidpoint(thisPoint.valley, thisPoint.point, nextPoint.valley, thisPoint.pointArc1, thisPoint.pointArc2);
final Offset valleyMidpoint = getCurveMidpoint(
thisPoint.point, nextPoint.valley, nextPoint.point, thisPoint.valleyArc2, nextPoint.valleyArc1);
valleyRadius = (valleyMidpoint - center).distance;
pointRadius = (pointMidpoint - center).distance;
// Add the final point to close the shape if there are fractional sides to
// account for.
if (!hasIntegerSides) {
final double effectiveInnerRadius = math.max(valleyRadius, innerRadius);
final double endingRadius = effectiveInnerRadius + remainder * (radius - effectiveInnerRadius);
addPoint(angle, step * remainder, endingRadius, innerRadius);
}
// The rounding added to the valley radius can sometimes push it outside of
// the rounding of the point, since the rounding amount can be different, so
// we have to evaluate both the valley and the point radii, and pick the
// largest.
return math.max(valleyRadius, pointRadius);
}
void _drawPoints(Path path, List<_PointInfo> points) {
final Offset startingPoint = points.first.pointArc1;
path.moveTo(startingPoint.dx, startingPoint.dy);
final double pointAngle = _getAngle(points[0].valley, points[0].point, points[1].valley);
final double pointWeight = _getWeight(pointAngle);
final double valleyAngle = _getAngle(points[1].point, points[1].valley, points[0].point);
final double valleyWeight = _getWeight(valleyAngle);
for (int i = 0; i < points.length; i += 1) {
final _PointInfo point = points[i];
final _PointInfo nextPoint = points[(i + 1) % points.length];
path.lineTo(point.pointArc1.dx, point.pointArc1.dy);
if (pointAngle != 180 && pointAngle != 0) {
path.conicTo(point.point.dx, point.point.dy, point.pointArc2.dx, point.pointArc2.dy, pointWeight);
} else {
path.lineTo(point.pointArc2.dx, point.pointArc2.dy);
}
path.lineTo(point.valleyArc2.dx, point.valleyArc2.dy);
if (valleyAngle != 180 && valleyAngle != 0) {
path.conicTo(
nextPoint.valley.dx, nextPoint.valley.dy, nextPoint.valleyArc1.dx, nextPoint.valleyArc1.dy, valleyWeight);
} else {
path.lineTo(nextPoint.valleyArc1.dx, nextPoint.valleyArc1.dy);
}
}
path.close();
}
double _getWeight(double angle) {
return math.cos((angle / 2) % (math.pi / 2));
}
// Returns the included angle between points ABC in radians.
double _getAngle(Offset a, Offset b, Offset c) {
if (a == c || b == c || b == a) {
return 0;
}
final Offset u = a - b;
final Offset v = c - b;
final double dot = u.dx * v.dx + u.dy * v.dy;
final double m1 = b.dx == a.dx ? double.infinity : -u.dy / -u.dx;
final double m2 = b.dx == c.dx ? double.infinity : -v.dy / -v.dx;
double angle = math.atan2(m1 - m2, 1 + m1 * m2).abs();
if (dot < 0) {
angle += math.pi;
}
return angle;
}
}
// 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.
@Tags(<String>['reduced-test-set'])
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
Future<void> testBorder(WidgetTester tester, String name, StarBorder border,
{ShapeBorder? lerpTo, ShapeBorder? lerpFrom, double lerpAmount = 0}) async {
assert(lerpTo == null || lerpFrom == null); // They can't both be set.
ShapeBorder shape;
if (lerpTo != null) {
shape = border.lerpTo(lerpTo, lerpAmount)!;
} else if (lerpFrom != null) {
shape = border.lerpFrom(lerpFrom, lerpAmount)!;
} else {
shape = border;
}
await tester.pumpWidget(
Container(
alignment: Alignment.center,
width: 200,
height: 100,
decoration: ShapeDecoration(
color: const Color(0xff000000),
shape: shape,
),
),
);
await expectLater(
find.byType(Container),
matchesGoldenFile('painting.star_border.$name.png'),
);
}
test('StarBorder defaults', () {
const StarBorder star = StarBorder();
expect(star.side, BorderSide.none);
expect(star.points, 5);
expect(star.innerRadiusRatio, 0.4);
expect(star.rotation, 0);
expect(star.pointRounding, 0);
expect(star.valleyRounding, 0);
expect(star.squash, 0);
const StarBorder polygon = StarBorder.polygon();
expect(polygon.points, 5);
expect(polygon.pointRounding, 0);
expect(polygon.rotation, 0);
expect(polygon.squash, 0);
});
test('StarBorder copyWith, ==, hashCode', () {
const BorderSide side = BorderSide(width: 10.0, color: Color(0xff123456));
final StarBorder copy = const StarBorder().copyWith(
side: side,
points: 3,
innerRadiusRatio: 0.1,
pointRounding: 0.2,
valleyRounding: 0.3,
rotation: 180,
squash: 0.4,
);
const StarBorder expected = StarBorder(
side: side,
points: 3,
innerRadiusRatio: 0.1,
pointRounding: 0.2,
valleyRounding: 0.3,
rotation: 180,
squash: 0.4,
);
expect(const StarBorder(), equals(const StarBorder().copyWith()));
expect(copy, equals(expected));
expect(copy.hashCode, equals(expected.hashCode));
});
testWidgets('StarBorder basic geometry', (WidgetTester tester) async {
await testBorder(tester, 'basic_star', const StarBorder());
await testBorder(tester, 'basic_polygon', const StarBorder.polygon());
});
testWidgets('StarBorder parameters', (WidgetTester tester) async {
await testBorder(tester, 'points_6', const StarBorder(points: 6));
await testBorder(tester, 'points_2', const StarBorder(points: 2));
await testBorder(tester, 'inner_radius_0', const StarBorder(innerRadiusRatio: 0.0));
await testBorder(tester, 'inner_radius_2', const StarBorder(innerRadiusRatio: 0.2));
await testBorder(tester, 'inner_radius_7', const StarBorder(innerRadiusRatio: 0.7));
await testBorder(tester, 'point_rounding_2', const StarBorder(pointRounding: 0.2));
await testBorder(tester, 'point_rounding_7', const StarBorder(pointRounding: 0.7));
await testBorder(tester, 'point_rounding_10', const StarBorder(pointRounding: 1.0));
await testBorder(tester, 'valley_rounding_2', const StarBorder(valleyRounding: 0.2));
await testBorder(tester, 'valley_rounding_7', const StarBorder(valleyRounding: 0.7));
await testBorder(tester, 'valley_rounding_10', const StarBorder(valleyRounding: 1.0));
await testBorder(tester, 'squash_2', const StarBorder(squash: 0.2));
await testBorder(tester, 'squash_7', const StarBorder(squash: 0.7));
await testBorder(tester, 'squash_10', const StarBorder(squash: 1.0));
await testBorder(tester, 'rotate_27', const StarBorder(rotation: 27));
await testBorder(tester, 'rotate_270', const StarBorder(rotation: 270));
await testBorder(tester, 'rotate_360', const StarBorder(rotation: 360));
await testBorder(tester, 'side_none', const StarBorder(side: BorderSide(style: BorderStyle.none)));
await testBorder(tester, 'side_1', const StarBorder(side: BorderSide(color: Color(0xffff0000))));
await testBorder(tester, 'side_10', const StarBorder(side: BorderSide(color: Color(0xffff0000), width: 10)));
await testBorder(tester, 'side_align_center',
const StarBorder(side: BorderSide(color: Color(0xffff0000), strokeAlign: StrokeAlign.center)));
await testBorder(tester, 'side_align_outside',
const StarBorder(side: BorderSide(color: Color(0xffff0000), strokeAlign: StrokeAlign.outside)));
});
testWidgets('StarBorder.polygon parameters', (WidgetTester tester) async {
await testBorder(tester, 'poly_sides_6', const StarBorder.polygon(sides: 6));
await testBorder(tester, 'poly_sides_2', const StarBorder.polygon(sides: 2));
await testBorder(tester, 'poly_point_rounding_2', const StarBorder.polygon(pointRounding: 0.2));
await testBorder(tester, 'poly_point_rounding_7', const StarBorder.polygon(pointRounding: 0.7));
await testBorder(tester, 'poly_point_rounding_10', const StarBorder.polygon(pointRounding: 1.0));
await testBorder(tester, 'poly_squash_2', const StarBorder.polygon(squash: 0.2));
await testBorder(tester, 'poly_squash_7', const StarBorder.polygon(squash: 0.7));
await testBorder(tester, 'poly_squash_10', const StarBorder.polygon(squash: 1.0));
await testBorder(tester, 'poly_rotate_27', const StarBorder.polygon(rotation: 27));
await testBorder(tester, 'poly_rotate_270', const StarBorder.polygon(rotation: 270));
await testBorder(tester, 'poly_rotate_360', const StarBorder.polygon(rotation: 360));
await testBorder(tester, 'poly_side_none', const StarBorder.polygon(side: BorderSide(style: BorderStyle.none)));
await testBorder(tester, 'poly_side_1', const StarBorder.polygon(side: BorderSide(color: Color(0xffff0000))));
await testBorder(
tester, 'poly_side_10', const StarBorder.polygon(side: BorderSide(color: Color(0xffff0000), width: 10)));
await testBorder(tester, 'poly_side_align_center',
const StarBorder.polygon(side: BorderSide(color: Color(0xffff0000), strokeAlign: StrokeAlign.center)));
await testBorder(tester, 'poly_side_align_outside',
const StarBorder.polygon(side: BorderSide(color: Color(0xffff0000), strokeAlign: StrokeAlign.outside)));
});
testWidgets('StarBorder lerped with StarBorder', (WidgetTester tester) async {
const StarBorder from = StarBorder();
const ShapeBorder otherBorder = StarBorder(
points: 6,
pointRounding: 0.5,
valleyRounding: 0.5,
innerRadiusRatio: 0.5,
rotation: 90,
);
await testBorder(tester, 'to_star_border_2', from, lerpTo: otherBorder, lerpAmount: 0.2);
await testBorder(tester, 'to_star_border_7', from, lerpTo: otherBorder, lerpAmount: 0.7);
await testBorder(tester, 'to_star_border_10', from, lerpTo: otherBorder, lerpAmount: 1.0);
await testBorder(tester, 'from_star_border_2', from, lerpFrom: otherBorder, lerpAmount: 0.2);
await testBorder(tester, 'from_star_border_7', from, lerpFrom: otherBorder, lerpAmount: 0.7);
await testBorder(tester, 'from_star_border_10', from, lerpFrom: otherBorder, lerpAmount: 1.0);
});
testWidgets('StarBorder lerped with CircleBorder', (WidgetTester tester) async {
const StarBorder from = StarBorder();
const ShapeBorder otherBorder = CircleBorder();
await testBorder(tester, 'to_circle_border_2', from, lerpTo: otherBorder, lerpAmount: 0.2);
await testBorder(tester, 'to_circle_border_7', from, lerpTo: otherBorder, lerpAmount: 0.7);
await testBorder(tester, 'to_circle_border_10', from, lerpTo: otherBorder, lerpAmount: 1.0);
await testBorder(tester, 'from_circle_border_2', from, lerpFrom: otherBorder, lerpAmount: 0.2);
await testBorder(tester, 'from_circle_border_7', from, lerpFrom: otherBorder, lerpAmount: 0.7);
await testBorder(tester, 'from_circle_border_10', from, lerpFrom: otherBorder, lerpAmount: 1.0);
});
testWidgets('StarBorder lerped with RoundedRectangleBorder', (WidgetTester tester) async {
const StarBorder from = StarBorder();
const RoundedRectangleBorder rectangleBorder = RoundedRectangleBorder();
await testBorder(tester, 'to_rect_border_2', from, lerpTo: rectangleBorder, lerpAmount: 0.2);
await testBorder(tester, 'to_rect_border_7', from, lerpTo: rectangleBorder, lerpAmount: 0.7);
await testBorder(tester, 'to_rect_border_10', from, lerpTo: rectangleBorder, lerpAmount: 1.0);
await testBorder(tester, 'from_rect_border_2', from, lerpFrom: rectangleBorder, lerpAmount: 0.2);
await testBorder(tester, 'from_rect_border_7', from, lerpFrom: rectangleBorder, lerpAmount: 0.7);
await testBorder(tester, 'from_rect_border_10', from, lerpFrom: rectangleBorder, lerpAmount: 1.0);
const RoundedRectangleBorder roundedRectBorder = RoundedRectangleBorder(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(10.0),
bottomRight: Radius.circular(10.0),
),
);
await testBorder(tester, 'to_rrect_border_2', from, lerpTo: roundedRectBorder, lerpAmount: 0.2);
await testBorder(tester, 'to_rrect_border_7', from, lerpTo: roundedRectBorder, lerpAmount: 0.7);
await testBorder(tester, 'to_rrect_border_10', from, lerpTo: roundedRectBorder, lerpAmount: 1.0);
await testBorder(tester, 'from_rrect_border_2', from, lerpFrom: roundedRectBorder, lerpAmount: 0.2);
await testBorder(tester, 'from_rrect_border_7', from, lerpFrom: roundedRectBorder, lerpAmount: 0.7);
await testBorder(tester, 'from_rrect_border_10', from, lerpFrom: roundedRectBorder, lerpAmount: 1.0);
});
testWidgets('StarBorder lerped with StadiumBorder', (WidgetTester tester) async {
const StarBorder from = StarBorder();
const StadiumBorder stadiumBorder = StadiumBorder();
await testBorder(tester, 'to_stadium_border_2', from, lerpTo: stadiumBorder, lerpAmount: 0.2);
await testBorder(tester, 'to_stadium_border_7', from, lerpTo: stadiumBorder, lerpAmount: 0.7);
await testBorder(tester, 'to_stadium_border_10', from, lerpTo: stadiumBorder, lerpAmount: 1.0);
await testBorder(tester, 'from_stadium_border_2', from, lerpFrom: stadiumBorder, lerpAmount: 0.2);
await testBorder(tester, 'from_stadium_border_7', from, lerpFrom: stadiumBorder, lerpAmount: 0.7);
await testBorder(tester, 'from_stadium_border_10', from, lerpFrom: stadiumBorder, lerpAmount: 1.0);
});
}
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