mirror of
https://github.com/PranshulGG/WeatherMaster.git
synced 2026-10-22 19:13:39 -05:00
Convert expressive_loading_indicator_custom from submodule to normal folder
This commit is contained in:
1 parent
91a9f9d8ad
commit
b4e69a3ee2
13 files changed
+938
-2
No files matched your search
@@ -0,0 +1,31 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||
/pubspec.lock
|
||||
**/doc/api/
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
build/
|
||||
@@ -0,0 +1,10 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "edada7c56edf4a183c1735310e123c7f923584f1"
|
||||
channel: "stable"
|
||||
|
||||
project_type: package
|
||||
@@ -0,0 +1,3 @@
|
||||
## 0.0.1
|
||||
|
||||
* Initial release of the ExpressiveLoadingIndicator package
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
Copyright 2025 Tamim Arafat
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,20 @@
|
||||
This product includes software derived from the Android Open Source Project.
|
||||
|
||||
Specifically, the following files are Dart ports of AOSP Kotlin components:
|
||||
- lib/expressive_loading_indicator.dart (derived from compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/LoadingIndicator.kt)
|
||||
|
||||
The Android Open Source Project is licensed under the Apache License, Version 2.0.
|
||||
You may obtain a copy of the License at:
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
Copyright 2024 The Android Open Source Project
|
||||
|
||||
Additionally, this product includes software developed by Tamim Arafat
|
||||
and licensed under the MIT License.
|
||||
@@ -0,0 +1,75 @@
|
||||
# Expressive Loading Indicator
|
||||
|
||||
> Archival notice: Refer to the implementation at [ksokolovskyi/material_loading_indicator](https://github.com/ksokolovskyi/material_loading_indicator).
|
||||
|
||||
[](https://pub.dartlang.org/packages/expressive_loading_indicator)
|
||||
|
||||
Material 3 Expressive loading indicator ported to Flutter.
|
||||
|
||||
[](https://github.com/user-attachments/assets/d5ee926f-7cae-4824-8a37-e844b7ebee4f)
|
||||
|
||||
## Package
|
||||
|
||||
[`expressive_loading_indicator` on pub.dev](https://pub.dev/packages/expressive_loading_indicator).
|
||||
|
||||
## Usage
|
||||
|
||||
```dart
|
||||
import 'package:expressive_loading_indicator/expressive_loading_indicator.dart';
|
||||
import 'package:material_new_shapes/material_new_shapes.dart';
|
||||
|
||||
ExpressiveLoadingIndicator(
|
||||
// Custom color
|
||||
color: Colors.purple,
|
||||
|
||||
// Custom size constraints
|
||||
constraints: BoxConstraints(
|
||||
minWidth: 64.0,
|
||||
minHeight: 64.0,
|
||||
maxWidth: 64.0,
|
||||
maxHeight: 64.0,
|
||||
),
|
||||
|
||||
// Custom polygon shapes
|
||||
polygons: [
|
||||
MaterialShapes.softBurst,
|
||||
MaterialShapes.pentagon,
|
||||
MaterialShapes.pill,
|
||||
],
|
||||
|
||||
// Accessibility
|
||||
semanticsLabel: 'Loading',
|
||||
semanticsValue: 'In progress',
|
||||
)
|
||||
```
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] Add support for ContainedLoadingIndicator - A version that shows the loading indicator inside a container with a background
|
||||
- [ ] Add support for DeterminateLoadingIndicator - A version that morphs depending on the progress value
|
||||
|
||||
## Credits
|
||||
|
||||
- [Kostia Sokolovskyi](https://github.com/ksokolovskyi) for their initial implementation of [material_shapes](https://github.com/ksokolovskyi/material_shapes).
|
||||
|
||||
## External Links
|
||||
|
||||
- [Loading Indicator](https://m3.material.io/components/loading-indicator/overview) on Material Design.
|
||||
- [Kotlin source implementation](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/LoadingIndicator.kt) on Android Code Search.
|
||||
|
||||
## License & Attribution
|
||||
|
||||
This package is available under the [MIT License](LICENSE).
|
||||
|
||||
This package contains Dart ports of components from the Android Open Source Project (AOSP). The original Android components are licensed under the Apache License 2.0.
|
||||
See [NOTICE](NOTICE) for complete attribution and license details.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit an Issue or Pull Request.
|
||||
|
||||
1. Fork the Project
|
||||
2. Create your Feature Branch (`git checkout -b feature/amazing-feature`)
|
||||
3. Commit your Changes (`git commit -m 'Add some amazing feature'`)
|
||||
4. Push to the Branch (`git push origin feature/amazing-feature`)
|
||||
5. Open a Pull Request
|
||||
@@ -0,0 +1,4 @@
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
@@ -0,0 +1,91 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:expressive_loading_indicator/expressive_loading_indicator.dart';
|
||||
import 'package:material_new_shapes/material_new_shapes.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Expressive Loading Indicator Demo',
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
||||
useMaterial3: true,
|
||||
),
|
||||
home: const MyHomePage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyHomePage extends StatelessWidget {
|
||||
const MyHomePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Expressive Loading Indicator Demo'),
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const Text(
|
||||
'Default Loading Indicator:',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Default loading indicator
|
||||
const ExpressiveLoadingIndicator(),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
const Text(
|
||||
'Custom Color Loading Indicator:',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Customized with color
|
||||
const ExpressiveLoadingIndicator(color: Colors.orange),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
const Text(
|
||||
'Custom Size Loading Indicator:',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Customized with size
|
||||
const ExpressiveLoadingIndicator(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: 72.0,
|
||||
minHeight: 72.0,
|
||||
maxWidth: 72.0,
|
||||
maxHeight: 72.0,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
const Text(
|
||||
'Custom Shapes Loading Indicator:',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Customized with shapes
|
||||
ExpressiveLoadingIndicator(
|
||||
polygons: [
|
||||
MaterialShapes.softBurst,
|
||||
MaterialShapes.pill,
|
||||
MaterialShapes.pentagon,
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
// Port of Android's LoadingIndicator
|
||||
// Source: androidx/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/LoadingIndicator.kt
|
||||
// Copyright (c) 2024 The Android Open Source Project
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
//
|
||||
// Dart port by Tamim Arafat
|
||||
// Copyright (c) 2025 Tamim Arafat
|
||||
// Licensed under MIT License
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/physics.dart';
|
||||
import 'package:flutter/semantics.dart';
|
||||
import 'package:material_new_shapes/material_new_shapes.dart';
|
||||
|
||||
/// A Material Design loading indicator.
|
||||
///
|
||||
/// This version of the loading indicator morphs between its [polygons] shapes.
|
||||
/// 
|
||||
class ExpressiveLoadingIndicator extends ProgressIndicator {
|
||||
/// A list of [RoundedPolygon]s for the sequence of shapes this loading indicator
|
||||
/// will morph between. The loading indicator expects at least two items in that list.
|
||||
final List<RoundedPolygon>? polygons;
|
||||
|
||||
/// Defines minimum and maximum sizes for an [ExpressiveLoadingIndicator].
|
||||
/// If null, then the [ProgressIndicatorThemeData.constraints] will be used. Otherwise, defaults to a minimum width and height of 48 pixels.
|
||||
final BoxConstraints? constraints;
|
||||
|
||||
final double activeSize;
|
||||
|
||||
const ExpressiveLoadingIndicator({
|
||||
super.key,
|
||||
super.color,
|
||||
this.polygons,
|
||||
this.activeSize = 38.0,
|
||||
this.constraints,
|
||||
super.semanticsLabel,
|
||||
super.semanticsValue,
|
||||
}) : assert(polygons != null ? polygons.length > 1 : true);
|
||||
|
||||
@override
|
||||
State<ExpressiveLoadingIndicator> createState() =>
|
||||
_ExpressiveLoadingIndicatorState();
|
||||
}
|
||||
|
||||
class _ExpressiveLoadingIndicatorState extends State<ExpressiveLoadingIndicator>
|
||||
with TickerProviderStateMixin {
|
||||
static final List<RoundedPolygon> _defaultPolygons = [
|
||||
MaterialShapes.softBurst,
|
||||
MaterialShapes.cookie9Sided,
|
||||
MaterialShapes.pentagon,
|
||||
MaterialShapes.pill,
|
||||
MaterialShapes.sunny,
|
||||
MaterialShapes.cookie4Sided,
|
||||
MaterialShapes.oval,
|
||||
];
|
||||
|
||||
static final BoxConstraints _defaultConstraints = BoxConstraints(
|
||||
minWidth: 48.0,
|
||||
minHeight: 48.0,
|
||||
maxWidth: 48.0,
|
||||
maxHeight: 48.0,
|
||||
); // default from kotlin source
|
||||
|
||||
late final List<RoundedPolygon> _polygons;
|
||||
|
||||
static const int _globalRotationDurationMs = 4666;
|
||||
static const int _morphIntervalMs = 650;
|
||||
static const double _fullRotation = 360.0;
|
||||
|
||||
static const double _quarterRotation = _fullRotation / 4;
|
||||
static const double _activeSize = 48; // based on source spec
|
||||
|
||||
late final List<Morph> _morphSequence;
|
||||
|
||||
late final AnimationController _morphController;
|
||||
late final AnimationController _globalRotationController;
|
||||
int _currentMorphIndex = 0;
|
||||
double _morphRotationTargetAngle = _quarterRotation;
|
||||
|
||||
Timer? _morphTimer;
|
||||
|
||||
final _morphAnimationSpec = SpringSimulation(
|
||||
SpringDescription.withDampingRatio(ratio: 0.6, stiffness: 200.0, mass: 1.0),
|
||||
0.0,
|
||||
1.0,
|
||||
5.0,
|
||||
snapToEnd: true,
|
||||
);
|
||||
|
||||
late BoxConstraints _constraints;
|
||||
late Color _color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final indicatorTheme = ProgressIndicatorTheme.of(context);
|
||||
_color =
|
||||
widget.color ??
|
||||
indicatorTheme.color ??
|
||||
Theme.of(context).colorScheme.primary;
|
||||
_constraints =
|
||||
widget.constraints ?? indicatorTheme.constraints ?? _defaultConstraints;
|
||||
|
||||
final activeIndicatorScale =
|
||||
widget.activeSize /
|
||||
math.min(_constraints.maxWidth, _constraints.maxHeight);
|
||||
|
||||
final shapesScaleFactor =
|
||||
_calculateScaleFactor(_polygons) * activeIndicatorScale;
|
||||
|
||||
return Semantics.fromProperties(
|
||||
properties: SemanticsProperties(
|
||||
label: widget.semanticsLabel,
|
||||
value: widget.semanticsValue,
|
||||
),
|
||||
child: RepaintBoundary(
|
||||
child: ConstrainedBox(
|
||||
constraints: _constraints,
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.0,
|
||||
child: AnimatedBuilder(
|
||||
animation: Listenable.merge([
|
||||
_morphController,
|
||||
_globalRotationController,
|
||||
]),
|
||||
builder: (context, child) {
|
||||
final morphProgress = _morphController.value.clamp(0.0, 1.0);
|
||||
final globalRotationDegrees =
|
||||
_globalRotationController.value * _fullRotation;
|
||||
|
||||
// calculate total rotation (clockwise, matching Kotlin implementation)
|
||||
final totalRotationDegrees =
|
||||
morphProgress * _quarterRotation +
|
||||
_morphRotationTargetAngle +
|
||||
globalRotationDegrees;
|
||||
|
||||
final totalRotationRadians =
|
||||
totalRotationDegrees * (math.pi / 180.0);
|
||||
|
||||
return Transform.rotate(
|
||||
angle: totalRotationRadians,
|
||||
child: CustomPaint(
|
||||
painter: _MorphPainter(
|
||||
morph: _morphSequence[_currentMorphIndex],
|
||||
progress: morphProgress,
|
||||
color: _color,
|
||||
scaleFactor: shapesScaleFactor,
|
||||
repaint: Listenable.merge([
|
||||
_morphController,
|
||||
_globalRotationController,
|
||||
]),
|
||||
),
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_morphTimer?.cancel();
|
||||
_morphController.dispose();
|
||||
_globalRotationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_polygons = widget.polygons ?? _defaultPolygons;
|
||||
|
||||
_morphSequence = _createMorphSequence(_polygons, circularSequence: true);
|
||||
|
||||
_morphController = AnimationController.unbounded(vsync: this);
|
||||
|
||||
// continuous linear rotation
|
||||
_globalRotationController = AnimationController(
|
||||
duration: const Duration(milliseconds: _globalRotationDurationMs),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_startAnimations();
|
||||
}
|
||||
|
||||
List<Morph> _createMorphSequence(
|
||||
List<RoundedPolygon> polygons, {
|
||||
required bool circularSequence,
|
||||
}) {
|
||||
final morphs = <Morph>[];
|
||||
|
||||
for (int i = 0; i < polygons.length; i++) {
|
||||
if (i + 1 < polygons.length) {
|
||||
morphs.add(Morph(polygons[i], polygons[i + 1]));
|
||||
} else if (circularSequence) {
|
||||
// morph from last shape back to first shape
|
||||
morphs.add(Morph(polygons[i], polygons[0]));
|
||||
}
|
||||
}
|
||||
|
||||
return morphs;
|
||||
}
|
||||
|
||||
/// Calculates a scale factor that will be used when scaling the provided [RoundedPolygon]s into a
|
||||
/// specified sized container.
|
||||
///
|
||||
/// Since the polygons may rotate, a simple [RoundedPolygon.calculateBounds] is not enough to
|
||||
/// determine the size the polygon will occupy as it rotates. Using the simple bounds calculation may
|
||||
/// result in a clipped shape.
|
||||
///
|
||||
/// This function calculates and returns a scale factor by utilizing the
|
||||
/// [RoundedPolygon.calculateMaxBounds] and comparing its result to the
|
||||
/// [RoundedPolygon.calculateBounds]. The scale factor can later be used when calling [processPath].
|
||||
///
|
||||
/// Port of Kotlin implementation.
|
||||
double _calculateScaleFactor(List<RoundedPolygon> polygons) {
|
||||
var scaleFactor = 1.0;
|
||||
|
||||
for (final polygon in polygons) {
|
||||
final bounds = polygon.calculateBounds();
|
||||
final maxBounds = polygon.calculateMaxBounds();
|
||||
|
||||
final boundsWidth = bounds[2] - bounds[0];
|
||||
final boundsHeight = bounds[3] - bounds[1];
|
||||
|
||||
final maxBoundsWidth = maxBounds[2] - maxBounds[0];
|
||||
final maxBoundsHeight = maxBounds[3] - maxBounds[1];
|
||||
|
||||
final scaleX = boundsWidth / maxBoundsWidth;
|
||||
final scaleY = boundsHeight / maxBoundsHeight;
|
||||
|
||||
// We use max(scaleX, scaleY) to handle cases like a pill-shape that can throw off the
|
||||
// entire calculation.
|
||||
scaleFactor = math.min(scaleFactor, math.max(scaleX, scaleY));
|
||||
}
|
||||
|
||||
return scaleFactor;
|
||||
}
|
||||
|
||||
void _startAnimations() {
|
||||
// infinite global rotation
|
||||
_globalRotationController.repeat();
|
||||
|
||||
// periodic morph cycle
|
||||
_morphTimer = Timer.periodic(
|
||||
const Duration(milliseconds: _morphIntervalMs),
|
||||
(_) => _startMorphCycle(),
|
||||
);
|
||||
|
||||
_startMorphCycle();
|
||||
}
|
||||
|
||||
void _startMorphCycle() {
|
||||
if (!mounted) return;
|
||||
|
||||
// move to next morph in sequence
|
||||
_currentMorphIndex = (_currentMorphIndex + 1) % _morphSequence.length;
|
||||
|
||||
// accumulate rotation target
|
||||
_morphRotationTargetAngle =
|
||||
(_morphRotationTargetAngle + _quarterRotation) % _fullRotation;
|
||||
|
||||
// Reset and start morph animation
|
||||
_morphController
|
||||
..value = 0.0
|
||||
..animateWith(_morphAnimationSpec);
|
||||
}
|
||||
}
|
||||
|
||||
class _MorphPainter extends CustomPainter {
|
||||
final Morph morph;
|
||||
final double progress;
|
||||
final Color color;
|
||||
|
||||
/// A scale factor that will be taken into account uniformly when the [path] is
|
||||
/// scaled (i.e. the scaleX would be the [size] width x the scale factor, and the scaleY would be
|
||||
/// the [size] height x the scale factor)
|
||||
final double scaleFactor;
|
||||
|
||||
_MorphPainter({
|
||||
required this.morph,
|
||||
required this.progress,
|
||||
required this.color,
|
||||
this.scaleFactor = 1.0,
|
||||
super.repaint,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final path = morph.toPath(progress: progress);
|
||||
final processedPath = _processPath(path, size);
|
||||
canvas.drawPath(
|
||||
processedPath,
|
||||
Paint()
|
||||
..style = PaintingStyle.fill
|
||||
..color = color,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_MorphPainter oldDelegate) {
|
||||
return oldDelegate.morph != morph ||
|
||||
oldDelegate.progress != progress ||
|
||||
oldDelegate.color != color ||
|
||||
oldDelegate.scaleFactor != scaleFactor;
|
||||
}
|
||||
|
||||
/// Process a given path to scale it and center it inside the given size.
|
||||
///
|
||||
/// [path] takes a [Path] that was generated by a _normalized_ [Morph] or [RoundedPolygon].
|
||||
/// [size] takes a [Size] that the provided [path] is going to be scaled and centered into.
|
||||
Path _processPath(Path path, Size size) {
|
||||
// a [Matrix] that would be used to apply the scaling. Note that any provided
|
||||
// matrix will be reset in this function.
|
||||
final Matrix4 scaleMatrix = Matrix4.diagonal3Values(
|
||||
size.width * scaleFactor,
|
||||
size.height * scaleFactor,
|
||||
1,
|
||||
);
|
||||
final Path scaledPath = path.transform(scaleMatrix.storage);
|
||||
|
||||
// Translate the path so that its center aligns with the center of the container.
|
||||
final Rect bounds = scaledPath.getBounds();
|
||||
final Offset translation =
|
||||
Offset(size.width / 2, size.height / 2) - bounds.center;
|
||||
final Path finalPath = scaledPath.shift(translation);
|
||||
|
||||
return finalPath;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
name: expressive_loading_indicator
|
||||
description: Material Design 3 Expressive loading indicator port for Flutter
|
||||
version: 0.0.1
|
||||
homepage: https://github.com/arafatamim/expressive_loading_indicator
|
||||
|
||||
environment:
|
||||
sdk: ^3.8.1
|
||||
flutter: ">=1.17.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
material_new_shapes: ^1.0.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^5.0.0
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
# The following section is specific to Flutter packages.
|
||||
flutter:
|
||||
|
||||
# To add assets to your package, add an assets section, like this:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
#
|
||||
# For details regarding assets in packages, see
|
||||
# https://flutter.dev/to/asset-from-package
|
||||
#
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/to/resolution-aware-images
|
||||
|
||||
# To add custom fonts to your package, add a fonts section here,
|
||||
# in this "flutter" section. Each entry in this list should have a
|
||||
# "family" key with the font family name, and a "fonts" key with a
|
||||
# list giving the asset and other descriptors for the font. For
|
||||
# example:
|
||||
# fonts:
|
||||
# - family: Schyler
|
||||
# fonts:
|
||||
# - asset: fonts/Schyler-Regular.ttf
|
||||
# - asset: fonts/Schyler-Italic.ttf
|
||||
# style: italic
|
||||
# - family: Trajan Pro
|
||||
# fonts:
|
||||
# - asset: fonts/TrajanPro.ttf
|
||||
# - asset: fonts/TrajanPro_Bold.ttf
|
||||
# weight: 700
|
||||
#
|
||||
# For details regarding fonts in packages, see
|
||||
# https://flutter.dev/to/font-from-package
|
||||
@@ -0,0 +1,302 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:material_new_shapes/material_new_shapes.dart';
|
||||
import 'package:expressive_loading_indicator/expressive_loading_indicator.dart';
|
||||
|
||||
void main() {
|
||||
group('ExpressiveLoadingIndicator', () {
|
||||
testWidgets('renders with default polygons', (WidgetTester tester) async {
|
||||
// Build our widget
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(body: Center(child: ExpressiveLoadingIndicator())),
|
||||
),
|
||||
);
|
||||
|
||||
// Verify the widget renders
|
||||
expect(find.byType(ExpressiveLoadingIndicator), findsOneWidget);
|
||||
|
||||
// Let the animation run for a bit
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
// No errors should occur during animation
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('renders with custom polygons', (WidgetTester tester) async {
|
||||
final customPolygons = [
|
||||
MaterialShapes.pill,
|
||||
MaterialShapes.pentagon,
|
||||
MaterialShapes.oval,
|
||||
];
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Center(
|
||||
child: ExpressiveLoadingIndicator(polygons: customPolygons),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(ExpressiveLoadingIndicator), findsOneWidget);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('renders with custom color', (WidgetTester tester) async {
|
||||
const customColor = Colors.red;
|
||||
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Center(child: ExpressiveLoadingIndicator(color: customColor)),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(ExpressiveLoadingIndicator), findsOneWidget);
|
||||
|
||||
// Extract the widget to verify its properties
|
||||
final ExpressiveLoadingIndicator widget = tester.widget(
|
||||
find.byType(ExpressiveLoadingIndicator),
|
||||
);
|
||||
|
||||
expect(widget.color, equals(customColor));
|
||||
});
|
||||
|
||||
testWidgets('renders with custom constraints', (WidgetTester tester) async {
|
||||
const customConstraints = BoxConstraints(
|
||||
minWidth: 100.0,
|
||||
minHeight: 100.0,
|
||||
maxWidth: 100.0,
|
||||
maxHeight: 100.0,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Center(
|
||||
child: ExpressiveLoadingIndicator(constraints: customConstraints),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(ExpressiveLoadingIndicator), findsOneWidget);
|
||||
|
||||
// Find the ConstrainedBox widget
|
||||
final ConstrainedBox constrainedBox = tester.widget(
|
||||
find.descendant(
|
||||
of: find.byType(ExpressiveLoadingIndicator),
|
||||
matching: find.byType(ConstrainedBox),
|
||||
),
|
||||
);
|
||||
|
||||
expect(constrainedBox.constraints, equals(customConstraints));
|
||||
});
|
||||
|
||||
testWidgets('applies semantics properties correctly', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
const String semanticsLabel = 'Loading';
|
||||
const String semanticsValue = '50%';
|
||||
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Center(
|
||||
child: ExpressiveLoadingIndicator(
|
||||
semanticsLabel: semanticsLabel,
|
||||
semanticsValue: semanticsValue,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(ExpressiveLoadingIndicator), findsOneWidget);
|
||||
|
||||
// Verify semantics are applied to the widget
|
||||
final semanticsWidget = find.byType(Semantics);
|
||||
expect(semanticsWidget, findsWidgets);
|
||||
});
|
||||
|
||||
testWidgets('throws assertion error with single polygon', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
expect(
|
||||
() => ExpressiveLoadingIndicator(polygons: [MaterialShapes.pill]),
|
||||
throwsAssertionError,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('renders in theme with correct color', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
const Color themeColor = Colors.green;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(
|
||||
progressIndicatorTheme: const ProgressIndicatorThemeData(
|
||||
color: themeColor,
|
||||
),
|
||||
),
|
||||
home: const Scaffold(
|
||||
body: Center(child: ExpressiveLoadingIndicator()),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(ExpressiveLoadingIndicator), findsOneWidget);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
// The widget should use the theme color
|
||||
// This test verifies the widget respects the theme
|
||||
final ExpressiveLoadingIndicator widget = tester.widget(
|
||||
find.byType(ExpressiveLoadingIndicator),
|
||||
);
|
||||
|
||||
expect(widget.color, isNull); // Widget itself doesn't have a color set
|
||||
});
|
||||
|
||||
testWidgets('renders in theme with custom constraints', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
const BoxConstraints themeConstraints = BoxConstraints(
|
||||
minWidth: 80.0,
|
||||
minHeight: 80.0,
|
||||
maxWidth: 80.0,
|
||||
maxHeight: 80.0,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(
|
||||
progressIndicatorTheme: const ProgressIndicatorThemeData(
|
||||
constraints: themeConstraints,
|
||||
),
|
||||
),
|
||||
home: const Scaffold(
|
||||
body: Center(child: ExpressiveLoadingIndicator()),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(ExpressiveLoadingIndicator), findsOneWidget);
|
||||
|
||||
// The ConstrainedBox should use theme constraints
|
||||
final ConstrainedBox constrainedBox = tester.widget(
|
||||
find.descendant(
|
||||
of: find.byType(ExpressiveLoadingIndicator),
|
||||
matching: find.byType(ConstrainedBox),
|
||||
),
|
||||
);
|
||||
|
||||
expect(constrainedBox.constraints, equals(themeConstraints));
|
||||
});
|
||||
|
||||
testWidgets('disposes resources properly', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(body: Center(child: ExpressiveLoadingIndicator())),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(ExpressiveLoadingIndicator), findsOneWidget);
|
||||
|
||||
// Replace the widget with something else
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(body: Center(child: Text('No indicator'))),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(ExpressiveLoadingIndicator), findsNothing);
|
||||
expect(find.text('No indicator'), findsOneWidget);
|
||||
|
||||
// No errors should occur during disposal
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('handles widget rebuilds during animation', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(body: Center(child: ExpressiveLoadingIndicator())),
|
||||
),
|
||||
);
|
||||
|
||||
// Let animation start
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
// Rebuild with the same widget
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(body: Center(child: ExpressiveLoadingIndicator())),
|
||||
),
|
||||
);
|
||||
|
||||
// Continue animation
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
// No exceptions should occur
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('handles rapid animation frames', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(body: Center(child: ExpressiveLoadingIndicator())),
|
||||
),
|
||||
);
|
||||
|
||||
// Rapid succession of small animation steps
|
||||
for (int i = 0; i < 5; i++) {
|
||||
await tester.pump(const Duration(milliseconds: 16)); // ~60fps
|
||||
}
|
||||
|
||||
// Widget should handle rapid animation frames without issues
|
||||
expect(find.byType(ExpressiveLoadingIndicator), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('handles navigation away during animation', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
initialRoute: '/',
|
||||
routes: {
|
||||
'/': (context) => const Scaffold(
|
||||
body: Center(child: ExpressiveLoadingIndicator()),
|
||||
),
|
||||
'/next': (context) =>
|
||||
const Scaffold(body: Center(child: Text('Next Page'))),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Start animation
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
// Navigate away
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(body: Center(child: Text('Next Page'))),
|
||||
),
|
||||
);
|
||||
|
||||
// No exceptions should occur
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
+1
-1
@@ -164,7 +164,7 @@ packages:
|
||||
expressive_loading_indicator:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: expressive_loading_indicator_custom
|
||||
path: expressive_loading_indicator_modified
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ dependencies:
|
||||
path: ./settings_tiles_custom
|
||||
|
||||
expressive_loading_indicator:
|
||||
path: ./expressive_loading_indicator_custom
|
||||
path: ./expressive_loading_indicator_modified
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in new issue
Block a user