98 lines
2.8 KiB
Dart
98 lines
2.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:hgdj/assets_tool/images.dart';
|
|
|
|
/// 双击点赞爱心动效的触发器。onDoubleTapDown 里 [markAt] 记落点,onDoubleTap 里 [burst] 放动画
|
|
class LikeBurstController extends ChangeNotifier {
|
|
Offset? at;
|
|
|
|
/// 记下双击落点(相对 Stack 左上角)
|
|
void markAt(Offset offset) => at = offset;
|
|
|
|
/// 在落点弹一颗爱心;没记过落点就不弹
|
|
void burst() {
|
|
if (at == null) return;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
/// 双击处弹出的爱心:弹起放大 → 停顿 → 淡出。必须直接挂在 Stack 里
|
|
class LikeBurstView extends StatefulWidget {
|
|
const LikeBurstView({
|
|
super.key,
|
|
required this.controller,
|
|
this.icon = 'like_red.png',
|
|
this.size = 90,
|
|
});
|
|
|
|
final LikeBurstController controller;
|
|
final String icon; // assets/images/common 下的图名
|
|
final double size;
|
|
|
|
@override
|
|
State<LikeBurstView> createState() => _LikeBurstViewState();
|
|
}
|
|
|
|
class _LikeBurstViewState extends State<LikeBurstView>
|
|
with SingleTickerProviderStateMixin {
|
|
late final AnimationController _ctr;
|
|
Offset? _at;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_ctr = AnimationController(
|
|
vsync: this, duration: const Duration(milliseconds: 600))
|
|
..addStatusListener((status) {
|
|
if (status == AnimationStatus.completed && mounted)
|
|
setState(() => _at = null);
|
|
});
|
|
widget.controller.addListener(_onBurst);
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(covariant LikeBurstView oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (oldWidget.controller != widget.controller) {
|
|
oldWidget.controller.removeListener(_onBurst);
|
|
widget.controller.addListener(_onBurst);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
widget.controller.removeListener(_onBurst);
|
|
_ctr.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _onBurst() {
|
|
setState(() => _at = widget.controller.at);
|
|
_ctr.forward(from: 0); // 连点就从头重放
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final at = _at;
|
|
if (at == null) return const SizedBox.shrink();
|
|
return Positioned(
|
|
left: at.dx - widget.size / 2,
|
|
top: at.dy - widget.size / 2,
|
|
child: IgnorePointer(
|
|
child: FadeTransition(
|
|
opacity: TweenSequence<double>([
|
|
TweenSequenceItem(tween: Tween(begin: 0.0, end: 1.0), weight: 20),
|
|
TweenSequenceItem(tween: ConstantTween(1.0), weight: 40),
|
|
TweenSequenceItem(tween: Tween(begin: 1.0, end: 0.0), weight: 40),
|
|
]).animate(_ctr),
|
|
child: ScaleTransition(
|
|
scale: Tween(begin: 0.5, end: 1.3).animate(
|
|
CurvedAnimation(parent: _ctr, curve: Curves.easeOutBack)),
|
|
child: Image.asset(widget.icon.commonImgPath,
|
|
width: widget.size, height: widget.size),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|