-
Notifications
You must be signed in to change notification settings - Fork 3.9k
[material_ui] Add the Material shapes. #12771
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ksokolovskyi
wants to merge
60
commits into
main
Choose a base branch
from
add-material-shapes-to-material-ui
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
60 commits
Select commit
Hold shift + click to select a range
1fccb31
Copy ksokolovskyi/material_shapes into material_ui.
ksokolovskyi 6c460a7
Apply dart format and fix.
ksokolovskyi e17a104
Add license headers.
ksokolovskyi 33011e8
Collapse the doubled shapes directory.
ksokolovskyi 3e688cf
Split the shapes part library into separate libraries.
ksokolovskyi 729715b
Rename Cubic to CubicBezier.
ksokolovskyi 07a87d1
Align shapes visibility with the AndroidX public API.
ksokolovskyi 36c8770
Document the public members of the shapes API.
ksokolovskyi 0e1f0fa
Fix analyzer issues.
ksokolovskyi dcb65e9
Expose material shapes from material_ui.
ksokolovskyi 9bcdadf
Add material shapes example.
ksokolovskyi 92a3707
Add golden tests for all MaterialShapes.
ksokolovskyi 5907f49
Make the internal Point an alias for Offset.
ksokolovskyi eb8195c
Return Rect from the shapes bounds methods.
ksokolovskyi ff1cae7
Use an Offset center in the RoundedPolygon API.
ksokolovskyi 2492e0b
Use a List<Offset> for the RoundedPolygon vertices.
ksokolovskyi 25304df
Use Offset in the CubicBezier API.
ksokolovskyi 5147b00
Clean up the shapes toPath parameters and fix the rotation pivot.
ksokolovskyi b0d2c7a
Fix the shapes equality and hashCode list-identity bugs.
ksokolovskyi 0bd1924
Add value equality to the shapes features.
ksokolovskyi b0d14b2
Add more MaterialShapeBorder tests.
ksokolovskyi 70be604
Cache the MaterialShapeBorder morph across lerps.
ksokolovskyi 214221d
Fix the MaterialShapeBorder crash on an interrupted transition.
ksokolovskyi ee72777
Rename CubicBezier.zeroLength() to isZeroLength.
ksokolovskyi fd2e02b
Rename Morph.asCubics() to toCubics().
ksokolovskyi 6009439
Make RoundedPolygon.fromVerticesNum the default constructor.
ksokolovskyi fb5d3d8
Rename the Feature factories off their Kotlin build* prefix.
ksokolovskyi a9eacd7
Unify the shapes reverse naming on a reversed getter.
ksokolovskyi 64fc89f
Turn the shapes bounds methods into getters.
ksokolovskyi f4f02a8
Turn RoundedPolygon.normalized into a getter.
ksokolovskyi 628df26
Name the shapes transformed() parameter transformer.
ksokolovskyi 06fd546
Delete the RoundedPolygon copy constructor.
ksokolovskyi 0a2d2fb
Rename CubicBezier.pointOnCurve() to pointAt().
ksokolovskyi 99e3732
Rename CubicBezier.empty() to CubicBezier.point().
ksokolovskyi 239ed78
Add value equality to CornerRounding.
ksokolovskyi f593808
Add value equality and public shapes to Morph.
ksokolovskyi bb43b01
Make RoundedPolygon.cubics and .features unmodifiable.
ksokolovskyi 866d7b7
Make the Feature generative constructor private.
ksokolovskyi ebf0f77
Name the type in the shapes toString implementations.
ksokolovskyi 09e3573
Update CornerRounding assert message.
ksokolovskyi 8adf348
Check RoundedPolygon contiguity outside an assert.
ksokolovskyi 288f313
Use Offset's own geometry in PointGeometry.
ksokolovskyi db5ea6f
Get rid of x/y-argument geometry twins.
ksokolovskyi d837c0a
Rename PointGeometry.clockwise to turnsClockwiseTo.
ksokolovskyi 1a01e46
Type cutAdjusts as (double, double) rather than (num, num).
ksokolovskyi 28ddebf
Use listEquals in the CubicBezier and RoundedPolygon equality.
ksokolovskyi b88ec77
Rename float_mapping.dart to double_mapping.dart.
ksokolovskyi edd17b0
Delete the MeasuredFeatures typedef.
ksokolovskyi 2dc43fe
Rename the MeasuredPolygon measure and cubic accessors.
ksokolovskyi f6edb8e
Name the Measurer parameters cubic and measure.
ksokolovskyi 6c16dce
Update docs and comments.
ksokolovskyi 95fdb70
Add missing const.
ksokolovskyi 916f784
Merge branch 'main' into add-material-shapes-to-material-ui
ksokolovskyi 2e3f482
Update pending changelog.
ksokolovskyi be59266
Fix typo in innerRadiusRatio error message.
ksokolovskyi 1d11158
Remove List.generate calls in CubicBezier + and * operators.
ksokolovskyi dc776d0
Remove List.generate call in Morph.toCubics function.
ksokolovskyi 3ca9ee6
Guard LengthMeasurer._closestProgressTo against dividing by zero.
ksokolovskyi 01b88fc
Guard RoundedPolygon.normalized against dividing by zero for a point-…
ksokolovskyi df808f7
Format doubles in CubicBezier.toString and CornerRounding.toString wi…
ksokolovskyi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
191 changes: 191 additions & 0 deletions
191
packages/material_ui/example/lib/material_shapes/material_shapes.0.dart
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,191 @@ | ||
| // Copyright 2013 The Flutter Authors | ||
| // Use of this source code is governed by a BSD-style license that can be | ||
| // found in the LICENSE file. | ||
|
|
||
| // #region body | ||
| import 'dart:async'; | ||
|
|
||
| import 'package:flutter/foundation.dart'; | ||
| import 'package:flutter/physics.dart'; | ||
| import 'package:material_ui/material_ui.dart'; | ||
|
|
||
| /// Flutter code sample for [MaterialShapes]. | ||
|
|
||
| void main() { | ||
| runApp(const MaterialShapesExampleApp()); | ||
| } | ||
|
|
||
| class MaterialShapesExampleApp extends StatelessWidget { | ||
| const MaterialShapesExampleApp({super.key}); | ||
|
|
||
| @override | ||
| Widget build(BuildContext context) { | ||
| return MaterialApp( | ||
| theme: ThemeData(colorSchemeSeed: const Color(0xFF6750A4)), | ||
| home: const MaterialShapesExample(), | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| class MaterialShapesExample extends StatefulWidget { | ||
| const MaterialShapesExample({super.key}); | ||
|
|
||
| @override | ||
| State<MaterialShapesExample> createState() => _MaterialShapesExampleState(); | ||
| } | ||
|
|
||
| class _MaterialShapesExampleState extends State<MaterialShapesExample> | ||
| with SingleTickerProviderStateMixin { | ||
| /// The shapes to morph through, in the order they are listed in | ||
| /// [MaterialShapes.all]. | ||
| static final List<RoundedPolygon> _shapes = MaterialShapes.all; | ||
|
|
||
| /// A [Morph] from each shape in [_shapes] to the next one, wrapping around | ||
| /// from the last shape back to the first. | ||
| /// | ||
| /// These are built up front because a [Morph] computes the mapping between | ||
| /// its two shapes when it is constructed. | ||
| late final List<Morph> _morphs; | ||
|
|
||
| /// The morph that is currently animating. | ||
| late final ValueNotifier<Morph> _morph; | ||
|
|
||
| /// Drives the progress of [_morph]. | ||
| /// | ||
| /// The controller is unbounded so that the springs below can overshoot past a | ||
| /// progress of 1, which carries the morph slightly beyond its end shape. | ||
| late final AnimationController _controller; | ||
|
|
||
| int _morphIndex = 0; | ||
|
|
||
| Timer? _startTimer; | ||
|
|
||
| final _bouncySimulation = SpringSimulation( | ||
| SpringDescription.withDampingRatio(ratio: 0.5, stiffness: 400, mass: 1), | ||
| 0, | ||
| 1, | ||
| 5, | ||
| snapToEnd: true, | ||
| ); | ||
|
|
||
| // Overshooting past a progress of 1 extrapolates a morph beyond its end | ||
| // shape, which distorts the heart into sharp spikes. The heart therefore | ||
| // animates with a spring that starts at rest and barely overshoots. | ||
| final _lessBouncySimulation = SpringSimulation( | ||
| SpringDescription.withDampingRatio(ratio: 0.8, stiffness: 300, mass: 1), | ||
| 0, | ||
| 1, | ||
| 0, | ||
| snapToEnd: true, | ||
| ); | ||
|
|
||
| /// The shape that the currently animating morph ends on. | ||
| RoundedPolygon get _targetShape => | ||
| _shapes[(_morphIndex + 1) % _shapes.length]; | ||
|
|
||
| @override | ||
| void initState() { | ||
| super.initState(); | ||
|
|
||
| _morphs = <Morph>[ | ||
| for (var i = 0; i < _shapes.length; i++) | ||
| Morph(_shapes[i], _shapes[(i + 1) % _shapes.length]), | ||
| ]; | ||
| _morph = ValueNotifier<Morph>(_morphs.first); | ||
| _controller = AnimationController.unbounded(vsync: this); | ||
|
|
||
| // Hold the first shape for a moment so that it can be seen before it starts | ||
| // morphing. | ||
| _startTimer = Timer(const Duration(seconds: 1), _morphThroughShapes); | ||
| } | ||
|
|
||
| @override | ||
| void dispose() { | ||
| _startTimer?.cancel(); | ||
| _controller.dispose(); | ||
| _morph.dispose(); | ||
| super.dispose(); | ||
| } | ||
|
|
||
| /// Animates the morphs in [_morphs] one after another, looping back to the | ||
| /// first one after the last, until this widget is disposed. | ||
| /// | ||
| /// Waiting for each spring to settle, rather than advancing on a fixed | ||
| /// interval, keeps a morph from being cut short and popping to the next | ||
| /// shape. | ||
| Future<void> _morphThroughShapes() async { | ||
| while (mounted) { | ||
| _morph.value = _morphs[_morphIndex]; | ||
| _controller.value = 0; | ||
| await _controller.animateWith( | ||
| _targetShape == MaterialShapes.heart | ||
| ? _lessBouncySimulation | ||
| : _bouncySimulation, | ||
| ); | ||
| _morphIndex = (_morphIndex + 1) % _morphs.length; | ||
| } | ||
| } | ||
|
|
||
| @override | ||
| Widget build(BuildContext context) { | ||
| return Scaffold( | ||
| appBar: AppBar(title: const Text('Material Shapes Sample')), | ||
| body: Center( | ||
| child: ConstrainedBox( | ||
| constraints: const BoxConstraints(maxWidth: 300, maxHeight: 300), | ||
| child: AspectRatio( | ||
| aspectRatio: 1, | ||
| child: RepaintBoundary( | ||
| child: CustomPaint( | ||
| painter: _MorphPainter( | ||
| morph: _morph, | ||
| progress: _controller, | ||
| color: Theme.of(context).colorScheme.primary, | ||
| ), | ||
| willChange: true, | ||
| child: const SizedBox.expand(), | ||
| ), | ||
| ), | ||
| ), | ||
| ), | ||
| ), | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| class _MorphPainter extends CustomPainter { | ||
| _MorphPainter({ | ||
| required this.morph, | ||
| required this.progress, | ||
| required this.color, | ||
| }) : super(repaint: Listenable.merge(<Listenable>[morph, progress])); | ||
|
|
||
| final ValueListenable<Morph> morph; | ||
|
|
||
| final Animation<double> progress; | ||
|
|
||
| final Color color; | ||
|
|
||
| late final Paint _paint = Paint() | ||
| ..style = .fill | ||
| ..color = color; | ||
|
|
||
| @override | ||
| void paint(Canvas canvas, Size size) { | ||
| // Every shape in MaterialShapes is normalized, so its path fits in a one by | ||
| // one square that has to be scaled up to the size being painted. | ||
| canvas | ||
| ..save() | ||
| ..scale(size.width) | ||
| ..drawPath(morph.value.toPath(progress.value), _paint) | ||
| ..restore(); | ||
| } | ||
|
|
||
| @override | ||
| bool shouldRepaint(_MorphPainter oldDelegate) { | ||
| return oldDelegate.morph != morph || | ||
| oldDelegate.progress != progress || | ||
| oldDelegate.color != color; | ||
| } | ||
| } | ||
| // #endregion body | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
99 changes: 99 additions & 0 deletions
99
packages/material_ui/example/test/material_shapes/material_shapes.0_test.dart
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| // Copyright 2013 The Flutter Authors | ||
| // Use of this source code is governed by a BSD-style license that can be | ||
| // found in the LICENSE file. | ||
|
|
||
| import 'package:flutter_test/flutter_test.dart'; | ||
| import 'package:material_ui/material_ui.dart'; | ||
| import 'package:material_ui_examples/material_shapes/material_shapes.0.dart' | ||
| as example; | ||
|
|
||
| void main() { | ||
| /// Comfortably longer than it takes either of the example's springs to | ||
| /// settle. | ||
| const Duration morphDuration = Duration(seconds: 3); | ||
|
|
||
| /// Samples points on a grid over the unit square that are clearly [inside] | ||
| /// (or outside) of [path]. | ||
| List<Offset> samplePoints(Path path, {required bool inside}) { | ||
| const step = 0.09; | ||
| const margin = 0.02; | ||
|
|
||
| final List<Offset> points = <Offset>[]; | ||
| for (var x = step; x < 1; x += step) { | ||
| for (var y = step; y < 1; y += step) { | ||
| final bool clearlyInOrOut = <Offset>[ | ||
| Offset(x, y), | ||
| Offset(x - margin, y), | ||
| Offset(x + margin, y), | ||
| Offset(x, y - margin), | ||
| Offset(x, y + margin), | ||
| ].every((Offset point) => path.contains(point) == inside); | ||
| if (clearlyInOrOut) { | ||
| points.add(Offset(x, y)); | ||
| } | ||
| } | ||
| } | ||
| return points; | ||
| } | ||
|
|
||
| testWidgets('Material shapes morph through every shape and wrap around', ( | ||
| WidgetTester tester, | ||
| ) async { | ||
| await tester.pumpWidget(const example.MaterialShapesExampleApp()); | ||
|
|
||
| expect(find.text('Material Shapes Sample'), findsOne); | ||
|
|
||
| final Finder shape = find.byWidgetPredicate( | ||
| (Widget widget) => widget is CustomPaint && widget.willChange, | ||
| ); | ||
| expect(tester.getSize(shape), const Size.square(300)); | ||
|
|
||
| final ThemeData theme = Theme.of( | ||
| tester.element(find.byType(example.MaterialShapesExample)), | ||
| ); | ||
|
|
||
| // The circle is painted first, scaled up to fill the paint area, and is | ||
| // held still before the morphing starts. | ||
| expect( | ||
| shape, | ||
| paints | ||
| ..scale(x: 300) | ||
| ..path( | ||
| color: theme.colorScheme.primary, | ||
| style: PaintingStyle.fill, | ||
| includes: const <Offset>[Offset(0.5, 0.5)], | ||
| excludes: const <Offset>[Offset(0.1, 0.1)], | ||
| ), | ||
| ); | ||
| expect(tester.hasRunningAnimations, isFalse); | ||
|
|
||
| // The morphing starts after holding the first shape for a second. | ||
| await tester.pump(const Duration(seconds: 1)); | ||
|
|
||
| // Morph through the whole list of shapes, wrapping around from the last | ||
| // shape back to the first one. | ||
| final shapeCount = MaterialShapes.all.length; | ||
| for (var i = 0; i < shapeCount; i++) { | ||
| expect(tester.hasRunningAnimations, isTrue); | ||
| await tester.pump(morphDuration); | ||
|
|
||
| final RoundedPolygon target = MaterialShapes.all[(i + 1) % shapeCount]; | ||
| final Path targetPath = target.toPath(); | ||
| expect( | ||
| shape, | ||
| paints..path( | ||
| color: theme.colorScheme.primary, | ||
| style: PaintingStyle.fill, | ||
| includes: samplePoints(targetPath, inside: true), | ||
| excludes: samplePoints(targetPath, inside: false), | ||
| ), | ||
| reason: | ||
| 'Morph $i should have settled on shape ${(i + 1) % shapeCount}.', | ||
| ); | ||
| } | ||
|
|
||
| // Disposing the example stops the animation. | ||
| await tester.pumpWidget(const SizedBox()); | ||
| expect(tester.hasRunningAnimations, isFalse); | ||
| }); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.