mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-07-13 12:50:10 +08:00
245
lib/common/widgets/animated_height.dart
Normal file
245
lib/common/widgets/animated_height.dart
Normal file
@@ -0,0 +1,245 @@
|
||||
import 'package:PiliPlus/utils/extension/num_ext.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart'
|
||||
show ClipRectLayer, LayerHandle, RenderAnimatedSize, RenderProxyBox;
|
||||
|
||||
typedef Heights = ({double from, double to});
|
||||
|
||||
/// ref [AnimatedSize]
|
||||
class AnimatedHeight extends StatefulWidget {
|
||||
const AnimatedHeight({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.curve = Curves.linear,
|
||||
required this.duration,
|
||||
this.reverseDuration,
|
||||
this.clipBehavior = .hardEdge,
|
||||
required this.expand,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final Curve curve;
|
||||
final Duration duration;
|
||||
final Duration? reverseDuration;
|
||||
final Clip clipBehavior;
|
||||
final bool expand;
|
||||
|
||||
@override
|
||||
State<AnimatedHeight> createState() => _AnimatedHeightState();
|
||||
}
|
||||
|
||||
class _AnimatedHeightState extends State<AnimatedHeight>
|
||||
with SingleTickerProviderStateMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _AnimatedHeight(
|
||||
curve: widget.curve,
|
||||
duration: widget.duration,
|
||||
reverseDuration: widget.reverseDuration,
|
||||
vsync: this,
|
||||
clipBehavior: widget.clipBehavior,
|
||||
expand: widget.expand,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AnimatedHeight extends SingleChildRenderObjectWidget {
|
||||
const _AnimatedHeight({
|
||||
required Widget super.child,
|
||||
this.curve = Curves.linear,
|
||||
required this.duration,
|
||||
this.reverseDuration,
|
||||
required this.vsync,
|
||||
this.clipBehavior = .hardEdge,
|
||||
required this.expand,
|
||||
});
|
||||
|
||||
final Curve curve;
|
||||
final Duration duration;
|
||||
final Duration? reverseDuration;
|
||||
final TickerProvider vsync;
|
||||
final Clip clipBehavior;
|
||||
final bool expand;
|
||||
|
||||
@override
|
||||
RenderAnimatedHeight createRenderObject(BuildContext context) {
|
||||
return RenderAnimatedHeight(
|
||||
duration: duration,
|
||||
reverseDuration: reverseDuration,
|
||||
curve: curve,
|
||||
vsync: vsync,
|
||||
clipBehavior: clipBehavior,
|
||||
expand: expand,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(
|
||||
BuildContext context,
|
||||
RenderAnimatedHeight renderObject,
|
||||
) {
|
||||
renderObject
|
||||
..duration = duration
|
||||
..reverseDuration = reverseDuration
|
||||
..curve = curve
|
||||
..vsync = vsync
|
||||
..clipBehavior = clipBehavior
|
||||
..expand = expand;
|
||||
}
|
||||
}
|
||||
|
||||
/// ref [RenderAnimatedSize]
|
||||
class RenderAnimatedHeight extends RenderProxyBox {
|
||||
RenderAnimatedHeight({
|
||||
required this._vsync,
|
||||
required Duration duration,
|
||||
Duration? reverseDuration,
|
||||
this._curve = Curves.linear,
|
||||
this._clipBehavior = .hardEdge,
|
||||
required this._expand,
|
||||
}) {
|
||||
_controller =
|
||||
AnimationController(
|
||||
vsync: vsync,
|
||||
value: expand ? 1.0 : 0.0,
|
||||
duration: duration,
|
||||
reverseDuration: reverseDuration,
|
||||
)..addListener(() {
|
||||
if (_controller.value != _lastValue) {
|
||||
markNeedsLayout();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool _expand;
|
||||
bool get expand => _expand;
|
||||
set expand(bool value) {
|
||||
if (_expand == value) return;
|
||||
_expand = value;
|
||||
_lastValue = 0.0;
|
||||
_controller.forward(from: 0);
|
||||
}
|
||||
|
||||
late final AnimationController _controller;
|
||||
bool get isAnimating => _controller.isAnimating;
|
||||
bool get _isInvisible => !isAnimating && !expand;
|
||||
|
||||
double? _lastValue;
|
||||
Heights? _heights;
|
||||
|
||||
Duration get duration => _controller.duration!;
|
||||
set duration(Duration value) {
|
||||
if (value == _controller.duration) {
|
||||
return;
|
||||
}
|
||||
_controller.duration = value;
|
||||
}
|
||||
|
||||
Duration? get reverseDuration => _controller.reverseDuration;
|
||||
set reverseDuration(Duration? value) {
|
||||
if (value == _controller.reverseDuration) {
|
||||
return;
|
||||
}
|
||||
_controller.reverseDuration = value;
|
||||
}
|
||||
|
||||
Curve _curve;
|
||||
Curve get curve => _curve;
|
||||
set curve(Curve value) {
|
||||
if (value == _curve) {
|
||||
return;
|
||||
}
|
||||
_curve = value;
|
||||
}
|
||||
|
||||
Clip get clipBehavior => _clipBehavior;
|
||||
Clip _clipBehavior = .hardEdge;
|
||||
set clipBehavior(Clip value) {
|
||||
if (value != _clipBehavior) {
|
||||
_clipBehavior = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
}
|
||||
|
||||
TickerProvider get vsync => _vsync;
|
||||
TickerProvider _vsync;
|
||||
set vsync(TickerProvider value) {
|
||||
if (value == _vsync) {
|
||||
return;
|
||||
}
|
||||
_vsync = value;
|
||||
_controller.resync(vsync);
|
||||
}
|
||||
|
||||
@override
|
||||
void detach() {
|
||||
_controller.stop();
|
||||
super.detach();
|
||||
}
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
if (_isInvisible) {
|
||||
_heights = const (from: 0, to: 0);
|
||||
size = .zero;
|
||||
return;
|
||||
}
|
||||
|
||||
_lastValue = _controller.value;
|
||||
|
||||
final BoxConstraints constraints = this.constraints;
|
||||
final childSize = (child!..layout(constraints, parentUsesSize: true)).size;
|
||||
|
||||
final Size animatedSize;
|
||||
|
||||
if (isAnimating && _heights != null) {
|
||||
final to = expand ? childSize.height : 0.0;
|
||||
if (_heights!.to != to) {
|
||||
_heights = (from: size.height, to: to);
|
||||
}
|
||||
animatedSize = Size(
|
||||
childSize.width,
|
||||
curve.transform(_controller.value).lerp(_heights!.from, _heights!.to),
|
||||
);
|
||||
} else {
|
||||
animatedSize = childSize;
|
||||
_heights = (from: childSize.height, to: childSize.height);
|
||||
}
|
||||
|
||||
size = constraints.constrain(animatedSize);
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
if (_isInvisible) {
|
||||
_clipRectLayer.layer = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (isAnimating && clipBehavior != .none) {
|
||||
final Rect rect = Offset.zero & size;
|
||||
_clipRectLayer.layer = context.pushClipRect(
|
||||
needsCompositing,
|
||||
offset,
|
||||
rect,
|
||||
super.paint,
|
||||
clipBehavior: clipBehavior,
|
||||
oldLayer: _clipRectLayer.layer,
|
||||
);
|
||||
} else {
|
||||
_clipRectLayer.layer = null;
|
||||
super.paint(context, offset);
|
||||
}
|
||||
}
|
||||
|
||||
final LayerHandle<ClipRectLayer> _clipRectLayer =
|
||||
LayerHandle<ClipRectLayer>();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_clipRectLayer.layer = null;
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user