You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
213 lines
5.6 KiB
213 lines
5.6 KiB
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
|
|
enum ShimmerDirection { ltr, rtl, ttb, btt }
|
|
|
|
@immutable
|
|
class Shimmer extends StatefulWidget {
|
|
final Widget child;
|
|
final Duration period;
|
|
final ShimmerDirection direction;
|
|
final Gradient gradient;
|
|
final int loop;
|
|
final bool enabled;
|
|
|
|
const Shimmer({
|
|
super.key,
|
|
required this.child,
|
|
required this.gradient,
|
|
this.direction = ShimmerDirection.rtl,
|
|
this.period = const Duration(milliseconds: 1500),
|
|
this.loop = 0,
|
|
this.enabled = true,
|
|
});
|
|
|
|
Shimmer.fromColors({
|
|
super.key,
|
|
required this.child,
|
|
required Color baseColor,
|
|
required Color highlightColor,
|
|
this.period = const Duration(milliseconds: 1500),
|
|
this.direction = ShimmerDirection.rtl,
|
|
this.loop = 0,
|
|
this.enabled = true,
|
|
}) : gradient = LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.centerRight,
|
|
colors: <Color>[baseColor, baseColor, highlightColor, baseColor, baseColor],
|
|
stops: const <double>[0.0, 0.35, 0.5, 0.65, 1.0],
|
|
);
|
|
|
|
@override
|
|
State<Shimmer> createState() => _ShimmerState();
|
|
|
|
@override
|
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
|
super.debugFillProperties(properties);
|
|
properties.add(DiagnosticsProperty<Gradient>('gradient', gradient, defaultValue: null));
|
|
properties.add(EnumProperty<ShimmerDirection>('direction', direction));
|
|
properties.add(DiagnosticsProperty<Duration>('period', period, defaultValue: null));
|
|
properties.add(DiagnosticsProperty<bool>('enabled', enabled, defaultValue: null));
|
|
properties.add(DiagnosticsProperty<int>('loop', loop, defaultValue: 0));
|
|
}
|
|
}
|
|
|
|
class _ShimmerState extends State<Shimmer> with SingleTickerProviderStateMixin {
|
|
late AnimationController _controller;
|
|
int _count = 0;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = AnimationController(vsync: this, duration: widget.period)
|
|
..addStatusListener((status) {
|
|
if (status != AnimationStatus.completed) {
|
|
return;
|
|
}
|
|
_count++;
|
|
if (widget.loop <= 0) {
|
|
_controller.repeat();
|
|
} else if (_count < widget.loop) {
|
|
_controller.forward(from: 0.0);
|
|
}
|
|
});
|
|
if (widget.enabled) {
|
|
_controller.forward();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(Shimmer oldWidget) {
|
|
if (widget.enabled) {
|
|
_controller.forward();
|
|
} else {
|
|
_controller.stop();
|
|
}
|
|
super.didUpdateWidget(oldWidget);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return AnimatedBuilder(
|
|
animation: _controller,
|
|
child: widget.child,
|
|
builder: (context, child) => _Shimmer(
|
|
direction: widget.direction,
|
|
gradient: widget.gradient,
|
|
percent: _controller.value,
|
|
child: child,
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
}
|
|
|
|
@immutable
|
|
class _Shimmer extends SingleChildRenderObjectWidget {
|
|
final double percent;
|
|
final ShimmerDirection direction;
|
|
final Gradient gradient;
|
|
|
|
const _Shimmer({
|
|
super.child,
|
|
required this.percent,
|
|
required this.direction,
|
|
required this.gradient,
|
|
});
|
|
|
|
@override
|
|
_ShimmerFilter createRenderObject(BuildContext context) {
|
|
return _ShimmerFilter(percent, direction, gradient);
|
|
}
|
|
|
|
@override
|
|
void updateRenderObject(BuildContext context, _ShimmerFilter shimmer) {
|
|
shimmer.percent = percent;
|
|
shimmer.gradient = gradient;
|
|
shimmer.direction = direction;
|
|
}
|
|
}
|
|
|
|
class _ShimmerFilter extends RenderProxyBox {
|
|
ShimmerDirection _direction;
|
|
Gradient _gradient;
|
|
double _percent;
|
|
|
|
_ShimmerFilter(this._percent, this._direction, this._gradient);
|
|
|
|
@override
|
|
ShaderMaskLayer? get layer => super.layer as ShaderMaskLayer?;
|
|
|
|
@override
|
|
bool get alwaysNeedsCompositing => child != null;
|
|
|
|
set percent(double newValue) {
|
|
if (newValue == _percent) {
|
|
return;
|
|
}
|
|
_percent = newValue;
|
|
markNeedsPaint();
|
|
}
|
|
|
|
set gradient(Gradient newValue) {
|
|
if (newValue == _gradient) {
|
|
return;
|
|
}
|
|
_gradient = newValue;
|
|
markNeedsPaint();
|
|
}
|
|
|
|
set direction(ShimmerDirection newDirection) {
|
|
if (newDirection == _direction) {
|
|
return;
|
|
}
|
|
_direction = newDirection;
|
|
markNeedsLayout();
|
|
}
|
|
|
|
@override
|
|
void paint(PaintingContext context, Offset offset) {
|
|
if (child != null) {
|
|
assert(needsCompositing);
|
|
|
|
final double width = child!.size.width;
|
|
final double height = child!.size.height;
|
|
Rect rect;
|
|
double dx, dy;
|
|
if (_direction == ShimmerDirection.rtl) {
|
|
dx = _offset(width, -width, _percent);
|
|
dy = 0.0;
|
|
rect = Rect.fromLTWH(dx - width, dy, 3 * width, height);
|
|
} else if (_direction == ShimmerDirection.ttb) {
|
|
dx = 0.0;
|
|
dy = _offset(-height, height, _percent);
|
|
rect = Rect.fromLTWH(dx, dy - height, width, 3 * height);
|
|
} else if (_direction == ShimmerDirection.btt) {
|
|
dx = 0.0;
|
|
dy = _offset(height, -height, _percent);
|
|
rect = Rect.fromLTWH(dx, dy - height, width, 3 * height);
|
|
} else {
|
|
dx = _offset(-width, width, _percent);
|
|
dy = 0.0;
|
|
rect = Rect.fromLTWH(dx - width, dy, 3 * width, height);
|
|
}
|
|
layer ??= ShaderMaskLayer();
|
|
layer!
|
|
..shader = _gradient.createShader(rect)
|
|
..maskRect = offset & size
|
|
..blendMode = BlendMode.srcIn;
|
|
context.pushLayer(layer!, super.paint, offset);
|
|
} else {
|
|
layer = null;
|
|
}
|
|
}
|
|
|
|
double _offset(double start, double end, double percent) {
|
|
return start + (end - start) * percent;
|
|
}
|
|
}
|