bool initialLiked; final ValueChanged<bool>? onChanged; const LikeButton({super.key, this.initialLiked = false, this.onChanged}); @override State<LikeButton> createState() => _LikeButtonState(); } class _LikeButtonState extends State<LikeButton> with TickerProviderStateMixin { late bool _liked; // ϦϯάΞχϝʔγϣϯ༻ late final AnimationController _burstController = AnimationController( vsync: this, duration: const Duration(milliseconds: 820), ); // ͍͍ͶΞΠίϯΞχϝʔγϣϯ༻ late final AnimationController _scaleController = AnimationController( vsync: this, duration: const Duration(milliseconds: 100), ); // λοϓ࣌ͷεέʔϧʢ1.5ഒʣ late final Animation<double> _scale = Tween<double>(begin: 1.0, end: 1.5) .chain(CurveTween(curve: Curves.easeOut)) .animate(_scaleController); // ϦϯάΞχϝʔγϣϯʢ0→1ͰܘͱϑΣʔυΞτΛදݱʣ late final Animation<double> _ring = CurvedAnimation( parent: _burstController, curve: Curves.easeOutCubic, ); @override void initState() { super.initState(); _liked = widget.initialLiked; } @override void dispose() { _burstController.dispose(); _scaleController.dispose(); super.dispose(); } void _toggleLike() async { setState(() => _liked = !_liked); widget.onChanged?.call(_liked); if (_liked) { // ͍͍Ͷ࣌ɿ௨ৗͷϦϯάΞχϝʔγϣϯʢଆ͔Β֎ଆʣ _burstController.forward(from: 0); } else { // ͍͍Ͷղআ࣌ɿٯͷϦϯάΞχϝʔγϣϯʢ֎ଆ͔Βଆʣ // forward Λ͏͕ɼϦϯάͷܭࢉΛٯʹ͢Δ // ୯७ʹreverseΛ͏ͱബ͍Ϧϯά͔Βೱ͍ϦϯάʹมԽͯ͠͠·͏ _burstController.forward(from: 0); } } void _onTapDown() { // λοϓμϯ࣌ʹεέʔϧΞοϓ _scaleController.forward(); } void _onTapUp() { // λοϓΞοϓ࣌ʹεέʔϧμϯ _scaleController.reverse(); } void _onTapCancel() { // λοϓΩϟϯηϧ࣌ʹεέʔϧμϯ _scaleController.reverse(); } @override Widget build(BuildContext context) { // ϋʔτΞΠίϯͷαΠζ const double size = 48; // Ϧϯάͷ࠷େܘʢsize * 1.2ʣ const double maxRingRadius = size * 1.2; return GestureDetector( behavior: HitTestBehavior.opaque, onTap: _toggleLike, onTapDown: (_) => _onTapDown(), onTapUp: (_) => _onTapUp(), onTapCancel: _onTapCancel, child: SizedBox( width: maxRingRadius * 2, height: maxRingRadius * 2, child: Stack( alignment: Alignment.center, children: [ // λοϓ࣌ͷϦϯά AnimatedBuilder( animation: _ring, builder: (context, _) { // Ξχϝʔγϣϯ͕ಈ͍͍ͯͳ͍ɼ͔͕ͭ0·ͨ1ͷ߹ඇදࣔ if (!_burstController.isAnimating && (_ring.value == 0.0 || _ring.value == 1.0)) { return const SizedBox.shrink(); } double radius; double opacity; Color ringColor; if (_liked) { radius = lerpDouble(size * 0.6, size * 1.2, _ring.value) ?? size * 0.6; opacity = lerpDouble(0.35, 0.0, _ring.value) ?? 0.0; ringColor = Colors.pink; } else { // ͍͍Ͷղআ࣌ɿ֎ଆ͔ΒଆॖΜͰফ͑ΔʢάϨʔʣ radius = lerpDouble(size * 1.2, size * 0.6, _ring.value) ?? size * 1.2; opacity = lerpDouble(0.35, 0.0, _ring.value) ?? 0.0; ringColor = Colors.grey; } return Container( width: radius * 2, height: radius * 2, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( width: 4, color: ringColor.withAlpha((255 * opacity).toInt()), ), ), ); }, ), // ϋʔτΞΠίϯ ScaleTransition( scale: _scale, child: Icon( _liked ? Icons.favorite : Icons.favorite_border, size: size, color: _liked ? Colors.pink : Colors.grey, ), ), ], ), ), ); } } import 'package:flutter/material.dart'; import 'package:rive/rive.dart'; class RiveLikeButton extends StatefulWidget { const RiveLikeButton({super.key}); @override State<RiveLikeButton> createState() => _RiveLikeButtonState(); } class _RiveLikeButtonState extends State<RiveLikeButton> { StateMachineController? _controller; SMIInput<bool>? _riveIsLiked; SMITrigger? _riveTap; bool _liked = false; @override void initState() { super.initState(); _liked = false; } void _onInit(Artboard artboard) { final controller = StateMachineController.fromArtboard( artboard, ‘State machine’, ); if (controller == null) return; artboard.addController(controller); _controller = controller; _riveIsLiked = controller.getBoolInput('isLiked'); _riveTap = controller.getTriggerInput('tap'); // ॳظঢ়ଶΛRiveಉظ _riveIsLiked?.value = _liked; } void _handleTap() { _liked = !_liked; // RiveͷBoolʹө _riveIsLiked?.value = _liked; // RiveͷTriggerΛൃՐ _riveTap?.fire(); } @override Widget build(BuildContext context) { // ϋʔτΞΠίϯͷαΠζ const double size = 48; return GestureDetector( onTap: _handleTap, behavior: HitTestBehavior.opaque, child: SizedBox( width: size * 1.2 * 2, // AnimationControllerͷํͷେ͖͞ʹ߹Θͤͨ height: size * 1.2 * 2, child: RiveAnimation.asset( 'assets/like_button.riv', fit: BoxFit.contain, onInit: _onInit, ), ), ); } @override void dispose() { _controller?.dispose(); super.dispose(); } } レビューしやすい! デザイナさんとつくれる!